RISC-V CPU
The CPU is a single-issue, in-order, 5-stage RV32IM pipeline implemented in rtl/riscv_core.sv. It supports the full RV32I base ISA plus most of the M-extension: MUL/MULH via a 3-cycle interlocked multiplier, and DIV/DIVU/REM/REMU via a 34-cycle interlocked divider (rtl/alu.sv). MULHSU/MULHU are not yet implemented — see reference.md.
Pipeline Stages
Cycle: 1 2 3 4 5
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│ IF │─▶│ ID │─▶│ EX │─▶│ MEM │─▶│ WB │
└──────┘ └──────┘ └──────┘ └──────┘ └──────┘
Fetch Decode Execute Memory Writeback
Stage 1 — Fetch (IF)
- Drives
instr_addr = if_pcto the instruction SRAM insystem_top. if_pcincrements by 4 each cycle unless stalled (MUL busy, load-use) or redirected (branch taken).- The SRAM returns
instr_datacombinationally in the same cycle. instr_validis asserted whenif_pc < imem_wr_byte_addr(instruction has been loaded).
Stall / redirect priority (highest first):
1. MUL busy — hold if_pc, hold IF/ID register
2. Load-use hazard — hold if_pc, hold IF/ID register
3. Branch taken — redirect if_pc to branch_target, flush IF/ID
Stage 2 — Decode (ID)
All field extractions use assign wires (not slices inside always) for Icarus Verilog compatibility:
id_opcode = if_id_instr[6:0]
id_rd = if_id_instr[11:7]
id_funct3 = if_id_instr[14:12]
id_rs1 = if_id_instr[19:15]
id_rs2 = if_id_instr[24:20]
id_funct7 = if_id_instr[31:25]
Sub-modules instantiated here:
- imm_decode — produces sign-extended immediate for all RV32 formats
- register_file — reads rs1, rs2; write-back from WB stage feeds in here
Control signals decoded:
| Signal | Meaning |
|---|---|
id_alu_ctrl[3:0] |
ALU operation (see reference.md) |
id_alu_src |
0 = rs2 data, 1 = immediate to ALU B input |
id_reg_write |
1 = write result to rd at WB |
id_mem_read |
1 = load instruction |
id_mem_write_ctrl |
1 = store instruction |
id_wb_sel[1:0] |
Writeback mux select (see below) |
id_is_branch/jal/jalr |
Control-flow type |
id_is_mul |
MUL/MULH instruction — triggers 3-cycle stall |
Stage 3 — Execute (EX)
Forwarding unit resolves data hazards without stalls for ALU-to-ALU and ALU-to-store sequences:
fwd_a_sel:
2'b10 → ex_mem_alu_result (EX/MEM stage result)
2'b01 → wb_result (MEM/WB stage result)
2'b00 → id_ex_rs1_data (register file)
fwd_b_sel: same logic for rs2
ALU inputs:
ex_alu_a = (AUIPC) ? id_ex_pc : ex_fwd_a
ex_alu_b = (alu_src) ? id_ex_imm : ex_fwd_rs2
Branch resolution happens here (EX stage) — one-cycle penalty. The ALU op decoded for a branch depends on funct3: BEQ/BNE use ALU_SUB (they only need the zero flag), but BLT/BGE need a real signed compare and BLTU/BGEU a real unsigned one, so those select ALU_SLT/ALU_SLTU instead — alu_result[0] is only a valid stand-in for "a < b" when the ALU actually computed a comparison, not a subtraction:
| funct3 | Instruction | ALU op | Condition |
|---|---|---|---|
3'h0 |
BEQ | ALU_SUB |
zero == 1 |
3'h1 |
BNE | ALU_SUB |
zero == 0 |
3'h4 |
BLT | ALU_SLT |
alu_result[0] == 1 |
3'h5 |
BGE | ALU_SLT |
alu_result[0] == 0 |
3'h6 |
BLTU | ALU_SLTU |
alu_result[0] == 1 |
3'h7 |
BGEU | ALU_SLTU |
alu_result[0] == 0 |
JAL and JALR also resolve in EX, setting branch_taken and branch_target. flush_if_id is asserted for 1 cycle, which does two things on that same edge: turns IF/ID's next value into a NOP, and makes the ID/EX register insert a bubble instead of latching IF/ID's current (about-to-be-flushed) content — both are needed, since without the second one the instruction sitting in IF/ID at resolution time (fetched down the not-taken path) would otherwise advance into EX and execute for one extra cycle before being caught.
MUL stall protocol:
1. mul_start fires once (gated by mul_fired) when a MUL/MULH reaches EX.
2. ALU asserts mul_busy for 3 cycles while its internal counter runs.
3. All stages freeze (if_pc holds, pipeline registers hold).
4. mul_done = mul_was_busy & !mul_busy — one-cycle pulse when result is ready.
5. On mul_done, EX/MEM captures the fresh ALU result and pipeline resumes.
Stage 4 — Memory (MEM)
- Stores:
mem_write,mem_byte_en,mem_addr,mem_wdatadriven tosystem_topexternal memory interface. - Byte enable encoding from funct3:
| funct3[1:0] | Instruction | mem_byte_en |
|---|---|---|
2'b00 |
SB | 0001 / 0010 / 0100 / 1000 (by addr[1:0]) |
2'b01 |
SH | 0011 / 1100 (by addr[1]) |
2'b10 |
SW | 1111 |
- Load sign extension from funct3:
| funct3 | Instruction | Extension |
|---|---|---|
3'h0 |
LB | sign-extend byte |
3'h1 |
LH | sign-extend halfword |
3'h2 |
LW | no extension |
3'h4 |
LBU | zero-extend byte |
3'h5 |
LHU | zero-extend halfword |
Stage 5 — Writeback (WB)
4-way mux selects what is written back to rd:
wb_sel[1:0] |
Source | Used by |
|---|---|---|
2'b00 |
alu_result |
ALU-reg, ALU-imm, AUIPC, JALR |
2'b01 |
mem_rdata (after load extension) |
LW/LH/LB/LHU/LBU |
2'b10 |
pc + 4 |
JAL, JALR (link address) |
2'b11 |
imm_ext |
LUI (upper immediate, no ALU) |
Hazard Handling Summary
| Hazard | Detection | Resolution |
|---|---|---|
| Load-use | id_ex_mem_read && (id_ex_rd == id_rs1 \|\| id_ex_rs2) |
Stall 1 cycle, insert EX bubble |
| RAW (ALU→ALU) | Forwarding unit compares rd vs rs1/rs2 | Forward from EX/MEM or MEM/WB |
| RAW (ALU→store) | Same forwarding unit, covers rs2 | Forward from EX/MEM or MEM/WB |
| Branch | Branch taken signal from EX | Flush IF/ID (1-cycle penalty) |
| MUL (3-cycle) | mul_busy from ALU |
Freeze all stages for 3 cycles |
ALU — rtl/alu.sv
Supports 12 operations. Base arithmetic resolves combinationally in 1 cycle. Multiply uses a 3-cycle counter.
alu_control[3:0] |
Operation | Notes |
|---|---|---|
4'd0 ALU_ADD |
a + b |
|
4'd1 ALU_SUB |
a - b |
also drives zero flag for BEQ/BNE |
4'd2 ALU_AND |
a & b |
|
4'd3 ALU_OR |
a \| b |
|
4'd4 ALU_XOR |
a ^ b |
|
4'd5 ALU_SLT |
$signed(a) < $signed(b) |
1 or 0 |
4'd6 ALU_SLTU |
a < b (unsigned) |
1 or 0 |
4'd7 ALU_SLL |
a << b[4:0] |
|
4'd8 ALU_SRL |
a >> b[4:0] (logical) |
|
4'd9 ALU_SRA |
a >>> b[4:0] (arithmetic) |
|
4'd10 ALU_MUL |
a × b lower 32 bits |
3-cycle stall |
4'd11 ALU_MULH |
a × b upper 32 bits |
3-cycle stall, signed |
Output signals:
- result — the computed value (or MUL result captured at mul_done)
- zero — result == 0 (used by branch logic in EX)
- math_busy — high for 3 cycles during MUL; drives core_stall
Immediate Decode — rtl/imm_decode.sv
Extracts and sign-extends immediates for all 5 RV32 formats:
| Format | Instruction class | Bit layout |
|---|---|---|
| I-type | ADDI, LW, JALR, shifts | {sign[11], instr[30:20]} |
| S-type | SW, SH, SB | {sign[11], instr[30:25], instr[11:7]} |
| B-type | BEQ, BNE, BLT, BGE… | {sign[12], instr[7], instr[30:25], instr[11:8], 1'b0} |
| U-type | LUI, AUIPC | {instr[31:12], 12'b0} |
| J-type | JAL | {sign[20], instr[19:12], instr[20], instr[30:21], 1'b0} |
All field extractions are assign wires — no bit-slicing inside always blocks (Icarus Verilog compat).
Register File — rtl/register_file.sv
- 31 × 32-bit physical registers (
rf[31:1]);x0is never stored — reads always return 0. - Asynchronous read with bypass forwarding: if
reg_write && rd == rs1, returnswdimmediately without waiting for the clock edge (write-then-read in the same cycle). - Synchronous write on
posedge clkwhenreg_write && rd != 0. - WB stage drives
reg_write,rd, andwd; ID stage readsrs1,rs2.