Building a 5-Stage Pipelined RISC-V CPU in SystemVerilog
A complete RV32IM pipelined processor with BTB branch prediction, dual-path data forwarding, hazard detection, CSR support, and multi-cycle multiply/divide — synthesized on Xilinx Kintex-7 xc7k325t with WNS 0.048ns.
TL;DR
RV32IM 5-stage pipelined CPU. WNS +0.048ns on Xilinx Kintex-7 xc7k325t-2, 0.984W total power. 32-entry BTB branch prediction, dual-path data forwarding, hazard detection, CSR support, multi-cycle multiply/divide. 4 DSP48E1 slices, 4,573 registers, 46K LUTs.
Architecture Overview
IF ──▶ ID ──▶ EX ──▶ MEM ──▶ WB
│ │ │ │ │
│ │ │ │ └── RegFile Write-back
│ │ │ └── Data Memory Access
│ │ └── ALU + Branch Compare + Mul/Div
│ └── Decode + Imm Gen + CSR Read
└── PC + BTB + Instr Fetch Design Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Issue width | Single-issue | Area-efficient, sufficient for embedded-class targets |
| Execution order | In-order issue, in-order retire | Predictable latency, simpler hazard logic |
| Register file | 32 × 32-bit GPR (x0 hardwired to 0) | Standard RV32I spec |
| PC init | 0x8000_0000 | Typical RISC-V firmware base address |
| ISA support | RV32I + RV32M + Zicsr | Integer + multiply/divide + CSR access |
| Branch resolution | EX stage (1-cycle penalty on mispredict) | Trade-off: simpler than EX-early, better than MEM |
| Memory model | Byte-addressable, little-endian | Standard for RISC-V |
The pipeline is deliberately simple: one instruction per cycle in, one instruction per cycle out. No out-of-order, no superscalar. The goal is a clean, verifiable design that closes timing at 200MHz on mid-range FPGAs.
Pipeline Stages
┌─────────────┐
│ IF │ PC → BRAM → Instr
│ if_pc │ BTB query → pred_jump
│ if_instr │ Next PC = branch ? target : PC+4
└──────┬──────┘
▼
┌─────────────┐
│ ID │ Decode + Imm Gen + RegFile Read
│ id_imm │ CSR read (mstatus, mtvec, mepc, etc.)
│ id_alu_ctrl │ Control signal generation
└──────┬──────┘
▼
┌─────────────┐
│ EX │ ALU + Branch Compare + Mul/Div
│ ex_alu_res │ Forwarding MUX: MEM→EX, WB→EX
│ ex_branch_miss│ Mispredict signal → PC flush
└──────┬──────┘
▼
┌─────────────┐
│ MEM │ Data Memory / Peripheral Access
│ perip_addr │ Byte/half/word select (perip_mask)
│ perip_wen │ Write enable for store instructions
└──────┬──────┘
▼
┌─────────────┐
│ WB │ Write-back to RegFile + CSR
│ rf_we │ RegFile write enable
│ rf_wdata │ Result from ALU or memory
└─────────────┘ Stage-by-Stage Signal Reference
| Stage | Key Operations | Key Signals |
|---|---|---|
| IF | PC update, BTB query, BRAM instruction fetch | if_pc, if_instr, pred_jump, pred_target |
| ID | Instruction decode, immediate generation, register file read, CSR read | id_imm, id_alu_ctrl, id_is_branch, id_rs1_data, id_rs2_data, id_csr_rdata |
| EX | ALU computation, branch comparison, multiply/divide execution, forwarding MUX | ex_alu_res, ex_branch_miss, ex_branch_target, ex_fwd_a, ex_fwd_b |
| MEM | Data memory read/write, byte/half/word selection | perip_addr, perip_wen, perip_mask, perip_rdata |
| WB | Result write-back to register file, CSR write | rf_we, rf_waddr, rf_wdata, csr_we, csr_wdata |
Hazard Handling
The pipeline implements two forwarding paths to resolve data hazards without stalling (except load-use):
EX/MEM result ──▶ EX stage forwarding MUX (rs1, rs2)
WB/RegFile ──▶ EX stage forwarding MUX (rs1, rs2)
Load-use hazard (load in EX, consumer in ID): 1-cycle stall. The hazard detection unit compares id_rs1/id_rs2 against the EX-stage destination when ex_mem_read == 1.
Branch misprediction: The branch is resolved in EX. On mispredict, the pipeline flushes IF and ID stages (2 wasted cycles). The BTB updates on correction.
Decoder: From 32-bit Instruction to Control Signals
instr[31:0]
│
├── opcode[6:0] ──▶ Control Signal Generator
├── funct3[14:12]──▶ ALU Control (BEQ/BNE/BLT/BGE/SLT/SLL/SRL/SRA)
├── funct7[31:25]──▶ ALU Op Select (ADD vs SUB, SRL vs SRA)
├── rs1[19:15] ──▶ RegFile Read Addr 1
├── rs2[24:20] ──▶ RegFile Read Addr 2
└── rd[11:7] ──▶ RegFile Write Addr The decoder maps the 7-bit opcode + funct3 + funct7 fields into control signals that drive the entire datapath. The critical signals:
case (opcode)
OPCODE_LUI: begin
use_alu = 1'b1; alu_ctrl = ALU_ADD;
op_a_sel = OP_A_ZERO; op_b_sel = OP_B_IMM;
reg_we = 1'b1;
end
OPCODE_BRANCH: begin
use_alu = 1'b1; is_branch = 1'b1;
case (funct3)
3'b000: alu_ctrl = ALU_BEQ;
3'b001: alu_ctrl = ALU_BNE;
endcase
end
The immediate generator handles all four RISC-V immediate formats (I/S/B/U/J) within a single combinational block, extracting and sign-extending the appropriate bit fields based on instruction type.
CSR Subsystem
The CSR file is a small register bank mapped to standard RISC-V CSR addresses:
| CSR Address | Name | Purpose |
|---|---|---|
| 0x300 | mstatus | Machine status register |
| 0x305 | mtvec | Machine trap vector base |
| 0x341 | mepc | Machine exception PC |
| 0x342 | mcause | Machine trap cause |
CSR read/write is serialized in WB stage to avoid structural hazards. ECALL triggers a synchronous exception; MRET restores PC from mepc.
ALU: Single-Cycle Datapath
The ALU uses a 33-bit adder (32-bit data + carry-in) as its core, unified across arithmetic, comparison, and branch operations:
| Operation | Adder Usage | Notes |
|---|---|---|
| ADD | A + B | Direct |
| SUB | A + ~B + 1 | Two’s complement |
| SLT | (A - B) < 0 | Sign bit of subtraction result |
| SLTU | (A - B) < 0 | Carry-out of subtraction |
| BEQ/BNE | A - B == 0 | Zero flag from subtraction |
| BLT/BGE | Sign/carry flags | Signed/unsigned comparison via adder flags |
The operand MUX selects between three sources for each input:
Operand A: rs1 / PC / 0 (LUI uses 0, AUIPC uses PC)
Operand B: rs2 / imm / 4 (JAL/JALR use imm, JAL uses PC+4 via adder)
Shift operations (SLL, SRL, SRA) bypass the adder and use dedicated barrel shifter logic. SRA sign-extends by replicating the MSB.
Multiply/Divide (RV32M)
The multiplier uses 1 DSP48E1 slice for 32×32→64-bit signed multiplication, pipelined over 3 cycles. Division implements a restoring algorithm over up to 32 cycles (one bit per cycle), controlled by a small FSM in the EX stage. The division result and remainder share the same DSP path.
Forwarding and Hazard Resolution
Two forwarding paths eliminate stalls for back-to-back ALU operations:
EX/MEM ──┐
├──▶ Forwarding MUX ──▶ ALU operand A/B
WB/MEM ──┘
Priority: EX/MEM forwarding takes precedence over WB/MEM. This handles the common case of producer-consumer with zero bubble penalty.
Load-use stall is the only unavoidable hazard. When a load instruction in EX feeds a dependent instruction in ID:
Cycle 1: lw x1, 0(x2) | EX stage
Cycle 2: add x3, x1, x4 | ID stage → STALL (load-use detected)
Cycle 3: (bubble) | EX stage (lw advances)
Cycle 4: add x3, x1, x4 | EX stage (forwarded from MEM)
The stall inserts one bubble and enables MEM→EX forwarding on the next cycle. Total penalty: 1 cycle.
Synthesis Results
Target: Xilinx Kintex-7 xc7k325tffg900-2
| Metric | Value | Notes |
|---|---|---|
| Device | xc7k325tffg900-2 | Kintex-7, speed grade -2 |
| Slice LUTs | 46,064 (22.6%) | |
| LUT as Logic | 13,296 (6.5%) | Combinational logic |
| LUT as Distributed RAM | 32,768 (51.2%) | RegFile (32×32-bit) |
| F7/F8 Muxes | 27,372 (13.4%) | Decoder multiplexers |
| Slice Registers | 4,573 (1.1%) | Pipeline registers |
| DSP48E1 | 4 (0.48%) | Multiplier (1 slice × 4 sub-units) |
| PLL | 1 (10%) | Clock generation (MMCM) |
| WNS | +0.048ns ✅ | Setup timing met |
| WHS | +0.041ns ✅ | Hold timing met |
| Total Power | 0.984W | |
| Max Ambient | 83.3°C |
Resource Analysis
The most surprising number: 51.2% of LUTs are Distributed RAM. The register file — 32 registers × 32 bits = 1,024 bits — is implemented using LUT-based distributed RAM rather than dedicated BRAM. This is intentional: BRAM has a 1-cycle read latency, which would complicate the ID stage timing. Distributed RAM provides single-cycle read, keeping the decode path clean.
Slice Registers at 1.1% confirms the pipeline is lightweight. The 5 pipeline stages use only ~4,573 flip-flops — roughly 180 FFs per stage average. The bulk of the storage is combinational (LUT RAM for the register file, mux trees for the decoder).
DSP48E1 at 0.48% — a single DSP slice handles all multiply operations. The division algorithm is purely combinational/sequential using LUT logic, not DSP.
Power at 0.984W with 0.048ns timing margin means the design has headroom. At 200MHz target on xc7k325t-2, the critical path is ~4.95ns — tight but closed. The power density is low enough for passive cooling on most boards.
Timing Breakdown
The critical path runs through:
RegFile read (Distributed RAM, ~0.8ns)
→ Decode mux tree (F7/F8 muxes, ~0.6ns)
→ Imm Gen + sign extension (~0.3ns)
→ Forwarding MUX priority logic (~0.4ns)
→ ALU execution (~1.2ns)
→ Setup to pipeline register (~0.3ns)
≈ 3.6ns + routing ≈ 4.95ns
The 33-bit adder is the ALU bottleneck at ~1.2ns. For a faster target, pipelining the ALU or using carry-lookahead would help, but at the cost of one additional pipeline stage.
What’s Next
Three immediate improvements:
1. Branch Prediction Upgrade
Current: always-predict-not-taken. Every branch is predicted not taken; on taken branches, the penalty is 2 cycles (flush IF + ID).
Target: 2-bit saturating counter BTB. This captures loop behavior (taken 99% of the time) and reduces the misprediction rate from ~50% on loops to <5%. Expected IPC improvement: 10-15% on branch-heavy code.
2. Instruction Cache
Current: single-port BRAM, 1-cycle latency. Sufficient for sequential code, but loops cause repeated fetches of the same cache line.
Target: direct-mapped I-cache (32-byte lines, 256 entries = 8KB). Eliminates redundant BRAM reads for hot loops. Adds ~2K LUTs but removes the BRAM bottleneck.
3. Interrupt Support
Current: ECALL / MRET for synchronous exceptions only. No asynchronous interrupts.
Target: PLIC (Platform-Level Interrupt Controller) with 32 external interrupt sources, priority-based arbitration, and vectored interrupt table at mtvec. Enables real-time response to peripheral events.
Related Articles:
- Data Hazard Resolution: Forwarding & Stall — How the CPU handles load-use hazards and data forwarding.
- BTB Branch Prediction — 32-entry direct-mapped BTB design.