feat: complete Phase 4 SPI RTL (engine, arbiter, top) + real-RAM e2e test

Implements the rest of the SPI interface (docs §8.1) on top of
spi_slave.v from the previous commit:

- rtl/spi_engine.v: opcode FSM + register bank, all 8 opcodes (NOP,
  WRITE_RAM, READ_RAM, RESET, SET_BASE, START, STATUS, READ_OUTPUT,
  READ_CONFIG). tx_byte is driven combinationally from live state
  (not reactively on tx_byte_req), applying the prefetch-vs-consume
  contract documented on spi_slave.v. STATUS.done is a sticky,
  clear-on-read latch. RAM master port uses the same byte-level
  convention as neuron_memory.v's external mem_* port.
- rtl/mem_arbiter.v: fixed-priority (neuron_memory > spi_engine)
  grant-and-forward arbiter sharing one byte-level memory port
  between spi_engine's WRITE_RAM/READ_RAM and neuron_memory's own
  X/W/bias reads during a run.
- rtl/spi_neuron_top.v: full integration -- spi_slave -> spi_engine
  -> mem_arbiter -> a single shared int8_memory_access ->
  memory_interface -> psram_controller -> PSRAM pins. neuron_memory's
  rst is global rst OR'd with the RESET opcode's soft-reset pulse.
  The host has no direct electrical path to the RAM, only through
  this chain.

Testing:
- sim/spi_engine_tb.v: 10 tests (one per opcode + WRITE_RAM/READ_RAM,
  START idle-vs-busy, STATUS sticky/clear-on-read, extra-MOSI-bytes-
  ignored, back-to-back transactions) against a synthetic 2-cycle-
  latency RAM model, isolating the opcode FSM from PSRAM timing.
  Found and fixed two testbench-only bugs (RTL needed no change):
  the same delta-zero clock-edge race as spi_slave_tb.v (blocking
  `nm_done=1` landing on the same sim time as a posedge -- fixed via
  negedge-based pulsing) and a missing RAM sentinel initialization.
- sim/spi_neuron_top_tb.v: end-to-end test against the **real**
  psram_model.v (not a mock) -- RESET/READ_CONFIG/WRITE_RAM/
  READ_RAM/SET_BASE/START/STATUS/READ_OUTPUT all driven purely over
  simulated SPI. 3/3 scenarios (sum, saturation, ReLU) pass on the
  first attempt; confirms the arbiter and shared byte<->word bridge
  are correct against real PSRAM timing, not just a synthetic mock.

Real-toolchain verification (Yosys + nextpnr-ecp5 + ecppack):
spi_slave.v and spi_engine.v synthesize clean and comfortably clear
80 MHz in isolation (403 MHz / 191 MHz, no DSP usage). The full
spi_neuron_top.v integration, however, does NOT meet 80 MHz
(~52-56 MHz depending on PARALLEL) -- the critical path is entirely
inside neuron_parallel.v's existing saturation comparator (no
contribution from the new SPI/arbiter logic), but its routed delay
is ~57% worse than in the isolated benchmark due to placement/
routing congestion once SPI + PSRAM logic shares the fabric with
it, not resource exhaustion (2% DSP usage). Documented as a Phase
4/7 finding in docs/FPGA-NeuralNetwork-Engine.md -- a floorplanning/
pipelining problem for Phase 7, not a functional-correctness issue
(verified independently in simulation against real PSRAM timing).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WQV3vS9TXaGDJ5cRfnfidt
This commit is contained in:
2026-09-02 15:44:03 +02:00
co-authored by Claude Sonnet 5
parent d716eb04dd
commit a2bd60e305
11 changed files with 1102984 additions and 4 deletions
+27 -4
View File
@@ -753,10 +753,33 @@ Implement:
- status and control.
- [x] Protocol/opcode set drafted — see §8.1 SPI Protocol v1
- [ ] SPI controller RTL (physical layer: shift register, CS/clock sync)
- [ ] Register bank RTL (SET_BASE, sticky STATUS, READ_CONFIG constants)
- [ ] RAM access passthrough RTL (WRITE_RAM/READ_RAM -> memory_interface)
- [ ] Testbench (SPI master BFM + full stack, mirroring neuron_memory_tb.v style)
- [x] SPI controller RTL `rtl/spi_slave.v` (physical layer: Mode 0, MSB-first, 3-stage CDC synchronizer for SCLK/MOSI/CS_N)
- [x] Register bank RTL `rtl/spi_engine.v` (all 8 opcodes: NOP, WRITE_RAM, READ_RAM, RESET, SET_BASE, START, STATUS, READ_OUTPUT, READ_CONFIG; sticky clear-on-read STATUS.done)
- [x] RAM access passthrough RTL `rtl/mem_arbiter.v` (fixed-priority arbiter, neuron_memory > spi_engine) + shared `int8_memory_access` instance in `rtl/spi_neuron_top.v`
- [x] Testbenches: `sim/spi_slave_tb.v` (4 tests), `sim/spi_engine_tb.v` (10 tests, synthetic RAM), `sim/spi_neuron_top_tb.v` (end-to-end, **real** `psram_model.v`, no synthetic mock — RESET/READ_CONFIG/WRITE_RAM/READ_RAM/SET_BASE/START/STATUS/READ_OUTPUT all exercised purely over simulated SPI)
**Real-toolchain verification (Yosys + nextpnr-ecp5 + ecppack, 2026-09-02):**
- `spi_slave.v` alone: PASS, Fmax 403.23 MHz.
- `spi_engine.v` alone: PASS, Fmax 191.31 MHz. Neither uses any DSP.
- `spi_neuron_top.v` (full integration: SPI + arbiter + neuron_memory
+ PSRAM chain), N_NEURONS=1: **FAIL at 80 MHz** — Fmax ~52.58 MHz
(PARALLEL=8) / ~55.85 MHz (PARALLEL=2, the benchmark's own
80 MHz-passing config in isolation). The critical path in both
cases is entirely inside `neuron_parallel.v`'s saturation
comparator (`> 127`, `rtl/neuron_parallel.v:127`) — zero
contribution from the new SPI/arbiter logic — but its routed delay
is ~57% worse than in the isolated benchmark (17.91 ns vs.
11.38 ns) due to placement/routing congestion once the SPI +
PSRAM logic shares the fabric with it, not resource exhaustion
(DSP utilization only 2%). This means the current auto-placed
full system would need to run around 50-56 MHz to stay within
timing margin at speed grade -8, not the 80 MHz target — a
system-level floorplanning/pipelining problem, out of scope here
and left for Phase 7 (Optimization: "pipeline depth", "FPGA
resource utilization"). It does not affect functional correctness
(verified independently, in simulation, against real PSRAM
timing) or synthesizability (0 CHECK-pass problems, no latches).
## Phase 5 — Multi-Layer Network