Skip to content

Latest commit

 

History

34 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

RISC-V_Processor

I built a 5-stage pipelined RISC-V Processor (32-bit) in SystemVerilog that supports the full RISC-V base integer ISA and Zifencei extension. This core was verified against the Spike ISA Simulator on the full rv32ui suite and 400+ constrained-random programs (0.4M instructions, 0 mismatches). Fmax = ~133 MHz targeting an Artix-7, measured in Vivado.

Screenshot 2026-08-05 155513 Screenshot 2026-08-05 155500 Screenshot 2026-08-05 152457

Features

This core includes:

  • 32-bit RISC-V CPU core
  • Parameterizable N-set associative I/D cache subsystems
  • Full hazard handling
  • Supports all RV32I base integer instructions
  • Supports self-modifying code through implementing the Zifencei extension
  • UVM verification in lockstep with the Spike ISA Simulator

Stage-by-Stage Analysis

In the sections below, I will cover each of the 5 stages (Fetch/Decode/Execute/Memory/Writeback) of the processor and highlight significant features and design decisions. This analysis mainly covers the differentiating design decisions of my processor, and does not address most standard features that are required of the RISC-V architecture. I recommend that you view the RTL for a full overview of the project, organized by stage.

Fetch

  • The instruction cache is parameterizable in several ways. It has N-set associativity, N index bits, and N word bits. Index bits determine how big the instruction cache is, while word bits determine how many instructions the cache fetches from the main memory at a time. The only caveat is that the top module passes the same word bits parameter to the instruction cache and data cache, as they have to match so that main memory only requires one-sized write port and infers BRAM.
  • The instruction cache includes an LRU eviction policy through an LRU matrix, so that instructions that were recently used, i.e. a looped branched sequence, stay in the cache while less-recently used instructions are evicted first. This decreases the amount of instruction cache misses for most programs, especially those with loops.
  • PC is fairly standard, as are the rest of the modules in fetch.

Decode

  • The ALU control unit and the forwarding unit were located in the decode stage so that they can run in parallel with the register file rather than in series with the ALU. This allows for greater efficiency and takes weight off of the ALU, a likely otherwise critical path.
  • The forwarding unit in decode generates the control signals for two muxes sitting at the start of the execute stage. Rather than calculate the control signals in series with execute and feed the muxes, we calculate them in decode so that all that happens in execute is choosing between the current data and forwarded data from the memory stage or writeback stage. If we encounter the load-use hazard, the unit stalls the fetch/decode register while bubbling the decode/execute register, then evaluates again so load can reach writeback.
  • We have a dedicated adder for calculating PC + immext and 1'b0 + immext. This is useful for lui, auipc, jal, and branches, which takes significant weight off of our ALU by generating the calculations for these instructions in parallel with the register file access. Furthermore, this shaves a clock cycle penalty off of JAL. Because we have the JAL signal and its target in decode, we can trigger it from decode rather than execute, generating only one unnecessary instruction rather than two.
  • We calculate the JALR target in execute and can therefore only trigger JALR from execute. However, because we have the JALR signal in decode, we know the next instruction that fetch generates will be flushed and is invalid. Therefore, I routed JALR from decode to fetch, telling the instruction cache to automatically feed a no-op to pc. This avoids any potential instruction cache misses on an instruction that will be flushed next cycle anyway.

Execute

  • The execute stage is fairly simple. It begins with two source muxes, rs1 and rs2 that choose between the current data from the decode stage and forwarded data from the memory and writeback stages, using the control signals generated by the forwarding unit in decode to choose between them.
  • It also features the main ALU unit to perform arithmetic, which feeds the branch evaluator to determine if branches are taken or not.

Memory

  • The data cache, similar to the instruction cache, has parameterizable N-set associativity, N index bits, N word bits, and LRU eviction. However, it also features write-back/write-allocate policies, so that every store doesn't result in a lengthy main memory access. This is a huge plus for nearly all programs, however, it comes with the downside of a fence.i penalty. Fence.i, rather than having already written back all dirty entries, has to scan all dirty bits for each entry and send them back to the main memory, one by one.
  • The forwarding unit in the memory stage is a dual-purpose module. It muxes every writeback value other than loads to be forwarded to the source muxes in execute. It also acts as a way to split the writeback mux logic, as the result of this is sent to writeback, and feeds a mux with loaded data to be sent to the register file.

Writeback

  • This is the simplest stage, we just send our data to the register file in decode and the forwarding muxes in execute.

Verification

This core was verified through a UVM testbench that runs in lockstep with the Spike ISA Simulator. For every valid retired instruction in writeback, Spike's commit log signals (pc, instruction, memory address, memory data, destination register, writeback data) are compared with those of the corresponding retired instruction, where UVM flags an error if any signals between Spike and the core aren't equal. This error marks the exact pc where the signals diverged or, if the pc diverged, it reports the order of the instruction.

Screenshot 2026-08-04 161936 Screenshot 2026-08-04 162003 Screenshot 2026-08-06 103311

UVM Verification Parts

All UVM components can be found in the testbench package. These include:

  • The RVFI item that carries all the signals needed for the RVFI comparison
  • The spike reference model that stores all of Spike's commit log signals in a queue ([$] data type)
  • Monitor/agent components that get the core's RVFI signals
  • A scoreboard component that compares the RVFI signals between the core and Spike
  • A env component that builds/connects all components
  • A program loader component that clears memory and loads the program into the main memory
  • A test that builds the env and program loader, then runs the testbench

Tests

  1. rv32ui (official directed suite): 41 programs that verify the entire base integer ISA, including the self-modifying-code fence.i test. Pass/fail is indicated by a store to a specific address labelled HALT_ADDR. Result: 41/41 pass, 0 mismatches

  2. Constrained-random tests: I wrote a SystemVerilog constrained-random generator that generates legal RV32I assembly under constraints that stress the core while preventing out-of-control programs (i.e. random jumps, infinite loops)

  • a small register pool (0-6) to force dependency density → exercises forwarding and load-use stalls;
  • loads/stores confined to an aligned data window via a reserved base register
  • forward-only, bounded branches/jumps so every program terminates
  • a weighted instruction mix biased toward loads/stores/branches
  • an initialization sequence at the start of each program so the core and Spike start from identical state
  • Every program is seeded, so any failure replays deterministically from its seed. Result: 400 programs × ~1000 instructions ≈ 0.4M instructions, 0 mismatches.

Recreate the Verification

Prerequisites: In a Linux/WSL environment, ensure you have Python 3, the RISC-V GNU toolchain (riscv64-unknown-elf-gcc), and the Spike ISA simulator installed. Run commands from the repository root.

  1. Directed rv32ui suite
  • Clone the rv32ui tests in git: git clone https://github.com/riscv-software-src/riscv-tests ~/riscv-tests

  • Build the tests in WSL/Linux: bash risc-v_RTL/Testbench/programs/build_suite.sh

  • Run the 41-program suite in Questa: do risc-v_RTL/Testbench/sim/run_suite.do

  1. Constrained-random tests
  • Generate the randomized tests in Questa (edit this file if you want to change the amount generated, the 400 programs in the file takes a long time): do risc-v_RTL/Testbench/sim/gen_randoms.do

  • Build the tests in WSL/Linux: bash risc-v_RTL/Testbench/programs/build_randoms.sh

  • Run the 400 constrained-random programs in Questa: do risc-v_RTL/Testbench/sim/run_randoms.do

About

I built a 5-stage pipelined RISC-V Processor in SystemVerilog.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages