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:
+29
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
File diff suppressed because it is too large
Load Diff
Executable
+3139
File diff suppressed because it is too large
Load Diff
@@ -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
File diff suppressed because it is too large
Load Diff
Executable
+29776
File diff suppressed because one or more lines are too long
@@ -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
|
||||||
Reference in New Issue
Block a user