08. KWS NPU on XC7Z020 Silicon
This page is the silicon canonical for eulernpu. It walks the full flow of putting the Keyword Spotting NPU onto a QMTECH XC7Z020 board. Both subsequent silicon demos — 09 / 10 — are variations on this pattern.
Two silicon patterns covered together:
- L2 (single-op offload) — only
matmul_systoliclives in PL, the rest runs on ARM. Locks the cycle model + proves the data path. - L3 (full-graph single IP) — all of conv→GRU→fc inside one IP (
kws_npu_top). ARM only does input + softmax + argmax. Apple-ANE / Edge-TPU pattern.
| Pattern | Result | Meaning |
|---|---|---|
| L2 (matmul only) | 11/11 MATCH, NPU 2.5× slower | DMA dominates, "data-path proof" |
| L3 (full graph) | 11/11 MATCH, NPU 2.11M cyc vs ARM 17M cyc = 8.07× speedup | A real NPU |
The Model
examples/silicon_kws/'s DS-CNN + GRU keyword spotter (11 commands).
- Input: 40-mel × 101-frame log-mel spectrogram (INT8)
- Graph:
conv2d → depthwise_conv2d → pointwise_conv2d → batchnorm → global_avgpool2d → 16-step gru_cell → fc1 → fc2 → softmax - Output: 11-way logits (silence + unknown + 9 commands)
Training + quantization + golden generation all finish inside
examples/silicon_kws/. P06 only takes that model to silicon.
7-Stage Silicon Flow (Full)
Project location: examples/silicon_kws/
Step 1 — Host CPU smoke
Before silicon, compile the same C with host gcc and verify accuracy. If math breaks on host, it breaks on silicon (the reverse is not true).
bash examples/silicon_kws/tests/build_host_smoke.sh
Expected: 11/11 PASS — every keyword answered correctly.
Step 2 — HLS C-synth
bash examples/silicon_kws/scripts/run_hls_synth.sh
This step instantiates eulernpu/hls/kernels/ templates
(matmul_systolic.h, conv2d_sliding.h, recurrent.h, ...) into an
RTL IP.
L2 synthesis report:
- Fmax: 114 MHz (target 100 MHz exceeded)
- DSP: 64 (29 % of 220)
- BRAM: < 5 %
L3 synthesis report:
- Fmax: 91.33 MHz (90 MHz target)
- DSP: 99 (45 %)
- BRAM: 24 % (includes weight ROM)
- LUT: < 50 %
L3 is heavier because the full graph + weights ROM are all on-chip.
Step 3 — Vivado bitstream
vivado -mode batch \
-source examples/silicon_kws/scripts/01_create_vivado_project.tcl
The TCL autogenerates the Block Design:
processing_system7_0
├── M_AXI_GP0 → IP s_axilite (control)
├── S_AXI_HP0 ← IP m_axi (data, if any)
└── EMIO UART1 → external pin (printf channel, P05 pattern)
IP (kws_npu_top or matmul_systolic)
Vivado impl result:
- L2: WNS +0.412 ns @ 100 MHz, 0 critical warnings
- L3: WNS +0.301 ns @ 90 MHz, 0 critical warnings
Step 4 — Vitis platform + ELF
vitis -s examples/silicon_kws/scripts/02_create_app.py
This Python script:
- Creates an SDT (System Device Tree) platform from the XSA
- Compiles the ARM standalone BSP
- Applies the EMIO UART MIO 48/49 routing patch (P05 X-1 carry-over)
- Builds two ELFs (demo1, demo2)
ELF location:
~/Developments/vitis_ws/kws_npu/llm_demo{1,2}/build/*.elf
Step 5 — Silicon run (xsdb + UART)
Board must be in just-reset state (P07 X-10 trap):
bash examples/silicon_kws/scripts/run_npu_test.sh
This harness:
xsdbconnect +fpga -filethe bitstream- Source
ps7_init.tcl+ re-route EMIO UART dowthe ELF +con- Simultaneously
pyserialcaptures/dev/ttyUSB1(P06 X-8 trap:catloses bursts) - Auto-grade RESULT: PASS / FAIL
Step 6 — Results
examples/silicon_kws/docs/results/ contains capture files and
SUMMARY.md.
demo1 (accuracy gate):
demo1: 11/11 CPU=NPU agreement
"yes" pred=yes conf=0.998 cyc_cpu=17,492,310 cyc_npu=2,114,832 → MATCH
"no" pred=no conf=0.999 cyc_cpu=17,489,221 cyc_npu=2,114,977 → MATCH
... (× 11)
RESULT: PASS
demo2 (200-frame stream):
demo2: 200 frames / 182 keywords detected / 5,166 ms wall
NPU avg 25.83 ms per frame, CPU avg 209 ms per frame
(8.07× speedup — Apple-ANE pattern)
Step 7 — Cycle model regression lock
The P06 measurement matches eulernpu/hls/cycle_model.py's
matmul_systolic lambda within ±1 % (predicted 456 vs measured 460):
def test_matmul_systolic_matches_p06_silicon():
cycles = matmul_systolic_cycles(M=32, K=48, N=8, T=8)
assert 414 <= cycles <= 506 # ±10% tolerance
This lock-test is in CI; CI fails if the cycle model formula drifts in
the future. The P06 silicon measurement is the trust anchor for every
subsequent npu_sim profile.
Two Silicon Traps (P05/P06 X-items)
X-8 — cat /dev/ttyUSB1 loses bursts
At UART 115200 baud with fast burst printf from the IP, cat drops
lines.
→ Fix: pyserial-based capture_uart.py. run_npu_test.sh auto-prefers
pyserial with a fallback.
X-10 — JTAG DAP sticky error
If the previous demo left the board halted, the next xsdb attach fails (DAP status 0x30000021).
→ Fix: press the board RESET button (or cycle USB power).
test_xsdb.tcl detects this state and prints explicit RESET advice.
Reproduce — One Line
# (board just-reset)
bash examples/silicon_kws/scripts/run_npu_test.sh
Expected: ~2–3 minutes later
demo1 : PASS
demo2 : PASS
Videos + SUMMARY:
Why P06 is Canonical
The three-stage silicon ladder:
- P06 (KWS) — first silicon, 8.07× speedup, cycle model locked. The cleanest "this is possible" proof.
- P07 (Bearing) — same L3 pattern, deeper graph. 11.13× speedup. Shows how to recover at a resource ceiling (II=64).
- P08 (LLM) — deliberately departs to L2 hybrid (the big model doesn't fit BRAM). Honest result of NPU being slower than CPU.
P06 is the clearest "FPGA NPU has meaning" proof; P07/P08 show how far the pattern stretches.
Next
- 09. bearing_silicon — same P06 pattern on a deeper 1D-CNN
- 10. llm_silicon_hybrid — deliberate L2 hybrid departure