Pure-RTL (VHDL) NN inference pipeline targeting the Digilent Nexys Video.
- Build bitstream (Vivado CLI):
make build
- Program FPGA (Vivado CLI):
vivado -mode batch -source scripts/program_fpga.tcl- or pass a specific bitfile:
vivado -mode batch -source scripts/program_fpga.tcl -tclargs /path/to.bit
rtl/: synthesizable VHDLconstraints/: board constraints (XDC)scripts/: build/sim helperssim/: testbenches and fixtureshost/python/: host-side toolingnn: train DNN, generate hls4ml rtl
I'm saving these here for future reference, as they were particularly tricky to solve. Future bugs may not be identical, but hopefully they will rhyme.
We hit a UART bring-up issue where the FPGA would not respond to STATUS requests. The key lessons:
- LED meaning matters: LD13/LD12 indicate PC->board and board->PC traffic, not raw FPGA RX/TX. Seeing TX blink without RX does not guarantee the UART path is parsing packets.
- On-board FT232R wiring: The Nexys Video USB-UART bridge (J13) uses V18 (uart_tx_in, PC->FPGA) and AA19 (uart_rx_out, FPGA->PC). These must match the
uart_rx/uart_txpins in the XDC. - UART RX verified: We added temporary LED instrumentation in
rtl/top/top_nexys_video.vhd:led(6)mirrors the rawuart_rxpin (idle high)led(7)toggles on eachrx_validpulse
- Root cause: The top-level only responded to
INFER_REQ, soSTATUS_REQnever produced a response. Fixing this by integratingmmio_status+perf_countersintop_nexys_video.vhdmade STATUS return correctly.
Rebuild and reprogram commands:
vivado -mode batch -source scripts/build_vivado.tcl &&
vivado -mode batch -source scripts/program_fpga.tclSend STATUS (--status) protocol packet using command:
python host/python/nnfpga/send_uart.py --port /dev/ttyUSB0 --status --verbose --baud 115200Output:
Using STATUS_REQ packet
Wrote response to sim/fixtures/uart_last_rsp.hex
Response type: 0x81, payload length: 20
Once the STATUS path returned a valid packet (0x81 with 20-byte payload), UART bring-up was confirmed and we proceeded to INFER tests.
We sent a real STATUS_REQ packet (type 0x01) using the same header/format as the inference packets, but make no mistake, it is not inference. It's a real protocol request and exercises the UART + packet parsing + response path.
If INFER responses mismatch the golden file, the most common cause is using a PyTorch golden while the FPGA runs hls4ml-quantized math. Generate fixtures from hls4ml to match the hardware:
python sim/models/nn_golden.py \
--config nn/configs/calhouse.yaml \
--checkpoint nn/outputs/calhouse/default/model.pt \
--use-hls4mlThen ensure the FPGA bitfile is built from the same model export:
python nn/scripts/run_hls4ml.py --config nn/hls4ml_config.yaml --clean --all
scripts/sync_hls4ml_ip.sh
vivado -mode batch -source scripts/build_vivado.tcl
vivado -mode batch -source scripts/program_fpga.tclRun inference over UART:
python host/python/nnfpga/send_uart.py \
--port /dev/ttyUSB0 \
--req sim/fixtures/nn_in.hex \
--expect sim/fixtures/nn_out.hex \
--baud 115200Symptom: RTL sim (tb_top_e2e) and hardware UART both returned a valid INFER_RSP, but payload bytes
did not match nn_out.hex (example mismatch at payload byte 0).
Root cause: The golden fixture was generated by running hls4ml on float inputs, while the hardware
receives fixed-point quantized inputs. That changes the final output by a few LSBs (e.g. FE89 vs FE83).
Fix: When generating golden fixtures with --use-hls4ml, first quantize the inputs exactly as the UART
payload will be transmitted (done in sim/models/nn_golden.py). After regenerating nn_out.hex, RTL sim
and hardware matched.