Squelette du projet

This commit is contained in:
Olivier Muller
2025-11-03 11:00:11 +01:00
commit ff440776bb
41 changed files with 2522 additions and 0 deletions

View File

@@ -0,0 +1,200 @@
// file: design_1_clk_wiz_0_0.v
// (c) Copyright 2017-2018, 2023 Advanced Micro Devices, Inc. All rights reserved.
//
// This file contains confidential and proprietary information
// of AMD and is protected under U.S. and international copyright
// and other intellectual property laws.
//
// DISCLAIMER
// This disclaimer is not a license and does not grant any
// rights to the materials distributed herewith. Except as
// otherwise provided in a valid license issued to you by
// AMD, and to the maximum extent permitted by applicable
// law: (1) THESE MATERIALS ARE MADE AVAILABLE "AS IS" AND
// WITH ALL FAULTS, AND AMD HEREBY DISCLAIMS ALL WARRANTIES
// AND CONDITIONS, EXPRESS, IMPLIED, OR STATUTORY, INCLUDING
// BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, NON-
// INFRINGEMENT, OR FITNESS FOR ANY PARTICULAR PURPOSE; and
// (2) AMD shall not be liable (whether in contract or tort,
// including negligence, or under any other theory of
// liability) for any loss or damage of any kind or nature
// related to, arising under or in connection with these
// materials, including for any direct, or any indirect,
// special, incidental, or consequential loss or damage
// (including loss of data, profits, goodwill, or any type of
// loss or damage suffered as a result of any action brought
// by a third party) even if such damage or loss was
// reasonably foreseeable or AMD had been advised of the
// possibility of the same.
//
// CRITICAL APPLICATIONS
// AMD products are not designed or intended to be fail-
// safe, or for use in any application requiring fail-safe
// performance, such as life-support or safety devices or
// systems, Class III medical devices, nuclear facilities,
// applications related to the deployment of airbags, or any
// other applications that could lead to death, personal
// injury, or severe property or environmental damage
// (individually and collectively, "Critical
// Applications"). Customer assumes the sole risk and
// liability of any use of AMD products in Critical
// Applications, subject only to applicable laws and
// regulations governing limitations on product liability.
//
// THIS COPYRIGHT NOTICE AND DISCLAIMER MUST BE RETAINED AS
// PART OF THIS FILE AT ALL TIMES.
//----------------------------------------------------------------------------
// User entered comments
//----------------------------------------------------------------------------
// None
//
//----------------------------------------------------------------------------
// Output Output Phase Duty Cycle Pk-to-Pk Phase
// Clock Freq (MHz) (degrees) (%) Jitter (ps) Error (ps)
//----------------------------------------------------------------------------
// clk_out1__10.00000______0.000______50.0______197.700_____96.948
// clk_out2__125.00000______0.000______50.0______119.348_____96.948
//
//----------------------------------------------------------------------------
// Input Clock Freq (MHz) Input Jitter (UI)
//----------------------------------------------------------------------------
// __primary_________125.000____________0.010
`timescale 1ps/1ps
module clk_wiz
(// Clock in ports
// Clock out ports
output clk_out1,
output clk_out2,
// Status and control signals
input reset,
output locked,
input clk_in1
);
// Input buffering
//------------------------------------
wire clk_in1_design_1_clk_wiz_0_0;
wire clk_in2_design_1_clk_wiz_0_0;
IBUF clkin1_ibufg
(.O (clk_in1_design_1_clk_wiz_0_0),
.I (clk_in1));
// Clocking PRIMITIVE
//------------------------------------
// Instantiation of the MMCM PRIMITIVE
// * Unused inputs are tied off
// * Unused outputs are labeled unused
wire clk_out1_design_1_clk_wiz_0_0;
wire clk_out2_design_1_clk_wiz_0_0;
wire clk_out3_design_1_clk_wiz_0_0;
wire clk_out4_design_1_clk_wiz_0_0;
wire clk_out5_design_1_clk_wiz_0_0;
wire clk_out6_design_1_clk_wiz_0_0;
wire clk_out7_design_1_clk_wiz_0_0;
wire [15:0] do_unused;
wire drdy_unused;
wire psdone_unused;
wire locked_int;
wire clkfbout_design_1_clk_wiz_0_0;
wire clkfbout_buf_design_1_clk_wiz_0_0;
wire clkfboutb_unused;
wire clkout0b_unused;
wire clkout1b_unused;
wire clkout2_unused;
wire clkout2b_unused;
wire clkout3_unused;
wire clkout3b_unused;
wire clkout4_unused;
wire clkout5_unused;
wire clkout6_unused;
wire clkfbstopped_unused;
wire clkinstopped_unused;
wire reset_high;
MMCME2_ADV
#(.BANDWIDTH ("OPTIMIZED"),
.CLKOUT4_CASCADE ("FALSE"),
.COMPENSATION ("ZHOLD"),
.STARTUP_WAIT ("FALSE"),
.DIVCLK_DIVIDE (1),
.CLKFBOUT_MULT_F (8.000),
.CLKFBOUT_PHASE (0.000),
.CLKFBOUT_USE_FINE_PS ("FALSE"),
.CLKOUT0_DIVIDE_F (100.000),
.CLKOUT0_PHASE (0.000),
.CLKOUT0_DUTY_CYCLE (0.500),
.CLKOUT0_USE_FINE_PS ("FALSE"),
.CLKOUT1_DIVIDE (8),
.CLKOUT1_PHASE (0.000),
.CLKOUT1_DUTY_CYCLE (0.500),
.CLKOUT1_USE_FINE_PS ("FALSE"),
.CLKIN1_PERIOD (8.000))
mmcm_adv_inst
// Output clocks
(
.CLKFBOUT (clkfbout_design_1_clk_wiz_0_0),
.CLKFBOUTB (clkfboutb_unused),
.CLKOUT0 (clk_out1_design_1_clk_wiz_0_0),
.CLKOUT0B (clkout0b_unused),
.CLKOUT1 (clk_out2_design_1_clk_wiz_0_0),
.CLKOUT1B (clkout1b_unused),
.CLKOUT2 (clkout2_unused),
.CLKOUT2B (clkout2b_unused),
.CLKOUT3 (clkout3_unused),
.CLKOUT3B (clkout3b_unused),
.CLKOUT4 (clkout4_unused),
.CLKOUT5 (clkout5_unused),
.CLKOUT6 (clkout6_unused),
// Input clock control
.CLKFBIN (clkfbout_buf_design_1_clk_wiz_0_0),
.CLKIN1 (clk_in1_design_1_clk_wiz_0_0),
.CLKIN2 (1'b0),
// Tied to always select the primary input clock
.CLKINSEL (1'b1),
// Ports for dynamic reconfiguration
.DADDR (7'h0),
.DCLK (1'b0),
.DEN (1'b0),
.DI (16'h0),
.DO (do_unused),
.DRDY (drdy_unused),
.DWE (1'b0),
// Ports for dynamic phase shift
.PSCLK (1'b0),
.PSEN (1'b0),
.PSINCDEC (1'b0),
.PSDONE (psdone_unused),
// Other control and status signals
.LOCKED (locked_int),
.CLKINSTOPPED (clkinstopped_unused),
.CLKFBSTOPPED (clkfbstopped_unused),
.PWRDWN (1'b0),
.RST (reset_high));
assign reset_high = reset;
assign locked = locked_int;
// Clock Monitor clock assigning
//--------------------------------------
// Output buffering
//-----------------------------------
BUFG clkf_buf
(.O (clkfbout_buf_design_1_clk_wiz_0_0),
.I (clkfbout_design_1_clk_wiz_0_0));
BUFG clkout1_buf
(.O (clk_out1),
.I (clk_out1_design_1_clk_wiz_0_0));
BUFG clkout2_buf
(.O (clk_out2),
.I (clk_out2_design_1_clk_wiz_0_0));
endmodule

View File

@@ -0,0 +1,72 @@
// Template pour la lecture d'un fichier à la simulation et
// la génération de block rams à la synthèse
// https://docs.amd.com/r/en-US/ug901-vivado-synthesis/Byte-Write-Enable-True-Dual-Port-with-Byte-Wide-Write-Enable-Verilog
// True-Dual-Port BRAM with Byte-wide Write Enable
// Read-First mode
module dpram
#(
//--------------------------------------------------------------------------
parameter NUM_COL = 4,
parameter COL_WIDTH = 8,
parameter ADDR_WIDTH = 10,
// Addr Width in bits : 2 *ADDR_WIDTH = RAM Depth
parameter DATA_WIDTH = NUM_COL*COL_WIDTH, // Data Width in bits
parameter INIT_FILE = "program.mem"
//----------------------------------------------------------------------
) (
input clkA,
input enaA,
input [NUM_COL-1:0] weA,
input [ADDR_WIDTH-1:0] addrA,
input [DATA_WIDTH-1:0] dinA,
output reg [DATA_WIDTH-1:0] doutA,
input clkB,
input enaB,
input [NUM_COL-1:0] weB,
input [ADDR_WIDTH-1:0] addrB,
input [DATA_WIDTH-1:0] dinB,
output reg [DATA_WIDTH-1:0] doutB
);
// Core Memory
(* ram_style = "block" *) reg [DATA_WIDTH-1:0] ram_block [(2**ADDR_WIDTH)-1:0];
// Initialization
initial begin
if (INIT_FILE != "NONE") begin
$readmemh(INIT_FILE, ram_block);
end
end
integer i;
// Port-A Operation
always @ (posedge clkA) begin
if(enaA) begin
for(i=0;i<NUM_COL;i=i+1) begin
if(weA[i]) begin
ram_block[addrA][i*COL_WIDTH +: COL_WIDTH] <= dinA[i*COL_WIDTH +: COL_WIDTH];
end
end
doutA <= ram_block[addrA];
end
end
// Port-B Operation:
always @ (posedge clkB) begin
if(enaB) begin
for(i=0;i<NUM_COL;i=i+1) begin
if(weB[i]) begin
ram_block[addrB][i*COL_WIDTH +: COL_WIDTH] <= dinB[i*COL_WIDTH +: COL_WIDTH];
end
end
doutB <= ram_block[addrB];
end
end
endmodule // dpram

View File

@@ -0,0 +1,52 @@
package common
import chisel3._
import _root_.circt.stage.ChiselStage
object EmitModule {
def main(args: Array[String]): Unit = {
if (args.length < 1) {
println("Usage: mill ... common.EmitModule <ModuleName> [constructor args...]")
sys.exit(1)
}
val moduleName = args(0)
val ctorArgs = args.drop(1) // reste des arguments (strings)
// Reflection scala : charger dynamiquement un module
val cls = Class.forName(moduleName)
val ctor = cls.getConstructors.head
// Fonction de conversion string -> objet typé
def parseArg(s: String): AnyRef = {
// essai en Int
if (s.matches("^-?\\d+$")) {
Integer.valueOf(s.toInt)
}
// essai en Bool
else if (s.equalsIgnoreCase("true") || s.equalsIgnoreCase("false")) {
java.lang.Boolean.valueOf(s.toBoolean)
}
// sinon, on laisse en String
else {
s
}
}
val generator = () => {
if (ctorArgs.isEmpty) {
ctor.newInstance().asInstanceOf[RawModule]
} else {
val argsObjects: Array[Object] = ctorArgs.map(parseArg).toArray
ctor.newInstance(argsObjects: _*).asInstanceOf[RawModule]
}
}
ChiselStage.emitSystemVerilogFile(
gen = generator(),
firtoolOpts = Array("-disable-all-randomization", "-strip-debug-info"),
args = Array("--target-dir", s"RTL/$moduleName")
)
}
}

View File

@@ -0,0 +1,89 @@
package projet
import chisel3._
import chisel3.util._
// Bus très simple : au temps t le maître fait une requête
// au temps t+1, il a la réponse, sinon c'est mort !
// Signaux utilisés par le bus, coté esclave, on fait un
// Flip pour l'avoir coté maître ensuite
class BusInterface extends Bundle {
val addr = Input(UInt(32.W)) // adresse cible de la transaction
val wdata = Input(UInt(32.W)) // donnée en écriture (max 32 bits)
val rdata = Output(UInt(32.W)) // donnée lue de l'esclave, avec 1 cycle de retard
val be = Input(Vec(4, Bool())) // byte-enable pour les accès écriture mémoire
val en = Input(Bool()) // enable de la mémoire, read si be.orR est égal à 0, écriture sinon
}
class BusInterconnect extends Module {
val io = IO(new Bundle {
val master = new BusInterface // Bus avec un unique maître
val dmem = Flipped(new BusInterface) // et plusieurs esclaves
val ldip = Flipped(new BusInterface)
val clnt = Flipped(new BusInterface)
val vmem = Flipped(new BusInterface)
})
// On récupère l'adresse émise par le maître pour trouver à quel
// esclave elle fait référence
val addr = io.master.addr
// On reprend nos bonnes vielles adresses, pas très efficaces, mais ça fait le job
// -- Slave 0 1 2 3 4 5
// -- Name RAM prog | IP led | IP pin | IP PLIC | IP_CLINT | DDR
// BASE => ( X"0000_1000", X"3000_0000", X"3000_0008", X"0C00_0000", X"0200_0000", X"8000_0000"),
// HIGH => ( X"0000_8FFF", X"3000_0004", X"3000_0008", X"1000_0000", X"0200_C000", X"8FFF_FFFF")
val DMEM_BASE = 0x00000000L.U // Une mémoire unifiée code + données
val DMEM_SIZE = 0x00010000L.U
val LDIP_BASE = 0x30000000L.U
val LDIP_SIZE = 0x00000004L.U
val CLNT_BASE = 0x02000000L.U
val CLNT_SIZE = 0x0000C000L.U
val VMEM_BASE = 0x80000000L.U // Mémoire vidéo dual port
val VMEM_SIZE = 0x00020000L.U
val is_dmem = addr >= DMEM_BASE && addr < (DMEM_BASE + DMEM_SIZE)
val is_ldip = addr >= LDIP_BASE && addr < (LDIP_BASE + LDIP_SIZE)
val is_clnt = addr >= CLNT_BASE && addr < (CLNT_BASE + CLNT_SIZE)
val is_vmem = addr >= VMEM_BASE && addr < (VMEM_BASE + VMEM_SIZE)
// On connecte directement ce qui doit l'être en faisant l'hypothèse que
io.dmem.en := io.master.en && is_dmem
io.ldip.en := io.master.en && is_ldip
io.clnt.en := io.master.en && is_clnt
io.vmem.en := io.master.en && is_vmem
// la chose connectée à ses adresses qui commencent à zéro
io.dmem.addr := io.master.addr - DMEM_BASE
io.ldip.addr := io.master.addr - LDIP_BASE
io.clnt.addr := io.master.addr - CLNT_BASE
io.vmem.addr := io.master.addr - VMEM_BASE
io.dmem.wdata := io.master.wdata
io.ldip.wdata := io.master.wdata
io.clnt.wdata := io.master.wdata
io.vmem.wdata := io.master.wdata
io.dmem.be := io.master.be
io.ldip.be := io.master.be
io.clnt.be := io.master.be
io.vmem.be := io.master.be
// On doit décaler d'un cycle (latence de l'accès mémoire)
// les signaux qui multiplexent la donnée en sortie
val is_pdmem = RegNext(is_dmem)
val is_pldip = RegNext(is_ldip)
val is_pclnt = RegNext(is_clnt)
val is_pvmem = RegNext(is_vmem)
io.master.rdata := MuxCase(0x0BADADDL.U(32.W),
Seq(
is_pdmem -> io.dmem.rdata,
is_pldip -> io.ldip.rdata,
is_pclnt -> io.clnt.rdata,
is_pvmem -> io.vmem.rdata
)
)
}

View File

@@ -0,0 +1,56 @@
package projet
import chisel3._
/*\
* Principalement le timer pour pouvoir faire tourner space invaders !
\*/
class CLint extends Module {
val io = IO(new Bundle {
val bus = new BusInterface
val tip = Output(Bool())
})
// Adresses des deux registres 64 bits du composant
val MTIMERCMP_ADDR = 0x4000
val MTIMER_ADDR = 0xBFF8
// Sur le rv32i le timer est constitué de 2 registres pour faire 64 bits
val mtime_lo = RegInit(0.U(32.W))
val mtime_hi = RegInit(0.U(32.W))
// Free running, comme on dit chez nous !
mtime_lo := mtime_lo + 1.U
mtime_hi := Mux(mtime_lo + 1.U === 0.U, mtime_hi + 1.U, mtime_hi)
// La comparaison nécessite également 2 registres, pour une
// comparaison sur 64 bits si nécessaire
val mtimecmp_lo = RegInit(0.U(32.W))
val mtimecmp_hi = RegInit(0.U(32.W))
// Sortie mtimer interrupt pending dont on se moque pour l'instant
io.tip := (mtime_hi > mtimecmp_hi) || ((mtime_hi === mtimecmp_hi) && (mtime_lo >= mtimecmp_lo))
// Valeur par défaut sur le bus de lecture
// avec toujours un cycle de latence
val clintRegValue = RegInit(0.U(32.W))
io.bus.rdata := clintRegValue
when(io.bus.en === true.B) {
when (io.bus.be.asUInt.orR) { // Écriture
when(io.bus.addr === MTIMERCMP_ADDR.U) {
mtimecmp_lo := io.bus.wdata
} .elsewhen(io.bus.addr === (MTIMERCMP_ADDR + 4).U) {
mtimecmp_hi := io.bus.wdata
}
} .otherwise { // Lecture
when(io.bus.addr === MTIMER_ADDR.U) {
clintRegValue := mtime_lo
} .elsewhen(io.bus.addr === (MTIMER_ADDR + 4).U) {
clintRegValue := mtime_hi
} .elsewhen(io.bus.addr === MTIMERCMP_ADDR.U) {
clintRegValue := mtimecmp_lo
} .elsewhen(io.bus.addr === (MTIMERCMP_ADDR + 4).U) {
clintRegValue := mtimecmp_hi
}
}
}
}

View File

@@ -0,0 +1,64 @@
package projet
import chisel3._
import chisel3.util._
/*\
* Interface chisel du fichier généré par le clock_wizard
* de Vivado pour générer une clock à 10 MHz
* Le nom *doit* être celui qui est dans le systemverilog
* (qui est dans TP/common/clk_wiz_0_clk_wiz.sv)
\*/
class clk_wiz extends BlackBox with HasBlackBoxResource {
addResource("/vsrc/clk_wiz.v")
val io = IO(new Bundle {
val clk_in1 = Input(Clock())
val reset = Input(Reset())
val clk_out1 = Output(Clock()) // 10 MHz
val clk_out2 = Output(Clock()) // 125 MHz
val locked = Output(Bool())
})
}
class ClockGen(val divBy: Int = 10, useSim: Boolean = false) extends Module {
val io = IO(new Bundle {
val clk_in1 = Input(Clock())
val reset = Input(Reset())
val clk_out1 = Output(Clock())
val clk_out2 = Output(Clock())
val locked = Output(Bool())
})
val maxCnt = (divBy - 1).U
val midCnt = ((divBy / 2) - 1).U
if (useSim) {
val cntReg = RegInit(0.U(log2Ceil(divBy).W))
cntReg := cntReg + 1.U
val outReg = RegInit(false.B)
val rstReg = RegInit(true.B)
rstReg := Mux(io.reset.asBool, true.B, rstReg)
io.locked := !rstReg
when (cntReg === midCnt) {
outReg := true.B
} .elsewhen (cntReg === maxCnt) {
outReg := false.B
cntReg := 0.U
rstReg := false.B
}
io.clk_out1 := outReg.asClock
io.clk_out2 := io.clk_in1
} else {
val ckgen = Module(new clk_wiz)
ckgen.io.clk_in1 := io.clk_in1
ckgen.io.reset := io.reset
io.clk_out1 := ckgen.io.clk_out1
io.clk_out2 := ckgen.io.clk_out2
io.locked := ckgen.io.locked
}
}

View File

@@ -0,0 +1,35 @@
package projet
import chisel3._
// Déclaration d'un classe Compteur très générique
class Compteur(
n: Int, // Le paramètre n permet de fixer le nombre de bits du compteur.
hasEnable: Boolean = false, // Le paramètre hasEnable permet d'ajouter une entrée optionnelle pour autoriser l'incrémentation.
hasMax: Boolean = false // Le paramètre hasMax permet d'ajouter : - une entrée optionnelle max, qui fait reboucler le compteur après qu'il est atteint la valeur fournie.
// - une sortie optionnelle atMax, qui vaut 1 lorsque le compteur atteint la valeur max définie.
) extends Module {
val io = IO(new Bundle {
// Grâce au parametre hasEnable, on peut activer optionnellement le port d'entrée en.
// En scala, la classe option a 2 classes dérivées : Some et None.
// Some permet donc de wrapper notre type optionnel.
val en = if (hasEnable) Some(Input(Bool())) else None
val max = if (hasMax) Some(Input(UInt(n.W))) else None
val atMax = if (hasMax) Some(Output(Bool())) else None
val out = Output(UInt(n.W)) // On utilise le paramètre n pour remplacer la constante de l'exercice prédédent.
})
val compteur = RegInit(0.U(n.W))
// condition d'incrément
val doInc = if (hasEnable) io.en.get else true.B // Attention, notez qu'il faut utiliser l'opérateur get pour récupérer l'option wrappée par le Some.
// condition d'égalité au maximum
val atMax = if (hasMax) compteur === io.max.get else false.B
compteur := Mux(doInc, Mux(atMax, 0.U(n.W), compteur + 1.U), compteur)
if (hasMax) io.atMax.get := atMax
io.out := compteur
}

View File

@@ -0,0 +1,63 @@
package projet
import chisel3._
class GeneSync extends Module {
val io = IO(new Bundle {
val hsync = Output(Bool())
val vsync = Output(Bool())
val img = Output(Bool())
val x = Output(UInt(10.W))
val y = Output(UInt(9.W))
})
// Paramètres de synchronisation horizontale et verticale
val synchroX = VecInit(Seq(
95.U, // HS pulse width
47.U, // horizontal back porch
639.U, // horizontal display time
15.U // horizontal front porch
))
val synchroY = VecInit(Seq(
1.U, // VS pulse width
28.U, // vertical back porch
479.U, // vertical display time
9.U // vertical front porch
))
// Instanciation des compteurs
val compteurCLK = Module(new Compteur(3, hasMax = true)) // Compteur horloge (3 bits)
val comptX = Module(new Compteur(10, hasEnable = true, hasMax = true)) // Compteur horizontal (10 bits)
val comptY = Module(new Compteur(9, hasEnable = true, hasMax = true)) // Compteur vertical (9 bits)
val phaseH = Module(new Compteur(2, hasEnable = true)) // Phases horizontales (2 bits)
val phaseV = Module(new Compteur(2, hasEnable = true)) // Phases verticales (2 bits)
// Signaux d'activation
val en25 = compteurCLK.io.atMax.get // Activation à 25MHz quand compteurCLK atteint 4
val enPhaseH = en25 && comptX.io.atMax.get
val enY = enPhaseH && phaseH.io.out === 3.U // Nouvelle ligne
// Configuration compteur CLK (divise 125MHz par 5 pour avoir 25MHz)
compteurCLK.io.max.get := 4.U
// Configuration compteur X (horizontal)
comptX.io.en.get := en25
comptX.io.max.get := synchroX(phaseH.io.out)
// Configuration compteur Y (vertical)
comptY.io.en.get := enY
comptY.io.max.get := synchroY(phaseV.io.out)
// Configuration compteurs de phases
phaseH.io.en.get := enPhaseH
phaseV.io.en.get := enY && comptY.io.atMax.get
// Génération des signaux de sortie
io.hsync := phaseH.io.out.orR // true si phaseH != 0 (pas en phase pulse)
io.vsync := phaseV.io.out.orR // true si phaseV != 0 (pas en phase pulse)
io.img := (phaseH.io.out === 2.U) && (phaseV.io.out === 2.U) // Zone d'affichage
// Coordonnées pixel
io.x := comptX.io.out
io.y := comptY.io.out
}

View File

@@ -0,0 +1,164 @@
package projet
import chisel3._
import chisel3.util._
import chisel3.experimental._
/*
* Blackbox Verilog pour la simu et la synthèse sans placement
*/
class dpram( // nom identique à celui du verilog
val NUM_COL: Int,
val COL_WIDTH: Int,
val ADDR_WIDTH: Int,
val DATA_WIDTH: Int,
val INIT_FILE: String = "program.mem"
) extends BlackBox(Map(
"NUM_COL" -> IntParam(NUM_COL),
"COL_WIDTH" -> IntParam(COL_WIDTH),
"ADDR_WIDTH" -> IntParam(ADDR_WIDTH),
"DATA_WIDTH" -> IntParam(DATA_WIDTH),
"INIT_FILE" -> StringParam(if (INIT_FILE != "") INIT_FILE else "NONE")
)) with HasBlackBoxResource {
require(DATA_WIDTH == NUM_COL * COL_WIDTH, "DATA_WIDTH must be equal to NUM_COL*COL_WIDTH.")
addResource("/vsrc/dpram.v")
val io = IO(new Bundle {
// Port A
val clkA = Input(Clock())
val enaA = Input(Bool())
val addrA = Input(UInt(ADDR_WIDTH.W))
val doutA = Output(UInt(DATA_WIDTH.W))
val dinA = Input(UInt(DATA_WIDTH.W))
val weA = Input(UInt(NUM_COL.W))
// Port B
val clkB = Input(Clock())
val enaB = Input(Bool())
val addrB = Input(UInt(ADDR_WIDTH.W))
val doutB = Output(UInt(DATA_WIDTH.W))
val dinB = Input(UInt(DATA_WIDTH.W))
val weB = Input(UInt(NUM_COL.W))
})
}
class xpm_memory_tdpram( // nom identique à celui de Xilinx
val ADDR_WIDTH: Int,
val INIT_FILE: String
) extends BlackBox(Map(
"ADDR_WIDTH_A" -> IntParam(ADDR_WIDTH),
"ADDR_WIDTH_B" -> IntParam(ADDR_WIDTH),
"BYTE_WRITE_WIDTH_A" -> 8,
"MEMORY_INIT_FILE" -> StringParam(INIT_FILE),
"MEMORY_PRIMITIVE" -> "block",
"MEMORY_SIZE" -> 524288,
"READ_DATA_WIDTH_A" -> 32,
"READ_DATA_WIDTH_B" -> 32,
"READ_LATENCY_A" -> 1,
"READ_LATENCY_B" -> 1,
"WRITE_DATA_WIDTH_A" -> 32,
"WRITE_DATA_WIDTH_B" -> 32
)) with HasBlackBoxResource {
val io = IO(new Bundle {
// Port A
val clka = Input(Clock())
val rsta = Input(Bool())
val ena = Input(Bool())
val regcea = Input(Bool())
val dina = Input(UInt(32.W))
val addra = Input(UInt(ADDR_WIDTH.W))
val wea = Input(UInt(4.W))
val injectdbiterra = Input(Bool())
val injectsbiterra = Input(Bool())
val douta = Output(UInt(32.W))
// val dbiterrA = Output(Bool())
// val sbiterrA = Output(Bool())
// Port B
val clkb = Input(Clock())
val rstb = Input(Bool())
val enb = Input(Bool())
val regceb = Input(Bool())
val dinb = Input(UInt(32.W))
val addrb = Input(UInt(ADDR_WIDTH.W))
val web = Input(UInt(4.W))
val injectdbiterrb = Input(Bool())
val injectsbiterrb = Input(Bool())
val doutb = Output(UInt(32.W))
// val dbiterrB = Output(Bool())
// val sbiterrB = Output(Bool())
val sleep = Input(Bool())
})
}
/*\
* Attention, les BRAM sont adressables en mots, et les adresses émises
* sur notre interconnect sont des adresses octets, qui en revanche
* doivent être alignées sur le type de la donnée transportée.
* Nous optons ici pour une mémoire dual-port pour l'accès concurrent
* aux instructions et aux données, avec un espace d'adressage unique
* pour faciliter le préchargement (on espère).
*
\*/
class IDMem(sim: Boolean, addrWidth: Int, memFile: String ="") extends Module {
val io = IO(new Bundle {
val clki = Input(Clock())
val ibus = new BusInterface
val clkd = Input(Clock())
val dbus = new BusInterface
})
// Pour la simulation ou la synthèse ne nécessitant pas de repérer les BRAM, on blackboxe avec le verilog Xilinx
if (sim || memFile == "") {
// Mémoire 32 bits : 4 colonnes de 8 bit
// l'accès BRAM est un accès mot, et non octet, on ignore les 2 bits de
// poids faible de l'adresse
val unifiedRam = Module(new dpram(
NUM_COL = 4,
COL_WIDTH = 8,
ADDR_WIDTH = addrWidth - 2,
DATA_WIDTH = 32,
INIT_FILE = memFile
))
unifiedRam.io.clkA := io.clki
unifiedRam.io.clkB := io.clkd
// instructions sur le port a
unifiedRam.io.enaA := io.ibus.en
unifiedRam.io.addrA := io.ibus.addr(addrWidth - 1, 2)
io.ibus.rdata := unifiedRam.io.doutA
unifiedRam.io.weA := 0.U
unifiedRam.io.dinA := 0.U
// données sur le port b
unifiedRam.io.enaB := io.dbus.en
unifiedRam.io.addrB := io.dbus.addr(addrWidth - 1, 2)
io.dbus.rdata := unifiedRam.io.doutB
unifiedRam.io.weB := io.dbus.be.asUInt
unifiedRam.io.dinB := io.dbus.wdata
} else {
val dpram = Module(new xpm_memory_tdpram(ADDR_WIDTH = addrWidth - 2,INIT_FILE = memFile))
dpram.io.clka := clock
dpram.io.rsta := 0.U
dpram.io.ena := io.dbus.en
dpram.io.regcea := false.B
dpram.io.dina := io.dbus.wdata
dpram.io.addra := io.dbus.addr(addrWidth - 1, 2)
dpram.io.wea := io.dbus.be.asUInt
dpram.io.injectdbiterra := false.B
dpram.io.injectsbiterra := false.B
io.dbus.rdata := dpram.io.douta
dpram.io.clkb := clock
dpram.io.rstb := 0.U
dpram.io.enb := io.ibus.en
dpram.io.regceb := false.B
dpram.io.dinb := 0.U
dpram.io.addrb := io.ibus.addr(addrWidth - 1, 2)
dpram.io.web := 0.U
dpram.io.injectdbiterrb := false.B
dpram.io.injectsbiterrb := false.B
io.ibus.rdata := dpram.io.doutb
dpram.io.sleep := false.B
}
}

View File

@@ -0,0 +1,46 @@
package projet
import chisel3._
import chisel3.util._
/*\
* Fusion de IP_LED et IP_PIN du projet VHDL
\*/
class LedIp extends Module {
val io = IO(new Bundle {
val x31 = Input(UInt(32.W))
val switch = Input(UInt(4.W))
val led = Output(UInt(4.W))
val button = Input(UInt(3.W))
val bus = new BusInterface
})
io.bus.rdata := 0x01ed01ed.U // On lira ça si on lit !
val dinReg = RegInit(0.U(32.W))
val x31Reg = RegNext(io.x31) // On lit x31 en continue
val btnReg = Reg(UInt(32.W))
// Le cpu fait une requête
// On se moque de l'adresse ...
// L'écriture écrit dans le registre, la lecture retourne l'état des boutons
when (io.bus.en === true.B) {
when (io.bus.be.asUInt.orR) { // Écriture dans le registre des leds internes
dinReg := io.bus.wdata
} .otherwise { // Lecture des boutons poussoirs, pas de débounce car lus par le soft
btnReg := Fill(29, 0.U) ## io.button
}
}
io.bus.rdata := btnReg
// On échantillonne la donnée d'entrée en continue
// C'est soit une donnée venant du registre x31 du
// proc, si le dernier switch est à 1, soit le registre écrit par le soft
val data = Mux(io.switch(3).asBool, x31Reg, dinReg)
// On l'affiche
io.led := MuxLookup(io.switch(2, 0), 0.U(4.W))(
(0 until 8).map { i =>
(i.U -> data((i * 4) + 3, i * 4))
})
}

View File

@@ -0,0 +1,14 @@
package projet
import chisel3._
class Rv32i(sim: Boolean = true) extends Module {
val io = IO(new Bundle {
val ibus = Flipped(new BusInterface)
val dbus = Flipped(new BusInterface)
val x31 = Output(UInt(32.W))
val valid_x31 = if (sim) Some(Output(Bool())) else None
})
locally(sim)
}

View File

@@ -0,0 +1,40 @@
package projet
import chisel3._
import chisel3.util._
class VgaIp extends Module {
val io = IO(new Bundle {
val clk = Input(Clock())
val bus = Flipped(new BusInterface)
val hs = Output(Bool())
val vs = Output(Bool())
val r = Output(UInt(5.W))
val g = Output(UInt(6.W))
val b = Output(UInt(5.W))
})
withClock(io.clk) {
val sync = Module(new GeneSync())
io.hs := RegNext(sync.io.hsync)
io.vs := RegNext(sync.io.vsync)
val img_sync = RegNext(sync.io.img)
val index = RegNext(sync.io.x(2, 1))
io.bus.en := true.B
io.bus.be := VecInit(false.B, false.B, false.B, false.B)
io.bus.wdata := 0.U
val addr = 80.U * sync.io.y(8, 1) + sync.io.x(9, 3)
// Adresse mot dans la ram vidéo
io.bus.addr := addr(14, 0) ## 0.U(2.W)
val pixel = MuxCase(0.U,
(0 until 4).map {i =>
(index === i.U) -> io.bus.rdata(8 * (3 - i) + 7, 8 * (3 - i))
})
io.r := Mux(img_sync, pixel(7, 6)<<3, 0.U)
io.g := Mux(img_sync, pixel(5, 3)<<3, 0.U)
io.b := Mux(img_sync, pixel(2, 0)<<2, 0.U)
}
}

View File

@@ -0,0 +1,74 @@
package projet
import chisel3._
class ZzTop(file: String ="",sim: Boolean = true) extends Module {
val io = IO(new Bundle {
// Interface debug minimale
val switch = Input(UInt(4.W))
val led = Output(UInt(4.W))
val push = Input(UInt(3.W))
// Debug simu pour ne pas s'embeter avec le switch dans le testbench
val x31 = if (sim) Some(Output(UInt(32.W))) else None
val valid_x31 = if (sim) Some(Output(Bool())) else None
// Port vga
val hs = Output(Bool())
val vs = Output(Bool())
val r = Output(UInt(5.W))
val g = Output(UInt(6.W))
val b = Output(UInt(5.W))
})
val clkg = Module(new ClockGen(12, sim)) // ~ 10 MHz
// clock et reset sont les signaux implicites
clkg.io.clk_in1 := clock
clkg.io.reset := reset
val rst = (reset.asBool || !clkg.io.locked).asAsyncReset
// Ces deux IPs doivent tourner à 125 MHz
val vmem = Module(new IDMem(sim,17))
vmem.io.clki := clkg.io.clk_out2 // Clock du pixel
vmem.io.clkd := clkg.io.clk_out1 // Clock du reste du système
val vgad = Module(new VgaIp)
vgad.io.clk := clkg.io.clk_out2 // Clock pixel
withClockAndReset(clkg.io.clk_out1, rst) {
val interconnect = Module(new BusInterconnect)
val core = Module(new Rv32i(sim))
val dmem = Module(new IDMem(sim,16,file))
val ldip = Module(new LedIp)
val clnt = Module(new CLint)
dmem.io.clki := clkg.io.clk_out1 // Ou clock, non ?
dmem.io.clkd := clkg.io.clk_out1 //
ldip.io.switch := io.switch
ldip.io.button := io.push
ldip.io.x31 := core.io.x31
io.led := ldip.io.led
if (sim){
io.x31.get := core.io.x31
io.valid_x31.get := core.io.valid_x31.get
}
interconnect.io.master <> core.io.dbus
interconnect.io.dmem <> dmem.io.dbus
interconnect.io.ldip <> ldip.io.bus
interconnect.io.clnt <> clnt.io.bus
interconnect.io.vmem <> vmem.io.dbus
// connexion ad-hoc pour le bus d'instructions
dmem.io.ibus <> core.io.ibus
// connexion ad-hoc pour l'IP Vga
vmem.io.ibus <> vgad.io.bus
io.hs := vgad.io.hs
io.vs := vgad.io.vs
io.r := vgad.io.r
io.g := vgad.io.g
io.b := vgad.io.b
}
}

View File

@@ -0,0 +1,101 @@
package projet
import chisel3.simulator.scalatest.ChiselSim
import org.scalatest.freespec.AnyFreeSpec
import org.scalatest.matchers.must.Matchers
import org.scalatest.prop.TableDrivenPropertyChecks._
import java.nio.file.{Files, Paths}
import scala.jdk.CollectionConverters._
import scala.io.Source
import scala.collection.mutable.ListBuffer
class ZzTopSpec extends AnyFreeSpec with Matchers with ChiselSim {
def expectedValuesFromAsm(path: String): Seq[BigInt] = {
val line = Source.fromFile(path).getLines().find(_.startsWith("# expected:"))
line match {
case Some(l) =>
l.stripPrefix("# expected:")
.split(",")
.map(_.trim)
.filter(_.nonEmpty)
.map(str => BigInt(str, 16))
.toSeq
case None =>
Seq.empty
}
}
def failTrace(msg: String = "Erreur",trace: ListBuffer[String]): Unit = {
val prefix = s"🛑 $msg\n"
val traceStr =
if (trace.nonEmpty) "Trace:\n" + trace.map(" - " + _).mkString("\n") + "\n"
else ""
fail(prefix + traceStr)
}
def expectNextValue(dut: ZzTop, expected: BigInt, timeout: Int, trace: ListBuffer[String]): Unit = {
var cycles = 0
dut.clock.step(12) // Attente d'un cycle proc (12 cycles systeme)
// Attendre une écriture dans x31
while (!dut.io.valid_x31.get.peek().litToBoolean && cycles < timeout) {
dut.clock.step(12) // Attente d'un cycle proc
cycles += 1
trace += f"Attente $cycles cycles"
}
if (cycles < timeout){
// Vérifier la nouvelle valeur
val got = dut.io.x31.get.peek().litValue
trace += f"Valeur attendue = 0x$expected%08X, reçue = 0x$got%08X"
if ( got != expected ) failTrace("Mauvaise sortie",trace)
} else {
trace += f"Valeur attendue = 0x$expected%08X, reçue = ⏰"
failTrace("Simulation bloquée",trace)
}
}
// Fonction de test commune
def testWith(file: String): Unit = {
val path = file.replace("/mem/","/").replace(".mem",".s")
if (Files.exists(Paths.get(path))){
val expected = expectedValuesFromAsm(path)
simulate(new ZzTop(file,true)) { dut =>
val trace = ListBuffer[String]()
expected.foreach { value =>
expectNextValue(dut, value, timeout = 100,trace)
}
}
}
}
val dir = sys.env("PROOT")+"/bench/mem/"
val suffix = ".mem"
val programFile = sys.env.get("PROG")
if (programFile.isDefined) {
val name = programFile.get
val path = dir+name+suffix
"Simulation pour gtkwave" in {
simulate(new ZzTop(path,true)) { dut =>
dut.io.switch.poke(8) // Paramétrage de l'affichage
dut.io.led.expect(dut.io.led.peek()) // test dummy
dut.clock.step(sys.env("CYCLES").toInt)
}
}
s"Test unitaire $name" in testWith(path)
} else {
// Récupération des fichiers .mem et génération de la table de programmes
val programs = Files.list(Paths.get(dir)).iterator().asScala.filter(_.toString.endsWith(suffix))
val p2 = programs.map(path => (path.getFileName.toString.stripSuffix(suffix), path.toString)).toSeq
val table = Table(("name", "path"), p2: _*)
// Déclaration des tests à partir de la table
forAll(table) { (name, path) =>
s"Programme $name" in testWith(path)
}
}
}