I've written a single-cycled CPU in Chisel3 which implements most of the RV32I instructions (except CSR, Fence, ECALL/BREAK, LB/SB, which may be included later). The instructions are currently hard coded in the instruction memory, but I will change this so it reads instructions from a file instead. I've run into trouble with how to actually simulate my design. Here is the code where I've "glued" all components together.
class Core extends Module {
val io = IO(new Bundle {
val dc = Input(Bool())
})
io := DontCare
val pc = RegInit(0.U)
val pcSelect = Module(new PcSelect())
val pcPlusFour = Module(new Adder())
val alu = Module(new ALU())
val aluControl = Module(new AluControl())
val control = Module(new Control())
val immGen = Module(new ImmGen())
val branchLogic = Module(new BranchLogic())
val branchUnit = Module(new Adder())
val jumpReg = Module(new JumpReg())
val regFile = Module(new RegFile())
val jumpAdder = Module(new Adder())
val dataMem = Module(new DataMemory())
val instrMem = Module(new InstructionMemory())
// Mux from data memory
val dataMux = Mux(control.io.memToReg, dataMem.io.readDataOutput, alu.io.result)
// Mux to register file
val regFileMux = Mux(control.io.writeSrc, pcPlusFour.io.result, dataMux)
// PC + 4
pcPlusFour.io.in1 := pc
pcPlusFour.io.in2 := 4.U
// Instruction memory
instrMem.io.address := pc
val instruction = instrMem.io.instruction
val opcode = instruction(6, 0)
// Control
control.io.opcode := opcode
// Register file
regFile.io.readReg1 := instruction(19, 15) // rs1
regFile.io.readReg2 := instruction(24, 20) // rs2
regFile.io.writeReg := instruction(11, 7) // rd
regFile.io.regWrite := control.io.regWrite
regFile.io.writeData := regFileMux
// ALU
val aluMux1 = Mux(control.io.aluSrc1, immGen.io.extendedU, regFile.io.readData1)
alu.io.in1 := aluMux1
val src = control.io.aluSrc2
val aluMux2 = Mux(src === 1.U, immGen.io.extendedI, Mux(src === 2.U, immGen.io.extendedS, Mux(src === 3.U, pc, regFile.io.readData2)))
alu.io.in2 := aluMux2
alu.io.aluOp := aluControl.io.output
// ALU control
aluControl.io.aluOp := control.io.aluOp
aluControl.io.funct7 := instruction(31, 25)
aluControl.io.funct3 := instruction(14, 12)
// Data Memory
dataMem.io.readAddress := alu.io.result
dataMem.io.writeData := regFile.io.readData2
dataMem.io.memWrite := control.io.memWrite
dataMem.io.memRead := control.io.memRead
// Immediate generator
immGen.io.instr := instruction
// Branch logic
branchLogic.io.reg1 := regFile.io.readData1
branchLogic.io.reg2 := regFile.io.readData2
branchLogic.io.branch := control.io.branch
branchLogic.io.funct3 := instruction(14, 12)
// Jump reg
jumpReg.io.reg1 := regFile.io.readData1
jumpReg.io.imm := immGen.io.extendedI
// Jump
jumpAdder.io.in1 := pc
jumpAdder.io.in2 := immGen.io.extendedJ
// Branch
branchUnit.io.in1 := pc
branchUnit.io.in2 := immGen.io.extendedB
// PC-select
pcSelect.io.pcPlus4 := pcPlusFour.io.result
pcSelect.io.branch := branchUnit.io.result
pcSelect.io.jump := jumpAdder.io.result
pcSelect.io.jalr := jumpReg.io.output
pcSelect.io.branchSignal := branchLogic.io.result
pcSelect.io.jumpSignal := control.io.jump
pcSelect.io.jalrSignal := control.io.jumpReg
pc := pcSelect.io.output
}
So my questions are:
Thanks in advance!
Great question: there are a number of ways to approach this.
A common approach is to take the generated Verilog from Chisel and write your own test-harness to instantiate your design. This test-harness can be written in C++, Verilog, SystemVerilog, or your other favorite test-harness/glue language.
This approach is used by Sodor (https://github.com/ucb-bar/riscv-sodor) and Rocket-Chip (https://github.com/freechipsproject/rocket-chip), with the outermost test-harness code written in C++, but able to interface with Verilog simulators like Verilator and VCS. The C++ test logic allows the user to pass in the test binary through the command line and to then load the binary -- through some sort of "magic" -- into the test memory. This magic may be an External Debug Interface, a Tether Serial Interface, or it may be a provided external RAM model that can be loaded by the test-harness (either something simple you write yourself or something as complex as dramsim2).
That stuff is fairly complex, so I recommend starting simple; one option is to create a blackbox memory in Chisel that is backed by a simple memory that uses readmemh to initialize itself. The nice feature here is you don't need to recompile your code to run new binaries, you just swap out the file you want to load into the test memory.
Chisel also provides its own self-contained testers, so perhaps you could do your test-harness entirely within Scala, but I have not seen this done for something as complex as a core which is very reliant on external stimuli and a need to communicate with the outside world.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With