feat(v2): scaffold hardware/v1 frozen baseline + M1 Neural Processor

Begins the V2 Neural Multiprocessor / Dataflow architecture per
docs/v2-description.md, per explicit user request to freeze V1 and
start V2 development, copying from V1 what's needed.

Scaffold:
- hardware/v1/: byte-exact, read-only copy of the current V1 codebase
  (rtl, testbenches, tools, constraints, a representative subset of
  synthesis results, and reference docs) -- verified identical via
  diff/cmp against the live top-level tree before being made
  filesystem-read-only. The live top-level tree is untouched and
  remains the project's "production" V1 (see hardware/v1/README.md
  and hardware/v2/logs/decisions.log DEC-0001 for why copy-not-move).
- hardware/v2/: mandatory structure (rtl/sim/constraints/synthesis/
  reports/scripts/logs/docs) plus the full logging system required by
  the spec (development/architecture/simulation/synthesis/timing/
  benchmark/decisions/experiments/errors.log).

M1 -- Neural Processor (hardware/v2/rtl/neural_processor.v):
- 8-stage pipelined perceptron unit (P_IN=8): input align, 8
  multipliers, 3-level adder tree, accumulator, bias+activation, INT8
  saturation. Genuine 1-tile/cycle throughput, not just a wider
  combinational datapath.
- 7-state FSM (NP_IDLE..NP_ERROR per docs/v2-description.md §6, with
  4 baseline states merged into NP_WAIT_OPERANDS -- see
  decisions.log DEC-0002); valid/ready/data/last stream interfaces
  per §7.
- Bit-exact vs the frozen hardware/v1/rtl/neuron_parallel.v + mac8.v
  + mac_unit.v: 7/7 tests pass (hardware/v2/sim/tb_neural_processor.v),
  covering regular/mixed-sign/extreme-INT8 vectors, both activations,
  a zero-idle-gap back-to-back-tiles throughput check, and an 8-tile
  job -- verified with Verilator (see below for why).
- Real synthesis + place&route (Yosys + nextpnr-ecp5): 0 CHECK
  problems, Fmax 183.12 MHz at ACC_WIDTH=32 (PASS at 80MHz, ~3x V1's
  isolated PARALLEL=8 Fmax of 61.71 MHz) and 176.21 MHz at ACC_WIDTH=24
  (a user-requested comparison experiment, also bit-exact-verified;
  see experiments.log EXP-0001/EXP-0002 and benchmark.log).

Three real bugs found and resolved during M1 development (full
diagnostic record in errors.log):
- Two independent, reproducible Icarus Verilog v13.0 scheduling
  defects (ERR-0001, ERR-0002) that silently produced wrong simulation
  results for standard sequential Verilog -- confirmed via Verilator
  5.050 giving correct results on the same minimal repros. Verilator
  is now the trusted simulator for hardware/v2/ (decisions.log
  DEC-0004); Icarus's affected protocol-violation check was removed
  from the RTL and deferred architecturally to the Neural Director
  (DEC-0003) rather than chased further.
- One real RTL bug (ERR-0003): last0 wasn't gated like valid0,
  letting a "last tile" tag leak into the pipeline ahead of its
  actual valid tile on back-to-back jobs. Fixed and verified.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
This commit is contained in:
2026-09-05 14:06:53 +02:00
co-authored by Claude Sonnet 5
parent 07a48e401f
commit dc0b331d3e
161 changed files with 1151210 additions and 0 deletions
@@ -0,0 +1,203 @@
# Fase 0 — Inventario reale della repo
Data: 2026-09-04. Metodo: lettura diretta dei file, non delle descrizioni in WORKLOG.md o
nei datasheet. Ogni claim qui sotto è verificato con un comando citato, rieseguibile.
---
## 0.1 Struttura dei file
| Area | File | Righe totali |
|---|---|---|
| `rtl/*.v` | 20 file | 7016 |
| `sim/*_tb.v` (testbench) | 33 file | 12934 |
| `sim/*.v` non-tb (modelli/benchmark) | 4 file: `flash_model.v`, `psram_model.v`, `flash_latency_bench.v`, `top.v` | — |
| `tools/` | `fpga_benchmark.py`, `netasm/` (assembler+parser+cli+frames, con test suite propria), `pinout/gen_lpf.py`, `flash_catalog/oracle.py`, **`run_regression.py` (nuovo, questa fase)** | — |
| `docs/` | 4 documenti `.md` + 2 PDF | — |
**Elenco RTL completo**: `act_buffer.v`, `crc32.v`, `flash_copy_engine.v`,
`flash_slot_manager.v`, `graph_engine.v`, `int8_memory_access.v`, `layer_sequencer.v`,
`layer.v`, `mac_unit.v`, `mac8.v`, `mem_arbiter.v`, `memory_interface.v`, `memory_model.v`,
`neuron_memory.v`, `neuron_parallel.v`, `psram_controller.v`, `spi_engine.v`,
`spi_flash_master.v`, `spi_neuron_top.v`, `spi_slave.v`.
### Discrepanza doc↔codice: conteggio testbench
Il prompt di questa campagna cita "la regressione dichiarata (22 testbench)". Il conteggio
reale via `find sim -name "*_tb.v" | wc -l` è **33** (più 1 file benchmark mal nominato con
suffisso `_tb.v`, vedi §0.3) — il numero 22 è superato da fasi successive del progetto
(sottosistema flash e Tipo #2 grafo, aggiunti dopo). Non è un errore nel senso di un bug: è
un documento/prompt che descrive uno stato precedente. Il numero corrente verificato è 33.
---
## 0.2 Codice morto/orfano trovato
### `sim/top.v` — **DEAD, non compila contro l'RTL corrente**
```
$ iverilog -g2012 -o /tmp/topcheck.out rtl/neuron_parallel.v rtl/mac8.v rtl/mac_unit.v sim/top.v
sim/top.v:17: error: parameter `FRAC_BITS` not found in `top.dut`.
2 error(s) during elaboration.
```
`sim/top.v` istanzia `neuron_parallel` con `.DATA_WIDTH(16)` e `.FRAC_BITS(8)` — un residuo
della versione a virgola fissa Q8.8 del progetto, prima che fosse convertito a INT8 puro
(coerente con `WORKLOG.md`, Fase 6: "rimosso FRAC\_BITS e funzione q8\_8"). Il modulo
`neuron_parallel.v` corrente non ha più un parametro `FRAC_BITS`. Il file è tracciato in git
(`git log --oneline -- sim/top.v``d4ae241 test: validate parametric 32x4 layer with
parallelism 8`, un commit storico) ma **non è referenziato da nessun testbench, script, o
tool di questo repo** — non partecipa alla regressione, non compila. Non toccato in questa
fase (nessuna modifica, per policy §E del prompt di certificazione — la rimozione, se
voluta, è una decisione separata dall'analisi).
### Nessun altro modulo RTL orfano
Ogni file in `rtl/*.v` è raggiungibile da almeno un testbench (direttamente o
transitivamente) — vedi matrice §0.4. Nessun modulo istanziato da zero testbench e da
nessun altro modulo RTL.
### Due file "memory model" distinti — non un bug, ma nome ambiguo
`rtl/memory_model.v` (interfaccia generica `req/wr/addr/wdata/rdata` con `READ_LATENCY`
parametrico, usato solo da `sim/memory_interface_tb.v` per isolare `memory_interface.v` dal
timing reale della PSRAM) è un modulo diverso da `sim/psram_model.v` (interfaccia
pin-accurate `ce_n/oe_n/we_n/lb_n/ub_n`, usato da praticamente tutti i test di integrazione
reali). Non è un bug — sono stub di fedeltà diversa per scopi diversi — ma il nome simile
(`memory_model` vs `psram_model`) e la collocazione di uno stub-solo-per-test dentro `rtl/`
anziché `sim/` è una scelta organizzativa che vale la pena segnalare per chi legge la repo
la prima volta.
---
## 0.3 Convenzione di naming inconsistente: benchmark con suffisso `_tb.v`
`sim/graph_engine_bandwidth_tb.v` ha il suffisso `_tb.v` (come i 33 testbench veri) ma è in
realtà un **benchmark** — stampa numeri misurati (`edges/sec`, `bandwidth`), non ha verdetto
PASS/FAIL, per progetto (stesso stile dichiarato di `sim/flash_latency_bench.v`, che invece
**non** ha il suffisso `_tb.v` e quindi non viene raccolto insieme ai testbench veri da un
comando generico `find sim -name "*_tb.v"`). Questa incoerenza di naming ha causato una
classificazione errata al primo giro del regression runner (§0.5) — corretta dopo aver letto
l'intento dichiarato nell'header del file, non assumendolo.
---
## 0.4 Matrice modulo → testbench (istanziazione diretta)
Costruita via analisi statica delle istanziazioni (`^\s*modulo\s+(#\(|nomeistanza\s*\()`),
non a memoria.
| Modulo RTL | Testbench che lo istanziano direttamente |
|---|---|
| `act_buffer` | `act_buffer_tb` |
| `crc32_byte` (in `crc32.v`) | `crc32_tb` |
| `flash_copy_engine` | `flash_copy_engine_{erase,load,save}_tb` |
| `flash_slot_manager` | `flash_slot_manager_tb`, `flash_slot_manager_raw_tb` |
| `graph_engine` | `graph_engine_tb`, `graph_engine_guard_tb`, `graph_engine_bandwidth_tb` |
| `int8_memory_access` | 10 testbench (tutti quelli con path PSRAM reale) |
| `layer_sequencer` | `layer_sequencer_tb` |
| `layer` | `layer_tb`, `parametric_tb` |
| **`mac_unit`** | **nessuno — 0 istanziazioni dirette in `sim/`** |
| **`mac8`** | **nessuno — 0 istanziazioni dirette in `sim/`** |
| `mem_arbiter` | 4 testbench (i 4 test flash con path PSRAM) |
| `memory_interface` | 12 testbench |
| `memory_model` | `memory_interface_tb` (solo questo) |
| `neuron_memory` | `neuron_memory_tb`, `neuron_memory_multi_tb` |
| `neuron_parallel` | 5 testbench (incl. i 2 negativi, §0.5) |
| `psram_controller` | 13 testbench |
| `spi_engine` | `spi_engine_tb` |
| `spi_flash_master` | `spi_flash_master_tb` |
| `spi_neuron_top` | 5 testbench (`_tb`, `_graph_tb`, `_irq_tb`, `_runnetwork_tb`, `_flash_tb`) |
| `spi_slave` | `spi_slave_tb`, `spi_engine_tb` |
### Finding da riportare in C.1 (Datapath aritmetico)
**`mac_unit.v` e `mac8.v` non hanno un testbench unitario dedicato.** Sono esercitati solo
indirettamente, come sotto-componenti di `neuron_parallel` nei test di livello superiore
(`neuron_parallel_tb`, `neuron_parallel_saturation_bounds_tb`, ecc.). Questo significa che
un comportamento scorretto isolato di `mac_unit`/`mac8` (es. estensione di segno errata sul
prodotto INT8×INT8, prima dell'accumulo) sarebbe rilevabile solo se si propaga fino
all'uscita finale del layer con un pattern di input che lo renda visibile — non c'è un
oracolo che verifichi `mac_unit` da solo. **Non certificabile come "coperto" fino a C.1.**
---
## 0.5 Regressione: eseguita da zero con harness nuovo, non fidandosi del WORKLOG
**Non esisteva alcuno script di regressione riproducibile nella repo** prima di questa fase
— ogni precedente affermazione "N testbench, tutti PASS" in `WORKLOG.md` è stata prodotta
assemblando a mano la lista file `iverilog` per ciascun test, mai da un harness unico
rieseguibile. Questo è di per sé un gap reale (nessuna prova automatizzata, riproducibile,
del claim di regressione) — colmato creando **`tools/run_regression.py`**: risolve le
dipendenze di ogni testbench per analisi statica delle istanziazioni (non a memoria/elenco
scritto a mano), compila con `iverilog -g2012` ed esegue con `vvp`, classifica il risultato.
**Primo run**: 2 falsi negativi e 1 "sconosciuto" — non erano bug, erano un **blind spot del
mio stesso harness**, corretto leggendo il codice sorgente dei test incriminati (non
assumendo):
- `neuron_parallel_guard_negative_{degenerate,nonmultiple}_tb.v` sono **test negativi
dichiarati**: il loro header dice esplicitamente "This file must FAIL TO
COMPILE/ELABORATE. That failure is the test" — verificano che il guard
`N_INPUTS % PARALLEL != 0` di `rtl/neuron_parallel.v:71-72` blocchi l'elaborazione
istanziando un modulo inesistente (`neuron_parallel_requires_N_INPUTS_multiple_of_PARALLEL`)
quando la condizione è violata. Il fallimento di compilazione **è** il PASS.
- `graph_engine_bandwidth_tb.v` è un benchmark (§0.3), nessun verdetto per progetto.
Corretto l'harness (whitelist esplicita per questi 3 casi, letta dal codice sorgente stesso
dei test, non inventata) e rilanciato:
```
$ python3 tools/run_regression.py
TOTAL: 34 PASS: 33 FAIL/ERROR: 0 OTHER/UNKNOWN: 1
```
**33/33 testbench reali PASS, 0 regressioni, 1 benchmark eseguito correttamente senza
verdetto (per progetto).** Il claim del WORKLOG ("tutti i testbench passano") è **confermato
vero** da un run indipendente e da zero — non solo creduto sulla parola.
`tools/netasm/tests/test_netasm.py` (20 test) verificato separatamente, anch'esso da zero:
**20/20 PASS**, invariato.
---
## 0.6 Osservazione preliminare, da verificare formalmente in C.1
Leggendo `rtl/neuron_parallel.v:70-73`, il guard elaboration-time è **un solo** controllo:
```verilog
if (N_INPUTS % PARALLEL != 0) begin : PARAMETER_ERROR_N_INPUTS_NOT_MULTIPLE_OF_PARALLEL
neuron_parallel_requires_N_INPUTS_multiple_of_PARALLEL invalid_parameter_combination();
end
```
Il commento del progetto dice che questo guard copre **sia** "N_INPUTS non multiplo di
PARALLEL" **sia** il caso degenere "PARALLEL > N_INPUTS" (con `GROUPS=0`, hang documentato).
Ma matematicamente: se `N_INPUTS == 0`, allora `N_INPUTS % PARALLEL == 0` per qualunque
`PARALLEL != 0` — il guard **non scatta**, eppure `GROUPS = 0/PARALLEL = 0`, la stessa
condizione di hang che il guard dichiara di prevenire. **Non ancora verificato se
`N_INPUTS=0` sia un caso raggiungibile/rilevante nella pratica** (nessun layer con zero
ingressi ha senso semantico, ma nessun controllo esplicito lo esclude) — portato come
finding aperto da chiudere formalmente in C.1 con un test avversariale dedicato e un
oracolo indipendente, non certificato né come bug né come non-bug qui.
---
## 0.7 Artefatti fuori dal codice sorgente (non toccati)
- `docs/FPGANeuralDatasheet.pdf` e `docs/FPGANeuralDatasheetEN.pdf`: comparsi come file non
tracciati, non generati da alcun processo di build di questo repo (il datasheet LaTeX vive
in `DataSheet/`, una directory separata non versionata — vedi memoria di progetto).
Probabile sottoprodotto del meccanismo di invio file usato in questa sessione. Non fanno
parte della fonte di verità RTL/documentazione; non modificati né cancellati (non è una
decisione di questa fase).
- `FPGA-Neural/` (progetto KiCad): non tracciato per policy di progetto pre-esistente
(vedi memoria), non toccato.
---
## 0.8 Prossimi passi
Procedo con gli aspetti C.1C.14 uno alla volta, ciascuno con: analisi statica, test
avversari con oracolo indipendente (§A.1/A.3), verdetto tracciato. Il finding aperto di
§0.6 (`N_INPUTS=0`) va chiuso in C.1. `mac_unit`/`mac8` senza test unitario (§0.4) va colmato
in C.1 prima di poter certificare il datapath aritmetico.
+162
View File
@@ -0,0 +1,162 @@
# C.1 — Datapath aritmetico (`mac_unit`, `mac8`, `neuron_parallel`)
Data: 2026-09-04. Chiude i due punti aperti in Fase 0 (§0.4 copertura `mac_unit`/`mac8`,
§0.6 gap del guard `N_INPUTS=0`).
---
## 1.1 `mac_unit.v` — CERTIFICATO
**Metodo**: test esaustivo, non a campione. `mac_unit` è puramente combinazionale
(`x`, `w`, `acc_in``acc_out = acc_in + sign_extend(x*w)`), a `DATA_WIDTH=8` esistono
esattamente 256×256=65536 combinazioni possibili di `(x,w)` — tutte esercitate, non un
sottoinsieme casuale.
**Oracolo**: `tools/validation/mac_oracle.py`, reimplementazione Python indipendente
dell'aritmetica complemento-a-due da zero (non trascritta dall'RTL) — auto-verificata contro
6 casi derivati a mano prima di essere usata come oracolo per chiunque altro
(`python3 tools/validation/mac_oracle.py``ALL SELF-CHECKS PASSED`).
**Test**: `sim/mac_unit_tb.v`, due batterie:
1. Le 65536 combinazioni esaustive di `(x,w)`, `acc_in=0` (l'uso reale in `mac8.v`, dove
`acc_in` è cablato a 0 per ogni istanza `mac_unit`).
2. 486 vettori a `acc_in` diverso da zero — inclusi i 4 valori limite (`0`, `±2³¹`, `±2³⁰`) e
i 4 angoli di grandezza massima del prodotto (`±128×±128`, `±128×∓127`) — a copertura del
**contratto di porta completo** del modulo, non solo di come viene usato oggi.
```
$ iverilog -g2012 -o /tmp/mac_unit_tb.out rtl/mac_unit.v sim/mac_unit_tb.v && vvp /tmp/mac_unit_tb.out
ALL TESTS PASSED (66022 vectors, 0 mismatches against independent Python oracle)
```
**Verdetto: CERTIFICATO.** 66022/66022 vettori, 0 mismatch, copertura esaustiva sullo
spazio degli input a INT8. Nessuna riserva.
---
## 1.2 `mac8.v` — adder tree bilanciato — CERTIFICATO
**Metodo**: `mac8.v` non aveva alcun test unitario dedicato (Fase 0, §0.4) — solo copertura
indiretta a un singolo `PARALLEL` tramite `neuron_parallel_tb.v`. Un bug di cablaggio
dell'albero (linea scambiata/duplicata/persa) a un `PARALLEL` diverso da quello usato dai
test esistenti sarebbe passato inosservato.
**Test**: `sim/mac8_tree_tb.v`, verificato a **PARALLEL=2, 8 (il default/omonimo del modulo)
e 32** — gli estremi realmente usati nei benchmark del progetto
(`docs/FPGA-Neural-Datapatch-Benchmark.md`), non un solo valore a piacere. Tre famiglie per
ciascun `PARALLEL` (939 vettori totali):
1. **Strutturale/avversariale**: `x=[1..PARALLEL]`, `w=1`, ordine sia ascendente che
invertito. La somma attesa (`PARALLEL×(PARALLEL+1)/2`) torna corretta **solo se ogni
linea è sommata esattamente una volta** — un modo efficace di scoprire un cablaggio
scambiato che un test casuale potrebbe non notare (uno scambio+una linea persa possono
annullarsi per caso su input casuali, mai su questo pattern esatto).
2. **300 coppie INT8 casuali per `PARALLEL`**, `acc_in` variato su un range realistico —
riproduce il collegamento reale (`neuron_parallel.v`: `mac8.acc_in = acc`, l'accumulatore
che cresce gruppo dopo gruppo, **non** cablato a 0 come si potrebbe erroneamente
assumere).
3. **Avversariale**: tutte le linee al prodotto di grandezza massima (`±16384`/`∓16256`)
simultaneamente, con `acc_in` ai bordi di `ACC_WIDTH=32` — conferma che l'eventuale
wraparound complemento-a-due dell'albero è ben definito (non X/indefinito), pur essendo
una magnitudine ben oltre quanto un layer reale (`N_INPUTS≤256`) accumulerebbe mai
(dichiarato, non presentato come condizione operativa reale).
```
$ iverilog -g2012 -o /tmp/mac8_tb.out rtl/mac_unit.v rtl/mac8.v sim/mac8_tree_tb.v && vvp /tmp/mac8_tb.out
ALL TESTS PASSED (939 vectors across PARALLEL=2/8/32, 0 mismatches against independent Python oracle)
```
**Verdetto: CERTIFICATO** a PARALLEL=2/8/32. **Riserva dichiarata**: non verificato ad ogni
altro `PARALLEL` usato nel progetto (es. 4, 16) — il rischio residuo è basso (la costruzione
dell'albero è generica via `$clog2(PARALLEL)`, identica per ogni potenza di due, e 3 valori
distinti già la esercitano a profondità diverse: 1, 3, 5 livelli), ma non è "esaustivo su
tutti i PARALLEL" nello stesso senso in cui §1.1 lo è su `(x,w)`.
---
## 1.3 Saturazione INT8 / attivazione — CERTIFICATO CON RISERVA (test pre-esistente, riverificato)
`sim/neuron_parallel_saturation_bounds_tb.v` (già presente prima di questa campagna) copre
gli 8 valori di bordo dichiarati nel task di timing-closure (126, 127, 128, 129, -128, -129,
-1, 0) per `ACT_NONE` e `ACT_RELU`, con oracolo **calcolato a mano** (non derivato dal
codice) — verificato bit-esatto per confermare che la riscrittura a bit-test delle
comparazioni di saturazione (da confronti aritmetici `>127`/`<-128` a riduzioni AND/OR)
durante la timing closure non ha alterato il comportamento.
Ri-eseguito da zero in questa campagna (non solo citato dal WORKLOG): **PASS**, invariato.
**Riserva**: il test usa `N_INPUTS=PARALLEL=1` (una sola corsia MAC), per raggiungere ogni
valore di bordo esattamente con una singola tripla `(x,w,bias)` scelta a mano. Non esercita
l'interazione tra l'albero a più corsie (§1.2, ora certificato separatamente) e la
saturazione finale nello stesso run — cioè non c'è un test che porti un accumulo
multi-gruppo/multi-corsia esattamente a uno di questi bordi. Rischio basso (la saturazione
opera sul valore finale di `acc+bias`, indipendentemente da come quel valore è stato
costruito), ma non è stato verificato esplicitamente in questa campagna.
---
## 1.4 Guard `N_INPUTS % PARALLEL != 0` — CERTIFICATO CON RISERVA GRAVE (BUG-002 confermato)
### Cosa funziona (già coperto, riverificato)
`sim/neuron_parallel_guard_negative_{nonmultiple,degenerate}_tb.v`: due test negativi che
provano che il guard blocca l'elaborazione per `N_INPUTS % PARALLEL != 0` (incl. il caso
degenere "PARALLEL > N_INPUTS" per N_INPUTS≥1, dove il resto della divisione coincide con
N_INPUTS stesso, quindi è comunque non-zero). Rieseguiti da zero: **entrambi falliscono a
compilare come previsto** — è il PASS.
### BUG-002 — confermato reale, su ENTRAMBI i piani di verifica (non solo ipotizzato)
**Il finding aperto in Fase 0 §0.6 era corretto nell'ipotesi ma la mia prima verifica
empirica era sbagliata per un bug nella MIA testbench** — narrativa completa perché è
rilevante per la fiducia nel resto della campagna:
1. Primo tentativo: un test con `repeat(50) @(posedge clk); if (done) ... else "HANG"` ha
riportato "hang" per `N_INPUTS=0`. **Metodologicamente invalido**: `done` è un impulso di
**un solo ciclo** (`rtl/neuron_parallel.v:207/227`, `done <= 0` incondizionato subito dopo
averlo asserito), quindi un controllo tardivo e singolo di `done` mostra sempre 0 **anche
quando tutto funziona correttamente** — confermato riproducendo lo stesso falso "HANG" su
una config nota-buona (`N_INPUTS=2, PARALLEL=2`, valida, mai dovrebbe fallire).
2. Corretto il metodo: osservare `done` **ogni ciclo** (non un controllo singolo tardivo).
Sulla config nota-buona, ora **PASS correttamente** (`done` pulsa al ciclo giusto, `y=5`
coerente col calcolo a mano). Sul caso `N_INPUTS=0`: **confermato, `busy` non si alza mai
e `done` non pulsa mai in 200 cicli** — non è più un'ipotesi, è un fatto osservato con un
metodo verificato corretto prima su un caso di controllo.
3. **Causa architetturale trovata** (non solo il sintomo): `x_bus`/`w_bus` sono dichiarati
`[DATA_WIDTH*N_INPUTS-1:0]` — per `N_INPUTS=0` questo è `[-1:0]`, che **non collassa a
larghezza zero**: sia Icarus che Yosys lo trattano come un vettore a **2 bit** reali
(larghezza = |MSB-LSB|+1 = |-1-0|+1 = 2), lasciato **non pilotato**. Confermato dai
warning di Yosys stesso: `Wire ...x_bus[1] is used but has no driver` (×2 per x_bus/w_bus).
4. **Confermato anche in sintesi reale**, non solo in simulazione (§A.4): `yosys synth_ecp5`
elabora `neuron_parallel #(.N_INPUTS(0), .PARALLEL(2))` con **0 problemi segnalati** dal
CHECK pass — lo stesso silenzio che permette al guard di non scattare in simulazione si
ripete identico sul secondo piano di verifica indipendente.
**Test di regressione permanente**: `sim/neuron_parallel_bug002_n_inputs_zero_tb.v`
riproduce il sintomo esatto in modo deterministico, PASSA oggi confermando che il bug è
ancora presente (non è un'asserzione che questo comportamento sia desiderabile — il file
stesso lo dichiara esplicitamente in testa, con l'istruzione di riscriverlo, non
allentarlo, quando il bug verrà corretto).
**Verdetto: NON CERTIFICATO per `N_INPUTS=0`.** Il guard copre correttamente ogni
combinazione `N_INPUTS≥1` non multipla di `PARALLEL` (incl. il caso degenere
`PARALLEL>N_INPUTS≥1`), ma **non copre `N_INPUTS=0`**, che produce un `start` silenziosamente
inefficace (non l'hang "busy alto per sempre" descritto nel commento originale del guard —
un sintomo diverso, osservato per la prima volta in questa campagna) su entrambi i piani di
verifica. Vedi `docs/validation/bugs.md` BUG-002 per severità e fix proposto.
---
## 1.5 Verdetto complessivo C.1
| Sotto-aspetto | Verdetto |
|---|---|
| `mac_unit.v` | **CERTIFICATO** (esaustivo, 66022 vettori, 0 riserve) |
| `mac8.v` (adder tree) | **CERTIFICATO** (939 vettori, PARALLEL=2/8/32; riserva: non esaustivo su ogni PARALLEL) |
| Saturazione/attivazione | **CERTIFICATO CON RISERVA** (test pre-esistente valido; riserva: non testato in combinazione con l'albero multi-corsia) |
| Guard `N_INPUTS%PARALLEL` | **NON CERTIFICATO per N_INPUTS=0** — BUG-002 confermato su sim + sintesi reale |
**Il datapath aritmetico di base (moltiplicazione, estensione di segno, albero di somma) è
solido** — certificato esaustivamente dove possibile. **Il guard di protezione attorno ad
esso ha un buco reale e confermato**, non un rischio teorico: `N_INPUTS=0` produce hardware
sintetizzabile (0 errori Yosys) che silenziosamente non fa nulla quando l'host tenta di
avviarlo.
@@ -0,0 +1,163 @@
# C.2 — Larghezza runtime (`n_inputs_real`, `n_neurons_real`)
Data: 2026-09-04. Verifica se la terminazione anticipata a runtime è reale (nessuna lettura
oltre il limite impostato) e chiude il rischio dichiarato nell'header di `neuron_parallel.v`
("`n_inputs_real` che ... è 0 ... riproduce lo stesso hang" di BUG-002).
Nota di processo: durante questa verifica ho ottenuto un altro falso risultato dalla mia
stessa testbench (un secondo, dopo quello di C.1) — riportato per intero in §2.2, non
nascosto, perché è rilevante per capire quanto vada verificato con cura ogni singolo
risultato anomalo prima di fidarsene.
---
## 2.1 Terminazione anticipata reale — `n_inputs_real` (livello `neuron_parallel.v`) — CERTIFICATO
**Metodo**: `N_INPUTS=32` (build-time, max), regione "reale" (indici 0-15) con `x=w=1`,
regione "veleno" (indici 16-31) con `x=w=100` — se l'RTL leggesse anche solo un elemento
oltre `n_inputs_real`, il prodotto enorme (100×100=10000) satura immediatamente il risultato
a 127, rendendolo distinguibile da un risultato corretto.
**Oracolo**: somma attesa calcolata a mano; range di cicli atteso calcolato da
`GROUPS_real = n_inputs_real/PARALLEL` gruppi + overhead fisso di pipeline.
```
n_inputs_real=32: PASS -- y=127 cycles=5 (legge anche il "veleno": saturazione attesa e corretta)
n_inputs_real=16: PASS -- y=16 cycles=3 (NON legge il "veleno": somma esatta, nessun over-read)
n_inputs_real=8: PASS -- y=8 cycles=2
```
**Verdetto: CERTIFICATO.** La terminazione anticipata è reale — non legge oltre il limite
impostato, e il numero di cicli scala proporzionalmente col numero di gruppi reali.
## 2.2 `n_inputs_real` non multiplo di `PARALLEL` — CERTIFICATO (comportamento come documentato)
**Test**: `n_inputs_real=17` (non multiplo di `PARALLEL=8`) con `N_INPUTS=32` build-time
valido. Atteso a mano: troncamento intero `17/8=2` gruppi → legge solo i primi 16 elementi,
`y=16` (non 17).
**Falso risultato iniziale, corretto**: un primo tentativo (script bespoke, non lo schema
già provato in §2.1) ha mostrato "busy=0, nessun done in 200 cicli" — sembrava un hang.
Anziché fidarmi, ho rieseguito lo STESSO caso riusando lo schema di task `run_case` già
dimostrato corretto in §2.1 (stessa sequenza di reset/start, tre invocazioni consecutive
nella stessa run per controllo di ripetibilità): **`n_inputs_real=17``done` al ciclo 3,
`y=16` — esattamente il troncamento silenzioso atteso, non un hang.** Il primo risultato era
un artefatto della mia testbench (probabile problema di temporizzazione nel setup di quel
singolo script), non un comportamento reale dell'RTL — non l'ho riportato come bug senza
prima riprodurlo con un metodo già affidabile.
**Verdetto: CERTIFICATO.** Il rischio dichiarato nell'header ("troncamento silenzioso,
risultato sbagliato, nessun errore") è confermato accurato per questo caso: comportamento
sbagliato-ma-silenzioso, non un hang.
## 2.3 `n_inputs_real=0` a runtime — BUG-003, comportamento INCOERENTE tra le mie stesse ripetizioni (non un verdetto singolo affidabile)
**Test**: `N_INPUTS=32, PARALLEL=8` (validi, compile-time, guard soddisfatto — nessun
problema di larghezza `[-1:0]` qui, a differenza di BUG-002; `GROUP_INDEX_WIDTH=2` bit per
questa build, non 1 come nel caso di BUG-002). A runtime, `n_inputs_real=0` via la porta (lo
stesso percorso che l'host raggiunge via `SET_BASE sel=7`,
`docs/FPGA-NeuralNetwork-Engine.md` §8.1).
**Qui la mia stessa verifica ha prodotto risultati DIVERSI tra run apparentemente
equivalenti, e lo riporto per intero invece di scegliere il risultato che sembra più
pulito:**
- Prima verifica (script isolato, dati tutti a `x=w=1`): **hang**`busy` mai alto, nessun
`done` in 200 cicli.
- Riprodotto con lo schema `run_case` già affidabile (§2.1), come PRIMA chiamata di una
simulazione fresca, dati con regione "veleno": **NESSUN hang**`done` al ciclo 5,
`y=127` (ha letto anche la regione veleno, cioè ha ignorato il limite e processato l'intera
larghezza, non si è bloccato).
- Stesso schema, PRIMA chiamata di una simulazione fresca ma con OGNI registro
esplicitamente inizializzato prima di qualunque reset (per escludere artefatti di
propagazione di X in simulazione): **ancora nessun hang**`y=32` (di nuovo, limite
ignorato, non bloccato).
- Stesso schema, ma con **una chiamata valida precedente** (`n_inputs_real=32`) prima della
chiamata a `n_inputs_real=0`, ripetuta due volte: **nessun hang in nessuna delle due**,
`y=32` entrambe le volte.
- Uno script con **quattro chiamate consecutive tutte a `n_inputs_real=0`** (variando solo
il numero di cicli di reset tra 1 e 5): **la primissima chiamata non si blocca** (`y=32`,
limite ignorato), **le tre chiamate successive SI bloccano** (nessun `done` in 200 cicli).
**Non sono riuscito a isolare la condizione esatta che decide tra i due esiti** entro un
tempo ragionevole per questa campagna — non è (solo) l'ordine delle chiamate (una sequenza
valida→zero non blocca; una sequenza zero→zero→zero dopo la prima blocca dalla seconda in
poi), non è il contenuto dei dati (`x_bus`/`w_bus`) dato che quello non dovrebbe influenzare
la logica di controllo `group_index`/`groups_real`, e non è propagazione di X (verificato
esplicitamente inizializzando tutto). **Analisi aritmetica**: per questa build
`GROUP_INDEX_WIDTH=2` bit, quindi `groups_real[1:0]-1` per `groups_real=0` avvolge a `3` (un
valore RAGGIUNGIBILE dal contatore a 2 bit, a differenza del caso a 1 bit di BUG-002) — il
che spiegherebbe l'esito "nessun hang, limite ignorato, processa tutta la larghezza" come
esito atteso per l'aritmetica di avvolgimento, ma NON spiega perché in alcune ripetizioni
compaia invece un hang vero.
**Verdetto: NON CERTIFICATO, e dichiarato esplicitamente NON PIENAMENTE CARATTERIZZATO**
non fingo un meccanismo che non ho isolato. Quello che è certo, indipendentemente da quale
dei due sintomi si manifesti: **nessuno dei due è corretto** (un host che chiede
`n_inputs_real=0` non dovrebbe né bloccarsi né ottenere silenziosamente l'intera larghezza
di build al posto di zero elementi), e **l'incoerenza stessa tra ripetizioni quasi identiche
è di per sé un problema segnalabile**, indipendente dal meccanismo esatto. Vedi
`docs/validation/bugs.md` BUG-003 per lo stato aggiornato.
## 2.4 Terminazione anticipata reale — `n_neurons_real` (livello `neuron_memory.v`) — CERTIFICATO per valori validi
**Metodo**: `N_NEURONS=3` (build-time), memoria stub minimale sempre-pronta (il contenuto
non conta per questo test, solo se il loop termina e in quanti cicli).
```
n_neurons_real=3: done al ciclo 155
n_neurons_real=2: done al ciclo 114
n_neurons_real=1: done al ciclo 73
```
Scala proporzionalmente (~41 cicli/neurone) — la terminazione anticipata funziona
correttamente per valori validi ≥1.
**Verdetto: CERTIFICATO per `n_neurons_real` ∈ [1, N_NEURONS].**
## 2.5 `n_neurons_real=0` — BUG-004 CONFERMATO (classe diversa: non hang, limite ignorato silenziosamente)
**Ipotesi iniziale** (per analogia con BUG-002/003): mi aspettavo lo stesso hang. **Non è
quello che succede.**
**Test 1** (`N_NEURONS=3`, `NEURON_INDEX_WIDTH=2` bit): `n_neurons_real=0`**`done` al
ciclo 196** (non un hang — termina, ma in PIÙ cicli di `n_neurons_real=3` stesso, 155).
**Test 2** (`N_NEURONS=2`, `NEURON_INDEX_WIDTH=1` bit — la stessa larghezza-1-bit che in
`neuron_parallel.v` causa l'hang di BUG-002): `n_neurons_real=0`**`done` al ciclo 114,
identico a `n_neurons_real=2`** (§2.4). Non un hang, ma il conteggio di cicli **coincide
esattamente** col caso "processa tutti i neuroni" — il valore richiesto (0) sembra essere
stato **ignorato silenziosamente**, con l'hardware che processa l'intero build invece che
zero neuroni, terminando in modo perfettamente normale (nessun errore, nessun sintomo
visibile all'host).
**Perché è diverso da BUG-002/003**: l'aritmetica di wraparound qui (`neuron_index ==
n_neurons_real[W-1:0]-1`) non blocca il contatore in uno stato irraggiungibile come accade
per `group_index` a 1 bit in `neuron_parallel.v` — piuttosto lo fa avvolgere su un valore
che, per coincidenza di larghezza, corrisponde al conteggio COMPLETO. Non ho ulteriormente
isolato la causa esatta bit-per-bit (a differenza di BUG-002, dove l'ho fatto) — dichiarato
come limite di questa verifica, non presentato come pienamente compreso.
**Verdetto: NON CERTIFICATO per `n_neurons_real=0`.** Vedi `docs/validation/bugs.md`
BUG-004. **Più insidioso di un hang**: un host che chiede (per errore) zero neuroni riceve
un completamento normale e apparentemente valido, ma calcolato sull'intero conteggio di
build — dato silenziosamente sbagliato, non un timeout rilevabile.
---
## 2.6 Verdetto complessivo C.2
| Sotto-aspetto | Verdetto |
|---|---|
| Terminazione anticipata `n_inputs_real` (valori validi) | **CERTIFICATO** |
| `n_inputs_real` non multiplo di PARALLEL | **CERTIFICATO** (comportamento = rischio documentato) |
| `n_inputs_real=0` | **NON CERTIFICATO, comportamento non pienamente caratterizzato** — BUG-003 (incoerente tra ripetizioni: a volte hang, a volte limite ignorato) |
| Terminazione anticipata `n_neurons_real` (valori validi) | **CERTIFICATO** |
| `n_neurons_real=0` | **NON CERTIFICATO** — BUG-004 (limite ignorato silenziosamente, non hang) |
**Il meccanismo di larghezza runtime funziona correttamente per ogni valore valido**
certificato con oracoli indipendenti e verifica del non-over-read. **Il valore limite 0, in
entrambi i punti di ingresso (`n_inputs_real` e `n_neurons_real`), produce due classi
DIVERSE di comportamento scorretto** — un hang silenzioso in un caso, un risultato
silenziosamente sbagliato-ma-dall'aspetto-normale nell'altro — entrambi raggiungibili
dall'host via il protocollo SPI documentato, senza bisogno di una nuova sintesi.
+98
View File
@@ -0,0 +1,98 @@
# C.3 — Sottosistema memoria (`int8_memory_access`, `memory_interface`, `psram_controller`)
Data: 2026-09-04.
---
## 3.1 `int8_memory_access.v` — conversione byte↔word e selezione byte-lane — CERTIFICATO
**Metodo**: nuovo test dedicato (`sim/int8_memory_access_bytelane_tb.v`), non esisteva prima
una verifica esaustiva della conversione indirizzo. Due batterie:
1. **Esaustiva su 2048 indirizzi** (ogni combinazione dei 12 bit bassi, entrambe le parità):
`mem_addr` (deve essere `addr>>1`), `mem_lb_n`/`mem_ub_n` (byte pari→basso attivo, dispari→
alto attivo) — ispezionati direttamente sui segnali combinazionali.
2. **Round-trip scrittura/lettura reale** attraverso l'handshake FSM (non un peek interno) a
6 indirizzi (pari/dispari, valori di bordo INT8, e un test esplicito che una scrittura
sull'indirizzo dispari di una parola NON corrompa il byte pari già scritto nella stessa
parola — verifica che le byte-lane siano davvero indipendenti).
**Oracolo**: formula dichiarata dal modulo stesso (`addr>>1`, `addr[0]`), applicata
indipendentemente, non letta dall'RTL.
**Due bug nella MIA testbench, trovati e corretti prima di fidarmi del risultato** (stesso
schema di trasparenza di C.1/C.2, riportato per intero):
1. Il controllo dei segnali `mem_addr`/`mem_lb_n`/`mem_ub_n` nel TEST 1 avveniva nello stesso
passo di simulazione dell'aggiornamento non-bloccante che li produce — leggeva il valore
dell'iterazione PRECEDENTE, non quella corrente (3071 "mismatch" su 2048 controlli, tutti
falsi). Corretto con un `#1` dopo il fronte di clock, per lasciare che l'aggiornamento si
assesti prima di leggerlo.
2. Il modello di memoria comportamentale della testbench scriveva l'intera parola a 16 bit
incondizionatamente, **ignorando `mem_lb_n`/`mem_ub_n`** — una scrittura sul byte dispari
di una parola cancellava il byte pari già scritto lì, anche con `mem_lb_n=1` (disabilitato).
Questo ha fatto fallire il test di round-trip "scrittura non deve corrompere il byte
fratello" — ma il difetto era nello STUB di test, non nell'RTL sotto test (il DUT
comunica correttamente `mem_lb_n`/`mem_ub_n`, era il modello di memoria a non rispettarli).
Corretto rendendo lo stub sensibile alle byte-lane come una vera memoria mascherabile a
byte.
```
$ iverilog -g2012 -o /tmp/bytelane2.out rtl/int8_memory_access.v sim/int8_memory_access_bytelane_tb.v && vvp /tmp/bytelane2.out
ALL TESTS PASSED (2048 decode checks + 6 round-trip checks, 0 mismatches)
```
**Verdetto: CERTIFICATO.** 2054/2054 controlli, 0 mismatch, dopo la correzione di due difetti
nella testbench stessa (non nell'RTL).
---
## 3.2 `memory_interface.v` — CERTIFICATO (via test pre-esistente)
Modulo semplice: stesso pattern di handshake req/ready di `int8_memory_access.v` ma a
granularità 16 bit, senza logica di byte-lane propria (inoltra `lb_n`/`ub_n` così come
ricevuti). `sim/memory_interface_tb.v` (pre-esistente, riverificato in Fase 0) copre
l'handshake. Non ripetuto da zero in questa fase: la logica è sufficientemente semplice
(nessuna aritmetica di indirizzo propria) da non giustificare una nuova campagna esaustiva
oltre a quanto già verificato.
**Verdetto: CERTIFICATO** (copertura pre-esistente, ritenuta adeguata alla semplicità del
modulo).
---
## 3.3 `psram_controller.v` — CERTIFICATO (lavoro estensivo già svolto in questa sessione, non ri-fatto da zero)
Questo modulo ha già ricevuto una verifica sostanziale **in questa stessa sessione**, non
solo dichiarata nel WORKLOG di sessioni precedenti:
- **Un bug reale pre-esistente trovato e corretto**: richieste arrivate durante
`STATE_INIT`/`STATE_CR_INIT` (~150µs di poweron) venivano perse silenziosamente; corretto
con un latch `req_pending` — trovato durante il lavoro sul sottosistema flash (Fase F2),
con una riproduzione minimale isolata prima e dopo il fix.
- **Timing di page-mode verificato contro il datasheet ISSI reale**: `ACCESS_CYCLES =
ceil(70ns × CLK_FREQ_MHZ/1000)`, `PAGE_CYCLES` per il burst, `tCEM` (idle timeout e budget
mid-burst) — `sim/psram_page_mode_tb.v`, con `sim/psram_model.v` che fa **`$fatal` su
qualunque violazione di timing reale** (non solo un controllo di valore atteso: un
meccanismo di oracolo attivo che blocca la simulazione se l'RTL viola una regola del
datasheet, indipendentemente da cosa la testbench stessa controlli esplicitamente).
- Rieseguito in Fase 0 di questa campagna (non solo citato): `psram_controller_tb.v` e
`psram_page_mode_tb.v` **PASS**, confermato da un harness di regressione indipendente
(`tools/run_regression.py`), non dalla parola del WORKLOG.
**Non ripetuto da zero in C.3**: rifare l'intera campagna di verifica del page-mode/tCEM già
completata con rigore comparabile in questa sessione sarebbe una duplicazione di lavoro già
tracciabile (WORKLOG, fasi F2/G7), non una nuova scoperta. Citato come evidenza, non dato per
buono senza verifica: il PASS è stato riconfermato da zero in Fase 0 di questa campagna.
**Verdetto: CERTIFICATO**, con la stessa evidenza di prima (riverificata, non solo citata).
---
## 3.4 Verdetto complessivo C.3
| Sotto-aspetto | Verdetto |
|---|---|
| `int8_memory_access.v` (byte↔word, byte-lane) | **CERTIFICATO** (nuovo, esaustivo su 2048 indirizzi) |
| `memory_interface.v` | **CERTIFICATO** (copertura pre-esistente adeguata) |
| `psram_controller.v` (incl. page-mode, tCEM) | **CERTIFICATO** (lavoro esteso di sessione, riverificato) |
Nessun bug nuovo trovato in questo aspetto — due difetti trovati erano nella mia stessa
testbench di verifica, corretti prima di trarre conclusioni sull'RTL.
+75
View File
@@ -0,0 +1,75 @@
# C.4 — Arbitro (`mem_arbiter.v`)
Data: 2026-09-04.
---
## 4.1 Priorità B>C>A>D — CERTIFICATO
**Metodo**: nuovo test dedicato (`sim/mem_arbiter_priority_tb.v`). Quattro scenari,
combinazioni decrescenti di richiedenti simultanei, ciascuno con dati distinguibili
(`m_rdata` eco dell'indirizzo) per confermare che la risposta torni al **richiedente
corretto**, non solo che "qualcuno" venga servito:
1. A+B+C+D simultanei → B vince.
2. A+C+D (B assente) → C vince.
3. A+D (B,C assenti) → A vince.
4. D da solo → viene comunque servito (bassa priorità ≠ mai servito).
```
$ iverilog -g2012 -o /tmp/arb6.out rtl/mem_arbiter.v sim/mem_arbiter_priority_tb.v && vvp /tmp/arb6.out
ALL TESTS PASSED (priority order B>C>A>D confirmed; ...)
```
**Nota di processo — race trovata nella mia stessa testbench**: la prima versione usava
assegnazioni bloccanti per ritirare le richieste dei "perdenti" nello stesso
`@(posedge clk)` che doveva concedere la richiesta — una race reale con il blocco
sincrono del DUT sullo stesso fronte (l'ordine di esecuzione tra processi diversi
sensibili allo stesso evento non è garantito da Verilog). Diagnosticato con un
riferimento gerarchico a `dut.owner`, mai uscito da `SEL_NONE` nonostante le richieste
fossero pilotate — non un difetto dell'RTL. Corretto passando ad assegnazioni non
bloccanti per i segnali di richiesta in tutta la testbench, come farebbe un master reale
sincrono allo stesso clock.
**Verdetto: CERTIFICATO.** L'ordine di priorità dichiarato nell'header è implementato
esattamente come descritto, dati instradati al richiedente corretto in ogni caso.
---
## 4.2 Starvation di D sotto contesa continua — comportamento reale, ambiguità nella documentazione
**Test**: `b_req` e `d_req` mantenuti entrambi asserti continuamente per 500 cicli
(B "ha sempre altro lavoro" nell'istante in cui si libera).
**Risultato**: **D non viene MAI concesso in 500 cicli** di contesa continua da B.
**Perché non lo classifico come bug**: l'header del modulo dichiara "flash operations
are ms-scale and never meant to compete with inference for memory bandwidth" e "In
normal operation B and C are temporally disjoint anyway" — la contesa continua e
sostenuta testata qui è esplicitamente fuori dallo scenario operativo previsto (un
`layer_sequencer`/`neuron_memory` che non lascia MAI un buco libero per centinaia di
cicli di fila non corrisponde a un'inferenza reale). Un arbitro a priorità fissa senza
invecchiamento (aging) che fa morire di fame il richiedente più basso sotto carico
sostenuto è un design standard e spesso intenzionale, non un difetto di per sé.
**Cosa segnalo**: la frase dell'header "gets stretched out, never starves or corrupts
A/B/C" è **ambigua** — può essere letta sia come "[D] non affama mai [se stesso]" sia
come "[la contesa] non fa mai affamare o corrompere A/B/C" (una garanzia solo su A/B/C,
non su D). Il comportamento osservato è coerente con la SECONDA lettura, non con la
prima. Non è un bug funzionale, ma la frase andrebbe disambiguata nel commento sorgente
per evitare che un futuro lettore assuma erroneamente che D abbia una garanzia di
progresso che il codice non implementa.
**Verdetto: CERTIFICATO come comportamento** (nessuna sorpresa rispetto a un arbitro a
priorità fissa senza aging), **riserva documentale** sulla frase ambigua dell'header.
---
## 4.3 Verdetto complessivo C.4
| Sotto-aspetto | Verdetto |
|---|---|
| Priorità B>C>A>D, instradamento dati corretto | **CERTIFICATO** |
| Starvation di D sotto contesa sostenuta | **CERTIFICATO come comportamento**, riserva sulla chiarezza della documentazione (non un bug) |
Nessun bug RTL trovato in questo aspetto. Un difetto di race trovato e corretto nella
testbench di verifica stessa (stesso schema del resto della campagna).
@@ -0,0 +1,75 @@
# C.5 — Sequencer dense (`layer_sequencer.v`)
Data: 2026-09-04.
---
## 5.1 Catena layer, ping-pong, busy/done — CERTIFICATO (test pre-esistente, valido)
`sim/layer_sequencer_tb.v` (pre-esistente, riverificato in Fase 0) copre un run a 2 layer
con verifica campo-per-campo del descrittore decodificato (`nm_w_base`, `nm_bias_addr`,
`nm_x_base`, `nm_activation`, `nm_n_inputs`, `nm_n_neurons`), e in modo particolarmente
solido: **verifica l'indirizzo del buffer ping-pong usato per layer 1, non solo il valore**
(conferma che layer 1 legge dal buffer che layer 0 ha effettivamente scritto — il punto
reale dello schema ping-pong, non solo che "un" buffer sia stato usato). Verifica anche che
`seq_busy` resti asserto per l'intero run a 2 layer (non cada tra un layer e l'altro) e che
`seq_done` pulsi esattamente una volta, dopo l'ULTIMO layer (un `nm_done` intermedio non deve
attivarlo).
**Verdetto: CERTIFICATO** per la catena a `run_num_layers` valido (test singolo ma
sufficientemente rigoroso nel verificare indirizzi, non solo valori).
---
## 5.2 `run_num_layers=0` — BUG-005 CONFERMATO, CRITICO
**Ipotesi**, per analogia col guard mancante già visto in BUG-002/003/004: `run_num_layers`
è documentato "1..N_LAYERS" ma **non ha alcun guard**, né a compile-time né a runtime.
`layer_idx` (`rtl/layer_sequencer.v:121`) è però un registro a **8 bit pieni** (non ristretto
a 1 bit come il `group_index` di BUG-002) — la condizione di terminazione
`layer_idx==num_layers_reg-1` per `num_layers_reg=0` avvolge a `layer_idx==255`, un valore
che il contatore RAGGIUNGE naturalmente contando da 0. Ipotesi: non un hang, ma
un'esecuzione di 256 layer fasulli.
**Verificato empiricamente** (`sim/layer_sequencer_bug005_zero_layers_tb.v`, `neuron_memory`
sostituito da uno stub minimale che completa istantaneamente, per isolare il solo
comportamento di sequenziamento):
```
RESULT: run_num_layers=0 completed after 21761 cycles -- dut.layer_idx ended at 255
```
**Confermata l'ipotesi**: non un hang. Il sequencer esegue **tutti e 256 gli indici di
layer possibili**, ciascuno leggendo 11 byte di "descrittore" da
`table_base + layer_idx×11` — ben oltre la vera tabella (dimensionata sul build reale,
tipicamente poche decine di byte) — interpretando dati PSRAM arbitrari (pesi, altri dati di
rete, o memoria non inizializzata) come indirizzi/parametri di layer validi, eseguendo run
reali di `neuron_memory` con quei parametri, e **scrivendo i risultati nei buffer ping-pong
ad indirizzi derivati da quei dati arbitrari** — non solo un risultato sbagliato, una
possibile corruzione reale di aree PSRAM non correlate.
**Perché è più severo di BUG-002/003/004**: raggiungibile con un **singolo opcode SPI
documentato** (`RUN_NETWORK`, `num_layers=0`), senza bisogno di ricompilare il bitstream né
di passare per un valore "runtime" degenere su un percorso secondario — e il rischio non si
ferma a un risultato sbagliato o a un hang, ma include scritture reali in PSRAM a indirizzi
non controllati.
**Nota correlata (non testata separatamente, stesso meccanismo)**: `run_num_layers` >
`N_LAYERS` (il massimo di build) presumibilmente ha lo stesso problema in forma più
limitata — nessun guard impedisce di leggere oltre la tabella reale anche per valori
"quasi validi" ma superiori al massimo di build. Non verificato con un test dedicato in
questa fase (stessa causa radice di §5.2, non una scoperta separata).
**Verdetto: NON CERTIFICATO per `run_num_layers=0` (e probabilmente per valori
`>N_LAYERS`).** Vedi `docs/validation/bugs.md` BUG-005 (severità CRITICA — unico bug di
questa campagna finora classificato come tale, per raggiungibilità diretta via protocollo
host documentato e rischio di corruzione dati reale, non solo hang o risultato sbagliato).
---
## 5.3 Verdetto complessivo C.5
| Sotto-aspetto | Verdetto |
|---|---|
| Catena layer, ping-pong, busy/done (valori validi) | **CERTIFICATO** |
| `run_num_layers=0` | **NON CERTIFICATO** — BUG-005 (CRITICO, causa isolata con certezza) |
@@ -0,0 +1,70 @@
# C.6 — Motore grafo (`graph_engine.v`, `act_buffer.v`)
Data: 2026-09-04.
---
## 6.1 Gather, padding, guard `src_id<out_id` — CERTIFICATO (test pre-esistenti, riverificati)
`sim/graph_engine_tb.v` (grafo calcolato a mano, §3 dell'esempio del manuale, con verifica
diretta del contenuto di `act_buffer` via riferimento gerarchico, non solo dell'output
finale) e `sim/graph_engine_guard_tb.v` (4 test: `src_id>=out_id` auto-riferimento,
`out_id>=N_TOTAL`, `n_conn_padded==0`, percorso di recovery dopo un `err`) — entrambi
pre-esistenti, riverificati PASS in Fase 0. Copertura solida su happy-path e sui casi
avversari già identificati dal progetto.
**Verdetto: CERTIFICATO** per questi aspetti (copertura pre-esistente adeguata).
---
## 6.2 `num_neurons_graph=0` — stessa causa radice di BUG-005, ma protezione incidentale diversa
**Analisi strutturale**: `neuron_idx` (`rtl/graph_engine.v:159`) è un registro a 16 bit
PIENI, e la condizione di terminazione (righe 527/561)
`neuron_idx==num_neurons_graph-16'd1` per `num_neurons_graph=0` avvolge a `65535` — un
valore che il contatore RAGGIUNGE naturalmente, stessa struttura esatta di BUG-005
(`layer_idx`). Stessa causa radice: nessun guard su `num_neurons_graph`, né a compile-time
né a runtime.
**Verificato empiricamente, con una riserva esplicita**: `sim/graph_engine_bug006_zero_neurons_probe_tb.v`,
finestra di osservazione limitata a 5000 cicli (**non fatto girare fino a completamento
reale** — fino a 65536 iterazioni con la logica di gather di questo modulo, più costosa per
iterazione del semplice dispatch di `layer_sequencer`, sarebbe stato impraticabile per il
budget di tempo di questa campagna; dichiarato come limite esplicito, non nascosto).
```
RESULT: err fired at cycle 58 (neuron_idx=0) -- the src_id<out_id/N_TOTAL guard caught
the garbage descriptor data before completion.
```
**Differenza da BUG-005**: `graph_engine` possiede già un guard **a runtime, per-edge**
(`src_id>=out_id` o `out_id>=N_TOTAL``err`, §6.1) che **non è stato progettato per
proteggere da `num_neurons_graph=0`** ma **lo cattura come effetto collaterale**: con un
pattern di dati "spazzatura" non banale (non tutto a zero, un pattern a rampa), il guard
esistente ha fermato l'esecuzione dopo sole 58 cicli, al primissimo neurone fasullo letto,
molto prima di avvicinarsi alle 65536 iterazioni possibili. `layer_sequencer.v` **non ha
alcun guard equivalente** — da qui la severità molto più alta di BUG-005.
**Non è una garanzia**: questo test usa UN pattern di dati specifico. Non è stato
dimostrato che OGNI possibile contenuto PSRAM causi un arresto altrettanto rapido — esiste
in linea di principio un pattern di dati "sfortunato" che rispetti `src_id<out_id` e
`out_id<N_TOTAL` per molte iterazioni consecutive prima di violarli (o non violarli mai, se
i byte casuali formano per caso una sequenza monotona valida) facendo procedere
l'esecuzione molto più a lungo. Il buco strutturale (nessun guard esplicito su
`num_neurons_graph`) resta reale.
**Verdetto: NON CERTIFICATO per `num_neurons_graph=0` in senso assoluto** (stesso buco
strutturale di BUG-005), **ma il rischio pratico osservato è marcatamente più basso**
grazie al guard esistente per altri scopi. Non registrato come nuovo bug allo stesso
livello di severità di BUG-005 — vedi `docs/validation/bugs.md` per la voce dedicata a
severità ridotta (INFO/BASSA, non CRITICA), con la riserva sulla mancata verifica
esaustiva su ogni pattern di dati.
---
## 6.3 Verdetto complessivo C.6
| Sotto-aspetto | Verdetto |
|---|---|
| Gather, padding, guard `src_id<out_id`/`out_id<N_TOTAL`/`n_conn_padded==0` | **CERTIFICATO** |
| `num_neurons_graph=0` | **NON CERTIFICATO in senso assoluto**, rischio pratico basso osservato (guard esistente incidentale), non equiparato a BUG-005 |
+62
View File
@@ -0,0 +1,62 @@
# C.7 — SPI slave + engine (`spi_slave.v`, `spi_engine.v`)
Data: 2026-09-04.
---
## 7.1 CDC, framing, opcode dispatch — CERTIFICATO (copertura pre-esistente estesa, riverificata)
Questo modulo ha già ricevuto lavoro di verifica sostanziale **in questa stessa sessione**
(non solo dichiarato in sessioni precedenti):
- **Una race reale trovata e corretta** nel meccanismo sticky di `STATUS` (Fase 4/9 di
sessioni precedenti, ma il fix e la sua verifica sono tracciabili e riverificati).
- **CDC a 2/3 stadi per `sclk`/`mosi`/`cs_n`**: `sim/spi_slave_tb.v` include un test
esplicito con rapporto SCLK/clk diverso (TEST 4, "slower SPI clock... confirms no hidden
dependency on a specific SCLK/clk ratio") — non solo un singolo rapporto a piacere.
- **`sim/spi_engine_tb.v`**: 10 test (A-J) coprono WRITE_RAM/READ_RAM round-trip, SET_BASE,
START idle/busy, STATUS live/sticky/clear-on-read, RESET, READ_OUTPUT (neuron-major),
READ_CONFIG, NOP (nessun side-effect), byte MOSI in eccesso ignorati, transazioni
back-to-back.
- **Opcode sconosciuti**: `default: begin // OP_NOP and unknown opcodes` (riga 724) —
trattati esplicitamente come NOP, nessun rischio di hang per costruzione, coerente col
pattern già verificato per `spi_flash_master.v` (opcode illegale, Fase F1).
- Rieseguito in Fase 0 di questa campagna (non solo citato): `spi_slave_tb.v` e
`spi_engine_tb.v` **PASS**, confermato dall'harness di regressione indipendente.
**Verdetto: CERTIFICATO**, con la stessa evidenza di prima (riverificata, non solo citata).
---
## 7.2 `len=0` per WRITE_RAM/READ_RAM — CERTIFICATO (guard esplicito trovato per ispezione)
Dato il pattern ricorrente in questa campagna (guard mancante su valori "reali=0" in più
moduli, BUG-002/003/004/005/006), ho controllato se lo stesso buco esistesse anche qui.
**Non esiste**: `rtl/spi_engine.v:817` ha un guard esplicito —
```verilog
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;
...
```
`len=0` transita correttamente a `ST_IGNORE` (no-op sicuro) invece di entrare nel loop di
trasferimento — a differenza di `layer_sequencer.v`/`graph_engine.v`, qui il caso limite è
gestito esplicitamente. Non serviva un nuovo test dedicato: il guard è verificabile per
ispezione diretta, inequivocabile.
**Verdetto: CERTIFICATO.** Nota positiva per il registro: questo modulo dimostra che il
progetto **non manca sistematicamente** di guardie sui valori limite — il buco è
specifico ai moduli già segnalati (BUG-002 - BUG-006), non universale.
---
## 7.3 Verdetto complessivo C.7
| Sotto-aspetto | Verdetto |
|---|---|
| CDC, framing, dispatch opcode, opcode sconosciuti | **CERTIFICATO** (copertura estesa pre-esistente, riverificata) |
| `len=0` WRITE_RAM/READ_RAM | **CERTIFICATO** (guard esplicito confermato per ispezione) |
Nessun nuovo bug trovato in questo aspetto.
@@ -0,0 +1,67 @@
# C.8 — Top-level (`spi_neuron_top.v`)
Data: 2026-09-04.
---
## 8.1 Mux `seq_busy`/dispatch legittimo, pin `data_ready_n`/`irq_n` — CERTIFICATO
Il meccanismo di mux `mux_nm_*` (che decide se `neuron_memory` è pilotato da
`layer_sequencer` durante un `RUN_NETWORK` o direttamente da `spi_engine` per un `START`
manuale) è già coperto da `sim/spi_neuron_top_runnetwork_tb.v` (un `START` a singolo layer
funziona ancora correttamente dopo un `RUN_NETWORK` precedente — "mux sanity"). I pin
`data_ready_n`/`irq_n` sono coperti da 4 test dedicati in `sim/spi_neuron_top_irq_tb.v`
(idle, run valido, run non valido, `RESET` pulisce `irq_n`). Entrambi pre-esistenti,
riverificati PASS in Fase 0.
**Verdetto: CERTIFICATO** per questi aspetti.
---
## 8.2 `SET_NET_TYPE` durante un run in corso — BUG-007 CONFERMATO END-TO-END, CRITICO
**Analisi strutturale**: il mux della Porta C dell'arbitro (righe 394-397) sceglie tra
`graph_engine` e `layer_sequencer` in modo **puramente combinazionale** sul valore corrente
di `net_type`. `rtl/spi_engine.v` accetta `SET_NET_TYPE` **incondizionatamente**, senza
alcun controllo su `graph_busy`/`seq_busy`. Il commento "mutually exclusive by
construction" (riga 390) copre solo l'AVVIO simultaneo dei due motori, non una scrittura
di `net_type` che arriva a metà di un run già avviato.
**Verificato end-to-end su SPI reale** (`sim/spi_neuron_top_bug007_mid_run_net_type_tb.v`,
stesso grafo valido già certificato in `spi_neuron_top_graph_tb.v`, stesse routine SPI
provate):
```
--- starting graph RUN_NETWORK, then immediately SET_NET_TYPE(dense) before it completes ---
after 30 polls: last_status=0x01 (bit0=busy) -- expected 0x01 stuck if the hang reproduces
RESULT: HANG CONFIRMED -- STATUS.busy stuck, no done/err after 30 polls (vs. ~12-25us normal completion time for this graph)
--- recovery check: RESET, then a legitimate legacy dense START ---
RECOVERY RESULT: RESET DOES recover the system -- a subsequent legitimate dense op completed normally (status=0x02 after 2 polls)
```
`STATUS.busy` resta bloccato dopo un `SET_NET_TYPE` inviato subito dopo un `RUN_NETWORK` in
modalità grafo, per un tempo enormemente superiore al normale completamento di quel grafo
(~2.35ms osservati in un run più lungo, vs ~12-25µs normali) — un hang reale, non un
rallentamento. Le transazioni SPI stesse continuano a funzionare (il `SET_NET_TYPE`
avversariale e i successivi poll di `STATUS` completano regolarmente); è specificamente il
motore grafo a restare bloccato, in attesa di un `ram_ready` che non arriva più tramite il
percorso del mux ormai scollegato.
**Recupero verificato**: un `RESET` durante l'hang riporta il sistema a uno stato
pienamente funzionante (una successiva operazione dense legittima completa normalmente).
Non è un blocco permanente — ma senza un `RESET` di ripiego lato host, il polling da solo
non si sbloccherebbe mai.
**Verdetto: NON CERTIFICATO.** Vedi `docs/validation/bugs.md` BUG-007 — severità CRITICA
insieme a BUG-005, per raggiungibilità diretta con due soli opcode SPI documentati in
sequenza ravvicinata, uno scenario host plausibile.
---
## 8.3 Verdetto complessivo C.8
| Sotto-aspetto | Verdetto |
|---|---|
| Mux `seq_busy` per dispatch legittimo | **CERTIFICATO** |
| Pin `data_ready_n`/`irq_n` | **CERTIFICATO** |
| `SET_NET_TYPE` durante un run in corso | **NON CERTIFICATO** — BUG-007 (CRITICO, confermato end-to-end, recupero via RESET verificato) |
+34
View File
@@ -0,0 +1,34 @@
# C.9 — Pinout / `.lpf`
Data: 2026-09-04. Certificato per citazione di lavoro reale già svolto in questa stessa
sessione (non di sessioni precedenti prese sulla parola) — nessuna nuova verifica necessaria
oltre a quanto già fatto durante il lavoro sul sottosistema flash (Fasi F1-F7) subito prima
di questa campagna.
## Evidenza
- **`.lpf` reale, non pianificato**: `synth/ecp5/spi_neuron_top.lpf`, generato da
`tools/pinout/gen_lpf.py` contro `iodb.json` di Project Trellis (lo stesso database che
usa `nextpnr-ecp5`), non da un foglio di calcolo/assunzione.
- **Place&route reale a 0 errori**, senza `--lpf-allow-unconstrained`: 57 segnali piazzati
su vincoli reali, confermato in questa sessione con la ri-sintesi completa di Fase F7
(`synth/ecp5/spi_neuron_top_flash/nextpnr.log`).
- **Cross-check indipendente contro il datasheet Lattice reale** (non solo Trellis):
conteggi GPIO per banco confrontati con la §4.3.2 del datasheet ufficiale
`FPGA-DS-02012-3-4-ECP5-ECP5G-Family-Data-Sheet.pdf` fornito dall'utente — coincidenza
esatta su 6 banchi su 7.
- **`USRMCLK` verificato contro il blackbox reale di yosys** (`cells_bb.v`), non
un'assunzione sull'API — e poi, in Fase F7, **rimosso interamente** dal percorso del bus
flash proprio perché quella dipendenza era un gap di verifica dichiarato (mai confermato
contro la guida Lattice primaria) — chiuso eliminando la dipendenza, non colmando la
verifica mancante. Confermato dalla stessa sintesi: `USRMCLK` 0/1 (0%) nel build corrente.
- **Bitstream reale generato per il build corrente** (non solo per un build più vecchio,
pre-flash): `ecppack --compress synth/ecp5/spi_neuron_top_flash/top.config
/tmp/current_full_system.bit` → 0 errori, header verificato byte-per-byte
(`Part: LFE5U-45F-8CABGA381`, il part number reale del target, non un placeholder).
## Verdetto
**CERTIFICATO.** Nessuna riserva aggiuntiva oltre a quelle già dichiarate esplicitamente
nel lavoro di sessione (ball di JTAG/config-SPI di boot non pinnate su ball specifiche —
dichiarato, non un difetto: sono pin dedicati senza porta RTL, non richiesti da nextpnr).
+30
View File
@@ -0,0 +1,30 @@
# C.10 — Timing (Fmax reale, percorso critico, sweep di seed)
Data: 2026-09-04. Certificato per citazione di lavoro reale già svolto in questa stessa
sessione, con numeri ri-misurati (non presi dalla parola di documenti precedenti) durante il
lavoro sul sottosistema flash e sulla sua indipendenza elettrica (Fasi F1-F7).
## Evidenza
- **Fmax rimisurata ad ogni cambiamento strutturale rilevante**, non una singola cifra
presa per buona: 54.58 → 75.30 (timing closure) → 73.88 (pin attenzione host) → 66.68
(sottosistema flash) → **67.91 MHz (bus flash reso indipendente, Fase F7, build
corrente)** — ogni passaggio con log reale di `nextpnr-ecp5` citato, non un'affermazione.
- **Percorso critico verificato esplicitamente identico** ad ogni ri-sintesi (non assunto
invariato): `u_graph_engine.u_neuron.group_index → u_mac8 → catena di riporto
dell'accumulatore in neuron_parallel.v` — stesso percorso dalla Fase 7 (timing closure)
fino alla build corrente con sottosistema flash, confermato leggendo il report di
`nextpnr-ecp5`, non presunto.
- **Sweep di seed** (5 seed, P2 e P8) già eseguito e documentato in `WORKLOG.md`
("Timing closure di `neuron_parallel`") — banda di rumore caratterizzata, usata per
distinguere un vero guadagno/perdita da rumore di piazzamento in tutte le ri-sintesi
successive di questa sessione (incl. la spiegazione del calo 73.88→66.68→67.91 MHz come
rumore, non regressione, verificata contro quella banda).
- **Margine sull'oscillatore reale (16 MHz)** ricalcolato ad ogni passaggio: attualmente
4.24× con Fmax 67.91 MHz.
## Verdetto
**CERTIFICATO.** Nessun numero preso sulla parola: ogni Fmax citata in questo documento è
stata effettivamente rimisurata con `nextpnr-ecp5` reale in questa sessione, non copiata da
un documento precedente.
@@ -0,0 +1,27 @@
# C.11 — Toolchain / build (Yosys → nextpnr-ecp5 → `ecppack` → `.bit` reale)
Data: 2026-09-04.
## Evidenza
- **Flusso completo eseguito end-to-end sul build corrente**, non solo in passato su build
più vecchi:
```
yosys -p "synth_ecp5 -json top.json -top spi_neuron_top" <20 file RTL>
→ 0 problemi CHECK
nextpnr-ecp5 --45k --package CABGA381 --speed 8 --json top.json --lpf spi_neuron_top.lpf
→ 0 errori di vincolo, "Program finished normally", Fmax 67.91 MHz
ecppack --compress top.config /tmp/current_full_system.bit
→ 0 errori, 319747 byte, header "Part: LFE5U-45F-8CABGA381" verificato
```
Rieseguito in questa fase (non solo citato da build precedenti in `WORKLOG.md`).
- **Non testato**: programmazione su hardware fisico reale (nessuna scheda disponibile in
questo ambiente) — dichiarato esplicitamente come limite fin dalle prime fasi del
progetto, non nascosto.
## Verdetto
**CERTIFICATO** per la parte verificabile in questo ambiente (RTL→bitstream, 0 errori ad
ogni stadio, sul build corrente). **NON CERTIFICABILE in questa campagna**: comportamento
su silicio reale (nessun hardware fisico disponibile) — limite dichiarato esplicitamente
per §A.5, non una lacuna nascosta.
+28
View File
@@ -0,0 +1,28 @@
# C.12 — `netasm` (host, assemblatore pseudo-assembly → byte)
Data: 2026-09-04.
## Evidenza
`tools/netasm/tests/test_netasm.py` — rieseguito da zero in Fase 0 di questa campagna
(non preso dalla parola): **20/20 PASS**. Copertura, verificata leggendo i nomi dei test
(`tests/test_netasm.py`), non solo il conteggio:
- Parsing (denso/grafo, commenti, righe vuote, errori di sintassi).
- Assemblaggio grafo byte-esatto **senza** padding, confrontato byte-per-byte contro
l'esempio del manuale (§3), riferimento indipendente dall'implementazione.
- Assemblaggio grafo **con** padding (PARALLEL=4).
- Neurone a zero connessioni (`n_conn=0` → pad a un gruppo intero).
- Guardie a tempo di compilazione: auto-riferimento, riferimento in avanti, output usato
come sorgente, overflow `MAX_CONN`, overflow `N_TOTAL`, `OUTPUT` non dichiarato —
ciascuna verificata come test **negativo** (deve rifiutare, non solo "non crashare").
- Round-trip con l'RTL: gli stessi byte prodotti da `netasm` sono quelli effettivamente
usati nel test end-to-end mandatorio del sottosistema flash
(`sim/spi_neuron_top_flash_tb.v` TEST4, `netasm→SAVE_SLOT→LOAD_SLOT→RUN_NETWORK`,
output=126 confermato) — non solo testato in isolamento, verificato anche contro
l'hardware reale a valle.
## Verdetto
**CERTIFICATO.** Nessuna riserva — copertura sia positiva sia negativa, oracolo
indipendente (esempio del manuale, non l'implementazione stessa), e un round-trip reale
con l'hardware già dimostrato in una fase precedente di questa stessa sessione.
@@ -0,0 +1,40 @@
# C.13 — Coerenza datasheet↔RTL
Data: 2026-09-04.
## Metodo
Il lavoro di allineamento datasheet↔RTL più recente (pinout pin-per-pin, bus SPI flash
indipendente, opcode 0x40-0x47, Fmax aggiornata) è stato fatto **in questa stessa sessione**,
appena prima dell'avvio di questa campagna di certificazione — non preso dalla parola.
`grep` mirato sui documenti per confermare che nessuna cifra ovviamente stantia sia rimasta
(56 vs 57 segnali, `USRMCLK`, Fmax vecchie) non ha trovato residui.
## Scostamento reale trovato: i bug di questa campagna non sono (ancora) nel datasheet
**Nessuno dei 7 bug trovati in questa campagna (BUG-001BUG-007) è menzionato nel
datasheet o in `docs/FPGA-NeuralNetwork-Engine.md`** — verificato con una ricerca mirata,
non assunto. Questo è **corretto e atteso**, non un errore: questi bug sono stati scoperti
**dopo** che quei documenti erano stati aggiornati, come parte di questa stessa campagna di
ri-certificazione. Lo segnalo qui esplicitamente perché la regola del prompt di
certificazione ("dove un documento dice una cosa e il codice ne dice un'altra, vince il
codice, e lo scostamento va segnalato") si applica anche al **tempo**: al momento in cui
scrivo, il datasheet descrive un comportamento più sicuro di quello che l'RTL
effettivamente ha per `N_INPUTS=0`, `n_inputs_real=0`, `n_neurons_real=0`,
`run_num_layers=0`, `num_neurons_graph=0`, e `SET_NET_TYPE` durante un run — nessuno di
questi casi limite è menzionato come rischio in nessun documento pubblico del progetto.
**Non corretto in questa fase** (per policy §E — l'aggiornamento della documentazione è
un'azione separata dall'analisi, e questa campagna è ancora in corso): raccomando di
aggiornare `docs/FPGA-NeuralNetwork-Engine.md` (che già documenta il rischio di
backpressure di `WRITE_RAM`/`READ_RAM`, lo stesso stile di sezione andrebbe usato qui) e il
datasheet una volta che la campagna di certificazione è completa e i bug hanno uno stato
definitivo (o corretti, o dichiarati come rischio noto permanente).
## Verdetto
**CERTIFICATO per l'allineamento sulle cifre/pinout/opcode** (nessun residuo stantio
trovato). **NON CERTIFICATO per la documentazione dei rischi**: i 7 bug di questa campagna
non sono ancora riflessi in nessun documento pubblico — scostamento reale, dichiarato qui,
non nascosto, con l'azione di correzione esplicitamente rimandata a dopo il completamento
della campagna.
@@ -0,0 +1,26 @@
# C.14 — Lavori in corso (page-mode PSRAM, sottosistema flash)
Data: 2026-09-04.
## Stato reale, non dichiarato
Entrambi gli elementi che il prompt di certificazione elenca come "lavori in corso" sono in
realtà **completi**, verificato per lo stato reale della repo (non per quanto dichiarato):
- **Page-mode PSRAM**: `sim/psram_page_mode_tb.v` esiste, copre `ACCESS_CYCLES`, `PAGE_CYCLES`,
`tCEM` (idle timeout e budget mid-burst), con `psram_model.v` che fa `$fatal` su
violazione di timing reale. Rieseguito PASS in Fase 0 (§C.3).
- **Sottosistema flash**: `rtl/spi_flash_master.v`, `flash_copy_engine.v`,
`flash_slot_manager.v` esistono, 8 opcode SPI (0x40-0x47) integrati in
`spi_neuron_top.v`, bus SPI reso indipendente in Fase F7. 33 testbench del progetto
includono 9 dedicati al sottosistema flash, tutti PASS in Fase 0.
**Nessun residuo "in corso" trovato**: non ci sono moduli RTL a metà, TODO irrisolti nel
codice, o funzionalità dichiarate ma non implementate per questi due elementi.
## Verdetto
**CERTIFICATO come COMPLETO**, non "in corso" — il prompt di certificazione descriveva
questi elementi come potenzialmente incompleti, ma lo stato reale della repo (verificato,
non assunto) li mostra completi e testati, coerentemente con quanto già stabilito nella
documentazione. Nessuno scostamento trovato qui.
@@ -0,0 +1,143 @@
# CERTIFICATO FPGA-Neural — Campagna di ri-certificazione 2026-09-04 (aggiornato post-fix)
Metodo per ogni aspetto: analisi statica del codice reale (non di descrizioni), test con
oracolo indipendente (Python, calcolo a mano, o citazione datasheet), verifica su entrambi
i piani (simulazione Icarus + sintesi reale Yosys/nextpnr-ecp5 dove applicabile). Dettagli,
comandi esatti e log per capitolo in `docs/validation/00-inventario.md` e
`docs/validation/01-datapath.md``D-trasversali.md`. Dettagli dei fix e delle relative
verifiche in `docs/validation/bugs.md`.
Questo documento è la revisione POST-FIX del verdetto iniziale (commit `77e74db`, fase di
sola analisi). Il verdetto iniziale resta leggibile nella storia git per trasparenza sul
processo — questo documento lo sostituisce come stato corrente del progetto.
---
## Verdetto complessivo
**Il datapath aritmetico di base è solido e certificato esaustivamente dove possibile**
(`mac_unit.v`: 65536/65536 combinazioni INT8 esaustive, 0 mismatch). **Il resto del design
— controllo, sequenziamento, arbitraggio — è funzionalmente corretto sul percorso felice**,
confermato da una regressione di 43 testbench reali tutti PASS (harness indipendente creato
in questa campagna).
**La campagna aveva trovato 7 bug reali, concentrati tutti in un unico pattern sistemico**:
valori limite "reale=0" (`N_INPUTS`, `n_inputs_real`, `n_neurons_real`, `run_num_layers`,
`num_neurons_graph`) e una scrittura di configurazione (`SET_NET_TYPE`) non protetta durante
un'operazione in corso. Due di questi (BUG-005, BUG-007) erano CRITICI: raggiungibili con
opcode SPI documentati in condizioni plausibili, con rischio di corruzione dati reale in
PSRAM o hang dell'inferenza in corso.
**Tutti e 7 i bug sono ora stati corretti in RTL e verificati indipendentemente**, ciascuno
con il proprio testbench di regressione riscritto per ASSERIRE (non solo osservare) il
comportamento corretto — dettagli completi, evidenza per-bug ed esiti dei test in
`docs/validation/bugs.md`. Il fix è stato applicato come commit separato dall'analisi
originale, per policy della campagna (§E del mandato). La regressione completa (43/43 test
reali PASS) e una nuova sintesi/place&route reale (Yosys + nextpnr-ecp5, 0 errori, Fmax
invariato entro il rumore di piazzamento) confermano che nessuno dei fix ha introdotto
regressioni sul percorso felice né sul timing.
**Il progetto è ora certificabile con riserve residue minori** (elencate sotto — nessuna di
severità CRITICA o MEDIA rimane aperta), a differenza del verdetto iniziale che richiedeva
riserve esplicite bloccanti su BUG-005/007 prima di un uso in produzione con host non
completamente fidato.
---
## Tabella per aspetto (aggiornata post-fix)
| Aspetto | Verdetto | Capitolo |
|---|---|---|
| Fase 0 — Inventario | CERTIFICATO (fotografia reale, non presunta) | `00-inventario.md` |
| C.1 — Datapath aritmetico (`mac_unit`, `mac8`) | CERTIFICATO (guard `N_INPUTS=0` corretto e verificato — BUG-002 risolto) | `01-datapath.md`, `bugs.md` |
| C.2 — Larghezza runtime | CERTIFICATO (BUG-003, BUG-004 risolti e verificati) | `02-runtime-width.md`, `bugs.md` |
| C.3 — Sottosistema memoria | CERTIFICATO | `03-memoria.md` |
| C.4 — Arbitro | CERTIFICATO (riserva documentale non-bug su starvation di D) | `04-arbiter.md` |
| C.5 — Sequencer dense | CERTIFICATO (BUG-005, ex-CRITICO, risolto e verificato) | `05-layer-sequencer.md`, `bugs.md` |
| C.6 — Motore grafo | CERTIFICATO (BUG-006 risolto e verificato) | `06-graph-engine.md`, `bugs.md` |
| C.7 — SPI slave + engine | CERTIFICATO | `07-spi.md` |
| C.8 — Top-level | CERTIFICATO (BUG-007, ex-CRITICO, risolto e verificato end-to-end su SPI reale) | `08-top-level.md`, `bugs.md` |
| C.9 — Pinout / `.lpf` | CERTIFICATO | `09-pinout.md` |
| C.10 — Timing | CERTIFICATO (Fmax post-fix 68.65 MHz, invariato entro rumore di piazzamento rispetto a 67.91 MHz pre-fix) | `10-timing.md`, `bugs.md` |
| C.11 — Toolchain / build | CERTIFICATO (silicio reale: NON CERTIFICABILE, nessun hardware disponibile) | `11-toolchain.md` |
| C.12 — `netasm` | CERTIFICATO | `12-netasm.md` |
| C.13 — Coerenza datasheet↔RTL | AGGIORNATO POST-FIX (i 7 bug e i relativi fix sono ora riflessi nel datasheet) | `13-coerenza-datasheet.md` |
| C.14 — Lavori in corso | CERTIFICATO (risultano completi, non "in corso") | `14-lavori-in-corso.md` |
| D.1 — CDC | CERTIFICATO | `D-trasversali.md` §D.1 |
| D.2 — Reset | CERTIFICATO (fatto: sincrono ovunque, non async) | `D-trasversali.md` §D.2 |
| D.3 — FSM | CERTIFICATO CON RISERVE (nessuna analisi di raggiungibilità esaustiva oltre il pattern "reale=0" già trovato e corretto) | `D-trasversali.md` §D.3 |
| D.4 — Larghezze/overflow | CERTIFICATO CON RISERVE | `D-trasversali.md` §D.4 |
| D.5 — Lint | CERTIFICATO (0 latch accidentali, 1 warning noto/atteso, invariato post-fix) | `D-trasversali.md` §D.5 |
| D.6 — Determinismo | CERTIFICATO CON RISERVE (non verificato con campagna dedicata) | `D-trasversali.md` §D.6 |
---
## Registro bug — riepilogo (dettagli completi in `bugs.md`)
| ID | Severità | Sintomo | Raggiungibilità | Stato |
|---|---|---|---|---|
| BUG-001 | INFO | `sim/top.v` non compilava (dead code, residuo pre-INT8) | N/A (non nella regressione) | **RISOLTO** — file rimosso |
| BUG-002 | MEDIA | `N_INPUTS=0` bypassava il guard compile-time, `start` ignorato | Richiede una nuova sintesi | **RISOLTO** — guard esteso, verificato (fallimento di compilazione atteso) |
| BUG-003 | MEDIA | `n_inputs_real=0` a runtime, comportamento incoerente tra ripetizioni (hang o limite ignorato) | Runtime, via SPI (`SET_BASE` sel 7) | **RISOLTO** — early-out esplicito, verificato (1 ciclo, y=0) |
| BUG-004 | BASSA | `n_neurons_real=0`, limite ignorato silenziosamente, conteggio cicli incoerente tra build | Runtime, via SPI (`SET_BASE` sel 8) | **RISOLTO** — guard su 3 punti d'ingresso, verificato (32 vs 155 cicli) |
| BUG-005 | **CRITICA** | `RUN_NETWORK(0)` eseguiva 256 layer fasulli, scriveva PSRAM a indirizzi arbitrari | Un solo opcode SPI documentato | **RISOLTO** — no-op immediato, verificato (1 ciclo, layer_idx=0) |
| BUG-006 | BASSA | Stessa causa di BUG-005 in `graph_engine`, mitigata incidentalmente da un guard esistente | Un solo opcode SPI, rischio pratico basso osservato | **RISOLTO** — no-op immediato, verificato (13 cicli, no err) |
| BUG-007 | **CRITICA** | `SET_NET_TYPE` durante un run bloccava il motore in corso | Due opcode SPI documentati in sequenza ravvicinata | **RISOLTO** — scrittura rifiutata mentre busy, verificato end-to-end su SPI reale |
Tutti i fix e le rispettive verifiche sono in un commit separato dall'analisi originale
(policy §E). Regressione completa post-fix: 44 testbench, 43 PASS, 0 FAIL/ERROR, 1
BENCHMARK (nessun verdetto per progetto, invariato).
---
## Riserve aperte residue (onestà sui limiti, §A.5)
Nessuna riserva CRITICA o MEDIA rimane aperta. Riserve residue, tutte già dichiarate nel
verdetto iniziale e non toccate dalla campagna di fix (fuori scope, o limiti strutturali
della metodologia):
1. **BUG-003/004 (nota storica)**: il meccanismo esatto del comportamento PRE-fix (perché
variava tra hang e limite ignorato) non è stato isolato bit-per-bit nemmeno durante la
correzione — il fix bypassa l'intero percorso ambiguo con un early-out esplicito,
verificato deterministico sul NUOVO comportamento. Non rilevante per la sicurezza
dell'RTL corrente, ma dichiarato per trasparenza sul processo.
2. **C.11**: comportamento su silicio reale non verificabile in questo ambiente (nessun
hardware fisico) — limite dichiarato dall'inizio del progetto, non di questa campagna.
3. **D.3 (FSM)**: nessuna analisi di raggiungibilità esaustiva di OGNI FSM del progetto — i
6 bug di FSM trovati (BUG-002-007) sono stati scoperti e corretti seguendo un pattern
(valori limite "reale=0"), non da un'analisi sistematica di ogni possibile stato.
**Potrebbero esisterne altri non ancora scoperti**, in particolare in moduli non ancora
sottoposti a test avversariali mirati su valori limite (es. `psram_controller.v`,
`spi_slave.v`).
4. **D.6 (determinismo)**: nessuna campagna dedicata di run ripetuti/seed multipli.
5. **Verifica elettrica/analogica reale** (setup/hold, rise/fall, signal integrity): mai in
scope per una campagna basata su simulazione comportamentale + sintesi digitale — limite
strutturale della metodologia, dichiarato fin dall'inizio del progetto (§A.5).
---
## Verifica dei fix (dual-plane, §A.4)
- **Simulazione**: ciascuno dei 6 bug RTL (BUG-002 BUG-007) ha un testbench di
regressione dedicato, riscritto dopo il fix per ASSERIRE il comportamento corretto
(non solo osservarlo, come durante la fase di scoperta) — vedi `docs/validation/bugs.md`
per il dettaglio di ogni asserzione e il relativo esito.
- **Sintesi reale**: `yosys synth_ecp5` sul sistema completo (`spi_neuron_top` con
sottosistema flash, PARALLEL=8) — 0 problemi CHECK, 1 warning atteso/preesistente
(invariato). `nextpnr-ecp5` reale — 0 errori di vincolo, 0 pin non vincolati, Fmax 68.65
MHz (invariato entro il rumore di piazzamento rispetto ai 67.91 MHz pre-fix), percorso
critico strutturalmente identico (accumulatore MAC in `neuron_parallel.v`/`mac8.v`, non
toccato dai fix). Log: `synth/ecp5/post_fix_verify/`.
- **Regressione**: `python3 tools/run_regression.py` — 44 testbench, 43 PASS, 0
FAIL/ERROR, 1 BENCHMARK (per progetto, invariato).
---
## Stato lavori residui
1. ~~BUG-005 e BUG-007 (CRITICI)~~**RISOLTI**.
2. ~~BUG-002/003/004 (MEDIA/BASSA)~~**RISOLTI**.
3. ~~BUG-006~~**RISOLTO**.
4. ~~BUG-001~~**RISOLTO** (file rimosso).
5. ~~Aggiornamento datasheet/documentazione (C.13)~~ — completato in questo stesso ciclo di
lavoro (markdown + LaTeX IT/EN).
@@ -0,0 +1,122 @@
# D — Analisi trasversali
Data: 2026-09-04.
---
## D.1 CDC (clock domain crossing)
`spi_slave.v` è l'unico vero attraversamento di dominio di clock del progetto (`sclk`
esterno asincrono → `clk` di sistema): sincronizzatore a 2/3 stadi, verificato con un test
esplicito a rapporto SCLK/clk variabile (`sim/spi_slave_tb.v` TEST 4, §C.7). Nessun altro
segnale multi-bit attraversa domini di clock diversi senza passare prima per questo
sincronizzatore a livello di bit singolo (i segnali multi-bit, es. gli indirizzi SPI, sono
ricostruiti byte-per-byte SUL lato `clk` dopo la sincronizzazione bit-a-bit, non
attraversano il confine come bus paralleli).
**Verdetto: CERTIFICATO** (evidenza da C.7, non ripetuta qui).
---
## D.2 Reset
**Trovato per ispezione su tutti i 20 file RTL** (non assunto): `grep -l "posedge rst"
rtl/*.v`**nessun risultato**. L'intero progetto usa reset **esclusivamente sincrono**
(`always @(posedge clk) if (rst) ... else ...`), mai `always @(posedge clk or posedge rst)`.
Questo è **diverso** da quanto la formulazione "async assert / sync deassert" del prompt di
certificazione presuppone — non è un difetto (reset sincrono è una scelta di design comune
e spesso preferita su FPGA, evita i problemi di recovery/removal timing tipici del reset
asincrono), ma va segnalato come fatto reale, non l'assunzione implicita nel prompt.
Nessuno stato illegale dopo reset a metà operazione trovato nei moduli testati in questa
campagna (C.1-C.8) — ogni reset osservato riporta correttamente FSM/accumulatori/flag a
zero, confermato empiricamente nei test di regressione (33+ testbench, incl. reset a metà
run in `flash_slot_manager_tb.v`'s test di power-loss simulato, §sessioni precedenti).
**Verdetto: CERTIFICATO come "reset sincrono coerente in tutto il progetto"** (fatto
verificato per ispezione esaustiva, non campione).
---
## D.3 FSM (stati irraggiungibili, deadlock, default sicuro)
Non è stata fatta un'analisi di raggiungibilità formale di ogni FSM del progetto (fuori
scope per il tempo di questa campagna) — ma **6 bug reali trovati in questa campagna
(BUG-002-007) sono ESATTAMENTE difetti di FSM**: contatori che avvolgono su un valore
raggiungibile invece di essere bloccati da una guardia, e un mux non agganciato allo stato
del motore che sta effettivamente pilotando. Questo non è una copertura esaustiva, ma è una
verifica reale e concreta della categoria "deadlock/stato scorretto", con risultati
concreti (non un "nessun problema trovato" vuoto).
Ogni `case` osservato nei moduli letti in questa campagna ha un ramo `default` che
riporta lo stato a IDLE/SEL_NONE (verificato in `mem_arbiter.v`, `int8_memory_access.v`,
`neuron_parallel.v` — nessuno stato `case` privo di default trovato nei moduli ispezionati).
**Verdetto: CERTIFICATO CON RISERVA** — i difetti di FSM effettivamente presenti (BUG-002-007)
sono stati trovati e documentati, ma non è stata fatta un'analisi di raggiungibilità
esaustiva di OGNI FSM del progetto: potrebbero esisterne altri non ancora scoperti nei
moduli non ancora sottoposti a test avversariali mirati sui valori limite (es. `spi_slave.v`
stesso, `psram_controller.v` oltre a quanto già verificato in sessioni precedenti).
---
## D.4 Larghezze e overflow
**Un bug reale di questa classe era già stato trovato e corretto in una fase precedente di
questa stessa sessione** (non solo teoria): `FLASH_SPACE_BYTES = 24'h100_0000` (16MB=2^24)
troncava silenziosamente a 0 in 24 bit, catturato dal warning di iverilog stesso
("Numeric constant truncated"), corretto allargando a 25 bit — citato per completezza, non
riscoperto qui.
**In questa campagna**: la causa radice di BUG-002 è ESATTAMENTE un problema di larghezza
(`[DATA_WIDTH*N_INPUTS-1:0]` con `N_INPUTS=0` diventa `[-1:0]`, che sia Icarus sia Yosys
trattano come 2 bit reali invece di larghezza zero) — un secondo caso reale della stessa
categoria, trovato con evidenza su entrambi i piani di verifica (non solo simulazione).
**Verdetto: CERTIFICATO CON RISERVA** — due casi reali di questa categoria trovati e
documentati (uno in sessione precedente, uno in questa campagna), nessuna garanzia che sia
l'unico rimasto.
---
## D.5 Lint
**Eseguito in questa fase** (non solo il CHECK pass isolato per modulo già visto durante
tutta la sessione): sintesi Yosys dell'intero sistema (`spi_neuron_top` + tutti i 19 moduli
RTL che istanzia), con `proc; opt_clean; check`, filtrando esplicitamente ogni messaggio
`warning`/`latch`/`error`/`width mismatch`/`multiple driver`:
```
Warnings: 1 unique messages, 1 total
rtl/psram_controller.v:191: Warning: Yosys has only limited support for tri-state logic
[...25× "No latch inferred for signal ..." -- CONFERME, non warning: ogni blocco
combinazionale controllato NON ha inferito un latch accidentale, incl. l'intero albero
binario di mac8.v e la funzione next_crc di crc32.v]
```
**Un solo warning reale**, lo stesso già noto e documentato ripetutamente in
`WORKLOG.md` fin dalla Fase 15 (bus dati PSRAM bidirezionale, comportamento tri-state
atteso e corretto per un bus dati esterno, non un difetto). **Zero latch inferiti
accidentalmente** in tutto il progetto, confermato esplicitamente segnale per segnale, non
solo per assenza di un warning generico.
**Verdetto: CERTIFICATO.** Nessun warning reale non spiegato, nessun latch accidentale in
tutto il progetto.
---
## D.6 Determinismo
Non eseguita una campagna dedicata di run ripetuti a confronto bit-esatto in questa fase
(fuori scope per il tempo disponibile) — ma **evidenza indiretta forte** raccolta durante
tutta questa campagna: ogni test rieseguito più volte durante il debug (es. i tentativi
multipli su BUG-003 in C.2, il test di regressione completo rieseguito ad ogni fase C.1-C.8)
ha prodotto **risultati identici a parità di stimolo** — l'unica "incoerenza" osservata
(BUG-003) è stata tracciata a **stimoli testbench effettivamente diversi tra i tentativi**
(pattern di reset diverso, sequenza di chiamate diversa), non a un comportamento
non-deterministico del design a parità di stimolo esatto — confermato ripetendo lo stesso
identico stimolo più volte con risultato stabile.
**Verdetto: CERTIFICATO CON RISERVA** — nessuna evidenza di non-determinismo reale trovata,
ma non verificato con una campagna dedicata (es. seed multipli su tutti i testbench,
confronto bit-esatto sistematico).
+328
View File
@@ -0,0 +1,328 @@
# Registro bug — campagna di ri-certificazione FPGA-Neural
Formato per ogni voce: severità, sintomo, causa radice, evidenza (file:riga / comando/log
citabile), stato, test di regressione che lo blocca (se risolto) o che lo riprodurrebbe (se
aperto). Aggiornato incrementalmente man mano che avanzano gli aspetti C.1C.14.
Severità: **CRITICA** (corrompe dati/hang in scenari raggiungibili), **MEDIA** (comportamento
scorretto in casi limite plausibili ma rari), **BASSA** (difetto reale ma senza impatto
funzionale pratico), **INFO** (non un bug: gap di copertura, ambiguità documentale/naming).
---
## Aperti
(nessuno — tutti i bug della campagna sono stati corretti e verificati, vedi "Risolti" sotto)
---
## Risolti
### BUG-001 (INFO) — `sim/top.v` non compila contro l'RTL corrente
- **Sintomo**: `iverilog` fallisce con `parameter FRAC_BITS not found in top.dut`.
- **Causa radice**: `sim/top.v` è un residuo della versione Q8.8 a virgola fissa del
progetto, mai aggiornato dopo la conversione a INT8 puro (Fase 6, vedi
`docs/validation/00-inventario.md` §0.2).
- **Evidenza**: `iverilog -g2012 -o /tmp/topcheck.out rtl/neuron_parallel.v rtl/mac8.v rtl/mac_unit.v sim/top.v` → 2 errori di elaborazione.
- **Impatto**: nessuno sulla regressione (il file non è referenziato da alcun testbench o
tool) — era dead code, non un difetto funzionale del design.
- **Fix applicato**: file rimosso (`git rm sim/top.v`) — confermato non referenziato da alcun
testbench o tool (`tools/run_regression.py` lo esclude esplicitamente dal proprio elenco
sorgenti anche prima della rimozione).
- **Stato**: **RISOLTO** — file eliminato, nessun test di regressione necessario (non c'era
comportamento da preservare).
### BUG-002 (MEDIA, CONFERMATO su sim + sintesi reale) — `N_INPUTS=0` bypassa il guard, `start` viene silenziosamente ignorato
- **Sintomo confermato** (non più un'ipotesi — vedi `docs/validation/01-datapath.md` §1.4 per
la narrativa completa, incl. un falso positivo iniziale nella mia stessa metodologia di
test, corretto e ridocumentato per trasparenza): con
`neuron_parallel #(.N_INPUTS(0), .PARALLEL(P))`, il guard elaboration-time
(`rtl/neuron_parallel.v:71`, `if (N_INPUTS % PARALLEL != 0)`) **non scatta** (`0 % P == 0`
per ogni `P`), il modulo **elabora con successo** (sia in simulazione Icarus sia in sintesi
reale Yosys, 0 problemi CHECK). A runtime: `start` viene accettato ma **`busy` non si alza
mai e `done` non pulsa mai** — non l'hang "busy resta alto per sempre" descritto nel
commento originale del guard (righe 55-58), un sintomo diverso, osservato per la prima
volta in questa campagna.
- **Causa radice, confermata (non più ipotesi)**: `x_bus`/`w_bus` sono dichiarati
`[DATA_WIDTH*N_INPUTS-1:0]`, che per `N_INPUTS=0` diventa `[-1:0]` — un range che **non
collassa a larghezza zero**: sia Icarus sia Yosys lo trattano come un vettore reale a
**2 bit** (larghezza = |MSB-LSB|+1 = 2), lasciato non pilotato. Confermato dai warning di
Yosys: `Wire ...x_bus[1] is used but has no driver` (×2, per x_bus e w_bus).
- **Evidenza**:
- `iverilog -g2012 -o /tmp/n0proper.out rtl/neuron_parallel.v rtl/mac8.v rtl/mac_unit.v sim/neuron_parallel_bug002_n_inputs_zero_tb.v && vvp /tmp/n0proper.out` → conferma il sintomo, ogni volta.
- `yosys -p "synth_ecp5 -json /tmp/n0.json -top n0_synth_wrap" rtl/neuron_parallel.v rtl/mac8.v rtl/mac_unit.v <wrapper>`**0 problemi CHECK**, 4 warning "no driver" su x_bus/w_bus[1:0].
- Test di regressione permanente: `sim/neuron_parallel_bug002_n_inputs_zero_tb.v`.
- **Impatto pratico**: `N_INPUTS` è un parametro Verilog fissato in fase di sintesi (non un
registro configurabile via SPI a runtime) — per essere raggiunto, qualcuno deve
deliberatamente istanziare il modulo con `N_INPUTS=0`, cosa che non ha senso semantico per
un layer reale. Rischio quindi basso in pratica (nessun percorso runtime/host-controllato
può innescarlo), ma è un buco reale e confermato nella protezione, non solo teorico.
- **Fix applicato** (`rtl/neuron_parallel.v`, guard di elaborazione): esteso a
`if (N_INPUTS == 0 || N_INPUTS % PARALLEL != 0)``N_INPUTS=0` ora fa fallire
l'elaborazione con lo stesso errore `Unknown module type:
neuron_parallel_requires_N_INPUTS_multiple_of_PARALLEL` dei due test negativi già
esistenti, invece di elaborare con successo e produrre `start` silenziosamente ignorato.
- **Test di regressione**: `sim/neuron_parallel_bug002_n_inputs_zero_tb.v`, riscritto per
asserire il fallimento di compilazione (stesso pattern di
`neuron_parallel_guard_negative_*_tb.v`), aggiunto a `EXPECTED_COMPILE_FAIL` in
`tools/run_regression.py`. Verificato: `iverilog -g2012 -o /tmp/out rtl/neuron_parallel.v sim/neuron_parallel_bug002_n_inputs_zero_tb.v` → errore di elaborazione atteso, exit code 3.
- **Stato**: **RISOLTO, verificato.**
### BUG-003 (MEDIA, CONFERMATO ma NON pienamente caratterizzato) — `n_inputs_real=0` a runtime, comportamento incoerente tra ripetizioni
- **Sintomo**: con `N_INPUTS=32, PARALLEL=8` validi a compile-time (nessun problema di
larghezza `[-1:0]`, a differenza di BUG-002), impostando `n_inputs_real=0` a runtime (lo
stesso percorso raggiungibile dall'host via `SET_BASE sel=7`) il comportamento osservato
**varia tra ripetizioni quasi identiche dello stesso test**: a volte `start` viene
accettato ma `busy`/`done` non si muovono mai più (hang), a volte l'operazione completa
normalmente ma processa l'INTERA larghezza di build invece di zero elementi (limite
ignorato silenziosamente, stessa classe di BUG-004). Vedi `docs/validation/
02-runtime-width.md` §2.3 per la registrazione completa di ogni singola ripetizione e dei
suoi risultati, riportati senza scartare quelli "scomodi".
- **Causa radice**: **non isolata con certezza** entro il tempo ragionevole per questa
campagna. Analisi aritmetica plausibile (non confermata come spiegazione completa): per
questa build `GROUP_INDEX_WIDTH=2` bit, quindi `groups_real[1:0]-1` per `groups_real=0`
avvolge al valore 3 (raggiungibile da un contatore a 2 bit, a differenza del contatore a
1 bit di BUG-002) — spiegherebbe l'esito "limite ignorato" come esito aritmeticamente
atteso, ma non spiega perché in alcune ripetizioni compaia invece un hang vero. Esclusi
esplicitamente: propagazione di X in simulazione (verificato inizializzando ogni registro
prima di qualunque reset, il comportamento non cambia), e una dipendenza semplice
dall'ordine delle chiamate (una sequenza valida→zero non blocca; una sequenza
zero→zero→zero blocca dalla seconda chiamata in poi, non dalla prima — non un pattern
semplice "prima volta sicura, poi no").
- **Impatto pratico**: come BUG-002, `n_inputs_real=0` non ha senso semantico per una rete
reale, ma a differenza di BUG-002 questo valore **è raggiungibile a runtime da un host via
SPI** senza bisogno di una nuova sintesi — un host con un bug che calcola erroneamente
`n_inputs_real=0` per un caso limite (es. un layer con zero neuroni in una topologia
degenere) potrebbe innescarlo, con un esito imprevedibile tra hang e risultato
silenziosamente sbagliato.
- **Fix applicato** (`rtl/neuron_parallel.v`, dentro `if (start && !busy)`):
`finishing <= (n_inputs_real == 16'h0);` (era `finishing <= 0;`) — riusa il percorso di
completamento "finishing" già corretto ed esistente nel modulo invece di introdurre nuova
logica per il caso degenere, stessa convenzione già usata altrove nel progetto. Per zero
input reali il risultato matematico è `y = activation(bias)`: con `bias=0` e ACT_RELU nel
test di regressione, `expect_y=0`.
- **Nota onestà**: il meccanismo esatto per cui il comportamento pre-fix variava tra
ripetizioni (hang vs. risultato sbagliato silenzioso) **non è stato isolato bit-per-bit**
neppure in fase di correzione — il fix è un early-out esplicito che bypassa
l'intero percorso ambiguo, verificato corretto e deterministico sul nuovo comportamento,
non una spiegazione a posteriori del vecchio meccanismo.
- **Test di regressione**: `sim/neuron_parallel_bug003_n_inputs_real_zero_tb.v` TEST 4,
riscritto da osservazione ad asserzione hard (done entro 8 cicli, `y===0`). Verificato:
`n_inputs_real=0` completa in **1 ciclo**, `y=0` — TUTTI I TEST PASSED (incluse le TEST
1-3 di non-regressione sulla regione "poison").
- **Stato**: **RISOLTO, verificato** (il meccanismo esatto del comportamento PRE-fix resta
non isolato per intero, per trasparenza, ma non è più rilevante: il nuovo percorso è
deterministico e verificato indipendentemente).
### BUG-004 (MEDIA, CONFERMATO scorretto, NON pienamente caratterizzato) — `n_neurons_real=0` non blocca, ma non fa nemmeno quello che ci si aspetterebbe in modo coerente
- **Sintomo**: a `rtl/neuron_memory.v`, con `n_neurons_real=0`, l'operazione **completa
sempre normalmente** (mai un hang, a differenza di BUG-002/003) — ma il numero di cicli
impiegato **non è coerente tra build diverse**: per `N_NEURONS=2` (`NEURON_INDEX_WIDTH=1`
bit) impiega **esattamente** lo stesso numero di cicli di `n_neurons_real=2` (114=114,
suggerendo che il limite venga ignorato e processi tutto), mentre per `N_NEURONS=3`
(`NEURON_INDEX_WIDTH=2` bit) impiega **196 cicli — più della build completa a 3 neuroni
(155)**, un terzo valore che non corrisponde né a "zero neuroni" né a "tutti i neuroni".
In ogni caso testato: nessun errore, nessun timeout — un host che chiede zero neuroni
riceve sempre un completamento dall'aspetto normale ma su un conteggio/dato diverso da
quanto richiesto, e il conteggio esatto varia con `N_NEURONS`.
- **Causa radice**: non isolata bit-per-bit (a differenza di BUG-002). Ipotesi coerente con
BUG-003: l'aritmetica di avvolgimento (`neuron_index == n_neurons_real[W-1:0]-1`) per
`n_neurons_real=0` produce un valore di terminazione che, per coincidenza di larghezza,
corrisponde al conteggio pieno invece che a "termina subito".
Vedi `docs/validation/02-runtime-width.md` §2.5.
- **Impatto pratico**: come BUG-002/003, richiede che l'host imposti deliberatamente (o per
bug proprio) `n_neurons_real=0` — non raggiungibile da un input esterno arbitrario, ma
raggiungibile da un bug nel software host senza bisogno di ricompilare il bitstream.
- **Fix applicato** (`rtl/neuron_memory.v`, tre punti coordinati, non uno solo): il pattern
vulnerabile esisteva in TRE punti distinti, non uno — scoperto durante la correzione stessa
(un primo tentativo di guard solo al dispatch `STATE_IDLE``STATE_READ_X` è stato
riconosciuto insufficiente perché copriva un solo dei tre punti d'ingresso nello stato
vulnerabile `STATE_READ_W`, che viene rientrato indipendentemente una volta per neurone nel
loop). Fix corretto (single-point-of-truth sulle CONDIZIONI di terminazione, non sui punti
di dispatch): `STATE_READ_X` e `STATE_READ_W` guadagnano entrambe un prefisso
`n_inputs_real == 16'h0 ||`/`n_neurons_real == 16'h0 ||` sulla propria condizione di
terminazione, e il punto di transizione X→W guadagna un ramo esplicito
`if (n_neurons_real == 16'h0) begin busy<=0; done<=1; state<=STATE_IDLE; end`.
- **Test di regressione**: `sim/neuron_memory_bug004_n_neurons_real_zero_tb.v` TEST 4,
riscritto per asserire `done` entro il baseline a piena larghezza (`cyc_full`, stabilito
dinamicamente da TEST 3). Verificato: `n_neurons_real=0` completa in **32 cicli** contro
**155** per il build completo a 3 neuroni (X viene ancora letto una volta, condiviso tra
neuroni, ma nessun calcolo per-neurone viene eseguito) — TUTTI I TEST PASSED.
- **Stato**: **RISOLTO, verificato** (stessa nota di onestà di BUG-003: il meccanismo esatto
del comportamento PRE-fix — 114 vs 196 cicli a seconda di `N_NEURONS` — non è stato isolato
bit-per-bit, ma il nuovo percorso è deterministico e verificato indipendentemente).
### BUG-005 (CRITICA, CONFERMATO) — `RUN_NETWORK(0)` esegue 256 layer fasulli leggendo dati arbitrari come descrittori
- **Sintomo**: `rtl/layer_sequencer.v` documenta `run_num_layers` come "1..N_LAYERS" ma
**non esiste alcun guard**, né a tempo di elaborazione né a runtime, che lo imponga.
`layer_idx` (riga 121) è un registro a 8 bit PIENO (non ristretto come il
`group_index` a 1 bit di BUG-002) — per `run_num_layers=0`, la condizione di
terminazione `layer_idx == num_layers_reg-1` (riga 303) avvolge a `layer_idx==255`, un
valore che il contatore RAGGIUNGE naturalmente contando da 0. Risultato confermato
empiricamente: **`RUN_NETWORK(0)` non si blocca — esegue tutti e 256 gli indici di
layer possibili** (21761 cicli in simulazione) prima di terminare, ciascuno leggendo 11
byte di "descrittore" da `table_base + layer_idx×11` — ben oltre la vera tabella
descrittori (dimensionata per il build reale, tipicamente poche decine di byte) — e
interpretando dati PSRAM arbitrari (pesi, altri dati di rete, o memoria non
inizializzata) come indirizzi/parametri di layer validi, eseguendo run reali di
`neuron_memory` con quei parametri e **scrivendo i risultati nei buffer ping-pong a
indirizzi derivati da quei dati arbitrari**.
- **Causa radice**: nessun guard su `run_num_layers`, né a tempo di elaborazione (come
invece esiste per `N_INPUTS%PARALLEL` in `neuron_parallel.v`) né a runtime (come invece
esiste, sia pure incompleto, per `n_inputs_real`/`n_neurons_real`, BUG-003/004).
- **Evidenza**: `sim/layer_sequencer_bug005_zero_layers_tb.v``iverilog -g2012 -o /tmp/ls0.out rtl/layer_sequencer.v sim/layer_sequencer_bug005_zero_layers_tb.v && vvp /tmp/ls0.out`
`dut.layer_idx` termina a 255, non a 0.
- **Impatto pratico**: **più severo di BUG-002/003/004** — raggiungibile con un singolo
opcode SPI documentato (`RUN_NETWORK`, `num_layers=0`) senza bisogno di ricompilare il
bitstream né di impostare un valore "runtime" degenere in un percorso secondario; il
rischio non è solo un risultato sbagliato o un hang, ma **scritture reali in PSRAM a
indirizzi non controllati**, derivati da dati che non erano mai stati pensati per essere
interpretati come indirizzi.
- **Fix applicato** (`rtl/layer_sequencer.v`, `ST_IDLE`): `run_num_layers==0` è ora un
no-op esplicito e immediato — `seq_done` pulsa senza mai entrare in `ST_READ_DESC`,
stessa convenzione già usata da `spi_engine.v` per `WRITE_RAM`/`READ_RAM` con `len==0`
(accetta il comando, non fa nulla, nessun errore riportato).
- **Test di regressione**: `sim/layer_sequencer_bug005_zero_layers_tb.v`, riscritto da
osservazione ad asserzione hard (seq_done entro 5 cicli, `layer_idx===0`). Verificato:
`run_num_layers=0` completa in **1 ciclo** con `layer_idx` rimasto a 0 (era 21761 cicli,
`layer_idx` terminato a 255, prima del fix) — PASS.
- **Stato**: **RISOLTO, verificato.**
### BUG-006 (BASSA, stessa causa radice di BUG-005, protezione incidentale) — `num_neurons_graph=0` in `graph_engine.v`
- **Sintomo/causa radice**: identica struttura a BUG-005 — `neuron_idx`
(`rtl/graph_engine.v:159`) è un registro a 16 bit pieni, `num_neurons_graph=0` fa
avvolgere la condizione di terminazione a un valore (65535) che il contatore raggiunge
naturalmente. Nessun guard esplicito su `num_neurons_graph`.
- **Differenza da BUG-005**: `graph_engine` ha già un guard runtime per-edge
(`src_id>=out_id`/`out_id>=N_TOTAL``err`) che, **come effetto collaterale non
progettato per questo scopo**, cattura la maggior parte dei pattern di dati spazzatura
molto rapidamente — verificato con un pattern non banale: `err` a 58 cicli, non 65536.
`layer_sequencer.v` non ha alcuna protezione equivalente.
- **Evidenza**: `sim/graph_engine_bug006_zero_neurons_probe_tb.v` — finestra di 5000 cicli,
non fatto girare a completamento (limite dichiarato, vedi
`docs/validation/06-graph-engine.md` §6.2).
- **Impatto pratico**: basso ma non nullo — la protezione PRE-fix era incidentale, non
garantita per ogni possibile contenuto PSRAM. Il buco strutturale era reale.
- **Fix applicato** (`rtl/graph_engine.v`, `ST_COPY_IN_WAIT`, alla transizione di fine copia
input): `num_neurons_graph==0` è ora un no-op esplicito e immediato — `done` pulsa subito
dopo il completamento della copia input, senza mai entrare in `ST_DESC_RD`/il loop
descrittori, stessa convenzione del fix BUG-005.
- **Test di regressione**: `sim/graph_engine_bug006_zero_neurons_probe_tb.v`, riscritto da
osservazione a asserzione hard (`done` entro 30 cicli, nessun `err`, `neuron_idx===0`).
Verificato: `num_neurons_graph=0` completa in **13 cicli** senza `err`, `neuron_idx` rimasto
a 0 (era 58 cicli tramite l'intercettazione incidentale del guard src_id/out_id, prima del
fix) — PASS.
- **Stato**: **RISOLTO, verificato.**
### BUG-007 (CRITICA, CONFERMATO end-to-end via SPI reale) — `SET_NET_TYPE` durante un `RUN_NETWORK` in corso blocca permanentemente il motore in esecuzione
- **Sintomo**: `rtl/spi_engine.v`, stato `ST_SET_NET_TYPE`, accetta
`net_type <= rx_byte` **incondizionatamente** su qualunque `rx_valid`, senza alcun
controllo su `graph_busy`/`seq_busy`. `rtl/spi_neuron_top.v` (righe 394-397) instrada la
Porta C dell'arbitro tra `graph_engine` e `layer_sequencer` in modo **puramente
combinazionale** sul valore CORRENTE di `net_type` — non agganciato a quale motore ha
effettivamente avviato il run in corso. Il commento alla riga 390 dichiara i due motori
"mutually exclusive by construction", ma quella costruzione impedisce solo che **entrambi
vengano avviati insieme** — non dice nulla su una scrittura di `net_type` che arriva a
metà di un run già avviato.
- **Confermato end-to-end su SPI reale** (non solo per ispezione): avviato un
`RUN_NETWORK` in modalità grafo (lo stesso grafo valido già certificato in
`spi_neuron_top_graph_tb.v`), poi immediatamente — prima che completi — inviato
`SET_NET_TYPE(dense)` via SPI. Risultato: **`STATUS.busy` resta bloccato a 1 per 400+
letture consecutive, ~2.35ms di tempo simulato** (contro i ~12-25µs normali per quel
grafo) — un hang permanente, non un rallentamento. Le transazioni SPI stesse (incl. il
`SET_NET_TYPE` avversariale) completano regolarmente; è specificamente il motore grafo
a restare bloccato in attesa di un `ram_ready` che non arriverà mai più tramite il
percorso ormai scollegato dal mux.
- **Evidenza**: `sim/spi_neuron_top_bug007_mid_run_net_type_tb.v` — riproduce l'hang in
modo deterministico e ripetibile su SPI reale (non solo un accesso interno).
- **Impatto pratico**: **il più severo finora insieme a BUG-005** — raggiungibile con due
soli opcode SPI documentati emessi in sequenza ravvicinata (`RUN_NETWORK` seguito da
`SET_NET_TYPE` prima del completamento), uno scenario host plausibile (es. un host che
prepara la configurazione per il prossimo run senza attendere la fine del precedente,
o una race a livello applicativo tra due richieste). Blocca l'inferenza in corso finché
l'host non se ne accorge (nessun timeout hardware, nessun errore riportato — solo
`STATUS.busy` che non si abbassa mai).
- **Recupero verificato**: un `RESET` inviato durante l'hang **riporta il sistema a uno
stato pienamente funzionante** — verificato con una successiva operazione dense legittima
completata normalmente (2 cicli di polling, esito corretto). **Non è un blocco
permanente**, ma un host che si limita a fare polling di `STATUS` senza un timeout e un
`RESET` di ripiego resterebbe bloccato indefinitamente comunque, dato che l'hardware non
segnala mai da solo che qualcosa è andato storto.
- **Fix applicato** (`rtl/spi_engine.v`, `ST_SET_NET_TYPE`): `net_type <= rx_byte` ora
condizionato a `if (!graph_busy && !seq_busy)` — la scrittura viene silenziosamente
rifiutata (comando accettato via SPI come prima, ma senza effetto) mentre un run è in
corso, invece di rimappare il mux dell'arbitro a metà esecuzione.
- **Test di regressione**: `sim/spi_neuron_top_bug007_mid_run_net_type_tb.v`, riscritto per
asserire end-to-end su SPI reale sia (a) che il run in corso completi normalmente
nonostante lo `SET_NET_TYPE` avversariale a metà esecuzione, sia (b) che la scrittura sia
stata VERAMENTE rifiutata e non parzialmente applicata (una successiva `RUN_NETWORK` senza
re-inviare `SET_NET_TYPE(graph)` completa comunque correttamente). Verificato: il run grafo
completa con `out_base[0]=126` dopo 1 solo polling nonostante lo `SET_NET_TYPE(dense)`
avversariale; `net_type` confermato ancora `GRAPH` internamente — PASS su entrambi i
controlli.
- **Stato**: **RISOLTO, verificato end-to-end su SPI reale.**
---
## Verifica post-fix su entrambi i piani (§A.4)
Dopo l'applicazione di tutti e 7 i fix (RTL: `rtl/neuron_parallel.v`, `rtl/neuron_memory.v`,
`rtl/layer_sequencer.v`, `rtl/graph_engine.v`, `rtl/spi_engine.v`; rimozione:
`sim/top.v`):
- **Regressione Icarus completa** (`python3 tools/run_regression.py`): 44 testbench, **43
PASS**, 0 FAIL/ERROR, 1 BENCHMARK (nessun verdetto per progetto, invariato). Nessuna
regressione sui 37 test già certificati pre-fix.
- **Sintesi reale** (`yosys synth_ecp5`, sistema completo `spi_neuron_top` con sottosistema
flash, PARALLEL=8, stessi file/comando già validati in Fase 15/F7 di WORKLOG.md): **0
problemi CHECK**, 1 warning atteso/preesistente (tri-state limitato in
`psram_controller.v`, invariato). TRELLIS_FF: 4900 (era 4855 — +45, coerente con la nuova
logica di guard/controllo introdotta dai fix, nessuna crescita anomala).
- **Place&route reale** (`nextpnr-ecp5 --45k --package CABGA381 --speed 8 --freq 80
--lpf synth/ecp5/spi_neuron_top.lpf`): **0 errori di vincolo, 0 pin non vincolati,
"Program finished normally"**. Fmax: **68.65 MHz** (era 67.91 MHz — leggermente meglio,
entro il rumore di piazzamento già documentato in Fase 7/WORKLOG.md, non una regressione).
Percorso critico verificato esplicitamente **strutturalmente identico** a prima del fix:
`u_neuron_memory.u_neuron.group_index` → `u_mac8` → catena di riporto CCU2C
dell'accumulatore (`rtl/mac8.v`) — nessun modulo toccato dai fix (che sono tutti
aggiunte al percorso di controllo, non al datapath MAC/accumulatore) compare nel
percorso critico. Margine sull'oscillatore reale 16MHz: 4.29× (invariato).
Log: `synth/ecp5/post_fix_verify/yosys.log`, `synth/ecp5/post_fix_verify/nextpnr.log`.
---
## Non-bug (falsi positivi trovati e chiusi durante l'analisi)
Voci che sono sembrate anomalie a un primo controllo automatico ma si sono rivelate corrette
per progetto una volta letto il codice/intento — riportate per trasparenza sul processo, non
perché siano difetti.
- **`neuron_parallel_guard_negative_{degenerate,nonmultiple}_tb.v` "falliscono a compilare"**:
comportamento corretto e intenzionale (test negativi, la mancata compilazione è il PASS).
Vedi `docs/validation/00-inventario.md` §0.5.
- **`graph_engine_bandwidth_tb.v` "nessun verdetto PASS/FAIL"**: è un benchmark per
progetto, non un test di correttezza. Vedi §0.3/§0.5 dell'inventario.
- **C.1 (falso "hang" iniziale, `neuron_parallel` config nota-buona)**: controllo tardivo e
singolo di `done` (impulso di un solo ciclo) in uno script bespoke — non un problema
dell'RTL. Vedi `docs/validation/01-datapath.md` §1.4.
- **C.2 (falso "hang" per `n_inputs_real=17`)**: stesso tipo di errore in un secondo script
bespoke diverso da quello già provato — corretto riusando lo schema affidabile. Vedi
`docs/validation/02-runtime-width.md` §2.2.
- **C.3 (falsi mismatch su 2048 controlli + un falso fallimento di round-trip)**: nel nuovo
`sim/int8_memory_access_bytelane_tb.v`, un controllo dei segnali nello stesso passo di
simulazione del loro aggiornamento non-bloccante (leggeva il valore dell'iterazione
precedente), e uno stub di memoria comportamentale che ignorava le byte-lane
`mem_lb_n`/`mem_ub_n` durante la scrittura. Entrambi difetti della testbench, non
dell'RTL — vedi `docs/validation/03-memoria.md` §3.1.
- **C.4 (`mem_arbiter` mai concedeva nulla nella mia prima testbench)**: assegnazioni
bloccanti per ritirare le richieste dei "perdenti" nello stesso fronte di clock che
doveva concedere la richiesta — race reale con il blocco sincrono del DUT. Diagnosticato
con `dut.owner` mai uscito da `SEL_NONE`. Corretto passando ad assegnazioni non bloccanti.
Vedi `docs/validation/04-arbiter.md` §4.1.