- Python 39.9%
- Verilog 34.9%
- C++ 20.2%
- C 2.8%
- Makefile 2.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| analysis | ||
| constraints | ||
| fixed_point | ||
| lib | ||
| matvec | ||
| .gitignore | ||
| .woodpecker.yml | ||
| Makefile | ||
| README.md | ||
Tang Nano 20K Lindblad Propagator Accelerator
FPGA implementation of a 9x9 complex matrix-vector multiply for real-time Lindblad master equation simulation. Targets the Sipeed Tang Nano 20K (Gowin GW2AR-18C) using a fully open-source toolchain.
The core algorithm applies a precomputed propagator matrix P to a vectorized density matrix rho, step after step. lindblad-bench implements the same operation in software. This version realizes it in hardware with deterministic cycle-exact timing, which suits real-time quantum feedback control where jitter matters more than raw throughput.
Architecture
27 MHz ──► rPLL ──► 108 MHz ──┬── matvec_top.v (UART protocol FSM)
│ └── matvec_9x9.v (core)
UART RX ──────────────────────┤
UART TX ◄─────────────────────┘
Matvec core pipeline (4 stages, 94 cycles/step amortized, 95-cycle single-step latency from a 1-cycle pipeline fill):
| Stage | Operation | Hardware |
|---|---|---|
| S0 | Address generation, latch rho[col] | BSRAM address |
| S1 | Delay rho 1 cycle to match BSRAM read latency | Register |
| S2 | 4 parallel 16x16 multiplies (ac, bd, ad, bc) | 4x DSP18 |
| S3 | Accumulate (42-bit), saturate, store result when col==8 | Adder + register |
Fixed-point format: Q1.15 (16-bit signed, 1 sign + 15 fractional bits, range [-1, +0.99997]). Row sums that exceed the format saturate at ±full scale.
Clock: 108 MHz (27 MHz × 4 rPLL). The operating point is stress-characterized, not STA-derived: nextpnr/apicula has no timing arcs for MULT18X18, so the DSP path is invisible to timing analysis. 135 MHz corrupts results under full-scale operands; 108 MHz passes 500 adversarial rounds (matvec/host/stress_probe.py).
Error budget (d=3 transmon trajectory, vs double precision): P quantization 5.7e-05 HS one-time. Floor truncation bounded (-1, 0] LSB per component per step. Measured drift ~1 LSB/step of trace (3.05e-05/step, 6.1% over 2000 steps). Numbers come from analysis/precision_test.py, whose fixed-point model matches the RTL bit-exact (board-verified).
Resource usage: 4 DSP18 blocks (of 48), ~880 LUTs (of 20736), P matrix in BSRAM (2 of 46 blocks, one each for the real and imaginary halves).
Project Structure
├── Makefile # Yosys → nextpnr → gowin_pack → openFPGALoader
├── constraints/
│ └── tangnano20k.cst # Board pin assignments
│
├── lib/
│ ├── uart_tx.v # 115200 baud 8N1 transmitter
│ ├── uart_rx.v # 115200 baud 8N1 receiver (2-FF sync)
│ └── uart_tx_tb.cpp # Verilator testbench (7 tests)
│
├── fixed_point/
│ ├── fixed_mul.v # Single 16x16 signed multiply → Q1.15
│ ├── complex_mul.v # 2-stage pipelined complex multiply (4 DSP18)
│ └── complex_mul_tb.cpp # Verilator testbench (10 tests)
│
├── matvec/
│ ├── matvec_9x9.v # 4-stage pipelined 9x9 complex matvec
│ ├── matvec_top.v # UART protocol wrapper (P/V/S/N/T commands)
│ ├── matvec_pll_top.v # PLL wrapper, 108 MHz
│ ├── matvec_9x9_tb.cpp # Verilator testbench (7 tests)
│ └── host/
│ ├── matvec_host.py # Load propagator over serial, validate vs reference
│ └── stress_probe.py # Adversarial full-scale stress vs saturating reference
│
└── analysis/
├── precision_test.py # Error budget: Q1.15 vs Q1.31 vs double
├── benchmark_compare.py # FPGA cycle timing vs C reference
├── c_jitter_test.c # C reference benchmark
├── plot_jitter_comparison.py
└── results/ # Board-run logs backing the figures
Test Results
All Verilator testbenches pass:
$ make sim PROJECT=lib SIM_TOP=uart_tx SIM_SRC=lib/uart_tx.v
7/7 tests passed.
$ make sim PROJECT=fixed_point SIM_TOP=complex_mul SIM_SRC="fixed_point/complex_mul.v fixed_point/fixed_mul.v"
10/10 tests passed.
$ make sim PROJECT=matvec
Test 1: Identity matrix... Completed in 94 cycles. PASS
Test 2: Known non-trivial P... Completed in 94 cycles. PASS (bit-exact match with C reference)
Test 3: Multi-step (10 steps)... Completed in 940 cycles (94.0 cycles/step). PASS (10-step bit-exact match)
Test 4: N=0 steps (no-op)... Completed in 0 cycles. PASS (rho unchanged)
Test 5: Overflow saturates... PASS (clamps at +32767/-32768, matches saturating ref)
Test 6: Random P/rho, 50 rounds.. PASS (50 rounds bit-exact)
Test 7: Back-to-back == N=2... PASS (paths agree; mid-run write ignored)
7/7 tests passed.
On hardware: a 100-step trajectory is bit-exact vs the Python reference. 500 adversarial full-scale rounds are bit-exact at 108 MHz.
UART Protocol
Serial commands at 115200 baud (8N1) control the matvec core. All multi-byte values are little-endian.
| Command | Payload | Response | Description |
|---|---|---|---|
P |
324 bytes (81 complex Q1.15 entries) | K |
Load propagator matrix |
V |
36 bytes (9 complex Q1.15 entries) | K |
Load state vector rho |
S |
none | 36 bytes (rho result) | Single propagation step |
N |
2 bytes (uint16 LE step count) | 36 bytes (rho result) | N steps, single result |
T |
none | 4 bytes (uint32 LE) | Read hardware cycle counter |
Each complex Q1.15 entry is 4 bytes: re_lo, re_hi, im_lo, im_hi.
Toolchain
Fully open-source, no vendor IDE required:
| Tool | Version | Purpose |
|---|---|---|
| Yosys | 0.63+ | Synthesis |
| nextpnr-himbaechel | 0.10+ | Place & route (Apicula database) |
| gowin_pack | Bitstream generation | |
| openFPGALoader | JTAG programming | |
| Verilator | 5.x | Simulation & testbenches |
Install via oss-cad-suite for a single-archive setup.
Quick Start
# Build and flash (defaults: PROJECT=matvec, TOP=matvec_pll_top, FREQ=108)
make flash
# Run all testbenches
make sim PROJECT=lib SIM_TOP=uart_tx SIM_SRC=lib/uart_tx.v
make sim PROJECT=fixed_point SIM_TOP=complex_mul SIM_SRC="fixed_point/complex_mul.v fixed_point/fixed_mul.v"
make sim PROJECT=matvec
# Validate against Python reference (board must be connected)
python matvec/host/matvec_host.py --port /dev/ttyUSB1 --steps 100
# Adversarial timing stress (board must be connected)
python matvec/host/stress_probe.py 500
# Precision analysis (no hardware needed)
python analysis/precision_test.py --steps 2000 --plot
Context
This project is a learning vehicle for FPGA-based quantum simulation, built alongside lindblad-bench (a bare-metal C implementation of the same algorithm, optimized for CPU cache hierarchies and SIMD). The FPGA version trades throughput for deterministic latency. A single step completes in exactly 95 clock cycles (94 amortized over a multi-step run) with zero jitter. Real-time feedback control of quantum hardware requires that property.
Hardware
- Board: Sipeed Tang Nano 20K (~$25)
- FPGA: Gowin GW2AR-LV18QN88C8/I7
- 20,736 LUTs
- 48 DSP18 blocks
- 828 Kbit block SRAM
- 64 Mbit SDRAM (not used)
- Clock: 27 MHz crystal oscillator
- I/O: USB-C (JTAG + UART via BL616), HDMI connector, 2 user buttons, 6 LEDs
License
MIT