Adds FPGA-exclusive access to the onboard W25Q128JV SPI NOR flash for weights/bias/network persistence, layered as spi_flash_master (raw SPI, USRMCLK-driven) -> flash_copy_engine (flash<->PSRAM streaming, erase- before-write, Page Program loop) -> flash_slot_manager (16-slot catalog with CRC32), exposed via 8 new SPI opcodes (0x40-0x47). Fixes two pre-existing bugs found during bring-up: a psram_controller.v request lost during power-up, and a one-cycle-pulse race in the PSRAM arbiter request handshake. Full simulation + real Yosys/nextpnr-ecp5 synthesis verification (0 errors, Fmax 66.68MHz) in WORKLOG.md and docs/FPGA-Neural-Flash-Subsystem-Verification.md. Also updates docs/pinout to reflect the 56-signal real .lpf (3 new flash pins) and documents the WRITE_RAM/READ_RAM host backpressure risk found while testing this subsystem. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
16 KiB
Sottosistema flash — documento di verifica (F1-F6)
Questo documento consolida, per modulo, cosa è verificato, come, contro quale oracolo, e cosa
NON è coperto — il deliverable richiesto esplicitamente da §9 del prompt di fase. La
narrazione cronologica completa (inclusi i bug trovati e corretti, con la loro esatta
diagnosi) resta in WORKLOG.md; questo documento è il riferimento strutturato, non una
sostituzione.
Standard applicato ovunque (§A del prompt di fase): oracolo indipendente dal design (mai auto-referenziale), due piani di verifica (simulazione Icarus + sintesi/place&route reale Yosys+nextpnr-ecp5), test avversari oltre al caso positivo, tracciabilità delle fonti.
rtl/spi_flash_master.v — master SPI verso la flash (F1)
Cosa è coperto: RDID, READ, WREN, PP (incl. la regola "solo AND, mai OR" del datasheet), SE, RDSR-1; gestione USRMCLK per CCLK post-configurazione; opcode non implementato non causa hang.
Come: sim/spi_flash_master_tb.v, 5 test contro sim/flash_model.v.
Oracolo: (1) valore JEDEC ID scritto indipendentemente nel testbench, dal datasheet
W25Q128JV(-DTR) §8.1.1 p.24 — non letto dalla costante di flash_model.v; (2) per READ, un
pattern piantato via riferimento gerarchico DIRETTAMENTE in flash_model.mem[], mai passato
per l'RTL sotto test; (3) per PP AND-only, un secondo programma con 0xFF che NON deve
alterare il primo valore (TEST3b) — distingue la regola vera da un "program sovrascrive"
ingenuo che passerebbe un test più debole.
Test avversari: opcode illegale (0xAB, comando reale W25Q128JV ma non implementato) → nessun hang, il master shifta un numero fisso di bit indipendentemente dalla semantica.
Sintesi reale: yosys synth_ecp5 (0 problemi) + nextpnr-ecp5 reale, modulo isolato:
Fmax 181.72 MHz (PASS a 80MHz), USRMCLK 1/1 piazzata.
NON coperto (§A.6): nessun test di power-loss (arriva a livello catalogo, F4); nessun
timeout RDSR a questo livello (responsabilità del chiamante, F3); nessuna verifica
elettrica/analogica reale (setup/hold, rise/fall) — solo comportamentale; polarità di
USRMCLKTS basata sulla convenzione open-source comune, non verificata contro la Technical
Note Lattice primaria (non presente nel set documentale locale).
sim/flash_model.v — modello comportamentale della flash (SOLO simulazione)
Cosa modella: erase→0xFF, program solo AND, WIP/WEL, timing tPP/tSE ai valori MAX del
datasheet (non TYP — un design che funziona solo col caso tipico non è corretto), power-loss
hook (forzatura di pending_pp/pending_se/busy via riferimento gerarchico).
Oracolo per ogni regola: citazione diretta del datasheet W25Q128JV(-DTR) locale, §/pagina specifica per ogni regola (vedi commenti nel sorgente).
Discrepanza dichiarata: il PDF locale è titolato "W25Q128JV-DTR" (variante Double Transfer
Rate), non il part number liscio "W25Q128JV" del BOM reale
(docs/FPGA-Neural-Hardware-Design.md §6/§7). Il set di istruzioni standard-SPI e i timing
§9.6 sono condivisi da tutta la famiglia (validi senza riserve); il JEDEC ID invece NO — il
modello usa EF4018h (part liscio, pubblicamente documentato) non EF7018h (quello di
questo PDF specifico). Il bring-up su hardware reale deve confermare l'RDID effettivo.
NON coperto (§A.6): Fast/Dual/Quad/QPI/DTR (fuori scope, solo SPI standard usato); bit di protezione/Individual Block Lock (fuori scope, la flash è esclusiva della FPGA); timing elettrico reale; il power-loss simulato rappresenta solo "operazione mai committata", non "committata a metà" (limite del modello, dichiarato).
rtl/flash_copy_engine.v — copy engine LOAD/SAVE/ERASE (F2/F3/F5)
Cosa è coperto: DIR_LOAD (flash→PSRAM, chunking automatico oltre 65535B),
DIR_SAVE (PSRAM→flash, erase-before-write, loop Page Program ≤256B, poll WIP),
DIR_ERASE (erase settore standalone, F5).
Come: sim/flash_copy_engine_load_tb.v (5 test), sim/flash_copy_engine_save_tb.v
(5 test), sim/flash_copy_engine_erase_tb.v (3 test) — stack PSRAM reale (mem_arbiter →
int8_memory_access → memory_interface → psram_controller → psram_model), non un modello
semplificato.
Oracolo: dati piantati indipendentemente in flash_model.mem[] (LOAD) o letti
direttamente da lì dopo una SAVE (non passati per l'RTL sotto test in nessuno dei due
sensi); per la regola AND-only, un settore pre-avvelenato con un pattern diverso da quello
salvato, verificando che la coda non scritta torni a 0xFF (prova un erase reale, non solo un
program sopra il vecchio contenuto).
Test avversari: lunghezza fuori range (flash e PSRAM), len==0, blocco non allineato al
settore (SAVE/ERASE), blocco che attraversa un confine di pagina 256B (300 byte, 2 pagine),
contesa reale dell'arbitro con un master a priorità più alta durante un intero LOAD, WIP
prolungato (forzato via hook power-loss, nessun timeout — poll continua, completa quando WIP
si libera davvero).
Due bug trovati e corretti durante il bring-up (dettagli completi in WORKLOG.md, F2):
- Bug pre-esistente in
rtl/psram_controller.v(non del sottosistema flash): richieste arrivate durante i primi ~150µs dopo reset venivano perse silenziosamente, e la sequenza interna di configurazione del chip generava impulsimem_readyspuri. Rischio reale per lo scenario "boot standalone da flash" per cui esiste questo sottosistema. Corretto con coda per richieste precoci + guardia sugli impulsi spuri. - Bug nel nuovo codice:
d_reqverso l'arbitro PSRAM era un impulso di un ciclo, perdibile in caso di contesa esatta con un'altra porta. Corretto rendendolo un segnale di livello tenuto alto finché non viene servito davvero (con un secondo sotto-bug — race sul ciclo esatto di rilascio dell'arbitro — trovato e corretto nello stesso giro).
Sintesi reale: yosys synth_ecp5 pulito + nextpnr-ecp5 reale, modulo isolato: F2
172.98 MHz, F3 158.28-158.55 MHz (PASS a 80MHz, numeri di solo modulo).
NON coperto (§A.6): blocco >65535B a piena dimensione (chunking implementato e ragionato, non esercitato a piena scala per tempo di simulazione); due SAVE consecutive su settori adiacenti (non richiesto per queste fasi).
rtl/crc32.v — CRC32 (F4)
Cosa è coperto: algoritmo CRC32 riflesso standard (IEEE 802.3/zlib, polinomio 0xEDB88320).
Come: sim/crc32_tb.v, 4 test.
Oracolo — TRE fonti indipendenti: (1) identità matematica del messaggio vuoto (init XOR
final = 0); (2) valore di controllo testuale standard pubblicato "123456789" → 0xCBF43926
(citato in ogni tabella di riferimento CRC32, non calcolato da nessuno dei due lati); (3)
tools/flash_catalog/oracle.py, che usa zlib.crc32 della libreria standard Python —
implementazione completamente separata, non derivata dalla stessa comprensione di design.
NON coperto: nessuna verifica di throughput/pipeline (l'accumulatore è un aggiornamento combinazionale per byte, non un collo di bottiglia dato il ritmo flash/PSRAM molto più lento).
rtl/flash_slot_manager.v — catalogo a slot fissi (F4/F5)
Cosa è coperto: CAT_READ (ricarica il catalogo on-chip da flash), CAT_WRITE_SLOT
(registra/aggiorna una voce, persiste l'intero catalogo), LOAD_SLOT/SAVE_SLOT (risolvono
lo slot dal catalogo on-chip, CRC calcolato dal vero stream di byte), più i 3 opcode raw
(F5: FLASH_READ_BLOCK/FLASH_WRITE_BLOCK/FLASH_ERASE, pass-through diretto senza
catalogo/CRC).
Come: sim/flash_slot_manager_tb.v (6 test, F4), sim/flash_slot_manager_raw_tb.v
(4 test, F5) — stesso stack PSRAM reale.
Oracolo: tools/flash_catalog/oracle.py — layout byte del catalogo e CRC32 di
riferimento, confrontati byte-a-byte contro i byte REALI persistiti in flash_model.mem[]
(non contro il decode dell'RTL stesso).
Test avversari: CRC corrotto (un bit capovolto direttamente in flash_model.mem[]) →
LOAD_SLOT rifiuta; slot mai salvato → LOAD_SLOT rifiuta immediatamente, nessuna
transazione tentata (sentinella PSRAM intatta); power-loss simulato durante il Sector
Erase di una SAVE_SLOT (hook di flash_model.v, settore pre-avvelenato con pattern
diverso) — il motore "completa con successo" secondo RDSR (nessun modo per saperlo
altrimenti), ma il CRC persistito non combacia più coi byte reali (mai davvero cancellati) →
una LOAD_SLOT successiva lo rileva. Dimostra che il meccanismo di rilevamento dipende dal
CRC sui byte realmente committed, non da un segnale di completamento che può mentire.
Bug trovato e corretto in revisione (prima di ogni simulazione): condizione di
aggiornamento del CRC basata su fce_d_req && d_ready — contraddizione logica sempre falsa,
dato come flash_copy_engine.v definisce il proprio d_req. Corretta in fce_busy && d_ready. Decode di una voce di catalogo inizialmente incompleto (solo offset). Gap trovato
in revisione durante F5: SAVE_SLOT non controllava fce_err prima di marcare il catalogo
valido.
Sintesi reale: yosys synth_ecp5 pulito (0 problemi CHECK). Place&route reale a livello
di modulo isolato NON eseguibile per questo modulo specifico (253 bit di porte a livello di
modulo superano i 245 pin fisici del package — artefatto del testare un modulo interno come
top fittizio, non un problema del design); la vera verifica di piazzamento arriva con
l'integrazione in spi_neuron_top (F5, sotto).
NON coperto (§A.6): due slot che si sovrappongono in flash (responsabilità dell'host, che
governa offset/length via CAT_WRITE_SLOT); CATALOG_PSRAM_ADDR è una convenzione non
imposta altrove — vedi §5 "limiti host" sotto per la conseguenza reale di questo limite.
rtl/spi_engine.v — opcode SPI del sottosistema flash (F5)
Cosa è coperto: decodifica degli 8 opcode (0x40-0x47), accumulatore di payload condiviso,
STATUS esteso (bit3=flash_err_sticky, bit4=flash_busy), done_event corretto per
includere il completamento flash come sorgente indipendente dall'inferenza.
Come: sim/spi_neuron_top_flash_tb.v (test end-to-end via SPI reale bit-banged) +
regressione di sim/spi_engine_tb.v (esistente, invariato).
Oracolo: derivazione a mano bit-per-bit di ogni campo del payload (stesso metodo di F1),
verificata via traccia diretta dei valori decodificati (flash_op_code/flash_slot_id/ecc.)
prima di fidarsi del comportamento a valle.
Bug trovato DALLA REGRESSIONE, non dalla progettazione (motivo per cui va sempre
rieseguita, mai solo assunta): la prima versione di done_event sostituiva un multiplexer a
priorità ESISTENTE (che maschera deliberatamente gli nm_done intermedi durante una
RUN_NETWORK multi-layer) con un OR piatto — sim/spi_engine_tb.v ha fallito immediatamente
("done bit set by an intermediate nm_done"). Corretto preservando la maschera esistente
INVARIATA e aggiungendo flash_done come sorgente OR separata, fuori dalla maschera.
NON coperto: nessuna verifica formale di tutte le 2^N combinazioni di byte payload malformati (fuori scope — i test coprono il caso positivo e gli specifici casi avversari già verificati ai livelli sottostanti, che l'SPI framing si limita a inoltrare fedelmente).
rtl/spi_neuron_top.v — integrazione finale (F5)
Cosa è coperto: istanziazione di flash_slot_manager, nuovi pin fisici
(flash_mosi/flash_miso/flash_cs_n, distinti dai pin SPI host), Port D dell'arbitro
collegata (non più tied-off), pinout reale rigenerato.
Come: sim/spi_neuron_top_flash_tb.v, 4 test sopra lo STACK COMPLETO REALE.
Il test end-to-end richiesto esplicitamente da §6 del piano: netasm → WRITE_RAM → SAVE_SLOT → (sovrascrittura PSRAM con garbage) → LOAD_SLOT → RUN_NETWORK → READ_RAM = 126
— lo stesso valore atteso indipendente già usato in sim/graph_engine_tb.v/
sim/spi_neuron_top_graph_tb.v (esempio da manuale §3), non ri-derivato.
Tre intoppi reali nella costruzione del test, nessun bug RTL (dettagliati in
WORKLOG.md, F5): collisione PSRAM tra il blob di rete e CATALOG_PSRAM_ADDR di default;
--parallel di netasm non corrispondente al PARALLEL reale dell'hardware; netasm
incorpora indirizzi ASSOLUTI (non relativi) nella tabella descrittore, quindi spostare il
blob compilato richiede rigenerarlo con --table-base/--edges-base corrispondenti — un
mismatch qui produce un risultato "riuscito" (STATUS pulito, nessun errore) ma
completamente sbagliato, senza alcun sintomo hardware-visibile.
Sintesi reale, livello di SISTEMA COMPLETO (prima volta per il sottosistema flash
integrato): 0 errori di vincolo, 56/56 celle piazzate. Fmax 66.68 MHz (FAIL a 80MHz,
atteso — percorso critico verificato esplicitamente IDENTICO a quello già noto dalla Fase 7,
neuron_parallel's catena di riporto dell'accumulatore, NESSUN modulo flash coinvolto). Il
calo dai 73.88MHz precedenti è rumore di piazzamento (banda già caratterizzata), non una
regressione — confermato dal percorso critico invariato.
Occupazione (sistema completo): TRELLIS_IO 56/245 (22%), TRELLIS_FF 4855/43848 (11%),
TRELLIS_COMB 9060/43848 (20%), DP16KD 2/108, MULT18X18D 16/72, USRMCLK 1/1 (100%) —
nessuna pressione sulle risorse del dispositivo.
Scoperta trasversale: limite reale di WRITE_RAM/READ_RAM (non specifica alla flash)
Durante il bring-up del test end-to-end (F5) è emerso che WRITE_RAM/READ_RAM (opcode
esistenti, PRE-esistenti a questa intera sessione, mai modificati) non hanno alcun
backpressure verso il master SPI — limite già dichiarato nell'header di spi_engine.v ma la
cui conseguenza pratica (corruzione silenziosa dei dati, non un hang, non un errore) non era
mai stata esposta prima. È emersa a causa del fix CORRETTO su psram_controller.v (sopra):
un bug pre-esistente diverso mascherava accidentalmente il rischio, dando all'host
"fortunatamente" abbastanza poco tempo da non disallinearsi. Rischio reale per QUALUNQUE
host che emetta WRITE_RAM/READ_RAM troppo presto dopo il reset (entro il power-up
~150µs di psram_controller), non solo per i test di questa sessione. Mitigazione
applicata qui: ogni testbench aspetta esplicitamente psram_ctrl.state == STATE_IDLE
prima del primo accesso PSRAM. Non risolto nell'RTL (fuori scope per il sottosistema
flash — richiederebbe un vero protocollo di backpressure): dichiarato come rischio aperto,
non nascosto.
Latenze reali misurate (F6)
Metodologia (dichiarata esplicitamente, §A.5): simulare l'intera attesa WIP a scala
temporale reale con il vero loop di poll RDSR si è rivelato impraticabile — a timing reale,
coprire un'attesa di 400ms con poll a ritmo SPI reale richiede circa 100.000+ transazioni di
poll, decine di milioni di eventi Icarus, oltre il limite pratico di tempo macchina (limite
del simulatore, non dell'hardware — un poll reale su silicio non costa tempo di simulazione).
Approccio usato: la fase di "issue" (WREN+SE/PP, limitata dal clock SPI) è misurata
direttamente in simulazione (accurata indipendentemente dalla scala temporale del modello
flash, che scala solo l'attesa WIP successiva); la durata dell'attesa WIP stessa è presa
dal valore MAX del datasheet (già citato in sim/flash_model.v, §9.6 p.90), sommata
analiticamente. Misurato con sim/flash_latency_bench.v.
| Operazione | @16MHz | @80MHz |
|---|---|---|
| ERASE (un settore 4KB) | 400.014 ms | 400.003 ms |
| SAVE (256B, incl. proprio erase) | 403.019 ms | 403.004 ms |
| LOAD (4096B) | 8.713 ms (0.470 MB/s) | 1.743 ms (2.351 MB/s) |
ERASE/SAVE sono dominati quasi interamente dal timing interno della flash (tSE_MAX=400ms, tPP_MAX=3ms — fisici, indipendenti dalla frequenza host); LOAD è invece puramente limitato dal clock SPI (nessuna attesa WIP per READ, §8 intro p.24 del datasheet) — la banda scala linearmente con la frequenza, come atteso fisicamente.
Riepilogo regressione (F6)
Tutti i 33 testbench del progetto (incluse le 2 varianti negative attese fallire in elaborazione) — PASS, nessuna regressione, eseguiti dopo ogni modifica significativa durante l'intera sessione, non solo alla fine.