Project 03 · Computer Architecture
A fully-pipelined, five-stage, in-order RV32I RISC-V core written in SystemVerilog. A dedicated hazard unit sustains near one-instruction-per-cycle throughput through data forwarding, load–use and write–decode stalls, and control-flow flushes — verified end-to-end against the full rv32-bmarks suite.
EECS 4201 · York University — Computer Architecture & Organization · final deliverable (PD5)
This is the culmination (PD5) of an incremental course project that builds a RISC-V processor across successive deliverables. It reuses the datapath blocks proven in the single-cycle design and transforms them into a genuine five-stage pipeline. The essential new engineering is not the arithmetic or decoding — those are inherited — but the machinery that makes a pipeline behave correctly under hazards: the pipeline registers that decouple the stages, the forwarding network that resolves most data dependencies with no penalty, the stall logic for the dependencies forwarding cannot cover, and the flush logic that discards wrongly-fetched instructions after control-flow changes.
Three classes of hazard can corrupt results in a pipeline. Structural hazards are avoided by design (separate instruction/data memory ports, and a register file that writes and reads in the same cycle). Data hazards are resolved by forwarding, with stalls for the rest. Control hazards are handled by fetching sequentially and flushing on taken branches and jumps.
The top-level pd5 module wires together the five stages, the two memory instances, the register file, the four pipeline registers, and the hazard unit. Solid paths are the normal per-cycle dataflow; the dashed feedback paths are forwarding from the EX/MEM (MX) and MEM/WB (WX) pipeline registers back into Execute.
| Stage | Module(s) | Responsibility |
|---|---|---|
| IF | fetch, memory (imem) | Hold/advance PC; read instruction word |
| ID | decode, igen, control, register_file | Extract fields; build immediate; generate control; read rs1/rs2 |
| EX | alu (execute), branch_control | Arithmetic/logic; address calc; branch decision |
| MEM | memory (dmem) | Load/store data-memory access |
| WB | writeback | Select ALU / memory / PC+4 result to write back |
0x01000000); advances by 4, holds during a stall, or loads a branch/jump target. Instruction word read from a read-only imem instance.
decode splits the fields, igen assembles the sign-extended immediate (I/S/B/U/J), and a combinational control decoder drives the datapath — with safe defaults so unknown opcodes are inert.
branch_control produces equal and less-than flags that resolve each branch type. Forwarded operands feed both the ALU and the comparator.
wbsel commits the ALU result (00), loaded data (01) or the return address PC+4 (10). A second instance of the block computes the next-PC target for Fetch.
All instructions are 32 bits wide and fall into six encoding formats distinguished by how the immediate is assembled. The opcode occupies the low seven bits in every format.
The ALU implements the full arithmetic, logic, shift and comparison repertoire, plus the upper-immediate operations:
| Class | regwren | memren | memwren | pcsel | wbsel | ALU op |
|---|---|---|---|---|---|---|
| R-type | 1 | 0 | 0 | 0 | 00 (ALU) | by funct3/funct7 |
| I-type | 1 | 0 | 0 | 0 | 00 (ALU) | by funct3 |
| Load | 1 | 1 | 0 | 0 | 01 (mem) | ADD (address) |
| Store | 0 | 0 | 1 | 0 | 00 | ADD (address) |
| Branch | 0 | 0 | 0 | 0 | 00 | SUB (compare) |
| jal / jalr | 1 | 0 | 0 | 1 | 10 (PC+4) | ADD (target) |
| lui | 1 | 0 | 0 | 0 | 00 | pass immediate |
| auipc | 1 | 0 | 0 | 0 | 00 | PC + immediate |
All four inter-stage registers live in one pipeline_registers module. Each latches the data and control its downstream stage needs, supports stalling via a write-enable, and resets to a defined idle state; the two front-end registers (IF/ID, ID/EX) additionally support flushing.
| Register | Carries (selected) |
|---|---|
| IF/ID | PC, fetched instruction word |
| ID/EX | PC, rs1/rs2 data & indices, rd, immediate, funct3/funct7, opcode, all control signals |
| EX/MEM | PC, ALU result, store data & rs2 index, rd, funct3, control, branch-taken flag |
| MEM/WB | PC, ALU result, loaded memory data, rd, regwren, wbsel |
A flush inserts a bubble — a benign no-op. In IF/ID a flush loads the canonical NOP encoding in place of the fetched instruction; in ID/EX it clears the memory and branch control signals so the squashed instruction touches nothing:
// IF/ID flush loads the canonical NOP:
0x00000013 // addi x0, x0, 0 → decodes to a harmless no-op
// ID/EX flush clears memren / memwren / branch controls
// so the bubbled instruction cannot access memory or alter control flow
The hazard unit is the defining contribution of this deliverable. It observes the register indices and control signals across ID, EX, MEM and WB and drives three families of output: forwarding selects, stall/write-enable signals, and flush signals.
A later stage produces a value an EX operand needs → bypass it directly into Execute. No penalty. MX (EX/MEM) has priority over WX (MEM/WB).
Load–use (data one cycle too late) or write–decode dependency → freeze PC & IF/ID, bubble ID/EX for exactly one cycle. It then becomes an ordinary forward.
Branch resolved taken, or jal/jalr in EX → squash the two wrongly-fetched instructions in IF/ID & ID/EX and redirect the PC to the target.
Most data dependencies are resolved without any stall by forwarding a result from a later pipeline stage straight into Execute, before it is written back. Each source operand has a 3:1 select — and crucially, the forwarded operands feed both the ALU and the branch comparator, so branches that depend on a just-computed value compare against the correct data.
| Hazard | Detection condition | Action |
|---|---|---|
| Forward MX | MEM stage writes a register (≠ x0) an EX operand needs | Bypass EX/MEM ALU result into EX |
| Forward WX | WB stage writes a register an EX operand needs (no MX match) | Bypass MEM/WB data into EX |
| Load–use | EX holds a load whose rd matches an ID source register | Stall 1 cycle + bubble |
| Write–decode | WB writes a register an ID source needs | Stall 1 cycle + bubble |
| Control | EX signals branch-taken, or EX opcode is jal/jalr | Flush IF/ID and ID/EX |
| Store forward (WM) | MEM holds a store whose rs2 equals the WB destination | Substitute WB data as store data |
Two cycle-by-cycle views of hazards resolving. In the first, back-to-back dependent instructions proceed with zero penalty thanks to forwarding, while a load–use dependency costs exactly one bubble.
In the second, a taken branch is resolved in Execute (c3). The two instructions already fetched onto the wrong path are squashed to bubbles, and the correct target is fetched from the next cycle.
The design was developed and verified on Linux, driven entirely from bash. The primary simulator was ModelSim (Intel FPGA / Quartus), with Verilator as an open-source alternative; waveforms were captured as VCD and inspected in gtkwave. Verification is anchored on probe signals exposed at every stage, compared each cycle against a golden reference so any mismatch pinpoints the exact diverging cycle and signal.
make compile -C verif/scripts/ VSIM=1
make run -C verif/scripts/ VSIM=1 TEST=<name>
make run -C verif/scripts/ VSIM=1 TEST=<name> PATTERN_CHECK=1
# a correct design reports "Checks passed"
Because the benchmarks are self-contained programs, the top level watches the instruction stream for termination:
ecall 0x00000073 // ends the simulation immediately
ret 0x00008067 // function test: end once x2 (sp) unwinds to top of memory
| Level | Count | Purpose |
|---|---|---|
| Directed pattern tests | 3 | Baseline datapath correctness via golden patterns |
| Pipeline micro-tests | 13 | One test per hazard path — each forward / stall / flush in isolation |
| rv32-bmarks — instruction | 38 | rv32ui-p-* ISA tests, one per RV32I instruction |
| rv32-bmarks — full programs | 10 | Complete C benchmarks (BubbleSort, Fibonacci, gcd, …) |
| Category | Tests | Result |
|---|---|---|
| Directed pattern tests | 3 | PASS |
| Pipeline hazard micro-tests | 13 | PASS |
| rv32-bmarks instruction tests | 38 | PASS |
| rv32-bmarks full-program benchmarks | 10 | PASS |
| Total | 64 | ALL PASS |
PD5 passes the complete verification suite. Waveform inspection confirmed the intended behaviour: forwarded operands arrive at Execute the same cycle they are needed; load–use and write–decode dependencies each cost exactly one bubble; and taken branches and jumps redirect the PC with the two mis-fetched instructions cleanly squashed.