feat: add spi_slave.v physical layer (Phase 4 SPI RTL, part 1/N)
First real RTL piece of the SPI interface (docs §8.1 protocol draft): the physical layer only -- Mode 0 (CPOL=0, CPHA=0), MSB-first, byte-level shift register with a 3-stage CDC synchronizer for SCLK/MOSI/CS_N (the SPI master clock is asynchronous to the FPGA system clock). Exposes rx_byte/rx_valid, tx_byte/tx_byte_req, and cs_active/cs_start/cs_end to the (not yet written) protocol engine. Documented an important consumer contract on tx_byte_req: it is a prefetch hint (fires once extra after the last byte of every transaction, since the slave cannot know in advance whether the master will keep clocking), not a "byte consumed" event -- a consumer must advance any stateful pointer (e.g. a RAM read address) on rx_valid instead, which fires exactly once per real byte transferred. sim/spi_slave_tb.v: bit-banged SPI master BFM (4 tests: single byte, multi-byte in one CS period, back-to-back transactions, slower SCLK). Two testbench-only bugs found and fixed during bring-up (RTL itself needed no functional change beyond the tx_byte_req contract comment): the BFM was advancing its tx queue on tx_byte_req instead of rx_valid (see contract above), and inter-test reset pulses raced against posedge clk (blocking `rst=1` landing on the same simulation time as a clock edge) -- fixed by asserting/deasserting reset on negedge clk instead. Verified two ways: Icarus Verilog (4/4 tests pass) and the real ECP5 toolchain used for prior benchmarks (Yosys 0.68 synth: 0 problems, 41 FF / 55 LUT4, no latches; nextpnr-ecp5 --45k --package CABGA381 --speed 8 --freq 80: PASS, Fmax 403.23 MHz; ecppack: bitstream generated with no errors). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WQV3vS9TXaGDJ5cRfnfidt
This commit is contained in:
+22
@@ -164,3 +164,25 @@
|
|||||||
- 2026-09-02T04:40 — [FASE 12] — Scritta la spec completa in docs/FPGA-NeuralNetwork-Engine.md, nuova sezione "## 8.1 SPI Protocol v1 (draft, 2026-09-02)": fisico (SPI mode 0, MSB-first, single SPI, un comando per ciclo di CS), convenzione campi multi-byte big-endian, indirizzi a 3 byte (ADDR_WIDTH=22 bit + 2 bit riservati), tabella opcode completa (NOP 0x00, WRITE_RAM 0x01, READ_RAM 0x02, RESET 0x0F, SET_BASE 0x10, START 0x20, STATUS 0x21, READ_OUTPUT 0x22, READ_CONFIG 0x30), layout payload di READ_CONFIG (8 byte: ADDR_WIDTH, N_INPUTS, N_NEURONS, PARALLEL, DATA_WIDTH, versione protocollo), sessione di esempio end-to-end, elenco esplicito di ciò che resta fuori scope per v1 (Dual SPI, CRC/checksum, comandi di sequenziamento multi-layer -> rimandati a Fase 5).
|
- 2026-09-02T04:40 — [FASE 12] — Scritta la spec completa in docs/FPGA-NeuralNetwork-Engine.md, nuova sezione "## 8.1 SPI Protocol v1 (draft, 2026-09-02)": fisico (SPI mode 0, MSB-first, single SPI, un comando per ciclo di CS), convenzione campi multi-byte big-endian, indirizzi a 3 byte (ADDR_WIDTH=22 bit + 2 bit riservati), tabella opcode completa (NOP 0x00, WRITE_RAM 0x01, READ_RAM 0x02, RESET 0x0F, SET_BASE 0x10, START 0x20, STATUS 0x21, READ_OUTPUT 0x22, READ_CONFIG 0x30), layout payload di READ_CONFIG (8 byte: ADDR_WIDTH, N_INPUTS, N_NEURONS, PARALLEL, DATA_WIDTH, versione protocollo), sessione di esempio end-to-end, elenco esplicito di ciò che resta fuori scope per v1 (Dual SPI, CRC/checksum, comandi di sequenziamento multi-layer -> rimandati a Fase 5).
|
||||||
- 2026-09-02T04:45 — [FASE 12] — Aggiornata la checklist della "## Phase 4 — SPI Interface" nel roadmap: spuntato "Protocol/opcode set drafted" con riferimento a §8.1; aggiunti come non ancora fatti: SPI controller RTL, register bank RTL, RAM access passthrough RTL, testbench dedicato (stile SPI master BFM + stack completo, sulla falsariga di neuron_memory_tb.v).
|
- 2026-09-02T04:45 — [FASE 12] — Aggiornata la checklist della "## Phase 4 — SPI Interface" nel roadmap: spuntato "Protocol/opcode set drafted" con riferimento a §8.1; aggiunti come non ancora fatti: SPI controller RTL, register bank RTL, RAM access passthrough RTL, testbench dedicato (stile SPI master BFM + stack completo, sulla falsariga di neuron_memory_tb.v).
|
||||||
- 2026-09-02T04:46 — [FASE 12] — CONCLUSIONE: nessuna modifica a codice RTL/testbench in questa fase, solo documentazione di design (docs/FPGA-NeuralNetwork-Engine.md). Nessuna compilazione/simulazione necessaria. Prossimo step naturale: implementazione RTL del controller SPI/register-bank secondo questa spec, quando l'utente conferma che il draft è sufficientemente maturo (gli opcode restano volutamente aperti a revisione).
|
- 2026-09-02T04:46 — [FASE 12] — CONCLUSIONE: nessuna modifica a codice RTL/testbench in questa fase, solo documentazione di design (docs/FPGA-NeuralNetwork-Engine.md). Nessuna compilazione/simulazione necessaria. Prossimo step naturale: implementazione RTL del controller SPI/register-bank secondo questa spec, quando l'utente conferma che il draft è sufficientemente maturo (gli opcode restano volutamente aperti a revisione).
|
||||||
|
|
||||||
|
## Fase: Implementazione RTL SPI - livello fisico (Fase 4 RTL) (2026-09-02)
|
||||||
|
|
||||||
|
- 2026-09-02T05:00 — [FASE 13] — Utente conferma priorità: matrice di sequenziamento layer-to-layer (Fase 5) rimandata; procedere ORA con l'implementazione RTL vera dell'SPI (Fase 4), "fatta bene". Successivamente chiarito con "#4" = implementazione hardware COMPLETA di SPI, testata in tutte le sue sfaccettature (non solo happy path).
|
||||||
|
- 2026-09-02T05:02 — [FASE 13] — Piano comunicato: (1) spi_slave.v livello fisico + tb dedicato, (2) spi_engine.v FSM opcode + register bank, (3) arbitro bus condiviso spi_engine/neuron_memory, (4) top-level integrazione, (5) tb end-to-end con master SPI simulato che esegue l'intera sessione della spec. Iniziato dal punto più a rischio (CDC tra SCLK del master e clock di sistema).
|
||||||
|
- 2026-09-02T05:05 — [FASE 13] — Creato rtl/spi_slave.v: livello fisico SPI Mode 0 (CPOL=0,CPHA=0), MSB-first. Sincronizzatori a doppio flip-flop (3 stadi) per SCLK/MOSI/CS_N in ingresso dal dominio di clock del master (asincrono). Edge-detect su segnali sincronizzati. Shift register a 8 bit per rx (campiona MOSI su fronte di salita SCLK sincronizzato) e tx (aggiorna MISO su fronte di discesa SCLK sincronizzato). Segnali cs_start/cs_end (impulsi) e cs_active (livello) per delimitare le transazioni. Rimossa durante la scrittura una riga placeholder/dead-code lasciata per errore (wire sclk_rising ridondante).
|
||||||
|
- 2026-09-02T05:10 — [FASE 13] — Creato sim/spi_slave_tb.v: master SPI simulato bit-banged (Mode 0, MSB-first) con 4 test: TEST1 singolo byte (rx_byte/rx_valid + readback MISO), TEST2 multi-byte in una sola sessione CS, TEST3 transazioni back-to-back separate, TEST4 SCLK più lento (verifica indipendenza dal rapporto SCLK/clk).
|
||||||
|
- 2026-09-02T05:12 — [FASE 13] — Prima esecuzione: TEST1 e TEST3 PASS, TEST2 e TEST4 FAIL su mismatch MISO (sequenza byte ricevuta dal master shiftata, es. atteso "de ad be ef" ottenuto "ad be ef 00").
|
||||||
|
- 2026-09-02T05:15 — [FASE 13] — Prima ipotesi (poi rivelatasi solo parzialmente corretta): margine di temporizzazione insufficiente nel BFM del testbench, basato su ritardi `#ns` non allineati al clock di sistema. Riscritto il BFM per usare conteggi di cicli di `clk` (task `clk_wait`) invece di `#ns` assoluti, con margini generosi (8 e 20 cicli per mezzo-bit, ben oltre la latenza di sincronizzazione CDC di ~4 cicli). Stesso esito di fallimento anche dopo questa riscrittura -> la causa non era (solo) di margine.
|
||||||
|
- 2026-09-02T05:20 — [FASE 13] — Aggiunta strumentazione di debug temporanea (blocco `always @(posedge clk)` con `$display`/poi `$strobe` che traccia tx_byte_req, sclk_rise/fall, cs_fell, rx_valid, bit_count, tx_shift, miso, tx_byte, tx_queue_idx). Individuata la causa reale: `tx_queue_idx` (nel testbench) veniva avanzato su OGNI impulso `tx_byte_req`, ma il DUT genera legittimamente un impulso "phantom" extra dopo l'ultimo bit di ogni transazione (non può sapere in anticipo se il master continuerà a inviare altri byte prima di rilasciare CS, quindi pre-carica comunque il prossimo byte per garantire la validità di MISO). Questo consumava un elemento di troppo dalla coda del testbench ad ogni transazione.
|
||||||
|
- 2026-09-02T05:25 — [FASE 13] — FIX 1 (contratto tx_byte_req): documentato esplicitamente in rtl/spi_slave.v (commento sul segnale `tx_byte_req`) che è un hint di prefetch, NON un evento "byte consumato" — un consumer non deve usarlo per avanzare un puntatore stateful (es. indirizzo di lettura RAM), pena un avanzamento di un byte in eccesso ad ogni transazione. Il segnale corretto per avanzare un puntatore è `rx_valid`, che scatta esattamente una volta per ogni byte REALMENTE trasferito (mai un impulso extra, essendo guidato dal conteggio di fronti SCLK realmente avvenuti). Aggiornato sim/spi_slave_tb.v: `tx_queue_idx` ora avanza su `rx_valid` invece che su `tx_byte_req`.
|
||||||
|
- 2026-09-02T05:28 — [FASE 13] — Rieseguito dopo il fix 1: STESSO fallimento identico (miso ancora shiftato). Approfondito ulteriormente con debug tracing esteso dall'inizio simulazione: scoperto che `tx_queue_idx` non veniva MAI azzerato dai reset intermedi tra un test e l'altro (`rst=1'b1; @(posedge clk); rst=1'b0; @(posedge clk);`), nonostante la logica `if(rst) tx_queue_idx<=0;` fosse corretta.
|
||||||
|
- 2026-09-02T05:32 — [FASE 13] — CAUSA REALE identificata: race classica a "ritardo zero" sul fronte di clock. L'assegnazione blocking `rst=1'b1;` nel testbench avveniva, per la cadenza temporale usata (nessun `#delay` tra istruzioni, solo `@(posedge clk)`), esattamente nello stesso istante di simulazione di un fronte di salita del clock — rendendo non deterministico se i vari blocchi `always @(posedge clk)` (nel testbench e nel DUT) vedessero il vecchio o il nuovo valore di `rst` per quel fronte. Risultato osservato: il reset "non prendeva mai" per l'indice della coda, che restava sporco da un test al successivo.
|
||||||
|
- 2026-09-02T05:35 — [FASE 13] — FIX 2 (eliminazione della race): sostituiti i 3 reset rapidi inter-test da `rst=1'b1; @(posedge clk); rst=1'b0; @(posedge clk);` a `@(negedge clk); rst=1'b1; @(negedge clk); rst=1'b0; @(posedge clk);` — assegnando rst sul FRONTE DI DISCESA del clock, garantendo per costruzione che sia già stabile e senza ambiguità al successivo fronte di salita.
|
||||||
|
- 2026-09-02T05:38 — [FASE 13] — Rieseguito dopo il fix 2: TUTTI E 4 I TEST PASSANO (TEST1 PASS, TEST2 miso=de ad be ef come atteso, TEST3 PASS, TEST4 rx=0x96/miso=0x5a come atteso). Rimossa tutta la strumentazione di debug temporanea (blocco always/$strobe, reg debug_on e relativi toggle) per lasciare il testbench pulito.
|
||||||
|
- 2026-09-02T05:40 — [FASE 13] — Ricompilato ed eseguito sim/spi_slave_tb.v pulito (senza debug): confermato TUTTI E 4 I TEST PASSANO, esito identico.
|
||||||
|
- 2026-09-02T05:42 — [FASE 13] — LEZIONE APPRESA (rilevante per ogni futuro testbench in questo repo che usa sequenziamento `@(posedge clk)` senza `#delay` espliciti): assegnare segnali di controllo come `rst` con blocking assignment immediatamente prima di un `@(posedge clk)` rischia una race a delta-zero se il tempo di simulazione corrente coincide esattamente con un fronte di clock. Pattern sicuro: assegnare su `@(negedge clk)` quando si vuole garantire che il valore sia stabile e non ambiguo al successivo fronte di salita.
|
||||||
|
- 2026-09-02T05:45 — [FASE 13] — VERIFICA CON TOOLCHAIN FPGA REALE (richiesta esplicita dell'utente: "hai verificato con il simulatore della FPGA specifica?"): fino a questo punto la verifica era stata fatta solo con Icarus Verilog (simulatore RTL generico, verifica solo correttezza logica/funzionale, non timing né sintetizzabilità specifica del vendor). Individuato il toolchain reale già presente in ambiente (stesso usato per i benchmark in docs/FPGA-Neural-Datapatch-Benchmark.md, Appendice A): Yosys 0.68+post (/opt/homebrew/bin/yosys), nextpnr-ecp5 0.11.1 (/tmp/nextpnr/build/nextpnr-ecp5), ecppack/Project Trellis (/opt/homebrew/bin/ecppack).
|
||||||
|
- 2026-09-02T05:48 — [FASE 13] — Eseguita sintesi reale: `yosys -p "synth_ecp5 -json spi_slave.json -top spi_slave" rtl/spi_slave.v`. Risultato: 0 problemi rilevati dal CHECK pass, nessun latch inferito, 41 TRELLIS_FF, 55 LUT4, 21 PFUMX, 10 L6MUX21 — footprint coerente con un semplice shift register SPI.
|
||||||
|
- 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: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).
|
||||||
|
|||||||
+206
@@ -0,0 +1,206 @@
|
|||||||
|
`timescale 1ns/1ps
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// SPI SLAVE - physical layer
|
||||||
|
//
|
||||||
|
// SPI Mode 0 (CPOL=0, CPHA=0), MSB-first, single SPI.
|
||||||
|
// Per docs/FPGA-NeuralNetwork-Engine.md §8.1: the FPGA is always
|
||||||
|
// SPI slave; one command per CS-low period.
|
||||||
|
//
|
||||||
|
// SCLK/MOSI/CS_N arrive from an external, clock-asynchronous SPI
|
||||||
|
// master, so they are double-flop synchronized into the `clk`
|
||||||
|
// domain before any edge detection. This module only implements
|
||||||
|
// the byte-level shift register and CS framing; opcode/protocol
|
||||||
|
// decoding lives in spi_engine.v.
|
||||||
|
//
|
||||||
|
// Mode 0 timing: MOSI is sampled on the RISING edge of SCLK; MISO
|
||||||
|
// is driven on the FALLING edge (so it is stable well before the
|
||||||
|
// master's next rising-edge sample).
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
module spi_slave (
|
||||||
|
input wire clk,
|
||||||
|
input wire rst,
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// External SPI pins
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
input wire sclk,
|
||||||
|
input wire mosi,
|
||||||
|
output reg miso,
|
||||||
|
input wire cs_n,
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Byte-level interface to spi_engine
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
output reg [7:0] rx_byte,
|
||||||
|
output reg rx_valid, // one clk pulse: rx_byte is valid
|
||||||
|
|
||||||
|
// IMPORTANT / load-bearing contract:
|
||||||
|
// tx_byte_req is a PREFETCH hint, not a "byte consumed" event.
|
||||||
|
// It fires once at cs_fell (to load byte 1) and once more after
|
||||||
|
// EVERY byte's last bit (to have the next byte ready in time
|
||||||
|
// for MISO, in case the master keeps clocking) -- including
|
||||||
|
// after the LAST byte of a transaction, since the slave cannot
|
||||||
|
// know in advance that no further byte will follow until CS
|
||||||
|
// actually deasserts. A consumer MUST NOT treat tx_byte_req as
|
||||||
|
// a destructive "advance/pop the next byte" trigger, or it will
|
||||||
|
// over-advance by exactly one byte on every transaction (e.g.
|
||||||
|
// over-incrementing a RAM read pointer). Use `rx_valid` instead
|
||||||
|
// to advance any stateful pointer: it pulses exactly once per
|
||||||
|
// REAL byte transferred, never an extra time, because it is
|
||||||
|
// driven purely by counted SCLK edges that actually happened.
|
||||||
|
input wire [7:0] tx_byte, // next byte to shift out on MISO
|
||||||
|
output reg tx_byte_req, // one clk pulse: refresh tx_byte now (prefetch hint, see above)
|
||||||
|
|
||||||
|
output wire cs_active, // level: transaction in progress
|
||||||
|
output reg cs_start, // one clk pulse: CS just went low
|
||||||
|
output reg cs_end // one clk pulse: CS just went high
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// CDC SYNCHRONIZERS (double flip-flop)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
reg [2:0] sclk_sync;
|
||||||
|
reg [2:0] mosi_sync;
|
||||||
|
reg [2:0] cs_n_sync;
|
||||||
|
|
||||||
|
always @(posedge clk) begin
|
||||||
|
if (rst) begin
|
||||||
|
sclk_sync <= 3'b000;
|
||||||
|
mosi_sync <= 3'b000;
|
||||||
|
cs_n_sync <= 3'b111;
|
||||||
|
end else begin
|
||||||
|
sclk_sync <= {sclk_sync[1:0], sclk};
|
||||||
|
mosi_sync <= {mosi_sync[1:0], mosi};
|
||||||
|
cs_n_sync <= {cs_n_sync[1:0], cs_n};
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
wire sclk_s = sclk_sync[2];
|
||||||
|
wire mosi_s = mosi_sync[2];
|
||||||
|
wire cs_n_s = cs_n_sync[2];
|
||||||
|
|
||||||
|
// Edge detects on the synchronized (2-deep) signal using one
|
||||||
|
// extra history bit, so "rising"/"falling" mean the edge that
|
||||||
|
// just became visible to `clk`.
|
||||||
|
reg sclk_prev;
|
||||||
|
reg cs_n_prev;
|
||||||
|
|
||||||
|
always @(posedge clk) begin
|
||||||
|
if (rst) begin
|
||||||
|
sclk_prev <= 1'b0;
|
||||||
|
cs_n_prev <= 1'b1;
|
||||||
|
end else begin
|
||||||
|
sclk_prev <= sclk_s;
|
||||||
|
cs_n_prev <= cs_n_s;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
wire sclk_rise = sclk_s & ~sclk_prev;
|
||||||
|
wire sclk_fall = ~sclk_s & sclk_prev;
|
||||||
|
|
||||||
|
wire cs_fell = ~cs_n_s & cs_n_prev; // CS just went active (low)
|
||||||
|
wire cs_rose = cs_n_s & ~cs_n_prev; // CS just went inactive (high)
|
||||||
|
|
||||||
|
assign cs_active = ~cs_n_s;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// BIT COUNTER / SHIFT REGISTERS
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
reg [2:0] bit_count; // 0..7, counts bits received/sent within a byte
|
||||||
|
reg [7:0] rx_shift;
|
||||||
|
reg [7:0] tx_shift;
|
||||||
|
|
||||||
|
always @(posedge clk) begin
|
||||||
|
|
||||||
|
if (rst) begin
|
||||||
|
|
||||||
|
bit_count <= 3'd0;
|
||||||
|
rx_shift <= 8'h00;
|
||||||
|
tx_shift <= 8'h00;
|
||||||
|
rx_byte <= 8'h00;
|
||||||
|
rx_valid <= 1'b0;
|
||||||
|
tx_byte_req <= 1'b0;
|
||||||
|
miso <= 1'b0;
|
||||||
|
cs_start <= 1'b0;
|
||||||
|
cs_end <= 1'b0;
|
||||||
|
|
||||||
|
end else begin
|
||||||
|
|
||||||
|
// ------------------------------------------------
|
||||||
|
// Default pulses
|
||||||
|
// ------------------------------------------------
|
||||||
|
rx_valid <= 1'b0;
|
||||||
|
tx_byte_req <= 1'b0;
|
||||||
|
cs_start <= 1'b0;
|
||||||
|
cs_end <= 1'b0;
|
||||||
|
|
||||||
|
if (cs_fell) begin
|
||||||
|
|
||||||
|
// New transaction: reset bit counter, arm the
|
||||||
|
// first tx byte load and pre-load MISO with its
|
||||||
|
// MSB so it is valid before the first SCLK rise.
|
||||||
|
bit_count <= 3'd0;
|
||||||
|
tx_shift <= tx_byte;
|
||||||
|
tx_byte_req <= 1'b1;
|
||||||
|
miso <= tx_byte[7];
|
||||||
|
cs_start <= 1'b1;
|
||||||
|
|
||||||
|
end else if (cs_rose) begin
|
||||||
|
|
||||||
|
cs_end <= 1'b1;
|
||||||
|
|
||||||
|
end else if (cs_active) begin
|
||||||
|
|
||||||
|
if (sclk_rise) begin
|
||||||
|
|
||||||
|
// Sample MOSI (mode 0: data valid on rising edge)
|
||||||
|
rx_shift <= {rx_shift[6:0], mosi_s};
|
||||||
|
|
||||||
|
if (bit_count == 3'd7) begin
|
||||||
|
|
||||||
|
bit_count <= 3'd0;
|
||||||
|
rx_byte <= {rx_shift[6:0], mosi_s};
|
||||||
|
rx_valid <= 1'b1;
|
||||||
|
|
||||||
|
end else begin
|
||||||
|
|
||||||
|
bit_count <= bit_count + 3'd1;
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end else if (sclk_fall) begin
|
||||||
|
|
||||||
|
// Drive next MISO bit (mode 0: output changes
|
||||||
|
// on the falling edge, ahead of the next
|
||||||
|
// master-side rising-edge sample).
|
||||||
|
if (bit_count == 3'd0) begin
|
||||||
|
|
||||||
|
// A byte boundary just completed on the
|
||||||
|
// matching rising edge above; load the
|
||||||
|
// next tx byte now.
|
||||||
|
tx_shift <= tx_byte;
|
||||||
|
tx_byte_req <= 1'b1;
|
||||||
|
miso <= tx_byte[7];
|
||||||
|
|
||||||
|
end else begin
|
||||||
|
|
||||||
|
tx_shift <= {tx_shift[6:0], 1'b0};
|
||||||
|
miso <= tx_shift[6];
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
endmodule
|
||||||
+13327
File diff suppressed because it is too large
Load Diff
Executable
+1006
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,380 @@
|
|||||||
|
`timescale 1ns/1ps
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// SPI_SLAVE PHYSICAL LAYER TESTBENCH
|
||||||
|
//
|
||||||
|
// Bit-bangs a simulated SPI master (Mode 0, MSB-first) against
|
||||||
|
// rtl/spi_slave.v and checks:
|
||||||
|
// TEST 1: single-byte transaction (rx_byte/rx_valid, MISO readback)
|
||||||
|
// TEST 2: multi-byte transaction within one CS-low period
|
||||||
|
// TEST 3: back-to-back separate transactions (state resets cleanly)
|
||||||
|
// TEST 4: a slower SPI clock (stresses nothing new, but confirms
|
||||||
|
// the module isn't implicitly tied to one SCLK/clk ratio)
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
module tb;
|
||||||
|
|
||||||
|
localparam CLK_PERIOD = 12.5; // 80 MHz system clock
|
||||||
|
|
||||||
|
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 [7:0] rx_byte;
|
||||||
|
wire rx_valid;
|
||||||
|
reg [7:0] tx_byte;
|
||||||
|
wire tx_byte_req;
|
||||||
|
wire cs_active;
|
||||||
|
wire cs_start;
|
||||||
|
wire cs_end;
|
||||||
|
|
||||||
|
spi_slave dut (
|
||||||
|
.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_active),
|
||||||
|
.cs_start(cs_start),
|
||||||
|
.cs_end(cs_end)
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// tx_byte queue: serves tx_queue[tx_queue_idx] combinationally
|
||||||
|
// at all times (spi_slave.v prefetches it via tx_byte_req).
|
||||||
|
//
|
||||||
|
// The index advances on `rx_valid`, NOT on `tx_byte_req`:
|
||||||
|
// tx_byte_req fires one extra ("phantom") time after the last
|
||||||
|
// byte of every transaction (see the contract note in
|
||||||
|
// rtl/spi_slave.v), while rx_valid fires exactly once per REAL
|
||||||
|
// byte transferred, in both directions (SPI is full-duplex) --
|
||||||
|
// the correct signal to retire one queue entry.
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
reg [7:0] tx_queue [0:7];
|
||||||
|
integer tx_queue_len;
|
||||||
|
integer tx_queue_idx;
|
||||||
|
|
||||||
|
always @(posedge clk) begin
|
||||||
|
if (rst) begin
|
||||||
|
tx_queue_idx <= 0;
|
||||||
|
end else if (rx_valid) begin
|
||||||
|
if (tx_queue_idx < tx_queue_len)
|
||||||
|
tx_queue_idx <= tx_queue_idx + 1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
always @(*) begin
|
||||||
|
tx_byte = (tx_queue_idx < tx_queue_len) ? tx_queue[tx_queue_idx] : 8'h00;
|
||||||
|
end
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// rx capture: record every received byte in order
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
reg [7:0] rx_log [0:7];
|
||||||
|
integer rx_log_len;
|
||||||
|
|
||||||
|
always @(posedge clk) begin
|
||||||
|
if (rst) begin
|
||||||
|
rx_log_len <= 0;
|
||||||
|
end else if (rx_valid) begin
|
||||||
|
rx_log[rx_log_len] <= rx_byte;
|
||||||
|
rx_log_len <= rx_log_len + 1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// cs_start / cs_end pulse counters
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
integer cs_start_count;
|
||||||
|
integer cs_end_count;
|
||||||
|
|
||||||
|
always @(posedge clk) begin
|
||||||
|
if (rst) begin
|
||||||
|
cs_start_count <= 0;
|
||||||
|
cs_end_count <= 0;
|
||||||
|
end else begin
|
||||||
|
if (cs_start) cs_start_count <= cs_start_count + 1;
|
||||||
|
if (cs_end) cs_end_count <= cs_end_count + 1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// SPI MASTER BFM (Mode 0, MSB-first, bit-banged)
|
||||||
|
//
|
||||||
|
// half_period is in ns; must stay large enough relative to
|
||||||
|
// CLK_PERIOD for the 2-flop CDC synchronizer in spi_slave.v to
|
||||||
|
// reliably catch every edge (>= ~3 system clocks per SCLK
|
||||||
|
// half-period is a safe margin).
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
reg [7:0] miso_capture [0:7];
|
||||||
|
integer miso_capture_len;
|
||||||
|
|
||||||
|
// clk-cycle-counted wait: every SPI edge in this BFM is placed a
|
||||||
|
// fixed number of `clk` cycles apart, instead of a raw `#ns`
|
||||||
|
// delay. This keeps the master deterministically phase-aligned
|
||||||
|
// to the system clock, so the fixed CDC latency of spi_slave.v
|
||||||
|
// (3-stage synchronizer + 1 cycle for edge detect, ~4 clk
|
||||||
|
// cycles) always falls comfortably inside the margin instead of
|
||||||
|
// drifting against it run to run.
|
||||||
|
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; // rising edge: slave samples MOSI
|
||||||
|
rx_acc[i] = miso; // master samples MISO (stable since the prior falling edge)
|
||||||
|
clk_wait(half_bit_cycles);
|
||||||
|
sclk = 1'b0; // falling edge: slave updates MISO
|
||||||
|
clk_wait(half_bit_cycles);
|
||||||
|
end
|
||||||
|
rx = rx_acc;
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
reg [7:0] rx_tmp;
|
||||||
|
integer errors;
|
||||||
|
integer errors_before;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// MAIN
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
initial begin
|
||||||
|
|
||||||
|
$dumpfile("sim/spi_slave.vcd");
|
||||||
|
$dumpvars(0, tb);
|
||||||
|
|
||||||
|
rst = 1'b1;
|
||||||
|
cs_n = 1'b1;
|
||||||
|
sclk = 1'b0;
|
||||||
|
mosi = 1'b0;
|
||||||
|
errors = 0;
|
||||||
|
tx_queue_len = 0;
|
||||||
|
rx_log_len = 0;
|
||||||
|
|
||||||
|
repeat (5) @(posedge clk);
|
||||||
|
rst = 1'b0;
|
||||||
|
repeat (5) @(posedge clk);
|
||||||
|
|
||||||
|
$display("");
|
||||||
|
$display("========================================");
|
||||||
|
$display("SPI_SLAVE PHYSICAL LAYER TEST");
|
||||||
|
$display("========================================");
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// TEST 1: single-byte transaction
|
||||||
|
// Master sends 0xA5, slave echoes back queued 0x3C.
|
||||||
|
// --------------------------------------------------------
|
||||||
|
|
||||||
|
errors_before = errors;
|
||||||
|
|
||||||
|
tx_queue[0] = 8'h3C;
|
||||||
|
tx_queue_len = 1;
|
||||||
|
|
||||||
|
spi_begin(8);
|
||||||
|
spi_xfer_byte(8'hA5, 8, rx_tmp);
|
||||||
|
spi_end(8);
|
||||||
|
|
||||||
|
@(posedge clk); @(posedge clk);
|
||||||
|
|
||||||
|
$display("");
|
||||||
|
$display("TEST 1: single byte");
|
||||||
|
$display(" MOSI sent = 0xA5, slave rx_byte = 0x%02x (expect 0xA5)", rx_log[0]);
|
||||||
|
$display(" MISO sent = 0x3C, master received = 0x%02x (expect 0x3C)", rx_tmp);
|
||||||
|
$display(" cs_start pulses = %0d (expect 1), cs_end pulses = %0d (expect 1)",
|
||||||
|
cs_start_count, cs_end_count);
|
||||||
|
|
||||||
|
if (rx_log[0] !== 8'hA5) begin $display(" FAIL: rx_byte mismatch"); errors = errors + 1; end
|
||||||
|
if (rx_tmp !== 8'h3C) begin $display(" FAIL: MISO readback mismatch"); errors = errors + 1; end
|
||||||
|
if (cs_start_count !== 1) begin $display(" FAIL: cs_start count"); errors = errors + 1; end
|
||||||
|
if (cs_end_count !== 1) begin $display(" FAIL: cs_end count"); errors = errors + 1; end
|
||||||
|
if (errors == errors_before) $display(" PASS");
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// TEST 2: multi-byte transaction, single CS-low period
|
||||||
|
// Master sends 0x11, 0x22, 0x33, 0x44.
|
||||||
|
// Slave echoes back 0xDE, 0xAD, 0xBE, 0xEF.
|
||||||
|
// --------------------------------------------------------
|
||||||
|
|
||||||
|
@(negedge clk); rst = 1'b1; @(negedge clk); rst = 1'b0; @(posedge clk);
|
||||||
|
rx_log_len = 0; cs_start_count = 0; cs_end_count = 0;
|
||||||
|
errors_before = errors;
|
||||||
|
|
||||||
|
tx_queue[0] = 8'hDE;
|
||||||
|
tx_queue[1] = 8'hAD;
|
||||||
|
tx_queue[2] = 8'hBE;
|
||||||
|
tx_queue[3] = 8'hEF;
|
||||||
|
tx_queue_len = 4;
|
||||||
|
|
||||||
|
spi_begin(8);
|
||||||
|
spi_xfer_byte(8'h11, 8, rx_tmp); miso_capture[0] = rx_tmp;
|
||||||
|
spi_xfer_byte(8'h22, 8, rx_tmp); miso_capture[1] = rx_tmp;
|
||||||
|
spi_xfer_byte(8'h33, 8, rx_tmp); miso_capture[2] = rx_tmp;
|
||||||
|
spi_xfer_byte(8'h44, 8, rx_tmp); miso_capture[3] = rx_tmp;
|
||||||
|
spi_end(8);
|
||||||
|
|
||||||
|
@(posedge clk); @(posedge clk);
|
||||||
|
|
||||||
|
$display("");
|
||||||
|
$display("TEST 2: multi-byte, one CS period");
|
||||||
|
$display(" rx_log = %02x %02x %02x %02x (expect 11 22 33 44)",
|
||||||
|
rx_log[0], rx_log[1], rx_log[2], rx_log[3]);
|
||||||
|
$display(" miso = %02x %02x %02x %02x (expect de ad be ef)",
|
||||||
|
miso_capture[0], miso_capture[1], miso_capture[2], miso_capture[3]);
|
||||||
|
$display(" cs_start pulses = %0d (expect 1), cs_end pulses = %0d (expect 1)",
|
||||||
|
cs_start_count, cs_end_count);
|
||||||
|
|
||||||
|
if (rx_log[0] !== 8'h11 || rx_log[1] !== 8'h22 ||
|
||||||
|
rx_log[2] !== 8'h33 || rx_log[3] !== 8'h44) begin
|
||||||
|
$display(" FAIL: rx sequence mismatch");
|
||||||
|
errors = errors + 1;
|
||||||
|
end
|
||||||
|
if (miso_capture[0] !== 8'hDE || miso_capture[1] !== 8'hAD ||
|
||||||
|
miso_capture[2] !== 8'hBE || miso_capture[3] !== 8'hEF) begin
|
||||||
|
$display(" FAIL: MISO sequence mismatch");
|
||||||
|
errors = errors + 1;
|
||||||
|
end
|
||||||
|
if (cs_start_count !== 1) begin $display(" FAIL: cs_start count"); errors = errors + 1; end
|
||||||
|
if (cs_end_count !== 1) begin $display(" FAIL: cs_end count"); errors = errors + 1; end
|
||||||
|
if (errors == errors_before) $display(" PASS");
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// TEST 3: back-to-back separate transactions
|
||||||
|
// Two independent single-byte transactions; state must
|
||||||
|
// reset cleanly between them (no leftover bit_count/shift).
|
||||||
|
// --------------------------------------------------------
|
||||||
|
|
||||||
|
@(negedge clk); rst = 1'b1; @(negedge clk); rst = 1'b0; @(posedge clk);
|
||||||
|
rx_log_len = 0; cs_start_count = 0; cs_end_count = 0;
|
||||||
|
errors_before = errors;
|
||||||
|
|
||||||
|
tx_queue[0] = 8'h01;
|
||||||
|
tx_queue_len = 1;
|
||||||
|
spi_begin(8);
|
||||||
|
spi_xfer_byte(8'h7E, 8, rx_tmp);
|
||||||
|
spi_end(8);
|
||||||
|
|
||||||
|
repeat (10) @(posedge clk);
|
||||||
|
|
||||||
|
tx_queue[0] = 8'h02;
|
||||||
|
tx_queue_len = 1;
|
||||||
|
spi_begin(8);
|
||||||
|
spi_xfer_byte(8'h81, 8, rx_tmp);
|
||||||
|
spi_end(8);
|
||||||
|
|
||||||
|
@(posedge clk); @(posedge clk);
|
||||||
|
|
||||||
|
$display("");
|
||||||
|
$display("TEST 3: back-to-back transactions");
|
||||||
|
$display(" rx_log = %02x %02x (expect 7e 81)", rx_log[0], rx_log[1]);
|
||||||
|
$display(" cs_start pulses = %0d (expect 2), cs_end pulses = %0d (expect 2)",
|
||||||
|
cs_start_count, cs_end_count);
|
||||||
|
|
||||||
|
if (rx_log[0] !== 8'h7E || rx_log[1] !== 8'h81) begin
|
||||||
|
$display(" FAIL: rx sequence mismatch");
|
||||||
|
errors = errors + 1;
|
||||||
|
end
|
||||||
|
if (cs_start_count !== 2) begin $display(" FAIL: cs_start count"); errors = errors + 1; end
|
||||||
|
if (cs_end_count !== 2) begin $display(" FAIL: cs_end count"); errors = errors + 1; end
|
||||||
|
if (errors == errors_before) $display(" PASS");
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// TEST 4: slower SPI clock (larger half_period), same
|
||||||
|
// single-byte check, confirms no hidden dependency on a
|
||||||
|
// specific SCLK/clk ratio (as long as the CDC margin holds).
|
||||||
|
// --------------------------------------------------------
|
||||||
|
|
||||||
|
@(negedge clk); rst = 1'b1; @(negedge clk); rst = 1'b0; @(posedge clk);
|
||||||
|
rx_log_len = 0; cs_start_count = 0; cs_end_count = 0;
|
||||||
|
errors_before = errors;
|
||||||
|
|
||||||
|
tx_queue[0] = 8'h5A;
|
||||||
|
tx_queue_len = 1;
|
||||||
|
|
||||||
|
spi_begin(20);
|
||||||
|
spi_xfer_byte(8'h96, 20, rx_tmp);
|
||||||
|
spi_end(20);
|
||||||
|
|
||||||
|
@(posedge clk); @(posedge clk);
|
||||||
|
|
||||||
|
$display("");
|
||||||
|
$display("TEST 4: slower SCLK (200ns half-period)");
|
||||||
|
$display(" rx_byte = 0x%02x (expect 0x96), MISO = 0x%02x (expect 0x5a)",
|
||||||
|
rx_log[0], rx_tmp);
|
||||||
|
|
||||||
|
if (rx_log[0] !== 8'h96) begin $display(" FAIL: rx_byte mismatch"); errors = errors + 1; end
|
||||||
|
if (rx_tmp !== 8'h5A) begin $display(" FAIL: MISO readback mismatch"); errors = errors + 1; end
|
||||||
|
if (errors == errors_before) $display(" PASS");
|
||||||
|
|
||||||
|
$display("");
|
||||||
|
$display("========================================");
|
||||||
|
if (errors == 0)
|
||||||
|
$display("SPI_SLAVE TEST PASSED");
|
||||||
|
else
|
||||||
|
$display("SPI_SLAVE TEST FAILED: %0d errors", errors);
|
||||||
|
$display("========================================");
|
||||||
|
$display("");
|
||||||
|
|
||||||
|
$finish;
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
endmodule
|
||||||
Reference in New Issue
Block a user