Level 58: Assembling the RV32I Core
Task
Goal: assemble the single-cycle core of the educational RV32I subset from the blocks of the previous levels. Seven editable files: top.v (glue, 3 muxes, address decode), control.v (control table in structural gates), decoder.v (field slicing), immgen.v (I/S/B/U/J), imem.v (4×ROM lines), mmio.v (LED/switches), err_unit.v (the single error point, err[3:0] output). The other 21 files are read-only golden copies of waves 1–4. The check is differential: your fib and isqrt(100)/isqrt(50) must match the reference core bitwise, and the negatives (lw at 0x1002/0x1003, the raw word 0) must produce the right err code with a frozen state. Manual debugging — the runner bar: Step/Run by cycles, fib is preloaded into ROM, switches and LEDs live at 0xFFFF_F004/0xFFFF_F000.
The integration level: the blocks of the previous waves come together into a single-cycle core of the educational RV32I subset. Seven editable boilerplate files — skeletons with ports and comments:
- top.v — the glue: the datapath PC → IMEM → decode → regfile → ALU → DMEM/MMIO → WB, three muxes (ALUSrc, WB, next-PC) and the address decode;
- control.v — the control table in structural gates: a one-hot decode of the eight opcode classes (LUI, OP, OP-IMM, LOAD, STORE, BRANCH, JAL, JALR; anything else — illegal) and the aluOp codes 0..9;
- decoder.v — slicing the instruction fields;
- immgen.v — the immediate by format: I and B via the read-only
SignExt32andBImm32instances, S/U/J by hand; - imem.v — 64 words on four builtin
ROMlines: line-k addr ={pc[7:2], 2'bK}, word bytes little-endian; - mmio.v — LEDs (write-only,
0xFFFF_F000) and switches (read-only,0xFFFF_F004, upper bits zero); - err_unit.v — the single error point with the
err[3:0]output.
The other 21 files are read-only golden copies of waves 1–4 (regfile, alu, dmem, signext, the eight I-modules, add32, bimm, pc). Name the RegFile32 instance rf and Dmem32 — mem: the platform uses these names to show the final register and memory state.
err[3:0] — the error code of the current instruction: 0 — none; 1 — ERR_DATA_ADDRESS_MISALIGNED; 2 — ERR_ILLEGAL_INSTRUCTION; 3 — ERR_IMEM_ADDRESS_OUT_OF_RANGE; 4 — ERR_DATA_ADDRESS_OUT_OF_RANGE; 5 — ERR_IMEM_DATA_READ; 6 — ERR_READONLY_WRITE; 7 — ERR_WRITEONLY_READ. The addr[1:0] check lives at the CPU→DMEM boundary before address truncation — in err_unit/top, not inside dmem.v: the memory module only sees addr[9:0] and physically cannot check the low bits. When err ≠ 0, the write signals are gated (RegWrite, MemWrite) and next-PC holds its value — the clock itself is never gated: clk must run directly from the port.
The check is differential. The platform runs six programs through your schematic and compares the final state bitwise against the reference risc-run core: fib (64 cycles), isqrt(100) (65), isqrt(50) (47), the negatives lw 0x1002 and lw 0x1003 (err = 1, a frozen state without side effects), and the raw word 0x00000000 in IMEM (err = 2 at PC = 0).
For manual debugging there is the runner bar: fib is preloaded into ROM, the Step/Run buttons walk the cycles, and the LEDs and switches live at the MMIO addresses.
Solution for self-checking
Assemble the datapath yourself first. The solution below is a checklist.
What you should get: the differential check passes bitwise: fib (64 cycles), isqrt(100) (65), isqrt(50) (47); the negatives give the right err codes with a frozen state — lw at 0x1002 yields err = 1, the raw word 0 — err = 2 at PC = 0.
Step 1 — the datapath in order. PC → IMEM → decoder → RegFile → ALU → DMEM/MMIO → write back to RegFile. Every arrow is wires; every fork is a mux. Build order: decoder → control → immgen → imem → mmio → err_unit, then top.v.
Step 2 — three muxes. ALUSrc: the second ALU operand is rs2 or imm (for branch it is rs2 with ALUOp = sub, so zero is computed from the difference). WB: what goes into rd — memRd, pc+4 (jal/jalr), imm (lui), or the ALU/OP-IMM result. next-PC: priority jalr > jal > branch > +4; at err ≠ 0 the PC value holds.
Step 3 — control. A table by classes: sw has no rd, lw has no memory write, branches have no result. cond = zero ^ funct3[0] — beq is taken on zero, bne on not(zero). Writes to x0 are masked by the level-49 RegFile decoder.
Step 4 — err_unit. Flags per the artifact 2 §5 catalog, then a priority encoder: code 1 has the highest priority. IMEM ends at 256 bytes (64 words) — pc[31:8] ≠ 0 gives code 3. The data window ends at 0x107F; the IMEM window 0x000–0x3FF read as data gives code 5.
Step 5 — acceptance. fib needs beq/add/addi; isqrt adds slt/sub and bne. The negatives need an honest err_unit: lw at 0x1002 — err = 1 at PC = 8, registers and memory unchanged; the word 0 — err = 2 at PC = 0. A bitwise match with the golden means you assembled a machine, not just a schematic.
Check yourself
Why is the clock never gated on an error?
With err ≠ 0 only the write signals are gated (RegWrite, MemWrite) and next-PC holds the PC value. clk runs from the port directly: freezing the state must never break the clocking — otherwise the machine could not continue after the fault is analysed.