Back to all projects

Sep 2025 – Dec 2025

EE

Last edited

Four Stages, Two ISAs: A Pipelined RV32IF Core on PYNQ-Z1 FPGA

This project implements a pipelined RISC-V SoC on the Digilent PYNQ-Z1 FPGA. The core is RV32I with CSR support, paired with a pipelined RV32F floating-point unit.

The four-stage in-order pipeline includes hazard detection, data forwarding, and precise control-flow handling, covering end-to-end hardware-software co-design from RTL to a processor that boots and runs C benchmarks.

The system was verified using the official RISC-V ISA tests and end-to-end workloads executed directly on FPGA hardware. Final numbers: 58 MHz operating frequency, ~1.16 integer CPI, ~1.83 floating-point CPI, and an FOM of 12.3.

Developed within a UC Berkeley hardware course supported by Apple's New Silicon Initiative and co-taught with industry researchers from NVIDIA.

Affiliation

UC Berkeley

Partners

Keywords

  • Verilog
  • Xilinx Vivado
  • RISC-V
  • FPGA
  • PYNQ-Z1
  • RTL Design
  • Hardware-Software Co-Design
  • ISA Compliance Testing

Building a RISC-V processor at Berkeley

The last lab of UC Berkeley’s EECS251A is a semester spent building a working RISC-V processor from an empty file and getting it to run on real silicon. The course is tied to Apple’s New Silicon Initiative and to NVIDIA, and the target board is a $100 Digilent PYNQ-Z1 with a Xilinx Zynq-7020 on it. My partner and I built the whole thing — a four-stage integer pipeline, a pipelined floating-point unit, and a UART-tethered boot flow — with nothing but the on-chip memories the FPGA already had.

A logo lockup on a dark background: the Apple logo, the UC Berkeley Electrical Engineering and Computer Sciences wordmark, and the NVIDIA logo, separated by thin vertical dividers.
The final project of Berkeley’s EECS251A, a course tied to Apple’s New Silicon Initiative and to NVIDIA.

Getting an RV32I core to execute instructions is a solved problem. Getting it to fit on a $100 board, stay architecturally correct while a floating-point unit runs alongside the integer pipe, and boot with no external memory controller at all: that was the work.

The constraints

The grade was a figure of merit: clock frequency times the inverse of cycles per instruction. A correct but slow core and a fast but wasteful one both lost points, so the clock had to go up and CPI had to stay down.

The board made that harder in one specific way. There was no DDR and no SD-card boot. Every byte of BIOS, instruction memory, and data memory had to live in on-chip block RAM, and every program had to arrive over a 115,200-baud UART. That single restriction shaped most of the interesting decisions.

How it works

EECS251A RISC-V CPU datapath: a PC feeding an IMEM / BIOS mux into fetch; decode with immediate generation and the integer and FP register files; execute/memory with ALU, branch comparator, forwarding muxes, DMEM, and MMIO; writeback with load extension and register-file writes.
The full datapath. Four stages with single-cycle forwarding, a dedicated FP register file feeding the FPU, and a write port from execute back into instruction memory that lets the BIOS install user code before jumping to it.

The integer pipeline. RV32I plus Zicsr across four stages. Forwarding is single-cycle by construction, so a result computed in execute is ready for the very next instruction. The one case it cannot cover is a load followed immediately by a use, which costs one bubble; a mispredicted branch is resolved in execute and kills the two instructions behind it. Textbook, and meant to be. The interesting choices are what hangs off it.

Hanging an FPU off the integer pipe. The FPU has its own register file with three read ports, so a fused multiply-add reads all its operands in one cycle, and it is a short pipeline: a combinational stage for the cheap operations, an align-and-normalise stage for add, subtract, and FMA, then retire. The hard part was keeping architectural state precise without spending hardware on it. A big core would use a reorder buffer; we could not afford the area, so each float’s latency is tracked explicitly and the integer pipe’s writeback stalls until the result retires. Precise state for almost nothing, paid for in throughput on float-heavy code.

The floating-point unit integrated with the integer pipeline: a 3-read/1-write FP register file feeds a combinational stage 1, an align/normalise stage 2 for add/sub and FMA, and a retire stage. A latency tracker stalls the integer pipeline's writeback until the float retires.
Rather than a reorder buffer, an explicit latency tracker stalls integer writeback until the float retires.

Proving it works. Correctness was a ladder: the official riscv-isa-tests harness with a tohost CSR as the pass/fail signal, then C micro-tests, then directed assembly for the corner cases, then five end-to-end benchmarks on the FPGA. The top rung is the one that matters. Passing in simulation does not mean passing on the board, so every benchmark reports its own cycles, retired instructions, and branch statistics from four memory-mapped counters inside the core, read back over UART. The CPI came from the hardware measuring itself.

A verification ladder in four rungs: riscv-isa-tests, C micro-tests, directed assembly, and five end-to-end benchmarks on the real FPGA reporting cycles and CPI from on-chip counters. The top rung is labelled 'measured on hardware, not simulation.'
From ISA tests to real hardware. Benchmarks measure themselves from the core’s own performance counters.

Getting it onto the FPGA. The standard Vivado flow, with directed assembly checked at each turn and a PLL deriving the CPU clock from the board’s 125 MHz input. Because there is no memory controller, boot is its own trick: a host script streams a program’s hex over UART to the BIOS running from ROM, and the BIOS writes those bytes straight into instruction memory through a write port from the execute stage before jumping into the freshly loaded code. The same numeric address means different physical memory depending on whether it is used as a program counter or a data address. That dual interpretation is what lets a processor with no DRAM load and run its own programs.

A left-to-right build flow: Verilog RTL, Vivado synthesis, implementation, and a bitstream programmed onto the PYNQ-Z1, with a directed-assembly regression check alongside synthesis and implementation.
RTL to bitstream, with directed assembly guarding each step.

Results

Everything, including BIOS, both memories, and the FPU, inferred cleanly as block RAM and fit on the Zynq-7020 with no external memory of any kind.

  • 58 MHz CPU clock from the board’s 125 MHz input.
  • ~1.16 integer CPI and ~1.83 floating-point CPI, measured on hardware, for a figure of merit of 12.3.
  • The full riscv-isa-tests suite passing, plus five benchmarks on real silicon: integer matrix multiply, two sorts, a BDD evaluator, and the floating-point matrix multiply that stresses the FPU stall path.

What I took away

An in-order core is easy. Making it fast, correct, and self-booting on a fixed slice of a $100 chip is where every real decision lives.