Binarized neural network (BNN) for MNIST digit classification on the Tang Nano 9k FPGA (GW1NR-9 C6/I5). Three fully-connected layers: 784 → 320 → 320 → 10 with XNOR-popcount arithmetic and on-chip batch-norm thresholds.
| Parameter | Value |
|---|---|
| Input | 784 × uint8 (28×28 pixel) |
| FC1 weights | 320 × 784 binary (±1) |
| FC2 weights | 320 × 320 binary (±1) |
| FC3 weights | 10 × 320 binary (±1) |
| Accumulator width | 18-bit signed |
| Parallelism (N_PAR) | 16 |
| Neuron groups per layer | FC1: 20, FC2: 20, FC3: 1 |
| Resource | Used | Available | % |
|---|---|---|---|
| LUT4 | 5,496 | 8,640 | 63% |
| DFF | 2,961 | 6,480 | 45% |
| ALU | 1,602 | 6,480 | 24% |
| BSRAM | 22 | 26 | 84% |
| DSP | 0 | 10+ | 0% |
| Metric | Value |
|---|---|
| Clock frequency | 66.857 MHz (PLL: 27 MHz × 25 / (7 × 4) / 6) |
| Max achievable | 74.39 MHz |
| Inference cycles | 22,482 |
| Image load cycles | 784 |
| Inference latency | 336 µs |
| Max framerate | ~2,900 fps |
| At 74.39 MHz (max path) | ~3,200 fps |
FC1: 20 groups × (784 + 2) cycles = 15,720
FC2: 20 groups × (320 + 2) cycles = 6,440
FC3: 1 group × (320 + 2) cycles = 322
Total = 22,482
Per group: 1 clear cycle + N_IN accumulate cycles + 1 batch-norm cycle.
├── hdl/
│ ├── src/ # Verilog RTL
│ │ ├── bnn_top.v # Top-level inference sequencer
│ │ ├── fc_layer.v # Single layer engine (FSM + BN)
│ │ ├── acc_bank.v # Parallel accumulator bank
│ │ ├── weight_bram.v # $readmemh-initialized weight storage
│ │ ├── bn_thresh_rom.v # Batch-norm threshold ROM
│ │ ├── img_buf.v # Input image buffer
│ │ └── mem/ # Weight & threshold .mem files
│ ├── impl/ # FPGA implementation
│ │ ├── tang_nano_9k_top.v # Board-level wrapper (PLL, buttons, LEDs)
│ │ ├── pll.v # 27 MHz → 66.857 MHz PLL
│ │ ├── tang_nano_9k.cst # Pin constraints
│ │ ├── Makefile # synth → pnr → pack flow
│ │ └── build/ # Build artifacts + bitstream
│ └── test/ # Cocotb testbenches
│ ├── test_sim.py # Pytest runner
│ ├── tb_bnn_top.py # Full integration test vs golden data
│ ├── tb_bnn_debug.py # Per-layer debug testbench
│ ├── tb_fc_layer.py # Unit test (synthetic tiny config)
│ ├── tb_acc_bank.py # Unit test (accumulator bank)
│ └── golden/ # Golden reference vectors (10 digits)
├── model/ # Python training & export
│ ├── bnn.py # BNN model definition
│ ├── train.py # Training script
│ ├── bnn_mnist.pth # Trained checkpoint
│ ├── export_weights.py # Export weights → .mem files
│ ├── export_bn_thresh.py # Export BN thresholds → bn_thresh.mem
│ └── gen_golden.py # Generate golden test vectors
└── README.md
uv run pytest hdl/test/test_sim.py -vmake -f hdl/impl/Makefile all # synth → pnr → pack
make -f hdl/impl/Makefile prog # flash via openFPGALoader| Signal | Pin |
|---|---|
| clk_27mhz | 52 |
| rst_n (S1) | 4 |
| start_btn_n (S2) | 3 |
| led_n[0] | 10 |
| led_n[1] | 11 |
| led_n[2] | 13 |
| led_n[3] | 14 |
| led_n[4] | 15 |
| led_n[5] | 16 |
LEDs are active-low. led_n[3:0] shows the predicted digit, led_n[4]
flashes on inference done, led_n[5] indicates PLL lock.
Image loading is not yet wired to physical I/O — start_btn_n triggers
inference on a pre-loaded (currently zeroed) image buffer.