Skip to content

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_pc to the instruction SRAM in system_top.
  • if_pc increments by 4 each cycle unless stalled (MUL busy, load-use) or redirected (branch taken).
  • The SRAM returns instr_data combinationally in the same cycle.
  • instr_valid is asserted when if_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_wdata driven to system_top external 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) - zeroresult == 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]); x0 is never stored — reads always return 0.
  • Asynchronous read with bypass forwarding: if reg_write && rd == rs1, returns wd immediately without waiting for the clock edge (write-then-read in the same cycle).
  • Synchronous write on posedge clk when reg_write && rd != 0.
  • WB stage drives reg_write, rd, and wd; ID stage reads rs1, rs2.