Sep 2025 – Dec 2025
EELast 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.

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
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.

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.

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.

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.