A complete system-on-chip: a pipelined RISC-V CPU, a boot ROM, RAM, and a memory-mapped systolic matrix-multiply NPU on one data bus — running real firmware. The CPU's program loads two matrices into the accelerator, starts it, polls for completion, reads the result back, and writes it to memory. This is hardware/software co-design: the same offload-to-an-accelerator pattern every real AI chip uses, built from the ground up and verified end to end.
┌───────────────┐ instructions ┌───────────────┐
│ Boot ROM │───────────────▶│ RV32I CPU │
│ (firmware) │ │ (5-stage │
└───────────────┘ │ pipeline) │
└───────┬───────┘
data bus │ (address-decoded)
┌─────────────────┴──────────────────┐
addr[31]=0 addr[31]=1
│ │
┌─────▼─────┐ ┌────────▼────────┐
│ RAM │ │ NPU (memory- │
│ │ │ mapped systolic │
└───────────┘ │ matrix-multiply)│
└─────────────────┘
The firmware (firmware/firmware.s) executes on the CPU and:
- writes matrix A and matrix B into the NPU's operand registers (stores
to
0x8000_00xx), - writes the CONTROL register to start the multiply,
- polls STATUS until the NPU raises
done, - reads the result matrix C back and stores it to RAM.
The testbench boots the SoC and confirms RAM holds
[[1,2],[3,4]] × [[5,6],[7,8]] = [[19,22],[43,50]] — computed by the NPU,
orchestrated entirely by software running on the CPU.
- CPU: riscv-rv32i-pipeline — the 5-stage core, reused here.
- NPU datapath: systolic-matmul — the systolic array, wrapped (
rtl/npu_periph.sv) as a memory-mapped accelerator with a start/done control FSM. - Toolchain:
tools/assemble.py— a small RV32I assembler I wrote; it turnsfirmware/firmware.sinto the machine code in the boot ROM (no hand-typed hex).
# (optional) re-assemble the firmware
python tools/assemble.py firmware/firmware.s
# build + run the SoC
iverilog -g2012 -o sim.out rtl/rv32i_core.sv rtl/systolic_mm.sv \
rtl/npu_periph.sv rtl/soc_top.sv tb/soc_tb.sv
vvp sim.out| Offset | Register | Access |
|---|---|---|
0x00–0x0C |
A[0][0] … A[1][1] |
write |
0x10–0x1C |
B[0][0] … B[1][1] |
write |
0x20 |
CONTROL (bit0 = start) |
write |
0x24 |
STATUS (bit0 = done) |
read |
0x30–0x3C |
C[0][0] … C[1][1] |
read |
- Memory-mapped accelerator. The NPU lives in the CPU's address space, so the
CPU talks to it with plain
lw/sw— exactly how a driver pokes registers on real hardware. No special instructions needed. - Decoupled, multi-cycle offload. The matrix multiply takes many cycles, so
the NPU exposes a
start/donehandshake and the CPU polls STATUS — the same pattern as a DMA engine or GPU command queue. Compute and control run concurrently. - Address decode is a single bit (
addr[31]): high → NPU, low → RAM. Trivial to extend to more peripherals with a wider decode. - One-bit-at-a-time bring-up. Each block (CPU, array, NPU wrapper) was verified in isolation first; only then integrated — which is why the full system passed on the first boot.
Built by Efe Demir.