DIGITAL DESIGN2025

RV32I RISC-V Processor

A processor, built from the spec by hand.

SystemVerilog · GTKWave · OSS-CAD Suite · Make

VIEW ON GITHUB  →

WHAT IT IS

A single-cycle RV32I processor in SystemVerilog, written from the instruction set specification. Decode, register file, ALU, branching and memory access, all mapped down to digital circuit design.

WHAT I BUILT

  • Instruction decode and register file, straight from the spec
  • ALU operations, branching, and byte, halfword and word memory sizing
  • A custom testbench with assembly and machine code test programs
  • Simulation toolchain on the OSS-CAD Suite, driven by Make

IN GTKWAVE

GTKWave with four signal groups open, annotated in red, tracing a store instruction through to the LED peripheral
a store, traced through to led_state

THE SIGNALS

The save file groups them by colour. Four groups, all open at once, because the bug was never in just one of them.

clk · program_counter · current_instruction
opcode · rs1 · rs2 · rd · funct3 · funct7
imm_i · imm_s · imm_b · imm_u · imm_j · alu_result
memory_data · data_write · data_write_done · register_write_done

THE BUGS

There were a lot of them. The worst was that PC-relative instructions ran twice. AUIPC, JAL, JALR and branches all got a second pass at the register file and the ALU, because the program counter moved before the instruction register caught up.

// ! - For some reason, the AUIPC instruction turns on PC enable
//     which makes pc_enable go high. Anyone else want to try debugging?
// ! - ...it will "happen twice" because it will still try to load in
//     the registers and do the ALU calculation when the program counter
//     changes, but before the instruction changes. This happens no matter
//     whether it's combinational or sequential.

left in instructions.sv while it was still unsolved

It reproduced identically whether the logic was combinational or sequential, which ruled out a whole class of causes. Control got restructured around an explicit pc_enable output and a fetch_done flag, with branches and jumps gated separately.

THE SHIFT BUG

Two faults stacked. Arithmetic right shift behaved as a logical shift, and shift_amount was being driven from two places at once.

shift_result = signed'(signed_rs1) >>> shift_amount;
// and shift_amount moved out of alu_top, passed in from decode

"something going on that's weird about shifting. unsure what it is"

THE TEST PROGRAM

Everything here is verified in simulation. These are the assembly programs the processor runs to prove each instruction group works. Every line carries the program counter, the expected register value and the encoding.

assembly/rv32i_test.s

lui   x1, 0xFEDCC     # x1  = 0xFEDCC000   fedcc0b7
addi  x1, x1, 0xA98   # x1  = 0xFEDCBA98   a9808093
srai  x3, x1, 4       # x3  = 0xFFEDCBA9   4040d193
add   x6, x5, x4      # x6  = 0x00123458   00428333
auipc x10, 0x12345    # x10 = 0x12345028   12345517
jal   x13, 0x28       # x13 = 0x00000038   028006ef
beq   x15, x0, 12     #                    00078663
jalr  x14, 0(x13)     # x14 = 0x00000060   00068767
sb    x17, -4(x0)     #                    ff100e23
lw    x18, -4(x0)     # x18 = 0xC0C0C0C0   ffc02903
lh    x20, -4(x0)     # x20 = 0xFFFFC0C0   ffc01a03
lb    x22, -4(x0)     # x22 = 0xFFFFFFC0   ffc00b03
lbu   x23, -4(x0)     # x23 = 0x000000C0   ffc04b83

WHERE IT LANDED

Verified in simulation, not deployed to hardware. An rv32m branch extends toward the M extension, where integer division works and multiply does not yet.