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
+29
View File
@@ -186,3 +186,32 @@
- 2026-09-02T05:50 — [FASE 13] — Eseguito place&route reale: `nextpnr-ecp5 --45k --package CABGA381 --speed 8 --json spi_slave.json --lpf-allow-unconstrained --freq 80 --textcfg spi_slave.config` (stessi parametri usati nei benchmark esistenti, target LFE5U-45F-8BG381C). Risultato: **Max frequency 403.23 MHz, PASS al target 80 MHz** (ampio margine, atteso per un modulo così piccolo). Nessun errore di routing, "Program finished normally". - 2026-09-02T05:50 — [FASE 13] — Eseguito place&route reale: `nextpnr-ecp5 --45k --package CABGA381 --speed 8 --json spi_slave.json --lpf-allow-unconstrained --freq 80 --textcfg spi_slave.config` (stessi parametri usati nei benchmark esistenti, target LFE5U-45F-8BG381C). Risultato: **Max frequency 403.23 MHz, PASS al target 80 MHz** (ampio margine, atteso per un modulo così piccolo). Nessun errore di routing, "Program finished normally".
- 2026-09-02T05:52 — [FASE 13] — Generato bitstream reale: `ecppack spi_slave.config spi_slave.bit` -> completato senza errori (file da ~1 MB, dimensione plausibile per ECP5-45F). Confermata l'intera catena di implementazione (Verilog -> sintesi -> place&route -> bitstream) funzionante per questo modulo, non solo la simulazione comportamentale. - 2026-09-02T05:52 — [FASE 13] — Generato bitstream reale: `ecppack spi_slave.config spi_slave.bit` -> completato senza errori (file da ~1 MB, dimensione plausibile per ECP5-45F). Confermata l'intera catena di implementazione (Verilog -> sintesi -> place&route -> bitstream) funzionante per questo modulo, non solo la simulazione comportamentale.
- 2026-09-02T05:53 — [FASE 13] — CONCLUSIONE: rtl/spi_slave.v verificato sia funzionalmente (Icarus, 4/4 test) sia a livello di implementazione reale sul target FPGA dichiarato nel progetto (Yosys+nextpnr-ecp5+ecppack, PASS a 80MHz con margine ampio). Il bug trovato durante il debug era interamente nel testbench (due bug distinti: contratto tx_byte_req mal interpretato + race di reset a delta-zero), NON nel DUT stesso — rtl/spi_slave.v non ha richiesto modifiche funzionali, solo l'aggiunta del commento di contratto su tx_byte_req. Prossimo step: rtl/spi_engine.v (FSM opcode + register bank). - 2026-09-02T05:53 — [FASE 13] — CONCLUSIONE: rtl/spi_slave.v verificato sia funzionalmente (Icarus, 4/4 test) sia a livello di implementazione reale sul target FPGA dichiarato nel progetto (Yosys+nextpnr-ecp5+ecppack, PASS a 80MHz con margine ampio). Il bug trovato durante il debug era interamente nel testbench (due bug distinti: contratto tx_byte_req mal interpretato + race di reset a delta-zero), NON nel DUT stesso — rtl/spi_slave.v non ha richiesto modifiche funzionali, solo l'aggiunta del commento di contratto su tx_byte_req. Prossimo step: rtl/spi_engine.v (FSM opcode + register bank).
- 2026-09-02T06:00 — [FASE 13] — Domanda utente (mentre si scriveva spi_engine.v): "quale è il numero massimo consigliato di neuroni per la FPGA?" Risposta basata sui dati reali del benchmark (docs/FPGA-Neural-Datapatch-Benchmark.md): LFE5U-45F ha 72 DSP MULT18X18D, 1 DSP per MAC, budget DSP di un layer = N_NEURONS × PARALLEL. Con PARALLEL=2 (config a miglior timing, 87.88MHz, unica PASS a 80MHz nel benchmark), 2 DSP/neurone fisico. Raccomandato ~50% di utilizzo DSP come margine (SPI/memoria non usano DSP, solo LUT/FF) -> circa 16-18 neuroni fisici paralleli come tetto pratico. Segnalata esplicitamente la cautela: il benchmark ha validato con place&route reale solo fino a 4 istanze fisiche, oltre serve riverifica nextpnr (il degrado di Fmax osservato nel benchmark dipende anche da routing/congestione LUT, non solo da %DSP).
- 2026-09-02T06:02 — [FASE 13] — Utente: "vorrei che questi fossero poi divisibili dall'utente tra i vari layer... e la matrice di configurazione si occupa di questo" — conferma/estende la visione già discussa per la Fase 5 (sequenziamento layer-to-layer, opzione scelta in precedenza): un numero FISICO limitato di neuroni (vincolato da DSP/routing) va riusato nel tempo per servire un numero LOGICO più grande di neuroni/layer. Fatto notare che l'architettura di neuron_memory.v (Fase 3, appena estesa) già implementa esattamente questo pattern per il caso "più neuroni in un layer" (1 neuron_parallel fisico riusato in sequenza per N_NEURONS logici via memoria) — la futura "matrice di configurazione" di Fase 5 estenderebbe lo stesso pattern al caso "più layer", riconfigurando x_base/w_base/bias_addr (e N_INPUTS/N_NEURONS per quel layer) tra un layer logico e il successivo. Nessuna azione di codice in questa nota, solo allineamento di visione per il design futuro di Fase 5; ripreso subito dopo il lavoro su rtl/spi_engine.v.
## Fase: Implementazione RTL SPI - motore opcode/register bank (2026-09-02)
- 2026-09-02T06:10 — [FASE 14] — Creato rtl/spi_engine.v: FSM opcode + register bank sopra l'interfaccia byte-level di spi_slave.v, secondo la spec docs §8.1. Implementati tutti gli 8 opcode: NOP, WRITE_RAM, READ_RAM, RESET, SET_BASE, START, STATUS, READ_OUTPUT, READ_CONFIG. Porta RAM master byte-level con la stessa convenzione del porto esterno "mem_*" di neuron_memory.v (indirizzo byte, dato byte, handshake req/ready), per poter condividere in futuro un arbitro + int8_memory_access + memory_interface con neuron_memory. `tx_byte` guidato interamente in modo COMBINATORIO dallo stato corrente (non reattivo a tx_byte_req), applicando la lezione del contratto tx_byte_req/rx_valid già documentata in spi_slave.v. Sticky STATUS.done latch: settato da nm_done (impulso), azzerato da nm_soft_rst o dalla lettura effettiva del byte STATUS (rx_valid mentre in ST_RESP con opcode STATUS).
- 2026-09-02T06:15 — [FASE 14] — Compilazione di verifica sintassi/elaborazione standalone (piccolo modulo tb con tutte le porte cablate a segnali dummy): pulita al primo tentativo dopo una piccola pulizia (rimossa un'assegnazione ridondante `opcode<={opcode[7:0]}` in ST_SETBASE_SEL, no-op lasciata per errore).
- 2026-09-02T06:20 — [FASE 14] — Creato sim/spi_engine_tb.v: testbench dedicato che instanzia spi_slave+spi_engine insieme, con un modello di RAM sintetico (latenza fissa a 2 cicli, isola la FSM degli opcode dalla complessità/temporizzazione dello stack PSRAM reale, che sarà verificato nel test end-to-end finale) e un mock manuale di neuron_memory (nm_busy/nm_done/y_bus pilotati dal test, x_base/w_base/bias_addr/nm_start/nm_soft_rst osservati). 10 test, uno per ciascun opcode più casi limite di protocollo: A) WRITE_RAM poi READ_RAM di verifica, B) SET_BASE per X/W/BIAS, C) START accettato da idle / ignorato da busy, D) STATUS (bit busy live, bit done sticky/clear-on-read), E) RESET (impulso nm_soft_rst + pulizia sticky done), F) READ_OUTPUT (N_NEURONS=3, neuron-major), G) READ_CONFIG (payload 8 byte), H) NOP (nessun effetto collaterale), I) WRITE_RAM con più byte MOSI del previsto (i byte in eccesso devono essere ignorati), J) transazioni back-to-back (stato si resetta correttamente su cs_end).
- 2026-09-02T06:25 — [FASE 14] — Prima esecuzione: 8/10 test PASS, 2 FAIL: TEST D (bit done non sticky dopo l'impulso nm_done) e TEST I (ram[0x302] sovrascritto, atteso ignorato).
- 2026-09-02T06:27 — [FASE 14] — TEST D: applicata preventivamente la stessa lezione già imparata nel debug di spi_slave_tb.v (Fase 13): il pattern `nm_done=1'b1; @(posedge clk); nm_done=1'b0;` rischia la stessa race a ritardo-zero sul fronte di clock. Sostituito con `@(negedge clk); nm_done=1'b1; @(negedge clk); nm_done=1'b0;` in entrambi i punti dove appariva (TEST D e TEST E). Rieseguito: TEST D ora PASS — confermata la stessa causa.
- 2026-09-02T06:30 — [FASE 14] — TEST I: bug diverso, individuato per ispezione (non serviva tracing esteso questa volta): il test inizializzava sentinel 0xFF solo su ram_mem[0x300] e ram_mem[0x301] (gli indirizzi che DEVONO essere scritti), ma MAI su ram_mem[0x302] (l'indirizzo che NON deve essere toccato) — il controllo `if (ram_mem[0x302] !== 8'hFF)` falliva quindi SEMPRE, confrontando il valore iniziale non scritto (0x00, da inizializzazione globale della RAM sintetica a inizio simulazione) contro un sentinel 0xFF mai effettivamente impostato. Bug nel TESTBENCH, non nell'RTL. Aggiunta `ram_mem[16'h0302] = 8'hFF;` prima della transazione. Anche corretta la larghezza del campo `label` (input reg) nei task `report`/`miso_check` da 128 a 512 bit, per evitare troncamento dei nomi dei test più lunghi nell'output (bug cosmetico separato, notato durante il debug).
- 2026-09-02T06:32 — [FASE 14] — Rieseguito dopo entrambi i fix: TUTTI E 10 I TEST PASSANO. Nessuna modifica a rtl/spi_engine.v richiesta in questa fase di bring-up: entrambi i bug erano nel testbench (stessa classe di race già vista in Fase 13, più un errore di inizializzazione del sentinel).
- 2026-09-02T06:35 — [FASE 14] — Verifica con toolchain ECP5 reale (stessa procedura di Fase 13): `yosys -p "synth_ecp5 -json spi_engine.json -top spi_engine" rtl/spi_engine.v` -> 0 problemi, nessun latch, 208 TRELLIS_FF, 240 LUT4 + 19 CCU2C (catene di riporto per gli incrementi/decrementi di addr/len), 0 DSP utilizzati (atteso, logica di controllo pura).
- 2026-09-02T06:37 — [FASE 14] — `nextpnr-ecp5 --45k --package CABGA381 --speed 8 --json spi_engine.json --lpf-allow-unconstrained --freq 80 --textcfg spi_engine.config`: **PASS, Fmax 191.31 MHz** (target 80MHz ampiamente superato). `ecppack spi_engine.config spi_engine.bit` -> bitstream generato senza errori.
- 2026-09-02T06:38 — [FASE 14] — CONCLUSIONE: rtl/spi_engine.v implementa tutti gli 8 opcode della spec draft, verificato funzionalmente (Icarus, 10/10 test su ogni opcode + edge case) e sul toolchain FPGA reale (Yosys+nextpnr-ecp5+ecppack, PASS a 80MHz con ampio margine, nessun DSP consumato). Prossimo step: arbitro bus condiviso tra spi_engine e neuron_memory verso int8_memory_access/memory_interface, poi top-level di integrazione, poi testbench end-to-end con la sessione SPI completa della spec.
## Fase: Integrazione end-to-end SPI + RAM reale + neuron_memory (2026-09-02)
- 2026-09-02T06:45 — [FASE 15] — Domanda utente: "hai verificato con la ram reale?" Risposta onesta: no, spi_engine.v era stato verificato solo contro un modello di RAM sintetico a 2 cicli di latenza fissa. Deciso di procedere subito con l'integrazione completa (arbitro + top-level) e il test end-to-end con lo stack PSRAM reale (memory_interface + psram_controller + psram_model), lo stesso già usato in neuron_memory_tb.v.
- 2026-09-02T06:47 — [FASE 15] — Utente, mentre si scriveva il top-level: "la ram non sta nella disponibilità di altri, solo della FPGA (giusto per essere precisi)" — confermato che il design rispetta già questo vincolo: l'host esterno non ha mai un collegamento elettrico diretto alla RAM, solo tramite SPI+FPGA; i pin psram_a/psram_dq/ce_n/... sono cablati esclusivamente tra psram_controller e il chip PSRAM dentro spi_neuron_top, mai esposti all'esterno.
- 2026-09-02T06:50 — [FASE 15] — Creato rtl/mem_arbiter.v: arbitro a priorità fissa (neuron_memory > spi_engine quando entrambi richiedono nello stesso ciclo idle) tra le due porte byte-level master (spi_engine per WRITE_RAM/READ_RAM, neuron_memory per le proprie letture X/W/bias durante un run), verso un'unica porta master condivisa. Design "grant-and-forward": nessuna coda/pipeline necessaria dato che entrambi i master emettono già `req` come impulso pulito di un ciclo (stesso contratto di int8_memory_access.v).
- 2026-09-02T06:55 — [FASE 15] — Creato rtl/spi_neuron_top.v: top-level di integrazione completo. spi_slave -> spi_engine -> mem_arbiter (porta A) / neuron_memory (porta B, con rst = global_rst OR nm_soft_rst da opcode RESET) -> arbitro -> UNA istanza condivisa di int8_memory_access (bridge byte<->word) -> memory_interface -> psram_controller -> pin fisici PSRAM esterni.
- 2026-09-02T07:00 — [FASE 15] — Creato sim/spi_neuron_top_tb.v: testbench end-to-end che pilota l'INTERO stack SOLO via SPI simulato (nessuna iniezione diretta nelle porte di neuron_memory), con psram_model.v reale (non un mock). Sessione: RESET -> READ_CONFIG (verifica) -> WRITE_RAM(X=1 x32, W=1 x32, bias=0) nella PSRAM reale -> READ_RAM di verifica (conferma che la scrittura sia realmente arrivata in RAM, non solo accettata) -> SET_BASE x3 -> START -> poll STATUS -> READ_OUTPUT, ripetuto per 3 scenari (SUM=32, SATURATION=127 dopo un secondo WRITE_RAM che ricarica i pesi a 4, RELU=0 dopo un terzo WRITE_RAM con pesi a -1). Margini di clock SPI differenziati: HB_RAM=40 cicli/mezzo-bit per i comandi che toccano la RAM (margine ampio rispetto alla latenza reale di psram_controller, ACCESS_CYCLES=ceil(70ns*80MHz)=6 cicli, più overhead della catena completa), HB_REG=8 cicli per i comandi di solo registro (SET_BASE/START/STATUS/READ_OUTPUT/READ_CONFIG/RESET, che non toccano mai l'arbitro/RAM).
- 2026-09-02T07:05 — [FASE 15] — Compilato: `iverilog -g2012 -o <tmp> rtl/*.v sim/psram_model.v sim/spi_neuron_top_tb.v`. Pulito al primo tentativo. Eseguito: TUTTO PASSA AL PRIMO TENTATIVO — READ_CONFIG PASS, READ_RAM verify PASS (X[0]=0x01 confermato scritto realmente in PSRAM), TEST 1 (SUM=32) PASS, TEST 2 (SATURATION=127) PASS, TEST 3 (RELU=0) PASS. Nessun bug trovato in questa integrazione: sia l'arbitro sia il ponte byte<->word condiviso funzionano correttamente al primo colpo con lo stack PSRAM reale.
- 2026-09-02T07:10 — [FASE 15] — Verifica con toolchain ECP5 reale del top-level completo (prima volta che il percorso PSRAM entra nella sintesi reale in questa sessione): `yosys -p "synth_ecp5 -json ... -top spi_neuron_top" rtl/spi_neuron_top.v rtl/spi_slave.v rtl/spi_engine.v rtl/mem_arbiter.v rtl/neuron_memory.v rtl/neuron_parallel.v rtl/mac8.v rtl/mac_unit.v rtl/int8_memory_access.v rtl/memory_interface.v rtl/psram_controller.v` (N_NEURONS=1, PARALLEL=8, N_INPUTS=32, come in neuron_memory_tb.v). Risultato: 0 problemi dal CHECK pass, 8 MULT18X18D (=N_NEURONS×PARALLEL, corretto), 16 $_TBUF_ (buffer tri-state per psram_dq bidirezionale, corretto), 1 warning noto/atteso ("Yosys has only limited support for tri-state logic") proveniente da psram_controller.v preesistente, non dal codice nuovo.
- 2026-09-02T07:15 — [FASE 15] — `nextpnr-ecp5 --45k --package CABGA381 --speed 8 --freq 80 --lpf-allow-unconstrained`: **FAIL, Fmax ~52.58 MHz** (target 80MHz). FINDING IMPORTANTE, verificato con attenzione prima di riportarlo: il percorso critico (19.02ns) è interamente contenuto dentro u_neuron_memory.u_neuron (rtl/neuron_parallel.v, in particolare la catena di riporto CCU2C del comparatore di saturazione ">127" a riga 127) — ZERO contributo da spi_slave, spi_engine, mem_arbiter o dallo stack PSRAM. Coerente con il benchmark preesistente (docs/FPGA-Neural-Datapatch-Benchmark.md): PARALLEL=8 era già noto FAIL a 80MHz (61.71MHz nel benchmark isolato).
- 2026-09-02T07:18 — [FASE 15] — Verificata l'ipotesi: risintetizzato con `chparam -set PARALLEL 2 spi_neuron_top` (confermato applicato: 2 MULT18X18D invece di 8) — la configurazione che nel benchmark isolato raggiungeva 87.88MHz PASS. Risultato nel design INTEGRATO completo: **ANCORA FAIL, Fmax ~55.85 MHz**. Il percorso critico resta identico in natura (stesso comparatore di saturazione in neuron_parallel.v, stesso genere di catena CCU2C), ma la sua temporizzazione peggiora sensibilmente rispetto al benchmark isolato (11.38ns isolato -> 17.91ns nel design integrato, +57% circa) per congestione di piazzamento/routing dovuta alla logica SPI/arbitro/PSRAM circostante che compete per le stesse risorse FPGA — non per esaurimento di risorse (utilizzo DSP solo 2%, LUT/FF anch'essi bassi rispetto alla capacità totale del dispositivo; `--lpf-allow-unconstrained` non aiuta il posizionamento).
- 2026-09-02T07:20 — [FASE 15] — VALUTAZIONE: questo NON è un bug introdotto dal lavoro SPI di questa sessione (la logica SPI/arbitro stessa sintetizza pulita e velocissima isolatamente: spi_slave 403MHz, spi_engine 191MHz). È un problema di temporizzazione a livello SISTEMA (placement/routing quando tutti i blocchi coesistono nello stesso design), che richiede tipicamente vincoli di floorplanning (region constraint LPF) o pipeline aggiuntive nel comparatore di saturazione di neuron_parallel.v per essere risolto — esplicitamente nello scope della Fase 7 (Optimization: "pipeline depth", "FPGA resource utilization") del roadmap, non di questa fase. NESSUNA modifica al core di calcolo tentata per questo motivo, coerentemente con la policy di progetto.
- 2026-09-02T07:22 — [FASE 15] — CONCLUSIONE: l'integrazione SPI+RAM reale+neuron_memory è funzionalmente CORRETTA e verificata end-to-end (simulazione Icarus con PSRAM reale, 3/3 scenari PASS) e la logica SPI stessa è velocissima in sintesi isolata. Il target di temporizzazione a 80MHz per il SISTEMA COMPLETO non è ancora raggiunto con l'attuale floorplanning automatico — finding onesto, documentato in dettaglio, da affrontare in una fase di ottimizzazione futura (Fase 7) e non bloccante per la correttezza funzionale della Fase 4 SPI appena completata.
+27 -4
View File
@@ -753,10 +753,33 @@ Implement:
- status and control. - status and control.
- [x] Protocol/opcode set drafted — see §8.1 SPI Protocol v1 - [x] Protocol/opcode set drafted — see §8.1 SPI Protocol v1
- [ ] SPI controller RTL (physical layer: shift register, CS/clock sync) - [x] SPI controller RTL `rtl/spi_slave.v` (physical layer: Mode 0, MSB-first, 3-stage CDC synchronizer for SCLK/MOSI/CS_N)
- [ ] Register bank RTL (SET_BASE, sticky STATUS, READ_CONFIG constants) - [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)
- [ ] RAM access passthrough RTL (WRITE_RAM/READ_RAM -> memory_interface) - [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`
- [ ] Testbench (SPI master BFM + full stack, mirroring neuron_memory_tb.v style) - [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 ## Phase 5 — Multi-Layer Network
+147
View File
@@ -0,0 +1,147 @@
`timescale 1ns/1ps
// ================================================================
// MEM_ARBITER
//
// Arbitrates a single shared byte-level memory master port (feeding
// a shared int8_memory_access -> memory_interface -> psram_controller
// chain) between two byte-level requesters:
//
// Port A: spi_engine.v (WRITE_RAM / READ_RAM opcodes)
// Port B: neuron_memory.v (its own X/W/bias reads during a run)
//
// Fixed priority B > A when both request on the same idle cycle
// (an in-progress inference is treated as more time-critical than a
// newly-arriving SPI RAM access). Once a port is granted, the
// arbiter holds ownership until that single transaction's m_ready
// pulse, then releases -- both A and B already issue `req` as a
// clean one-cycle pulse (matching int8_memory_access's own
// contract), so a simple grant-and-forward design is sufficient;
// no request queuing/pipelining is needed.
// ================================================================
module mem_arbiter #(
parameter ADDR_WIDTH = 22
)(
input wire clk,
input wire rst,
// ------------------------------------------------------------
// Port A - spi_engine
// ------------------------------------------------------------
input wire a_req,
input wire a_wr,
input wire [ADDR_WIDTH-1:0] a_addr,
input wire signed [7:0] a_wdata,
output reg signed [7:0] a_rdata,
output reg a_ready,
// ------------------------------------------------------------
// Port B - neuron_memory
// ------------------------------------------------------------
input wire b_req,
input wire b_wr,
input wire [ADDR_WIDTH-1:0] b_addr,
input wire signed [7:0] b_wdata,
output reg signed [7:0] b_rdata,
output reg b_ready,
// ------------------------------------------------------------
// Shared master port
// ------------------------------------------------------------
output reg m_req,
output reg m_wr,
output reg [ADDR_WIDTH-1:0] m_addr,
output reg signed [7:0] m_wdata,
input wire signed [7:0] m_rdata,
input wire m_ready
);
localparam SEL_NONE = 2'd0;
localparam SEL_A = 2'd1;
localparam SEL_B = 2'd2;
reg [1:0] owner;
always @(posedge clk) begin
if (rst) begin
owner <= SEL_NONE;
m_req <= 1'b0;
m_wr <= 1'b0;
m_addr <= {ADDR_WIDTH{1'b0}};
m_wdata <= 8'sd0;
a_rdata <= 8'sd0;
a_ready <= 1'b0;
b_rdata <= 8'sd0;
b_ready <= 1'b0;
end else begin
m_req <= 1'b0;
a_ready <= 1'b0;
b_ready <= 1'b0;
case (owner)
SEL_NONE: begin
if (b_req) begin
owner <= SEL_B;
m_req <= 1'b1;
m_wr <= b_wr;
m_addr <= b_addr;
m_wdata <= b_wdata;
end else if (a_req) begin
owner <= SEL_A;
m_req <= 1'b1;
m_wr <= a_wr;
m_addr <= a_addr;
m_wdata <= a_wdata;
end
end
SEL_A: begin
if (m_ready) begin
a_rdata <= m_rdata;
a_ready <= 1'b1;
owner <= SEL_NONE;
end
end
SEL_B: begin
if (m_ready) begin
b_rdata <= m_rdata;
b_ready <= 1'b1;
owner <= SEL_NONE;
end
end
default: begin
owner <= SEL_NONE;
end
endcase
end
end
endmodule
+554
View File
@@ -0,0 +1,554 @@
`timescale 1ns/1ps
// ================================================================
// SPI_ENGINE - opcode/protocol FSM + register bank
//
// Implements the v1 draft protocol in docs/FPGA-NeuralNetwork-Engine.md
// §8.1 on top of the byte-level interface exposed by spi_slave.v.
// One opcode byte per CS-low transaction (§8.1 framing).
//
// IMPORTANT (see rtl/spi_slave.v for the full contract):
// - tx_byte is driven COMBINATIONALLY from current state, so it is
// always correct whenever spi_slave.v prefetches it (at cs_fell
// and at every byte boundary) -- no explicit reaction needed.
// - Any stateful pointer (RAM address, response byte index) is
// advanced on rx_valid, which fires exactly once per REAL byte
// transferred -- never on tx_byte_req, which fires one extra
// "phantom" time after the last byte of a transaction.
//
// RAM byte-level master port uses the same convention as
// neuron_memory.v's external mem_* port (byte address, byte data,
// req/ready handshake) so it can share an arbiter + int8_memory_access
// + memory_interface chain with neuron_memory at the top level.
//
// v1 LIMITATION (documented, not yet solved): WRITE_RAM/READ_RAM
// have no backpressure to the SPI master. Each received/produced
// byte must be fully processed by this engine before the next
// SCLK-driven byte boundary arrives, i.e. the host must not clock
// RAM-touching commands faster than one RAM transaction (a handful
// of `clk` cycles) per SPI byte period. This is a reasonable
// constraint for bulk-loading weights/bias/input at initialization,
// not a real-time path.
// ================================================================
module spi_engine #(
parameter ADDR_WIDTH = 22,
parameter DATA_WIDTH = 8,
parameter N_INPUTS = 32,
parameter N_NEURONS = 1,
parameter PARALLEL = 8
)(
input wire clk,
input wire rst,
// ------------------------------------------------------------
// Byte-level interface from/to spi_slave.v
// ------------------------------------------------------------
input wire [7:0] rx_byte,
input wire rx_valid,
input wire cs_start,
input wire cs_end,
output wire [7:0] tx_byte,
input wire tx_byte_req, // unused on purpose, see header note
// ------------------------------------------------------------
// RAM byte-level master port (byte address, byte data)
// ------------------------------------------------------------
output reg ram_req,
output reg ram_wr,
output reg [ADDR_WIDTH-1:0] ram_addr,
output reg signed [7:0] ram_wdata,
input wire signed [7:0] ram_rdata,
input wire ram_ready,
// ------------------------------------------------------------
// neuron_memory control
// ------------------------------------------------------------
output reg [ADDR_WIDTH-1:0] x_base,
output reg [ADDR_WIDTH-1:0] w_base,
output reg [ADDR_WIDTH-1:0] bias_addr,
output reg nm_start,
input wire nm_busy,
input wire nm_done, // one-cycle pulse
input wire signed [DATA_WIDTH*N_NEURONS-1:0] y_bus,
output reg nm_soft_rst
);
// ============================================================
// OPCODES (docs §8.1 -- values are draft/example, see header)
// ============================================================
localparam OP_NOP = 8'h00;
localparam OP_WRITE_RAM = 8'h01;
localparam OP_READ_RAM = 8'h02;
localparam OP_RESET = 8'h0F;
localparam OP_SET_BASE = 8'h10;
localparam OP_START = 8'h20;
localparam OP_STATUS = 8'h21;
localparam OP_READ_OUTPUT = 8'h22;
localparam OP_READ_CONFIG = 8'h30;
// SET_BASE selector values
localparam SEL_X_BASE = 8'h00;
localparam SEL_W_BASE = 8'h01;
localparam SEL_BIAS_ADDR = 8'h02;
// ============================================================
// STATES
// ============================================================
localparam ST_OPCODE = 4'd0;
localparam ST_SETBASE_SEL = 4'd1;
localparam ST_ADDR = 4'd2; // 3 bytes, MSB first
localparam ST_LEN = 4'd3; // 2 bytes, MSB first
localparam ST_WRITE_DATA = 4'd4;
localparam ST_WRITE_ISSUE = 4'd5;
localparam ST_WRITE_WAIT = 4'd6;
localparam ST_READ_ISSUE = 4'd7;
localparam ST_READ_WAIT = 4'd8;
localparam ST_READ_DATA = 4'd9;
localparam ST_RESP = 4'd10; // STATUS / READ_OUTPUT / READ_CONFIG
localparam ST_IGNORE = 4'd11;
reg [3:0] state;
reg [7:0] opcode;
// Generic byte-position counter for ADDR (0..2) / LEN (0..1)
reg [1:0] byte_pos;
reg [23:0] addr_acc; // 3-byte accumulator, byte address
reg [15:0] len_acc; // 2-byte accumulator, transfer length
reg [15:0] len_remaining;
reg [ADDR_WIDTH-1:0] cur_addr;
reg pending_write;
reg [7:0] pending_wdata;
reg [7:0] cur_read_byte;
reg [3:0] resp_index; // response byte index (max needed: 8, READ_CONFIG)
reg [3:0] resp_len; // total bytes for the current response opcode
// ============================================================
// STICKY STATUS.done LATCH
//
// neuron_memory.done is a one-cycle pulse; STATUS must hold it
// until the host actually reads STATUS (or issues RESET), or a
// slow SPI poll would almost certainly miss it. See docs §8.1.
// ============================================================
reg status_done_sticky;
wire status_read_now = (state == ST_RESP) && (opcode == OP_STATUS) && rx_valid;
always @(posedge clk) begin
if (rst) begin
status_done_sticky <= 1'b0;
end else if (nm_soft_rst) begin
status_done_sticky <= 1'b0;
end else if (nm_done) begin
status_done_sticky <= 1'b1;
end else if (status_read_now) begin
status_done_sticky <= 1'b0;
end
end
// ============================================================
// tx_byte: fully combinational, always reflects "the byte to
// send right now" for the current state/response index. This
// is what spi_slave.v prefetches via tx_byte_req -- see the
// module header for why this must not depend on tx_byte_req.
// ============================================================
reg [7:0] tx_byte_comb;
always @(*) begin
tx_byte_comb = 8'h00;
case (state)
ST_READ_DATA: tx_byte_comb = cur_read_byte;
ST_RESP: begin
case (opcode)
OP_STATUS: tx_byte_comb = {6'b0, status_done_sticky, nm_busy};
OP_READ_OUTPUT: begin
if (resp_index < N_NEURONS)
tx_byte_comb = y_bus[resp_index*DATA_WIDTH +: DATA_WIDTH];
else
tx_byte_comb = 8'h00;
end
OP_READ_CONFIG: begin
case (resp_index)
4'd0: tx_byte_comb = ADDR_WIDTH[7:0];
4'd1: tx_byte_comb = N_INPUTS[15:8];
4'd2: tx_byte_comb = N_INPUTS[7:0];
4'd3: tx_byte_comb = N_NEURONS[7:0];
4'd4: tx_byte_comb = PARALLEL[7:0];
4'd5: tx_byte_comb = DATA_WIDTH[7:0];
4'd6: tx_byte_comb = 8'h00; // protocol version 0x0001, high byte
4'd7: tx_byte_comb = 8'h01; // protocol version 0x0001, low byte
default: tx_byte_comb = 8'h00;
endcase
end
default: tx_byte_comb = 8'h00;
endcase
end
default: tx_byte_comb = 8'h00;
endcase
end
assign tx_byte = tx_byte_comb;
// ============================================================
// MAIN FSM
// ============================================================
always @(posedge clk) begin
if (rst) begin
state <= ST_OPCODE;
opcode <= 8'h00;
byte_pos <= 2'd0;
addr_acc <= 24'h0;
len_acc <= 16'h0;
len_remaining <= 16'h0;
cur_addr <= {ADDR_WIDTH{1'b0}};
pending_write <= 1'b0;
pending_wdata <= 8'h00;
cur_read_byte <= 8'h00;
resp_index <= 4'd0;
resp_len <= 4'd0;
ram_req <= 1'b0;
ram_wr <= 1'b0;
ram_addr <= {ADDR_WIDTH{1'b0}};
ram_wdata <= 8'sd0;
x_base <= {ADDR_WIDTH{1'b0}};
w_base <= {ADDR_WIDTH{1'b0}};
bias_addr <= {ADDR_WIDTH{1'b0}};
nm_start <= 1'b0;
nm_soft_rst <= 1'b0;
end else begin
// --------------------------------------------------
// Default pulses
// --------------------------------------------------
ram_req <= 1'b0;
nm_start <= 1'b0;
nm_soft_rst <= 1'b0;
if (cs_end) begin
// End of transaction: always return to opcode wait,
// regardless of where we were (defensive: a short
// or malformed transaction cannot wedge the engine).
state <= ST_OPCODE;
end else begin
case (state)
// =============================================
// OPCODE
// =============================================
ST_OPCODE: begin
if (rx_valid) begin
opcode <= rx_byte;
byte_pos <= 2'd0;
case (rx_byte)
OP_WRITE_RAM, OP_READ_RAM: begin
addr_acc <= 24'h0;
state <= ST_ADDR;
end
OP_SET_BASE: begin
state <= ST_SETBASE_SEL;
end
OP_START: begin
if (!nm_busy)
nm_start <= 1'b1;
state <= ST_IGNORE;
end
OP_RESET: begin
nm_soft_rst <= 1'b1;
state <= ST_IGNORE;
end
OP_STATUS: begin
resp_index <= 4'd0;
resp_len <= 4'd1;
state <= ST_RESP;
end
OP_READ_OUTPUT: begin
resp_index <= 4'd0;
resp_len <= N_NEURONS[3:0];
state <= ST_RESP;
end
OP_READ_CONFIG: begin
resp_index <= 4'd0;
resp_len <= 4'd8;
state <= ST_RESP;
end
default: begin // OP_NOP and unknown opcodes
state <= ST_IGNORE;
end
endcase
end
end
// =============================================
// SET_BASE: 1 selector byte, then 3 addr bytes
// =============================================
ST_SETBASE_SEL: begin
if (rx_valid) begin
addr_acc <= 24'h0;
// Reuse `len_acc[7:0]` as a 1-byte stash
// for the selector between states.
len_acc[7:0] <= rx_byte;
state <= ST_ADDR;
end
end
// =============================================
// ADDR: 3 bytes, MSB first
// Shared by WRITE_RAM / READ_RAM / SET_BASE.
// =============================================
ST_ADDR: begin
if (rx_valid) begin
addr_acc <= {addr_acc[15:0], rx_byte};
if (byte_pos == 2'd2) begin
byte_pos <= 2'd0;
if (opcode == OP_SET_BASE) begin
case (len_acc[7:0])
SEL_X_BASE: x_base <= {addr_acc[15:0], rx_byte};
SEL_W_BASE: w_base <= {addr_acc[15:0], rx_byte};
SEL_BIAS_ADDR: bias_addr <= {addr_acc[15:0], rx_byte};
default: ; // reserved selector: ignored
endcase
state <= ST_IGNORE;
end else begin
cur_addr <= {addr_acc[15:0], rx_byte};
len_acc <= 16'h0;
state <= ST_LEN;
end
end else begin
byte_pos <= byte_pos + 2'd1;
end
end
end
// =============================================
// LEN: 2 bytes, MSB first (WRITE_RAM / READ_RAM)
// =============================================
ST_LEN: begin
if (rx_valid) begin
len_acc <= {len_acc[7:0], rx_byte};
if (byte_pos == 2'd1) begin
len_remaining <= {len_acc[7:0], rx_byte};
byte_pos <= 2'd0;
if ({len_acc[7:0], rx_byte} == 16'h0) begin
state <= ST_IGNORE;
end else if (opcode == OP_WRITE_RAM) begin
state <= ST_WRITE_DATA;
end else begin // OP_READ_RAM
state <= ST_READ_ISSUE;
end
end else begin
byte_pos <= byte_pos + 2'd1;
end
end
end
// =============================================
// WRITE_RAM: accept one data byte, write it,
// repeat for len_remaining bytes.
// =============================================
ST_WRITE_DATA: begin
if (rx_valid) begin
pending_wdata <= rx_byte;
state <= ST_WRITE_ISSUE;
end
end
ST_WRITE_ISSUE: begin
ram_req <= 1'b1;
ram_wr <= 1'b1;
ram_addr <= cur_addr;
ram_wdata <= $signed(pending_wdata);
state <= ST_WRITE_WAIT;
end
ST_WRITE_WAIT: begin
if (ram_ready) begin
cur_addr <= cur_addr + 1'b1;
len_remaining <= len_remaining - 16'd1;
if (len_remaining == 16'd1)
state <= ST_IGNORE;
else
state <= ST_WRITE_DATA;
end
end
// =============================================
// READ_RAM: prefetch one byte, serve it, repeat.
// =============================================
ST_READ_ISSUE: begin
ram_req <= 1'b1;
ram_wr <= 1'b0;
ram_addr <= cur_addr;
state <= ST_READ_WAIT;
end
ST_READ_WAIT: begin
if (ram_ready) begin
cur_read_byte <= ram_rdata[7:0];
state <= ST_READ_DATA;
end
end
ST_READ_DATA: begin
// rx_valid marks that the response byte
// currently on tx_byte has been shifted out
// and a (dummy) MOSI byte was received in
// exchange -- advance to the next one.
if (rx_valid) begin
cur_addr <= cur_addr + 1'b1;
len_remaining <= len_remaining - 16'd1;
if (len_remaining == 16'd1)
state <= ST_IGNORE;
else
state <= ST_READ_ISSUE;
end
end
// =============================================
// STATUS / READ_OUTPUT / READ_CONFIG response
// =============================================
ST_RESP: begin
if (rx_valid) begin
if (resp_index == resp_len - 4'd1)
state <= ST_IGNORE;
else
resp_index <= resp_index + 4'd1;
end
end
// =============================================
// IGNORE: transaction's meaningful bytes are
// done; ignore anything else until cs_end.
// =============================================
ST_IGNORE: begin
// intentionally empty
end
default: begin
state <= ST_OPCODE;
end
endcase
end
end
end
endmodule
+268
View File
@@ -0,0 +1,268 @@
`timescale 1ns/1ps
// ================================================================
// SPI_NEURON_TOP
//
// Full Phase 3 + Phase 4 integration: SPI host interface (spi_slave
// + spi_engine, docs §8.1) driving neuron_memory.v (Phase 3,
// N_NEURONS>=1) through a shared PSRAM (memory_interface +
// psram_controller), arbitrated between spi_engine's own RAM access
// (WRITE_RAM/READ_RAM opcodes) and neuron_memory's own X/W/bias
// reads during a run.
//
// neuron_memory's own `rst` is the global reset OR'd with the
// RESET opcode's soft-reset pulse from spi_engine, so a host can
// recover the compute engine over SPI without a physical reset
// (RAM contents are untouched either way).
// ================================================================
module spi_neuron_top #(
parameter ADDR_WIDTH = 22,
parameter DATA_WIDTH = 8,
parameter N_INPUTS = 32,
parameter N_NEURONS = 1,
parameter PARALLEL = 8,
parameter ACC_WIDTH = 32,
parameter MEM_DATA_WIDTH = 16,
parameter CLK_FREQ_MHZ = 80
)(
input wire clk,
input wire rst,
// ------------------------------------------------------------
// SPI host interface
// ------------------------------------------------------------
input wire sclk,
input wire mosi,
output wire miso,
input wire cs_n,
// ------------------------------------------------------------
// PSRAM physical interface
// ------------------------------------------------------------
output wire [ADDR_WIDTH-1:0] psram_a,
inout wire [MEM_DATA_WIDTH-1:0] psram_dq,
output wire psram_ce_n,
output wire psram_oe_n,
output wire psram_we_n,
output wire psram_lb_n,
output wire psram_ub_n,
output wire psram_zz_n
);
// ============================================================
// SPI PHYSICAL LAYER
// ============================================================
wire [7:0] rx_byte;
wire rx_valid;
wire cs_start;
wire cs_end;
wire [7:0] tx_byte;
wire tx_byte_req;
spi_slave u_spi_slave (
.clk(clk), .rst(rst),
.sclk(sclk), .mosi(mosi), .miso(miso), .cs_n(cs_n),
.rx_byte(rx_byte), .rx_valid(rx_valid),
.tx_byte(tx_byte), .tx_byte_req(tx_byte_req),
.cs_active(), .cs_start(cs_start), .cs_end(cs_end)
);
// ============================================================
// SPI PROTOCOL ENGINE
// ============================================================
wire spi_ram_req;
wire spi_ram_wr;
wire [ADDR_WIDTH-1:0] spi_ram_addr;
wire signed [7:0] spi_ram_wdata;
wire signed [7:0] spi_ram_rdata;
wire spi_ram_ready;
wire [ADDR_WIDTH-1:0] x_base;
wire [ADDR_WIDTH-1:0] w_base;
wire [ADDR_WIDTH-1:0] bias_addr;
wire nm_start;
wire nm_busy;
wire nm_done;
wire signed [DATA_WIDTH*N_NEURONS-1:0] y_bus;
wire nm_soft_rst;
spi_engine #(
.ADDR_WIDTH(ADDR_WIDTH),
.DATA_WIDTH(DATA_WIDTH),
.N_INPUTS(N_INPUTS),
.N_NEURONS(N_NEURONS),
.PARALLEL(PARALLEL)
) u_spi_engine (
.clk(clk), .rst(rst),
.rx_byte(rx_byte), .rx_valid(rx_valid),
.cs_start(cs_start), .cs_end(cs_end),
.tx_byte(tx_byte), .tx_byte_req(tx_byte_req),
.ram_req(spi_ram_req), .ram_wr(spi_ram_wr),
.ram_addr(spi_ram_addr), .ram_wdata(spi_ram_wdata),
.ram_rdata(spi_ram_rdata), .ram_ready(spi_ram_ready),
.x_base(x_base), .w_base(w_base), .bias_addr(bias_addr),
.nm_start(nm_start), .nm_busy(nm_busy), .nm_done(nm_done),
.y_bus(y_bus),
.nm_soft_rst(nm_soft_rst)
);
// ============================================================
// NEURON MEMORY
//
// rst is the global reset OR'd with the SPI RESET opcode pulse.
// ============================================================
wire nm_rst = rst | nm_soft_rst;
wire nm_ram_req;
wire nm_ram_wr;
wire [ADDR_WIDTH-1:0] nm_ram_addr;
wire signed [7:0] nm_ram_wdata;
wire signed [7:0] nm_ram_rdata;
wire nm_ram_ready;
neuron_memory #(
.ADDR_WIDTH(ADDR_WIDTH),
.DATA_WIDTH(DATA_WIDTH),
.N_INPUTS(N_INPUTS),
.N_NEURONS(N_NEURONS),
.PARALLEL(PARALLEL),
.ACC_WIDTH(ACC_WIDTH)
) u_neuron_memory (
.clk(clk), .rst(nm_rst),
.start(nm_start),
.mem_req(nm_ram_req), .mem_wr(nm_ram_wr),
.mem_addr(nm_ram_addr), .mem_wdata(nm_ram_wdata),
.mem_rdata(nm_ram_rdata), .mem_ready(nm_ram_ready),
.x_base(x_base), .w_base(w_base), .bias_addr(bias_addr),
.y_bus(y_bus), .busy(nm_busy), .done(nm_done)
);
// ============================================================
// SHARED MEMORY ARBITER
// ============================================================
wire arb_req;
wire arb_wr;
wire [ADDR_WIDTH-1:0] arb_addr;
wire signed [7:0] arb_wdata;
wire signed [7:0] arb_rdata;
wire arb_ready;
mem_arbiter #(
.ADDR_WIDTH(ADDR_WIDTH)
) u_arbiter (
.clk(clk), .rst(rst),
.a_req(spi_ram_req), .a_wr(spi_ram_wr),
.a_addr(spi_ram_addr), .a_wdata(spi_ram_wdata),
.a_rdata(spi_ram_rdata), .a_ready(spi_ram_ready),
.b_req(nm_ram_req), .b_wr(nm_ram_wr),
.b_addr(nm_ram_addr), .b_wdata(nm_ram_wdata),
.b_rdata(nm_ram_rdata), .b_ready(nm_ram_ready),
.m_req(arb_req), .m_wr(arb_wr),
.m_addr(arb_addr), .m_wdata(arb_wdata),
.m_rdata(arb_rdata), .m_ready(arb_ready)
);
// ============================================================
// BYTE <-> WORD BRIDGE (shared, single instance)
// ============================================================
wire i8_mem_req;
wire i8_mem_wr;
wire [ADDR_WIDTH-1:0] i8_mem_addr;
wire [MEM_DATA_WIDTH-1:0] i8_mem_wdata;
wire i8_mem_lb_n;
wire i8_mem_ub_n;
wire [MEM_DATA_WIDTH-1:0] i8_mem_rdata;
wire i8_mem_ready;
int8_memory_access #(
.ADDR_WIDTH(ADDR_WIDTH)
) u_int8_access (
.clk(clk), .rst(rst),
.req(arb_req), .wr(arb_wr), .addr(arb_addr), .wdata(arb_wdata),
.rdata(arb_rdata), .ready(arb_ready),
.mem_req(i8_mem_req), .mem_wr(i8_mem_wr),
.mem_addr(i8_mem_addr), .mem_wdata(i8_mem_wdata),
.mem_lb_n(i8_mem_lb_n), .mem_ub_n(i8_mem_ub_n),
.mem_rdata(i8_mem_rdata), .mem_ready(i8_mem_ready)
);
// ============================================================
// MEMORY INTERFACE / PSRAM CONTROLLER
// ============================================================
wire [MEM_DATA_WIDTH-1:0] psram_mem_rdata;
wire psram_mem_ready;
wire psram_mem_req;
wire psram_mem_wr;
wire [ADDR_WIDTH-1:0] psram_mem_addr;
wire [MEM_DATA_WIDTH-1:0] psram_mem_wdata;
wire psram_mem_lb_n;
wire psram_mem_ub_n;
memory_interface #(
.ADDR_WIDTH(ADDR_WIDTH),
.DATA_WIDTH(MEM_DATA_WIDTH)
) u_memory_if (
.clk(clk), .rst(rst),
.req(i8_mem_req), .wr(i8_mem_wr), .addr(i8_mem_addr), .wdata(i8_mem_wdata),
.lb_n(i8_mem_lb_n), .ub_n(i8_mem_ub_n),
.rdata(i8_mem_rdata), .ready(i8_mem_ready),
.mem_req(psram_mem_req), .mem_wr(psram_mem_wr),
.mem_addr(psram_mem_addr), .mem_wdata(psram_mem_wdata),
.mem_lb_n(psram_mem_lb_n), .mem_ub_n(psram_mem_ub_n),
.mem_rdata(psram_mem_rdata), .mem_ready(psram_mem_ready)
);
psram_controller #(
.ADDR_WIDTH(ADDR_WIDTH),
.DATA_WIDTH(MEM_DATA_WIDTH),
.CLK_FREQ_MHZ(CLK_FREQ_MHZ)
) u_psram_ctrl (
.clk(clk), .rst(rst),
.mem_req(psram_mem_req), .mem_wr(psram_mem_wr),
.mem_addr(psram_mem_addr), .mem_wdata(psram_mem_wdata),
.mem_lb_n(psram_mem_lb_n), .mem_ub_n(psram_mem_ub_n),
.mem_rdata(psram_mem_rdata), .mem_ready(psram_mem_ready),
.psram_a(psram_a), .psram_dq(psram_dq),
.psram_ce_n(psram_ce_n), .psram_oe_n(psram_oe_n), .psram_we_n(psram_we_n),
.psram_lb_n(psram_lb_n), .psram_ub_n(psram_ub_n), .psram_zz_n(psram_zz_n)
);
endmodule
+76960
View File
File diff suppressed because it is too large Load Diff
+3139
View File
File diff suppressed because it is too large Load Diff
+595
View File
@@ -0,0 +1,595 @@
`timescale 1ns/1ps
// ================================================================
// SPI_ENGINE TESTBENCH
//
// Drives spi_slave.v + spi_engine.v together through a simulated
// SPI master (same clk-cycle-counted BFM proven in spi_slave_tb.v),
// against a synthetic byte-RAM model (small fixed latency, isolates
// the opcode FSM from the full PSRAM stack) and a manually-driven
// neuron_memory mock (nm_busy/nm_done/y_bus driven by the test,
// x_base/w_base/bias_addr/nm_start/nm_soft_rst observed).
//
// Covers every opcode plus a few protocol edge cases:
// A: WRITE_RAM then READ_RAM back
// B: SET_BASE for X/W/BIAS
// C: START (accepted when idle, ignored when busy)
// D: STATUS (live busy bit, sticky done bit, clear-on-read)
// E: RESET (pulses nm_soft_rst, clears sticky done)
// F: READ_OUTPUT (N_NEURONS=3 bytes, neuron-major)
// G: READ_CONFIG (8-byte hardware record)
// H: NOP (no side effects)
// I: WRITE_RAM with more MOSI bytes than len (extra bytes ignored)
// J: back-to-back transactions (state resets cleanly via cs_end)
// ================================================================
module tb;
localparam ADDR_WIDTH = 22;
localparam DATA_WIDTH = 8;
localparam N_INPUTS = 32;
localparam N_NEURONS = 3;
localparam PARALLEL = 8;
localparam CLK_PERIOD = 12.5; // 80 MHz
reg clk;
reg rst;
initial begin
clk = 1'b0;
forever #(CLK_PERIOD / 2.0) clk = ~clk;
end
// ============================================================
// SPI PINS
// ============================================================
reg sclk;
reg mosi;
wire miso;
reg cs_n;
// ============================================================
// spi_slave <-> spi_engine byte-level bus
// ============================================================
wire [7:0] rx_byte;
wire rx_valid;
wire cs_start;
wire cs_end_w;
wire [7:0] tx_byte;
wire tx_byte_req;
spi_slave u_slave (
.clk(clk), .rst(rst),
.sclk(sclk), .mosi(mosi), .miso(miso), .cs_n(cs_n),
.rx_byte(rx_byte), .rx_valid(rx_valid),
.tx_byte(tx_byte), .tx_byte_req(tx_byte_req),
.cs_active(), .cs_start(cs_start), .cs_end(cs_end_w)
);
// ============================================================
// spi_engine <-> synthetic RAM
// ============================================================
wire ram_req;
wire ram_wr;
wire [ADDR_WIDTH-1:0] ram_addr;
wire signed [7:0] ram_wdata;
reg signed [7:0] ram_rdata;
reg ram_ready;
wire [ADDR_WIDTH-1:0] x_base;
wire [ADDR_WIDTH-1:0] w_base;
wire [ADDR_WIDTH-1:0] bias_addr;
wire nm_start;
reg nm_busy;
reg nm_done;
reg signed [DATA_WIDTH*N_NEURONS-1:0] y_bus;
wire nm_soft_rst;
spi_engine #(
.ADDR_WIDTH(ADDR_WIDTH),
.DATA_WIDTH(DATA_WIDTH),
.N_INPUTS(N_INPUTS),
.N_NEURONS(N_NEURONS),
.PARALLEL(PARALLEL)
) u_engine (
.clk(clk), .rst(rst),
.rx_byte(rx_byte), .rx_valid(rx_valid),
.cs_start(cs_start), .cs_end(cs_end_w),
.tx_byte(tx_byte), .tx_byte_req(tx_byte_req),
.ram_req(ram_req), .ram_wr(ram_wr), .ram_addr(ram_addr), .ram_wdata(ram_wdata),
.ram_rdata(ram_rdata), .ram_ready(ram_ready),
.x_base(x_base), .w_base(w_base), .bias_addr(bias_addr),
.nm_start(nm_start), .nm_busy(nm_busy), .nm_done(nm_done),
.y_bus(y_bus),
.nm_soft_rst(nm_soft_rst)
);
// ============================================================
// SYNTHETIC BYTE-RAM MODEL
//
// Fixed 2-cycle latency (req seen -> 1 extra cycle -> ready
// pulse), independent of the real PSRAM stack, to isolate
// spi_engine's own request/ready handshake correctness.
// ============================================================
reg [7:0] ram_mem [0:1023];
localparam RAM_IDLE = 1'b0;
localparam RAM_WAIT = 1'b1;
reg ram_state;
reg [ADDR_WIDTH-1:0] ram_addr_latched;
reg ram_wr_latched;
reg signed [7:0] ram_wdata_latched;
always @(posedge clk) begin
if (rst) begin
ram_state <= RAM_IDLE;
ram_ready <= 1'b0;
ram_rdata <= 8'sd0;
end else begin
ram_ready <= 1'b0;
case (ram_state)
RAM_IDLE: begin
if (ram_req) begin
ram_addr_latched <= ram_addr;
ram_wr_latched <= ram_wr;
ram_wdata_latched <= ram_wdata;
ram_state <= RAM_WAIT;
end
end
RAM_WAIT: begin
if (ram_wr_latched)
ram_mem[ram_addr_latched] <= ram_wdata_latched;
else
ram_rdata <= $signed(ram_mem[ram_addr_latched]);
ram_ready <= 1'b1;
ram_state <= RAM_IDLE;
end
endcase
end
end
// ============================================================
// SPI MASTER BFM (same pattern as sim/spi_slave_tb.v)
// ============================================================
task clk_wait;
input integer n;
integer k;
begin
for (k = 0; k < n; k = k + 1)
@(posedge clk);
end
endtask
task spi_begin;
input integer half_bit_cycles;
begin
cs_n = 1'b1;
sclk = 1'b0;
mosi = 1'b0;
clk_wait(half_bit_cycles * 2);
cs_n = 1'b0;
clk_wait(half_bit_cycles * 2);
end
endtask
task spi_end;
input integer half_bit_cycles;
begin
clk_wait(half_bit_cycles * 2);
cs_n = 1'b1;
clk_wait(half_bit_cycles * 2);
end
endtask
task spi_xfer_byte;
input [7:0] tx;
input integer half_bit_cycles;
output [7:0] rx;
integer i;
reg [7:0] rx_acc;
begin
rx_acc = 8'h00;
for (i = 7; i >= 0; i = i - 1) begin
mosi = tx[i];
clk_wait(half_bit_cycles);
sclk = 1'b1;
rx_acc[i] = miso;
clk_wait(half_bit_cycles);
sclk = 1'b0;
clk_wait(half_bit_cycles);
end
rx = rx_acc;
end
endtask
localparam HB = 6; // half-bit cycles for all transfers in this bench
// ============================================================
// nm_start / nm_soft_rst pulse latches (declared here, ahead of
// the main initial block below, since Icarus in -g2012 mode
// still requires declaration-before-use for plain Verilog regs)
// ============================================================
reg nm_start_seen;
reg nm_soft_rst_seen;
always @(posedge clk) begin
if (nm_start) nm_start_seen <= 1'b1;
if (nm_soft_rst) nm_soft_rst_seen <= 1'b1;
end
reg [7:0] rx_tmp;
integer errors;
integer errors_before;
integer i;
// ============================================================
// MAIN
// ============================================================
initial begin
$dumpfile("sim/spi_engine.vcd");
$dumpvars(0, tb);
rst = 1'b1;
cs_n = 1'b1;
sclk = 1'b0;
mosi = 1'b0;
nm_busy = 1'b0;
nm_done = 1'b0;
y_bus = 0;
errors = 0;
for (i = 0; i < 1024; i = i + 1)
ram_mem[i] = 8'h00;
repeat (5) @(posedge clk);
rst = 1'b0;
repeat (5) @(posedge clk);
$display("");
$display("========================================");
$display("SPI_ENGINE TEST");
$display("========================================");
// --------------------------------------------------------
// TEST A: WRITE_RAM then READ_RAM back
// WRITE_RAM opcode=0x01, addr=0x000010 (3B), len=0x0004 (2B),
// data = 11 22 33 44.
// READ_RAM opcode=0x02, addr=0x000010 (3B), len=0x0004 (2B).
// --------------------------------------------------------
errors_before = errors;
spi_begin(HB);
spi_xfer_byte(8'h01, HB, rx_tmp); // WRITE_RAM
spi_xfer_byte(8'h00, HB, rx_tmp); // addr[23:16]
spi_xfer_byte(8'h00, HB, rx_tmp); // addr[15:8]
spi_xfer_byte(8'h10, HB, rx_tmp); // addr[7:0]
spi_xfer_byte(8'h00, HB, rx_tmp); // len[15:8]
spi_xfer_byte(8'h04, HB, rx_tmp); // len[7:0]
spi_xfer_byte(8'h11, HB, rx_tmp);
spi_xfer_byte(8'h22, HB, rx_tmp);
spi_xfer_byte(8'h33, HB, rx_tmp);
spi_xfer_byte(8'h44, HB, rx_tmp);
spi_end(HB);
clk_wait(4);
spi_begin(HB);
spi_xfer_byte(8'h02, HB, rx_tmp); // READ_RAM
spi_xfer_byte(8'h00, HB, rx_tmp); // addr[23:16]
spi_xfer_byte(8'h00, HB, rx_tmp); // addr[15:8]
spi_xfer_byte(8'h10, HB, rx_tmp); // addr[7:0]
spi_xfer_byte(8'h00, HB, rx_tmp); // len[15:8]
spi_xfer_byte(8'h04, HB, rx_tmp); // len[7:0]
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, 8'h11, "A: byte0");
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, 8'h22, "A: byte1");
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, 8'h33, "A: byte2");
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, 8'h44, "A: byte3");
spi_end(HB);
report("TEST A: WRITE_RAM / READ_RAM");
// --------------------------------------------------------
// TEST B: SET_BASE for X, W, BIAS
// --------------------------------------------------------
errors_before = errors;
set_base(8'h00, 22'h000001); // X_BASE
set_base(8'h01, 22'h000101); // W_BASE
set_base(8'h02, 22'h000201); // BIAS_ADDR
clk_wait(2);
if (x_base !== 22'h000001) begin $display(" FAIL: x_base = 0x%06x", x_base); errors = errors + 1; end
if (w_base !== 22'h000101) begin $display(" FAIL: w_base = 0x%06x", w_base); errors = errors + 1; end
if (bias_addr !== 22'h000201) begin $display(" FAIL: bias_addr = 0x%06x", bias_addr); errors = errors + 1; end
report("TEST B: SET_BASE (X/W/BIAS)");
// --------------------------------------------------------
// TEST C: START -- accepted when idle, ignored when busy
// --------------------------------------------------------
errors_before = errors;
nm_busy = 1'b0;
nm_start_seen = 1'b0;
spi_begin(HB);
spi_xfer_byte(8'h20, HB, rx_tmp); // START
spi_end(HB);
clk_wait(4);
if (!nm_start_seen) begin
$display(" FAIL: nm_start not pulsed while idle");
errors = errors + 1;
end
// now busy: START must be ignored
nm_busy = 1'b1;
nm_start_seen = 1'b0;
spi_begin(HB);
spi_xfer_byte(8'h20, HB, rx_tmp); // START
spi_end(HB);
clk_wait(4);
if (nm_start_seen) begin
$display(" FAIL: nm_start pulsed while busy (should be ignored)");
errors = errors + 1;
end
nm_busy = 1'b0;
report("TEST C: START (idle vs busy)");
// --------------------------------------------------------
// TEST D: STATUS -- live busy, sticky/clear-on-read done
// --------------------------------------------------------
errors_before = errors;
nm_busy = 1'b1;
read_status(rx_tmp);
if (rx_tmp[0] !== 1'b1) begin $display(" FAIL: busy bit not set while nm_busy=1"); errors = errors + 1; end
if (rx_tmp[1] !== 1'b0) begin $display(" FAIL: done bit set before any nm_done pulse"); errors = errors + 1; end
nm_busy = 1'b0;
// pulse nm_done, then read STATUS well after the pulse: must still be set (sticky)
@(negedge clk); nm_done = 1'b1; @(negedge clk); nm_done = 1'b0;
clk_wait(10);
read_status(rx_tmp);
if (rx_tmp[1] !== 1'b1) begin $display(" FAIL: done bit not sticky after nm_done pulse"); errors = errors + 1; end
// reading STATUS must clear done
read_status(rx_tmp);
if (rx_tmp[1] !== 1'b0) begin $display(" FAIL: done bit not cleared after STATUS read"); errors = errors + 1; end
report("TEST D: STATUS (busy live, done sticky/clear-on-read)");
// --------------------------------------------------------
// TEST E: RESET -- pulses nm_soft_rst, clears sticky done
// --------------------------------------------------------
errors_before = errors;
@(negedge clk); nm_done = 1'b1; @(negedge clk); nm_done = 1'b0;
clk_wait(4);
nm_soft_rst_seen = 1'b0;
spi_begin(HB);
spi_xfer_byte(8'h0F, HB, rx_tmp); // RESET
spi_end(HB);
clk_wait(4);
if (!nm_soft_rst_seen) begin
$display(" FAIL: nm_soft_rst not pulsed by RESET opcode");
errors = errors + 1;
end
clk_wait(4);
read_status(rx_tmp);
if (rx_tmp[1] !== 1'b0) begin $display(" FAIL: done bit still set after RESET"); errors = errors + 1; end
report("TEST E: RESET");
// --------------------------------------------------------
// TEST F: READ_OUTPUT (N_NEURONS=3, neuron-major)
// --------------------------------------------------------
errors_before = errors;
y_bus[0*DATA_WIDTH +: DATA_WIDTH] = 8'sd10;
y_bus[1*DATA_WIDTH +: DATA_WIDTH] = -8'sd20;
y_bus[2*DATA_WIDTH +: DATA_WIDTH] = 8'sd127;
spi_begin(HB);
spi_xfer_byte(8'h22, HB, rx_tmp); // READ_OUTPUT
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, 8'sd10, "F: neuron0");
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, -8'sd20, "F: neuron1");
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, 8'sd127, "F: neuron2");
spi_end(HB);
report("TEST F: READ_OUTPUT");
// --------------------------------------------------------
// TEST G: READ_CONFIG
// --------------------------------------------------------
errors_before = errors;
spi_begin(HB);
spi_xfer_byte(8'h30, HB, rx_tmp); // READ_CONFIG
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, ADDR_WIDTH[7:0], "G: ADDR_WIDTH");
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, N_INPUTS[15:8], "G: N_INPUTS hi");
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, N_INPUTS[7:0], "G: N_INPUTS lo");
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, N_NEURONS[7:0], "G: N_NEURONS");
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, PARALLEL[7:0], "G: PARALLEL");
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, DATA_WIDTH[7:0], "G: DATA_WIDTH");
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, 8'h00, "G: version hi");
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, 8'h01, "G: version lo");
spi_end(HB);
report("TEST G: READ_CONFIG");
// --------------------------------------------------------
// TEST H: NOP -- no side effects
// --------------------------------------------------------
errors_before = errors;
spi_begin(HB);
spi_xfer_byte(8'h00, HB, rx_tmp); // NOP
spi_end(HB);
clk_wait(4);
if (x_base !== 22'h000001 || w_base !== 22'h000101 || bias_addr !== 22'h000201) begin
$display(" FAIL: NOP changed base registers");
errors = errors + 1;
end
report("TEST H: NOP");
// --------------------------------------------------------
// TEST I: WRITE_RAM with more MOSI bytes than len
// len=2 but 4 data bytes sent; only the first 2 must land.
// --------------------------------------------------------
errors_before = errors;
ram_mem[16'h0300] = 8'hFF; // sentinel: must NOT be overwritten
ram_mem[16'h0301] = 8'hFF;
ram_mem[16'h0302] = 8'hFF; // sentinel: extra byte must not land here
spi_begin(HB);
spi_xfer_byte(8'h01, HB, rx_tmp); // WRITE_RAM
spi_xfer_byte(8'h00, HB, rx_tmp);
spi_xfer_byte(8'h03, HB, rx_tmp);
spi_xfer_byte(8'h00, HB, rx_tmp); // addr = 0x000300
spi_xfer_byte(8'h00, HB, rx_tmp);
spi_xfer_byte(8'h02, HB, rx_tmp); // len = 2
spi_xfer_byte(8'hAA, HB, rx_tmp); // byte 0 (written)
spi_xfer_byte(8'hBB, HB, rx_tmp); // byte 1 (written)
spi_xfer_byte(8'hCC, HB, rx_tmp); // byte 2 (must be ignored)
spi_xfer_byte(8'hDD, HB, rx_tmp); // byte 3 (must be ignored)
spi_end(HB);
clk_wait(4);
if (ram_mem[16'h0300] !== 8'hAA) begin $display(" FAIL: ram[0x300]=0x%02x expected 0xAA", ram_mem[16'h0300]); errors = errors + 1; end
if (ram_mem[16'h0301] !== 8'hBB) begin $display(" FAIL: ram[0x301]=0x%02x expected 0xBB", ram_mem[16'h0301]); errors = errors + 1; end
if (ram_mem[16'h0302] !== 8'hFF) begin $display(" FAIL: ram[0x302] was overwritten (extra byte not ignored)"); errors = errors + 1; end
report("TEST I: WRITE_RAM extra MOSI bytes ignored");
// --------------------------------------------------------
// TEST J: back-to-back transactions
// --------------------------------------------------------
errors_before = errors;
set_base(8'h00, 22'h000005);
set_base(8'h01, 22'h000006);
if (x_base !== 22'h000005) begin $display(" FAIL: x_base after back-to-back = 0x%06x", x_base); errors = errors + 1; end
if (w_base !== 22'h000006) begin $display(" FAIL: w_base after back-to-back = 0x%06x", w_base); errors = errors + 1; end
report("TEST J: back-to-back transactions");
// --------------------------------------------------------
// SUMMARY
// --------------------------------------------------------
$display("");
$display("========================================");
if (errors == 0)
$display("SPI_ENGINE TEST PASSED");
else
$display("SPI_ENGINE TEST FAILED: %0d errors", errors);
$display("========================================");
$display("");
$finish;
end
// ============================================================
// HELPER TASKS
// ============================================================
task set_base;
input [7:0] sel;
input [ADDR_WIDTH-1:0] addr;
begin
spi_begin(HB);
spi_xfer_byte(8'h10, HB, rx_tmp); // SET_BASE
spi_xfer_byte(sel, HB, rx_tmp); // selector
spi_xfer_byte(addr[23:16], HB, rx_tmp);
spi_xfer_byte(addr[15:8], HB, rx_tmp);
spi_xfer_byte(addr[7:0], HB, rx_tmp);
spi_end(HB);
end
endtask
task read_status;
output [7:0] status;
begin
spi_begin(HB);
spi_xfer_byte(8'h21, HB, rx_tmp); // STATUS
spi_xfer_byte(8'h00, HB, status);
spi_end(HB);
end
endtask
task miso_check;
input [7:0] got;
input [7:0] expected;
input [511:0] label;
begin
if (got !== expected) begin
$display(" FAIL %0s: got 0x%02x expected 0x%02x", label, got, expected);
errors = errors + 1;
end
end
endtask
task report;
input [511:0] label;
begin
$display("");
if (errors == errors_before)
$display("%0s: PASS", label);
else
$display("%0s: FAIL", label);
end
endtask
initial begin
nm_start_seen = 1'b0;
nm_soft_rst_seen = 1'b0;
end
endmodule
+991081
View File
File diff suppressed because it is too large Load Diff
+29776
View File
File diff suppressed because one or more lines are too long
+408
View File
@@ -0,0 +1,408 @@
`timescale 1ns/1ps
// ================================================================
// SPI_NEURON_TOP END-TO-END TESTBENCH
//
// Drives the FULL real stack (spi_slave + spi_engine + mem_arbiter
// + int8_memory_access + memory_interface + psram_controller +
// psram_model) purely over simulated SPI -- no direct injection
// into neuron_memory's ports. This is the "verified with the real
// RAM" test: everything WRITE_RAM/READ_RAM/neuron_memory touches
// goes through the actual PSRAM timing model, not a synthetic mock.
//
// Session (matches docs §8.1 example + neuron_memory_tb.v scenarios,
// this time reached only through the SPI opcode set):
// RESET -> READ_CONFIG -> WRITE_RAM(X) -> WRITE_RAM(W) ->
// WRITE_RAM(bias) -> READ_RAM verify -> SET_BASE x3 -> START ->
// poll STATUS -> READ_OUTPUT
//
// TEST 1: X=1 (32x), W=1, bias=0 -> y = 32
// TEST 2: reload W=4 -> y saturates to 127
// TEST 3: reload W=-1 -> ReLU -> y = 0
// ================================================================
module tb;
localparam ADDR_WIDTH = 22;
localparam DATA_WIDTH = 8;
localparam N_INPUTS = 32;
localparam N_NEURONS = 1;
localparam PARALLEL = 8;
localparam ACC_WIDTH = 32;
localparam MEM_DATA_WIDTH = 16;
localparam CLK_PERIOD = 12.5; // 80 MHz
reg clk;
reg rst;
initial begin
clk = 1'b0;
forever #(CLK_PERIOD / 2.0) clk = ~clk;
end
reg sclk;
reg mosi;
wire miso;
reg cs_n;
wire [ADDR_WIDTH-1:0] psram_a;
wire [MEM_DATA_WIDTH-1:0] psram_dq;
wire psram_ce_n, psram_oe_n, psram_we_n, psram_lb_n, psram_ub_n, psram_zz_n;
spi_neuron_top #(
.ADDR_WIDTH(ADDR_WIDTH),
.DATA_WIDTH(DATA_WIDTH),
.N_INPUTS(N_INPUTS),
.N_NEURONS(N_NEURONS),
.PARALLEL(PARALLEL),
.ACC_WIDTH(ACC_WIDTH),
.MEM_DATA_WIDTH(MEM_DATA_WIDTH),
.CLK_FREQ_MHZ(80)
) dut (
.clk(clk), .rst(rst),
.sclk(sclk), .mosi(mosi), .miso(miso), .cs_n(cs_n),
.psram_a(psram_a), .psram_dq(psram_dq),
.psram_ce_n(psram_ce_n), .psram_oe_n(psram_oe_n), .psram_we_n(psram_we_n),
.psram_lb_n(psram_lb_n), .psram_ub_n(psram_ub_n), .psram_zz_n(psram_zz_n)
);
psram_model #(
.ADDR_WIDTH(ADDR_WIDTH),
.DATA_WIDTH(MEM_DATA_WIDTH),
.DEPTH(16384)
) u_psram (
.clk(clk),
.a(psram_a), .dq(psram_dq),
.ce_n(psram_ce_n), .oe_n(psram_oe_n), .we_n(psram_we_n),
.lb_n(psram_lb_n), .ub_n(psram_ub_n), .zz_n(psram_zz_n)
);
// ============================================================
// SPI MASTER BFM
// ============================================================
task clk_wait;
input integer n;
integer k;
begin
for (k = 0; k < n; k = k + 1)
@(posedge clk);
end
endtask
task spi_begin;
input integer half_bit_cycles;
begin
cs_n = 1'b1;
sclk = 1'b0;
mosi = 1'b0;
clk_wait(half_bit_cycles * 2);
cs_n = 1'b0;
clk_wait(half_bit_cycles * 2);
end
endtask
task spi_end;
input integer half_bit_cycles;
begin
clk_wait(half_bit_cycles * 2);
cs_n = 1'b1;
clk_wait(half_bit_cycles * 2);
end
endtask
task spi_xfer_byte;
input [7:0] tx;
input integer half_bit_cycles;
output [7:0] rx;
integer i;
reg [7:0] rx_acc;
begin
rx_acc = 8'h00;
for (i = 7; i >= 0; i = i - 1) begin
mosi = tx[i];
clk_wait(half_bit_cycles);
sclk = 1'b1;
rx_acc[i] = miso;
clk_wait(half_bit_cycles);
sclk = 1'b0;
clk_wait(half_bit_cycles);
end
rx = rx_acc;
end
endtask
// RAM-touching bytes need enough margin for the real PSRAM
// chain's latency (psram_controller ACCESS_CYCLES ~= ceil(70ns
// * 80MHz) = 6 cycles, plus memory_interface/int8_memory_access/
// arbiter overhead -- comfortably covered by 40 cycles/half-bit).
localparam HB_RAM = 40;
// Pure register opcodes (SET_BASE/START/STATUS/READ_OUTPUT/
// READ_CONFIG/RESET) only need CDC margin (~4 cycles).
localparam HB_REG = 8;
reg [7:0] rx_tmp;
integer errors;
integer i;
integer poll_count;
// ============================================================
// HELPER TASKS
// ============================================================
task do_reset;
begin
spi_begin(HB_REG);
spi_xfer_byte(8'h0F, HB_REG, rx_tmp); // RESET
spi_end(HB_REG);
end
endtask
task set_base;
input [7:0] sel;
input [ADDR_WIDTH-1:0] addr;
begin
spi_begin(HB_REG);
spi_xfer_byte(8'h10, HB_REG, rx_tmp);
spi_xfer_byte(sel, HB_REG, rx_tmp);
spi_xfer_byte(addr[23:16], HB_REG, rx_tmp);
spi_xfer_byte(addr[15:8], HB_REG, rx_tmp);
spi_xfer_byte(addr[7:0], HB_REG, rx_tmp);
spi_end(HB_REG);
end
endtask
task write_ram_const;
input [ADDR_WIDTH-1:0] addr;
input integer len;
input signed [7:0] value;
integer k;
begin
spi_begin(HB_RAM);
spi_xfer_byte(8'h01, HB_RAM, rx_tmp);
spi_xfer_byte(addr[23:16], HB_RAM, rx_tmp);
spi_xfer_byte(addr[15:8], HB_RAM, rx_tmp);
spi_xfer_byte(addr[7:0], HB_RAM, rx_tmp);
spi_xfer_byte(len[15:8], HB_RAM, rx_tmp);
spi_xfer_byte(len[7:0], HB_RAM, rx_tmp);
for (k = 0; k < len; k = k + 1)
spi_xfer_byte(value, HB_RAM, rx_tmp);
spi_end(HB_RAM);
end
endtask
task read_ram_byte;
input [ADDR_WIDTH-1:0] addr;
output [7:0] data;
begin
spi_begin(HB_RAM);
spi_xfer_byte(8'h02, HB_RAM, rx_tmp);
spi_xfer_byte(addr[23:16], HB_RAM, rx_tmp);
spi_xfer_byte(addr[15:8], HB_RAM, rx_tmp);
spi_xfer_byte(addr[7:0], HB_RAM, rx_tmp);
spi_xfer_byte(8'h00, HB_RAM, rx_tmp);
spi_xfer_byte(8'h01, HB_RAM, rx_tmp);
spi_xfer_byte(8'h00, HB_RAM, data);
spi_end(HB_RAM);
end
endtask
task read_status;
output [7:0] status;
begin
spi_begin(HB_REG);
spi_xfer_byte(8'h21, HB_REG, rx_tmp);
spi_xfer_byte(8'h00, HB_REG, status);
spi_end(HB_REG);
end
endtask
task do_start;
begin
spi_begin(HB_REG);
spi_xfer_byte(8'h20, HB_REG, rx_tmp);
spi_end(HB_REG);
end
endtask
task wait_done;
reg [7:0] status;
begin
poll_count = 0;
status = 8'h00;
while (!status[1] && poll_count < 1000) begin
clk_wait(20);
read_status(status);
poll_count = poll_count + 1;
end
if (!status[1]) begin
$display(" FAIL: done never asserted (poll_count=%0d)", poll_count);
errors = errors + 1;
end
end
endtask
task read_output_byte0;
output signed [7:0] y0;
begin
spi_begin(HB_REG);
spi_xfer_byte(8'h22, HB_REG, rx_tmp);
spi_xfer_byte(8'h00, HB_REG, y0);
spi_end(HB_REG);
end
endtask
// ============================================================
// MAIN
// ============================================================
reg signed [7:0] y_result;
reg [7:0] cfg_byte;
reg [7:0] verify_byte;
initial begin
$dumpfile("sim/spi_neuron_top.vcd");
$dumpvars(0, tb);
rst = 1'b1;
cs_n = 1'b1;
sclk = 1'b0;
mosi = 1'b0;
errors = 0;
repeat (5) @(posedge clk);
rst = 1'b0;
// Wait for PSRAM controller initialization (same as
// sim/neuron_memory_tb.v).
wait (dut.u_psram_ctrl.state == dut.u_psram_ctrl.STATE_IDLE);
$display("");
$display("========================================");
$display("SPI_NEURON_TOP END-TO-END TEST (real PSRAM)");
$display("========================================");
// --------------------------------------------------------
// RESET (opcode) + READ_CONFIG
// --------------------------------------------------------
do_reset;
spi_begin(HB_REG);
spi_xfer_byte(8'h30, HB_REG, rx_tmp);
spi_xfer_byte(8'h00, HB_REG, cfg_byte);
if (cfg_byte !== ADDR_WIDTH[7:0]) begin $display(" FAIL: READ_CONFIG ADDR_WIDTH"); errors = errors + 1; end
spi_xfer_byte(8'h00, HB_REG, cfg_byte);
if (cfg_byte !== N_INPUTS[15:8]) begin $display(" FAIL: READ_CONFIG N_INPUTS hi"); errors = errors + 1; end
spi_xfer_byte(8'h00, HB_REG, cfg_byte);
if (cfg_byte !== N_INPUTS[7:0]) begin $display(" FAIL: READ_CONFIG N_INPUTS lo"); errors = errors + 1; end
spi_xfer_byte(8'h00, HB_REG, cfg_byte);
if (cfg_byte !== N_NEURONS[7:0]) begin $display(" FAIL: READ_CONFIG N_NEURONS"); errors = errors + 1; end
spi_xfer_byte(8'h00, HB_REG, cfg_byte);
if (cfg_byte !== PARALLEL[7:0]) begin $display(" FAIL: READ_CONFIG PARALLEL"); errors = errors + 1; end
spi_xfer_byte(8'h00, HB_REG, cfg_byte);
if (cfg_byte !== DATA_WIDTH[7:0]) begin $display(" FAIL: READ_CONFIG DATA_WIDTH"); errors = errors + 1; end
spi_end(HB_REG);
$display("READ_CONFIG: %0s", (errors == 0) ? "PASS" : "FAIL");
// --------------------------------------------------------
// Load X=1 (32x), W=1 (32x), bias=0 into real PSRAM over SPI
// --------------------------------------------------------
write_ram_const(22'h000000, N_INPUTS, 8'sd1); // X
write_ram_const(22'h000100, N_INPUTS, 8'sd1); // W
write_ram_const(22'h000200, 1, 8'sd0); // bias
// Verify one written byte reads back correctly through the
// real PSRAM (READ_RAM), confirming the write actually
// landed and isn't just accepted-but-dropped.
read_ram_byte(22'h000000, verify_byte);
if (verify_byte !== 8'sd1) begin
$display(" FAIL: READ_RAM verify X[0] = 0x%02x expected 0x01", verify_byte);
errors = errors + 1;
end else begin
$display("READ_RAM verify: PASS (X[0]=0x%02x)", verify_byte);
end
set_base(8'h00, 22'h000000); // X_BASE
set_base(8'h01, 22'h000100); // W_BASE
set_base(8'h02, 22'h000200); // BIAS_ADDR
// --------------------------------------------------------
// TEST 1: 32 * 1 * 1 + 0 = 32
// --------------------------------------------------------
do_start;
wait_done;
read_output_byte0(y_result);
$display("");
$display("TEST 1 (SUM=32): y = %0d (expected 32)", y_result);
if (y_result !== 8'sd32) begin
$display(" FAIL");
errors = errors + 1;
end else begin
$display(" PASS");
end
// --------------------------------------------------------
// TEST 2: reload W=4 -> 32*1*4 = 128 -> saturate to 127
// --------------------------------------------------------
write_ram_const(22'h000100, N_INPUTS, 8'sd4);
do_start;
wait_done;
read_output_byte0(y_result);
$display("");
$display("TEST 2 (SATURATION): y = %0d (expected 127)", y_result);
if (y_result !== 8'sd127) begin
$display(" FAIL");
errors = errors + 1;
end else begin
$display(" PASS");
end
// --------------------------------------------------------
// TEST 3: reload W=-1 -> 32*1*(-1) = -32 -> ReLU -> 0
// --------------------------------------------------------
write_ram_const(22'h000100, N_INPUTS, -8'sd1);
do_start;
wait_done;
read_output_byte0(y_result);
$display("");
$display("TEST 3 (RELU): y = %0d (expected 0)", y_result);
if (y_result !== 8'sd0) begin
$display(" FAIL");
errors = errors + 1;
end else begin
$display(" PASS");
end
// --------------------------------------------------------
// SUMMARY
// --------------------------------------------------------
$display("");
$display("========================================");
if (errors == 0)
$display("SPI_NEURON_TOP END-TO-END TEST PASSED");
else
$display("SPI_NEURON_TOP END-TO-END TEST FAILED: %0d errors", errors);
$display("========================================");
$display("");
$finish;
end
endmodule