Files
FPGA-Neural/docs/FPGA-Neural-Datapatch-Benchmark.md
T
micheleandClaude Sonnet 5 55c827bedf feat: PSRAM page-mode reads + graph engine (Type #2) + real pinout/IRQ pins
PSRAM page-mode read burst support in psram_controller.v: enables the
ISSI IS66WVE4M16EBLL-70BLI's page mode via its configuration-register
software-access sequence at boot (disabled by default on the real
chip), then keeps CE#/OE# asserted after a read so a same-page
continuation only pays tAPA (20ns) instead of a full tAA (70ns)
random access, with automatic tCEM-safe session closing. Only a WRITE
closes the page -- byte-enable changes do not, since
int8_memory_access.v alternates them on nearly every access and an
early implementation attempt that treated them as a close condition
measured a real regression (53.25->61.25 cycles/edge) before being
corrected (53.25->37.53 cycles/edge, +42% gather bandwidth).
sim/psram_model.v gained independent tAPA/tAA and tCEM enforcement
(with a real Verilog same-timestep event-ordering race found and
fixed via a #0 sync) so the regression proves real timing compliance,
not just data correctness. New sim/psram_page_mode_tb.v; full 26-file
regression suite re-run clean. Real nextpnr-ecp5 Fmax re-measured on
the full spi_neuron_top system: 75.73MHz (P2, up from 55.59MHz) and
65.13MHz (P8) -- still under the 80MHz target but not regressed, with
the critical path confirmed (not assumed) to remain entirely inside
neuron_parallel's accumulate chain, never psram_controller.

Also includes this session's other already-validated work: the graph
engine (Type #2 sparse-graph network: act_buffer, graph_engine,
netasm host assembler), real CABGA381 pinout (.lpf, place&route
verified) and physical IRQ_N/DATA_READY_N pins, and Phase 7 timing
closure logs -- all previously uncommitted, documented in WORKLOG.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LH3jPeJ3eFMfF2v8SQhpkk
2026-09-03 17:12:05 +02:00

21 KiB
Raw Blame History

FPGA-Neural — Analisi datapath e benchmark ECP5

1. Obiettivo

Questa fase del progetto FPGA-Neural ha avuto lo scopo di verificare il comportamento sintetizzabile e le prestazioni del core neurale parametrico sul dispositivo:

Lattice LFE5U-45F-8BG381C

Configurazione FPGA:

  • ECP5-45F
  • Speed grade: -8
  • Package: CABGA381
  • 72 blocchi MULT18X18D
  • circa 43.8k LUT/FF equivalenti

La configurazione funzionale utilizzata nei benchmark è:

DATA_WIDTH  = 8 bit
ACC_WIDTH   = 32 bit
N_INPUTS    = 256
N_NEURONS   = 4
PARALLEL    = variabile

Il datapath implementa:

INT8 × INT8
      ↓
   INT16
      ↓
sign extension
      ↓
   INT32
      ↓
accumulation
      ↓
   + bias
      ↓
    ReLU
      ↓
INT8 saturation

Lo scopo principale del benchmark è stato determinare il compromesso tra:

numero di MAC paralleli;
utilizzo dei DSP;
complessità del datapath;
routing;
frequenza massima;
latenza di elaborazione.
2. Architettura RTL

L'attuale datapath è organizzato gerarchicamente:

                    layer
                      │
             ┌────────┴────────┐
             │                 │
          neuron 0           neuron N
             │                 │
             ▼                 ▼
       neuron_parallel   neuron_parallel
             │                 │
             ▼                 ▼
           mac8              mac8
             │                 │
       MAC × PARALLEL    MAC × PARALLEL

Ogni neurone elabora N_INPUTS ingressi a gruppi di PARALLEL.

Con:

N_INPUTS = 256

il numero di gruppi è:

GROUPS = 256 / PARALLEL

Pertanto:

PARALLEL	Gruppi per neurone
16	16
8	32
4	64
2	128

I quattro neuroni vengono elaborati contemporaneamente.

3. MAC unit

Il modulo mac_unit implementa un singolo prodotto-accumulatore.

Per la configurazione INT8:

x : signed INT8
w : signed INT8

Il prodotto è:

INT8 × INT8 = INT16

Il risultato viene quindi esteso con segno a 32 bit:

INT16 → INT32

e sommato all'accumulatore.

L'operazione fondamentale è quindi:

acc_out = acc_in + (x × w)

L'implementazione è completamente parametrica rispetto a:

DATA_WIDTH
ACC_WIDTH
4. Balanced adder tree

Una modifica importante rispetto alla prima implementazione è stata la sostituzione dell'accumulatore combinazionale lineare con un balanced binary adder tree.

Una riduzione lineare avrebbe prodotto:

((((p0 + p1) + p2) + p3) + ...)

con profondità:

O(PARALLEL)

La nuova implementazione utilizza invece:

             sum
           /     \
        sum       sum
       /   \     /   \
      p0   p1   p2   p3

La profondità diventa:

O(log2(PARALLEL))

Per esempio:

PARALLEL = 8
→ 3 livelli

PARALLEL = 16
→ 4 livelli

PARALLEL = 32
→ 5 livelli

Questa modifica riduce significativamente la profondità combinazionale del datapath.

5. neuron_parallel

neuron_parallel esegue il calcolo di un singolo neurone.

Il funzionamento è:

start
  ↓
group 0
  ↓
group 1
  ↓
...
  ↓
group N
  ↓
+ bias
  ↓
ReLU
  ↓
saturation
  ↓
done

Durante ogni ciclo viene elaborato un gruppo di:

PARALLEL

prodotti.

L'accumulatore mantiene il risultato tra un gruppo e il successivo.

6. Test funzionale

Il testbench sim/parametric_tb.v utilizza:

DATA_WIDTH = 8
N_INPUTS   = 256
N_NEURONS  = 4
PARALLEL   = variabile
ACC_WIDTH  = 32

Sono stati definiti quattro casi di test.

N0 — accumulazione su più gruppi

Input:

x = 1

Pesi:

primi 32 = +1
restanti = 0

Risultato:

32 × 1 × 1 = 32

Output atteso:

32

Questo test verifica soprattutto la corretta gestione dell'accumulatore attraverso più gruppi.

N1 — bias

Pesi:

tutti = 0

Bias:

+10

Output atteso:

10
N2 — ReLU

Pesi:

tutti = -1

Input:

tutti = +1

Il risultato è negativo.

La ReLU produce:

0
N3 — saturazione

Pesi:

primi 32 = +4
restanti = 0

Input:

tutti = +1

Risultato:

32 × 4 = 128

L'uscita INT8 positiva viene saturata:

128 → 127
7. Risultato simulazione

Il test è passato con PARALLEL=16:

PARALLEL  = 16

PASS N0: 32
PASS N1: 10
PASS N2: 0 (ReLU)
PASS N3: 127 (saturation)

INT8 PARAMETRIC TEST PASSED

È passato anche con PARALLEL=32:

PARALLEL  = 32

PASS N0: 32
PASS N1: 10
PASS N2: 0 (ReLU)
PASS N3: 127 (saturation)

INT8 PARAMETRIC TEST PASSED

La correttezza funzionale del datapath parametrico è quindi confermata.

8. Sintesi e Place & Route

Dopo la simulazione il datapath è stato sintetizzato per ECP5 utilizzando:

Yosys

e successivamente piazzato e instradato con:

nextpnr-ecp5

Target:

LFE5U-45F
CABGA381
Speed grade -8

Il wrapper di benchmark genera internamente:

input;
pesi;
bias;
segnali di test.

In questo modo non vengono portati all'esterno i giganteschi bus del modello neurale.

Il top-level espone solamente:

clk
rst
start
y_bus
busy
done

Questa modifica è stata fondamentale.

Il primo tentativo esponeva infatti direttamente:

x_bus      ≈ 2048 bit
weights    ≈ 8192 bit
bias       ≈ 32 bit

portando a oltre 10.000 I/O fisiche richieste.

Il risultato era:

TRELLIS_IO: 10309/245

e quindi un errore di placement.

Il problema non era la dimensione logica del circuito, ma esclusivamente il numero di I/O.

9. PARALLEL = 16

Risorse sintetizzate:

LUT4 ≈ 2531
DFF  = 186

Risorse FPGA:

MULT18X18D = 64 / 72

quindi:

88% dei DSP

Timing:

Fmax ≈ 52.13 MHz
Tcrit ≈ 19.18 ns

Il percorso critico risultava fortemente influenzato dal routing.

Configurazione:

PARALLEL = 16
N_NEURONS = 4

produce:

16 × 4 = 64 MAC simultanei
10. PARALLEL = 8

Risorse:

MULT18X18D = 32 / 72

quindi:

32 DSP

Con quattro neuroni:

8 × 4 = 32 MAC simultanei

Timing:

Fmax ≈ 61.71 MHz
Tcrit ≈ 16.20 ns

Composizione del percorso critico:

logic   ≈ 6.43 ns
routing ≈ 9.77 ns
total   ≈ 16.20 ns

Questa configurazione è particolarmente importante perché corrisponde esattamente al target originale di:

32 MAC hardware simultanei
11. PARALLEL = 4

Risorse:

LUT4 = 804
DFF  = 194

MULT18X18D = 16 / 72

Quindi:

16 MAC simultanei

Timing:

Fmax ≈ 75.01 MHz
Tcrit ≈ 13.33 ns

Composizione:

logic   ≈ 6.67 ns
routing ≈ 6.66 ns
total   ≈ 13.33 ns

In questa configurazione logica e routing sono quasi perfettamente bilanciati.

12. PARALLEL = 2

Risorse:

LUT4 = 481
DFF  = 198

MULT18X18D = 8 / 72

Quindi:

8 MAC simultanei

Timing finale dopo routing:

Fmax ≈ 87.88 MHz
Tcrit ≈ 11.38 ns

Composizione:

logic   ≈ 6.43 ns
routing ≈ 4.95 ns
total   ≈ 11.38 ns

Il design è stato quindi verificato con target:

80 MHz

ottenendo:

87.88 MHz

e:

PASS

Il margine teorico rispetto a 80 MHz è:

T80MHz = 12.50 ns

12.50 - 11.38 ≈ 1.12 ns
13. Tabella comparativa
PARALLEL	MAC/neurone	Neuroni	MAC totali	DSP	Fmax	Tcrit	80 MHz
16	16	4	64	64	52.13 MHz	19.18 ns	FAIL
8	8	4	32	32	61.71 MHz	16.20 ns	FAIL
4	4	4	16	16	75.01 MHz	13.33 ns	FAIL
2	2	4	8	8	87.88 MHz	11.38 ns	PASS
14. Interpretazione

I risultati mostrano chiaramente il trade-off fondamentale.

Riducendo PARALLEL:

PARALLEL ↓
    ↓
MAC simultanei ↓
    ↓
DSP ↓
    ↓
adder tree ↓
    ↓
routing/congestione ↓
    ↓
Fmax ↑

ma contemporaneamente:

PARALLEL ↓
    ↓
numero gruppi ↑
    ↓
cicli necessari ↑
    ↓
latenza ↑

Quindi la frequenza massima non è sufficiente per scegliere la configurazione.

Occorre considerare il throughput complessivo:

throughput ≈ MAC_per_cycle × clock_frequency

A parità di quattro neuroni:

P2
8 MAC × 87.88 MHz
≈ 703 M MAC/s
P4
16 MAC × 75.01 MHz
≈ 1.20 G MAC/s
P8
32 MAC × 61.71 MHz
≈ 1.97 G MAC/s
P16
64 MAC × 52.13 MHz
≈ 3.34 G MAC/s

Questi valori sono una misura teorica del throughput del datapath MAC, non ancora del throughput end-to-end della rete, perché non includono i limiti della memoria esterna, del trasferimento dei pesi e del controller.

15. Scelta architetturale

Il risultato più importante del benchmark è che PARALLEL=8 rimane il candidato naturale per l'architettura V1 se l'obiettivo iniziale di progetto è mantenere circa:

32 MAC simultanei

Infatti:

PARALLEL = 8
N_NEURONS = 4

→ 32 MAC
→ 32 DSP / 72
→ 61.71 MHz

L'utilizzo DSP è ancora relativamente basso:

44% circa

lasciando risorse per:

controller memoria;
buffer;
interfaccia SPI;
DMA;
gestione layer;
eventuali pipeline;
future funzioni di controllo.

PARALLEL=2 è invece la configurazione più semplice da temporizzare tra quelle testate:

87.88 MHz

e passa il target di 80 MHz.

Tuttavia richiede:

128 gruppi

per elaborare un neurone da 256 ingressi.

Per questo motivo non è opportuno adottarlo automaticamente come configurazione definitiva solo perché raggiunge la frequenza più elevata.

16. Timing a 100 MHz

Il target di:

100 MHz

non viene attualmente raggiunto.

Il miglior risultato è:

87.88 MHz

con PARALLEL=2.

Il percorso critico è ancora:

weight FF
    ↓
MULT18X18D
    ↓
products
    ↓
adder/carry chain
    ↓
acc_next
    ↓
ReLU / saturation
    ↓
output FF

Il problema non è quindi un'elevata occupazione delle risorse FPGA.

Al contrario, con P2 l'FPGA è utilizzato molto poco:

DSP ≈ 11%
LUT ≈ 1%
FF  ≈ 0%

Il limite è principalmente temporale e dipende dal datapath combinazionale.

Per superare 100 MHz sarà probabilmente necessario introdurre una o più pipeline interne.

Questa ottimizzazione non è però ancora necessaria per procedere con la prossima fase architetturale.

17. Vincoli LPF

Durante questi benchmark il file:

synth/ecp5/top.lpf

è stato lasciato senza vincoli di pin.

nextpnr viene eseguito con:

--lpf-allow-unconstrained

Pertanto i numerosi warning relativi a I/O non vincolate sono intenzionali.

Il benchmark verifica quindi:

sintesi
placement
routing
timing

e non:

pin assignment
I/O standard della scheda
signal integrity

I vincoli LPF reali verranno aggiunti quando sarà definito il pinout della scheda FPGA-Neural.

18. Decisione per la prossima fase

Non è utile proseguire con benchmark PARALLEL=1.

Il punto di interesse architetturale è già stato individuato.

La prossima fase deve spostare il progetto dal benchmark sintetico verso l'architettura reale:

                    HOST
                      │
                      │ SPI
                      ▼
              FPGA interface
                      │
                      ▼
               Memory interface
                      │
          ┌───────────┴───────────┐
          ▼                       ▼
      PSRAM 8 MB               FPGA BRAM
          │                       │
          └──────────┬────────────┘
                     ▼
                tile/buffer
                     │
                     ▼
                 MAC engine
                     │
                     ▼
                accumulator
                     │
                     ▼
                 activation
                     │
                     ▼
                  output

Il core layer e neuron_parallel dovrà quindi essere separato dalla memoria fisica attraverso una Memory Interface.

19. Memoria V1

La memoria di lavoro prevista è:

ISSI IS66WVE4M16EBLL-70BLI

Caratteristiche:

64 Mbit
8 MB
4M × 16
parallel PSRAM
asynchronous/page mode
70 ns
2.73.6 V
48-TFBGA
6 × 8 mm

La memoria volatile sarà utilizzata come working memory durante l'inferenza.

La memoria persistente prevista è:

Winbond W25Q128JVS

con:

128 Mbit
16 MB
SPI NOR Flash

La divisione dei ruoli è:

W25Q128JVS
    ↓
persistent storage
    ↓
weights
biases
network metadata
FPGA/network configuration

e:

IS66WVE4M16EBLL
    ↓
runtime working memory
    ↓
input/output tensors
intermediate data
weight tiles

Infine:

FPGA BRAM
    ↓
local working buffers
tiles
accumulators
20. Conclusioni

La fase di caratterizzazione del datapath ha prodotto i seguenti risultati:

Il datapath INT8/INT32 è funzionalmente corretto.
Il test parametrico è passato.
Il balanced adder tree ha sostituito con successo la precedente riduzione lineare.
Il design è sintetizzabile per LFE5U-45F.
Il placement e routing sono stati completati correttamente.
Il problema iniziale delle migliaia di I/O è stato eliminato spostando i test vector all'interno del wrapper.
PARALLEL=8 implementa esattamente 32 MAC simultanei con quattro neuroni.
PARALLEL=2 raggiunge 87.88 MHz e supera il target di 80 MHz.
Il limite attuale a 100 MHz è dovuto al percorso combinazionale, non alla saturazione delle risorse FPGA.
Non è necessario continuare il benchmark verso PARALLEL=1.

La baseline architetturale rimane quindi:

LFE5U-45F-8BG381C
INT8 / INT32
256 inputs
4 neurons
PARALLEL parametrico

con:

PARALLEL = 8

come candidato principale per la configurazione orientata al throughput e:

PARALLEL = 2

come riferimento per la configurazione orientata alla frequenza.

La prossima attività significativa è l'integrazione della Memory Interface con la PSRAM esterna, mantenendo PARALLEL come parametro del motore computazionale.

Appendice A — Toolchain
Yosys

Yosys è il tool di sintesi RTL.

Flusso:

Verilog RTL
    ↓
elaborazione
    ↓
ottimizzazione
    ↓
mapping ECP5
    ↓
JSON netlist

Versione utilizzata:

Yosys 0.68+post

Binary:

/opt/homebrew/bin/yosys
Project Trellis

Project Trellis fornisce il database open-source dell'architettura ECP5 e gli strumenti necessari all'implementazione.

Tra gli strumenti disponibili:

ecppack
ecppll
ecpbram
ecpunpack

Installazione utilizzata:

/tmp/prjtrellis/install
nextpnr-ecp5

nextpnr-ecp5 esegue:

placement
routing
timing analysis

Versione:

nextpnr-0.11.1-19-g8dbcee5

Binary:

/tmp/nextpnr/build/nextpnr-ecp5

Parametri principali:

--45k

seleziona LFE5U-45F.

--package CABGA381

seleziona il package.

--speed 8

seleziona speed grade -8.

--json

carica la netlist generata da Yosys.

--lpf

carica i vincoli di pin.

--lpf-allow-unconstrained

permette I/O non vincolate.

--freq 80

richiede un timing target di 80 MHz.

Icarus Verilog

Icarus Verilog viene utilizzato per la simulazione RTL.

Esempio:

iverilog -g2012 \
  -Ptb.PARALLEL=16 \
  -o sim/parametric_256x4_p16 \
  sim/parametric_tb.v \
  rtl/mac_unit.v \
  rtl/mac8.v \
  rtl/neuron_parallel.v \
  rtl/layer.v

Esecuzione:

vvp sim/parametric_256x4_p16

Icarus verifica principalmente la correttezza funzionale del RTL.

Appendice B — Differenza tra simulazione e implementazione
Simulazione
Icarus Verilog

verifica:

algebra signed;
prodotti;
accumulazione;
gruppi;
bias;
ReLU;
saturazione;
segnali busy e done.
Implementazione
Yosys
+
nextpnr-ecp5

verifica:

sintetizzabilità;
mapping FPGA;
LUT;
FF;
DSP;
placement;
routing;
timing;
Fmax.

Le due verifiche sono complementari.
 r
Appendice C — Stato attuale
RTL funzionale              PASS
Simulazione parametrica     PASS
Sintesi ECP5                PASS
Placement                   PASS
Routing                     PASS

PARALLEL=16                 52.13 MHz
PARALLEL=8                  61.71 MHz
PARALLEL=4                  75.01 MHz
PARALLEL=2                  87.88 MHz

Target 80 MHz, P2           PASS
Target 100 MHz              FAIL

Memory Interface            PASS (implementata, vedi Appendice D)
PSRAM controller            PASS (implementata, page mode incluso dal 2026-09-03, vedi Appendice D)
Host SPI interface          DA IMPLEMENTARE
Layer engine reale          PROSSIMA FASE

Appendice D — PSRAM page mode (2026-09-03)

Contesto: il chip di memoria previsto (ISSI IS66WVE4M16EBLL-70BLI,
§19) è "asynchronous/page mode", ma `rtl/psram_controller.v` fino a
questa data usava solo l'accesso asincrono a parola singola — ogni
transazione pagava sempre i 70ns pieni (`tAA`), il page mode non era
usato. Verificato leggendo il datasheet reale della famiglia
(IS66WVE1M16BLL, stesso layout di registro di configurazione/CR
della famiglia BLL — 4M×16 vs 1M×16 cambia solo la densità
dell'array, non la logica di page mode/CR): pagina di 16 word
(`A[3:0]`), `tAPA`/`tPC` = 20ns per accessi successivi nella stessa
pagina invece di `tAA` = 70ns, page mode disabilitato di default
all'accensione (serve un caricamento esplicito del registro di
configurazione).

Implementato in `rtl/psram_controller.v` (dettagli in WORKLOG.md,
sezione "PSRAM page mode"), interamente interno al controller —
nessuna modifica a `memory_interface.v`/`int8_memory_access.v`/
`mem_arbiter.v` o a chi li usa.

Banda del gather `graph_engine` (`sim/graph_engine_bandwidth_tb.v`,
stesso benchmark già citato in "Bitstream reale + banda del gather"),
misurata prima/dopo isolando i soli due file toccati con `git
stash`:

```text
                  PRIMA (no page mode)   DOPO (page mode)   Δ
cicli/edge        53.25                  37.53              -29.5%
edge/s  @80MHz     1 502 347              2 131 557          +41.9%
banda   @80MHz     6.01 MB/s              8.53 MB/s          +41.9%
edge/s  @16MHz*      300 469                426 311          +41.9%
banda   @16MHz*    1.20 MB/s              1.71 MB/s          +41.9%

* oscillatore reale raccomandato, docs/FPGA-Neural-Hardware-Design.md §4.

Nota: il primo tentativo di implementazione (chiusura pagina anche sul cambio di LB#/UB#) misurava 61.25 cicli/edge, peggio del "prima" — regressione reale, trovata proprio grazie a questo benchmark prima di considerare il lavoro finito, poi corretta (vedi WORKLOG.md per il dettaglio).

Impatto risorse ECP5 (Yosys synth_ecp5):

                     PRIMA         DOPO          Δ
psram_controller.v (isolato)
  LUT4               63            290           +227
  TRELLIS_FF         83            141           +58
  CCU2C               7             12           +5

spi_neuron_top (sistema completo, Tipo #2, PARALLEL=2)
  LUT4              2367          2619           +252
  TRELLIS_FF        2406          2467           +61
  DP16KD               2             2            0
  MULT18X18D           4             4            0

Su un dispositivo da ~43.8k LUT4 equivalenti l'incremento resta sotto il 6% di utilizzo totale — nessun impatto pratico sul budget risorse. Nessuna DP16KD/MULT18X18D aggiuntiva (il page mode è pura logica di controllo, non tocca la datapath).

Fmax reale (nextpnr-ecp5, stesso comando/vincoli già validati in "Bitstream reale + banda del gather" e nell'entry "Pin fisici IRQ_N/DATA_READY_N", spi_neuron_top, Tipo #2), misurata sia a PARALLEL=2 sia a PARALLEL=8 per completezza (prima c'era solo P2):

            PRIMA (sessione precedente)   DOPO (con page mode)
P2          55.59 MHz                     75.73 MHz
P8          non misurato in questa serie  65.13 MHz

Entrambe ancora FAIL all'obiettivo 80MHz, ma P2 è migliore, non peggiore, rispetto a prima di questa sessione. Il percorso critico è stato verificato esplicitamente leggendo il report nextpnr-ecp5 per entrambe le configurazioni, non assunto: la sorgente in entrambi i casi è u_graph_engine.u_neuron.group_index → catena mac8/neuron_parallel (lo stesso accumulatore CCU2C già identificato in Fase 7) — psram_controller non compare mai nel percorso critico, nonostante la crescita di risorse del page mode. Coerente con quanto stabilito nello sweep di seed di Fase 7 (FPGA-NeuralNetwork-Engine.md §15): il collo di bottiglia resta la catena di accumulo di neuron_parallel.v, non lo stack PSRAM.

Verifica tCEM rafforzata (richiesta esplicita dell'utente, dopo il lavoro sopra): sim/psram_model.v non aveva un controllo indipendente sul limite di 8µs di CE# basso — solo il controller lo rispettava, senza una rete di sicurezza nel modello di test. Aggiunto un check dedicato (TCEM_NS = 8000.0, verificato sia a fine sessione sia a metà burst) e un test dedicato che tiene una pagina aperta per centinaia di letture consecutive, mai idle, fino a superare il budget interno del controller: split confermato (8 chiusure di CE# durante il singolo burst), nessuna violazione rispetto al limite reale del modello — margine reale, non per coincidenza. Aggiungere questo controllo ha smascherato una race Verilog genuina nel modello stesso (due processi indipendenti triggerati nello stesso istante di simulazione, ordine di esecuzione non definito dallo standard — non un bug del controller), corretta con la tecnica standard (#0 di sincronizzazione). Dettagli completi in WORKLOG.md, sezione "PSRAM page mode — giro di rigore aggiuntivo".

Tentata anche la ri-esecuzione del benchmark di parallelizzazione P2/P4/P8 isolato (tools/fpga_benchmark.py) per completezza — quel benchmark sintetizza solo mac_unit.v/mac8.v/neuron_parallel.v/layer.v, non tocca psram_controller.v. Il sotto-processo Yosys per P8 è incappato in una patologia nota di abc -g simple sul netlist completamente srotolato (256×4×32 MAC), superando le 2 ore di CPU senza terminare; interrotto, non riprodotto in questa sessione. Verificato non essere un effetto del lavoro di oggi (stesso script, stesso RTL di input di prima, nessuna modifica su quel percorso). I numeri storici restano validi perché deterministici su input invariati:

PARALLEL=16   52.13 MHz
PARALLEL=8    61.71 MHz
PARALLEL=4    75.01 MHz
PARALLEL=2    87.88 MHz