chore: remove root-level V1 duplicates, superseded by hardware/v1/ freeze

hardware/v1/ was created (dc0b331) as a frozen snapshot of the V1
project that then lived at the repo root (rtl/, sim/, synth/, tools/,
docs/). Root received zero further commits to those files after the
freeze -- confirmed byte-identical to the hardware/v1/ copy for every
file removed here. Root was the "before", hardware/v1/ is the
curated, canonical "after".

Removed (all verified exact-hash duplicates of hardware/v1/ content):
  - rtl/ (20 files, 100% covered by hardware/v1/rtl/)
  - tools/{netasm,pinout,run_regression.py,flash_catalog,validation,
    fpga_benchmark.py} (19 files, 100% covered by hardware/v1/tools/;
    tools/neural_sim/ kept -- unique, post-freeze, no counterpart)
  - sim/*.v (47 testbenches, 100% covered by hardware/v1/sim/; the
    ~38 remaining sim/ entries are compiled binaries and .vcd
    waveform dumps, left as a separate cleanup decision)
  - synth/ecp5/{p2,p4,p8,post_fix_verify} (25 files, exact duplicates
    of hardware/v1/synthesis/; the other ~84 synth/ecp5/* experiment
    build directories are historical artifacts never carried into the
    freeze, left as a separate decision)
  - WORKLOG.md (duplicate of hardware/v1/docs/WORKLOG.md)
  - docs/{FPGA-Neural-Datapatch-Benchmark,FPGA-Neural-Hardware-Design,
    FPGA-NeuralNetwork-Engine}.md, docs/validation/*.md (18 files),
    docs/FPGA-Neural-Datasheet-{EN,IT}.pdf -- all exact duplicates of
    hardware/v1/docs/ content
  - hardware/v1/docs/DatasheetLatex/ (24 files) -- exact duplicate of
    hardware/v2/docs/datasheet/files/docs/datasheet/en/ (discovered
    during this audit; not the same DatasheetLatex already removed
    from hardware/v2/docs/ in an earlier commit)

Moved (genuine, unique, post-freeze V2 content -- not duplicated
anywhere, just living in the wrong/legacy root docs/ location):
  - docs/architecture/*.md -> hardware/v2/docs/architecture/
  - docs/pinouts.md, docs/FPGA_NEURAL_V2_DATASHEET.md,
    docs/FPGA_NEURAL_V2_SCHEMATIC.md,
    docs/FPGA-Neural-V2-Datasheet-EN.pdf -> hardware/v2/docs/

Left untouched (separate decisions, not part of this cleanup):
  - docs/FPGA-Neural-Flash-Subsystem-Verification.md, docs/
    v2-description.md -- orphaned root-only content, no duplicate
    found anywhere, but also not part of the reviewed plan
  - synth/ecp5/* experiment dirs and sim/*_sim + sim/*.vcd build
    artifacts -- not literal duplicates, flagged as candidates for a
    future, separate cleanup pass

Verified no functional breakage: grepped all remaining scripts/docs
for references to every removed path -- only prose/comment mentions
found, no executable imports or build-script paths broken.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
This commit is contained in:
2026-09-07 05:30:17 +02:00
co-authored by Claude Sonnet 5
parent e1cefd13db
commit 81a9619214
184 changed files with 0 additions and 1023781 deletions
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
-552
View File
@@ -1,552 +0,0 @@
# FPGA-Neural — Hardware Design Document
Status: draft, pre-schematic. Component choices below are researched
against current distributor listings (2026-09-02) but not yet
ordered/prototyped. No PCB layout exists yet.
Goal: a board carrying the project's actual target device
(`LFE5U-45F-8BG381C`) plus the parallel PSRAM the current RTL
(`rtl/psram_controller.v`) is written for, so real hardware exists
to run everything already synthesized/benchmarked in this repo.
---
## 1. Why a new board (not `basic-ecp5-pcb`)
A reference ECP5 dev board (Matt Venn's `basic-ecp5-pcb`,
OSHWA-approved, in this workspace at `../basic-ecp5-pcb`) exists and
is a useful source of **proven power/config circuitry** — but it
carries the wrong chip for this project and has no RAM at all:
| | `basic-ecp5-pcb` | This project's target |
|---|---|---|
| Device | `LFE5U-45F-6BG256C` | `LFE5U-45F-8BG381C` |
| Package | 256-ball CABGA | 381-ball CABGA |
| Speed grade | -6 (slowest ECP5 grade) | -8 (fastest ECP5 grade) |
| RAM | none (6 PMODs, no memory chip) | parallel PSRAM required |
Same die (`LFE5U-45F`, same 44K LUT / 72 DSP), different package and
a materially slower speed grade. All Fmax numbers measured so far in
this repo (`docs/FPGA-NeuralNetwork-Engine.md` §15 "Phase 7 — Optimization") target
the -8 grade; they do not directly transfer to a -6 part.
**What we reuse from it anyway:** the power tree and bitstream-config
approach (§4, §5) are package-independent and already validated on
real, shipped hardware — no reason to redesign those from scratch.
---
## 2. I/O pin budget (real ball data, CABGA381)
Extracted from Lattice's own ECP5-45 pinout table (`../basic-ecp5-pcb/docs/ECP5Upinouts.ods`,
sheet `ECP5U45Pinout`, `CABGA381` column) — not estimated. **Caveat added
2026-09-03**: that spreadsheet is no longer present in this environment, so
these per-bank counts could not be re-verified against it. §7's actual pin
assignment instead uses Project Trellis's own device database directly
(same data `nextpnr-ecp5` uses) and gets somewhat lower generic-I/O counts
per bank (e.g. 32/33 usable in banks 2/3 vs. 35/36 here) — Trellis's
`packages`/`pio_metadata` only enumerates *programmable* I/O, not every ball
a full datasheet table would list as "usable I/O" (some datasheet-usable
balls may not appear as ordinary PIO sites in Trellis's model). The totals
below are kept for historical reference; §7 is the verified source for
actual pin placement:
| Bank | Usable I/O balls |
|---|---|
| 0 | 29 |
| 1 | 35 |
| 2 | 35 |
| 3 | 36 |
| 6 | 36 |
| 7 | 35 |
| 8 | 22 |
| 40 (config-related) | 4 |
| **Total usable** | **~232** |
| Power/ground/NC (remaining of 381 balls) | 149 |
Signal budget this design actually needs:
| Function | Pins |
|---|---|
| PSRAM (`psram_a` 22b worst case, `psram_dq` 16b, `ce_n/oe_n/we_n/lb_n/ub_n/zz_n` 6b) | up to 44 (real usage likely less — see §3, address lines can be trimmed to match actual chip density) |
| Application SPI (`sclk/mosi/miso/cs_n`) | 4 |
| `clk`, `rst` | 2 |
| Host attention (`irq_n`, `data_ready_n`, added 2026-09-03 — §7) | 2 |
| Flash runtime SPI bus (`flash_sclk`/`flash_mosi`/`flash_miso`/`flash_cs_n` — fully independent, all ordinary GPIO, no config-primitive shared; implemented Phases F1-F7) | 4 |
| JTAG (recommended, for bring-up/debug) | 4 |
| **Total** | **~60** |
~60 of ~232 usable I/O used (57 actually placed and place&route-verified
in the flash-subsystem synthesis, `synth/ecp5/spi_neuron_top_flash/
nextpnr.log`) — **plenty of headroom** (~170+ spare
pins) for LEDs, buttons, a debug PMOD-style header, or a second SPI
host, without any pin-count pressure. This board does not need to be
pin-constrained the way a 256-ball/PMOD-only design would.
---
## 3. PSRAM subsystem (the piece `basic-ecp5-pcb` doesn't have)
`rtl/psram_controller.v` implements an **asynchronous parallel**
interface with **page-mode burst reads** — address bus, 16-bit data
bus, `ce_n`/`oe_n`/`we_n` and byte-lane `lb_n`/`ub_n`, plus `zz_n`
and its timing already hardcodes a **70&nbsp;ns random-access
latency** assumption (`ACCESS_CYCLES = ceil(70ns × CLK_FREQ_MHZ /
1000)`). This is a classic async-SRAM-style bus, not QSPI — most
"PSRAM" sold today (including what's on typical ESP32 boards) is
serial/QSPI and **will not** plug into this controller without a
rewrite.
**Page mode (2026-09-03, `psram_page_mode_tb.v`):** the ISSI part is
"asynchronous/**page mode**", meaning sequential reads inside the
same 16-word page (address bits above `A[3]` unchanged) don't need
the full 70&nbsp;ns each — only `tAPA`/`tPC` = **20&nbsp;ns**, once
CE#/OE# are already asserted. Earlier revisions of this controller
did not use that mode at all — every access, sequential or not, paid
the full random-access latency. The controller now:
- Enables page mode on the chip itself at power-up, via the
datasheet's software configuration-register sequence (2 dummy
reads + 2 writes at the top address, CR = `0x00F0`) — page mode is
**off by default** on the real chip, so this step is load-bearing,
not cosmetic.
- Keeps CE#/OE# asserted after a read completes (`STATE_PAGE_OPEN`)
instead of closing every single-word transaction; a following read
in the same page only pays `tAPA`; a following read in a
*different* page still avoids a CE# toggle but pays a full `tAA`
for that one word (matches the datasheet: "any change in addresses
A[4] or higher initiates a new tAA access time").
- Byte-enable (`lb_n`/`ub_n`) changes do **not** close the page.
`int8_memory_access.v` alternates them on nearly every access
(byte-granular reads over the 16-bit bus), so treating that as a
close condition — the first implementation attempt — made the real
workload *slower*, not faster (measured regression, corrected
before this was documented as done: see
`docs/FPGA-Neural-Datapatch-Benchmark.md` for before/after
numbers). Only a WRITE, or holding CE# low for close to the
8&nbsp;µs `tCEM` refresh limit, closes the page.
- `sim/psram_model.v` (the timing-strict simulation model used by
every PSRAM-backed testbench) was extended with its own
independent `tAPA`/`tAA` continuation check, so a passing
regression run is a real timing proof, not just a data-match.
**Recommended part: ISSI IS66WVE4M16EBLL-70BLI**
- 64&nbsp;Mbit (4M × 16), parallel pseudo-SRAM, async, **70&nbsp;ns
access** — matches the controller's timing assumption exactly, no
RTL change needed.
- TSOP-44/48 package — hand-solderable-adjacent, real distributor
listings (DigiKey, Mouser) at time of writing.
- **Address bus (2026-09-02: full addressing, all 22 chip lines
wired):** the chip is 4M×16 words (8&nbsp;MB total), needing a
real 22-bit word address, A0&ndash;A21. `ADDR_WIDTH` is now **23**
bits across every module (`rtl/neuron_memory.v`,
`rtl/psram_controller.v`, etc. — bumped from the earlier 22-bit/
4&nbsp;MiB default specifically to reach the full chip).
`int8_memory_access.v` right-shifts the 23-bit **byte** address by
1 (`addr >> 1`) into a 22-bit **word** address before it reaches
`psram_controller` — that 22-bit word address maps exactly onto
the chip's real A0&ndash;A21, with nothing left unconnected. Full
8&nbsp;MB is addressable today, not deferred.
**Fallback: ISSI IS61WV6416DBLL / IS61WV102416BLL** (true async
SRAM, not pseudo-SRAM) — electrically drop-in on the same
`ce_n/oe_n/we_n/lb_n/ub_n` signals, no internal refresh (so `zz_n`
can just be tied inactive), faster than needed (~10&nbsp;ns), useful
if the ISSI PSRAM specifically is out of stock. Smaller density
(1&ndash;16&nbsp;Mbit depending on exact part) — fine for this
project's current memory footprint (weights/biases/activations for
the networks exercised so far are well under 1&nbsp;MB).
Real part numbers, not yet ordered — verify current stock/pricing
before BOM lock.
---
## 4. Clock
`basic-ecp5-pcb` uses a fixed **16&nbsp;MHz** MEMS oscillator
(SiTime SiT2001B family) — no crystal driver on the ECP5, the clock
input must come from an oscillator IC into a `PCLK` pad.
**Recommendation: keep 16&nbsp;MHz**, same SiT2001B family (or
SiT1602/SiT8008, same vendor, also in stock). Rationale, not just
"reuse what worked":
- No PLL exists anywhere in this project's RTL yet — `CLK_FREQ_MHZ`
is a **timing parameter**, not a clock generator. Whatever
oscillator is fitted drives `clk` directly.
- Every Fmax measured so far for the *full* integrated system
(`spi_neuron_top`, Phase 5) sits at 39.5&ndash;45&nbsp;MHz across a
seed sweep (`docs/FPGA-NeuralNetwork-Engine.md` §15 "Phase 7 — Optimization") —
confirmed structural, not placement luck. 16&nbsp;MHz sits well
under that with real margin.
- **`CLK_FREQ_MHZ` must be set to match whatever oscillator is
actually fitted** (16, if this recommendation is taken) — it feeds
the PSRAM access-timing formulas directly (§3); using the RTL's
default of 80 with a 16&nbsp;MHz real clock would under-time the
PSRAM by 5×.
A higher oscillator (e.g. 25 or 32&nbsp;MHz) is possible with margin
to spare, but revisit once the Phase 7 timing-closure work
(`docs/FPGA-NeuralNetwork-Engine.md`) lands rather than guessing a
number now.
---
## 5. Power
Reuse `basic-ecp5-pcb`'s proven three-rail tree as-is (same device
family, same rail requirements regardless of package):
| Rail | Value | Part | Load | Status |
|---|---|---|---|---|
| Core | 1.1&nbsp;V | TLV62568 (buck) | ≥600&nbsp;mA | Confirmed in production, DigiKey/Mouser listed |
| I/O | 3.3&nbsp;V | TLV62568 (buck) | 1&nbsp;A (all banks + PSRAM + PMODs share this) | Confirmed in production |
| Auxiliary | 2.5&nbsp;V | TLV73325 (LDO) | 10&nbsp;mA | Confirmed in production |
Decoupling: one cap per I/O bank minimum, per Lattice's ECP5
Hardware Checklist (referenced by `basic-ecp5-pcb`, not re-derived
here).
---
## 6. Configuration (bitstream load)
Reuse `basic-ecp5-pcb`'s SPI-FLASH-boot approach:
- **W25Q128JV** SPI NOR flash (16&nbsp;MB) — confirmed in production,
multiple package options (WSON, SOIC) currently listed.
- ECP5 reads its bitstream from this flash at power-on (`sysCONFIG`
SPI master mode); no external programmer needed for normal
power-up, only for the initial flash write.
**Lessons reused from `basic-ecp5-pcb`'s errata (do not re-discover
these the hard way):**
- Config-mode select pins should tie directly to GND, not through a
10&nbsp;k resistor — the ECP5 test point is ~1&nbsp;V, too close to
the 3.3&nbsp;V bank's input threshold through a resistor divider.
- Not every SPI flash that claims QSPI actually has a usable QE
(quad-enable) bit in practice — `basic-ecp5-pcb` hit this with an
IS25LP016D and switched to the W25Q12x family instead. Stick with
W25Q128JV rather than substituting on price alone.
- **The dedicated config-SPI clock pin cannot be reused as a general
input** post-configuration without extra board-level workaround
(`basic-ecp5-pcb` needed a bodge wire to let a Raspberry Pi talk
SPI to the FPGA over the *same* physical pin used for flash boot).
**This project's application SPI** (`spi_neuron_top`'s
`sclk`/`mosi`/`miso`/`cs_n`, the host-facing protocol in
`docs/FPGA-NeuralNetwork-Engine.md` §8.1) **must land on separate,
ordinary I/O pins — never the config-SPI pins** — precisely to
avoid needing that same workaround.
---
## 7. Signal map — real `.lpf`, place&route-verified (2026-09-03)
**Source of truth changed from the plan in §2**: the `../basic-ecp5-pcb/docs/ECP5Upinouts.ods`
spreadsheet referenced there is no longer present in this environment. The
assignment below instead comes directly from **Project Trellis's own device
database** (`prjtrellis`'s `database/ECP5/LFE5U-45F/iodb.json`) — the same
data `nextpnr-ecp5` itself uses — joining each `CABGA381` ball to its bank
and any dual function via `pio_metadata`. This is real data, not invented
numbers, and it has been **verified by an actual `nextpnr-ecp5` place&route
run**, not just picked by eye:
```
nextpnr-ecp5 --45k --package CABGA381 --speed 8 --freq 80 \
--json synth/ecp5/spi_neuron_top_graph/top.json \
--lpf synth/ecp5/spi_neuron_top.lpf \
--textcfg <out>.config
```
Result: **0 constraint errors**, full route completes, `Program finished
normally` (log: `synth/ecp5/spi_neuron_top_graph/nextpnr_constrained.log`).
Fmax with this real, fixed pinout: **54.58&nbsp;MHz** at the time this pinout
was first verified (FAIL at the 80&nbsp;MHz target; the critical path was
then the saturation-comparator carry chain in `rtl/neuron_parallel.v`,
unrelated to pin placement). **Superseded 2026-09-03** by the timing-closure
work in `WORKLOG.md` ("Timing closure di `neuron_parallel`"): with the same
real pinout, Fmax is now **75.30&nbsp;MHz** (up from 55.59&nbsp;MHz
unconstrained originally) — the pin assignment itself did not need to
change, only the RTL critical path did. **Further updated 2026-09-03**
after adding the `irq_n`/`data_ready_n` host attention pins (2 more
signals, same bank 7, no other ball reassigned): re-verified with a fresh
place&route run, **0 constraint errors**, Fmax 73.88&nbsp;MHz (within the
same noise band already characterized for this pin count in
`WORKLOG.md`'s timing-closure seed sweep — not a regression). TRELLIS_IO
usage: 53/245 (21%) — still confirms the §2 headroom estimate.
**Further updated 2026-09-04** after the flash subsystem (Phases F1-F6,
`WORKLOG.md`) added 3 real pins (`flash_mosi`, `flash_miso`, `flash_cs_n`
— bank 7, generated additively by `tools/pinout/gen_lpf.py`, confirmed via
`git diff` on `synth/ecp5/spi_neuron_top.lpf` to leave every existing ball
unchanged). Full-system real Yosys+nextpnr-ecp5 synthesis (`synth/ecp5/
spi_neuron_top_flash/nextpnr.log`), **0 constraint errors**, full route
completes, Fmax **66.68&nbsp;MHz**. TRELLIS_IO usage: 56/245 (23%).
**Superseded again the same day (Phase F7)**: the flash bus's SCLK
originally reused the boot `CCLK` pad via the ECP5 `USRMCLK` primitive
(no dedicated pin) — dropped after the user pointed out this made the
"exclusive flash bus" claim electrically misleading (SCLK still
depended on the same pad as the config engine) and it carried an
unresolved verification gap (`USRMCLKTS` pad-enable timing never
checked against the primary Lattice sysCONFIG Usage Guide,
FPGA-TN-02039, absent from this project's document set). `flash_sclk`
is now a 4th ordinary GPIO ball (`E3`, bank 7), added purely additively
(`git diff` confirms only the new line, no existing ball moved) — the
flash bus is now 4 independent wires (`sclk`/`mosi`/`miso`/`cs_n`), zero
pins shared with any ECP5 config primitive. Re-verified with a fresh
full-system synthesis: **0 constraint errors**, Fmax **67.91&nbsp;MHz**
(slightly better than 66.68, placement noise, not a regression),
critical path confirmed unchanged (`neuron_parallel` accumulator carry
chain, no flash module involved), `USRMCLK` utilisation now **0/1
(0%)** — direct confirmation the primitive is no longer used at all.
TRELLIS_IO usage: 57/245 (23%). See `WORKLOG.md`'s Phase F7 entry.
**Cross-checked against the real Lattice datasheet (2026-09-03, user-
provided `FPGA-DS-02012-3-4-ECP5-ECP5G-Family-Data-Sheet.pdf`)**: its §4.3.2
"LFE5U" Pin Information Summary table gives, for LFE5U-45 / 381caBGA, GPIO
counts per bank of 27/33/32/32//33/32/13 (banks 0/1/2/3/4/6/7/8) — this
matches the Trellis-derived counts used above **exactly on 6 of 7 relevant
banks**, off by exactly 1 ball on bank 3 (33 in Trellis's model vs 32 in the
datasheet, immaterial here since only 33 of that bank's balls were even
candidates and none of the 51 actually assigned came from the contested
one). Strong independent confirmation that using Trellis's device database
in place of the no-longer-available spreadsheet was the right call, not a
shortcut that introduced drift.
**What the datasheet does NOT have, confirmed by reading all of it (115
pages, §4 "Pinout Information" in full)**: any ball-by-ball table. §4.1 is
purely functional signal descriptions (no ball numbers at all) and §4.3 is
only the summary-count table quoted above — Lattice ships the actual
per-ball assignment as a separate resource (spreadsheet/pinout file, e.g.
the `.ods` originally referenced, or Diamond/Radiant's own device
database), not inside this PDF. This means the config-SPI (`PROGRAMN`,
`INITN`, `DONE`, `CCLK`, `CFG[2:0]` — the datasheet's "Miscellaneous
Dedicated Pins", counted at 7 for this package) and JTAG (`TCK`/`TMS`/
`TDI`/`TDO` — its "TAP", counted at 4) ball numbers are still not pinned
down to specific balls here.
**That gap does not block anything in this repo, though**: those pins are
dedicated/fixed-function silicon, not part of any user netlist — `rtl/
spi_neuron_top.v` has no TCK/TMS/TDI/TDO/PROGRAMN/etc. ports, so `nextpnr-
ecp5` never needs a `LOCATE` for them (confirmed by the 0-error run above,
which never mentions them) and no `.lpf` entry is possible or necessary for
them regardless. Their exact ball numbers only matter for **PCB schematic
capture** — routing a JTAG header connector and the SPI config-flash chip —
which is the user's own separate, in-progress KiCad work (untracked
`FPGA-Neural/` directory at the repo root), not an RTL/synthesis
deliverable. Downgraded accordingly in the checklist below.
**Placement rationale** (die-edge geometry from Trellis's `globals.json`,
confirmed by joining ball → (col,row) → bank): banks 2 (die col=90, row
1132) and 3 (col=90, row 3568) sit contiguously along the chip's **right**
edge — used together for the whole 44+1-signal PSRAM bus, exactly the "one
or two adjacent banks" the appendix asks for. Bank 7 (col=0, row 1132, the
**left** edge, physically opposite the PSRAM bus) holds the application SPI
+ clock/reset, deliberately on the opposite side from PSRAM to keep the two
buses from crossing. `clk` is pinned to `H5` (`GR_PCLK7_0`), a dedicated
global-clock pad in bank 7. Plain (no dual-function) balls were preferred
first within each bank; where a bank ran out of plain balls (bank 2, for
part of `psram_dq`), the next-best dual-function ball was used as ordinary
GPIO — flagged individually below, and confirmed by the actual nextpnr run
above to be perfectly usable as such (including the one `VREF1_2` ball).
`psram_a[22]` is a real synthesized port bit (`ADDR_WIDTH`=23 sizes the
*byte* address everywhere in this design) that is always 0 in practice — see
§3: `int8_memory_access.v` shifts the byte address right by 1 before it
reaches the PSRAM, so only 22 bits (`psram_a[21:0]`) ever carry real
address information, matching the chip's actual 4M-word (2²²) capacity. It
still needs a physical pin for the tool, given a spare ball, and is a
no-connect on the actual board.
**Clock / reset:**
| Signal | Ball | Bank | Note |
|---|---|---|---|
| `clk` | H5 | 7 | `GR_PCLK7_0` — dedicated global clock pad |
| `rst` | B4 | 7 | |
**Application SPI:**
| Signal | Ball | Bank | Note |
|---|---|---|---|
| `cs_n` | B3 | 7 | |
| `miso` | A3 | 7 | |
| `mosi` | C5 | 7 | |
| `sclk` | B5 | 7 | |
**Host attention pins** (added 2026-09-03, active-low, level, driven
from already-registered sticky bits — see `rtl/spi_neuron_top.v` for
the exact rationale):
| Signal | Ball | Bank | Note |
|---|---|---|---|
| `data_ready_n` | C3 | 7 | low while a result is waiting to be read (mirrors STATUS.bit1, clear-on-STATUS-read) |
| `irq_n` | C4 | 7 | low while `graph_engine`'s load-time guard has tripped (mirrors STATUS.bit2 / §7 of the network-engine spec); clears only on RESET or a fresh graph `run_start`, NOT on a plain STATUS read |
**Flash subsystem — runtime SPI to the onboard W25Q128JV** (added
2026-09-04, Phases F1-F6, made fully independent in Phase F7 same day —
`rtl/spi_flash_master.v`, `WORKLOG.md`; all 4 signals ordinary GPIO,
generated the same additive way as every other row here, confirmed by
`git diff` against the pre-flash `.lpf` to leave every existing ball
unchanged):
| Signal | Ball | Bank | Note |
|---|---|---|---|
| `flash_cs_n` | E4 | 7 | |
| `flash_miso` | D5 | 7 | |
| `flash_mosi` | D3 | 7 | |
| `flash_sclk` | E3 | 7 | added Phase F7 — see below |
**Phase F7 (2026-09-04): `flash_sclk` is now a real, independent GPIO
ball, not a CCLK/`USRMCLK` reuse.** An earlier version drove SCLK
through the ECP5 `USRMCLK` primitive, reclaiming the same physical CCLK
net already used for bitstream boot, to save one pin. Dropped: it made
the "exclusive flash bus" claim electrically misleading (SCLK still
depended on the config engine's own pad) and carried an unresolved
verification gap (`USRMCLKTS` pad-enable timing never checked against
the primary Lattice sysCONFIG Usage Guide, FPGA-TN-02039, absent from
this project's document set). Re-synthesized full system: `USRMCLK`
utilisation now 0/1 (0%), directly confirming the primitive is no
longer used at all. See §6/§9 for the board-level wiring implication
(the flash chip's DI/DO/CS/CLK pins must be dual-wired to both the
dedicated sysCONFIG pins and these 4 ordinary balls — a board-level
duplication inherent to using one physical chip for both boot and
runtime persistence, not something Phase F7 changed).
**PSRAM address:**
| Signal | Ball | Bank | Note |
|---|---|---|---|
| `psram_a[0]` | E16 | 2 | |
| `psram_a[1]` | F16 | 2 | |
| `psram_a[2]` | D18 | 2 | |
| `psram_a[3]` | E17 | 2 | |
| `psram_a[4]` | E18 | 2 | |
| `psram_a[5]` | F18 | 2 | |
| `psram_a[6]` | F17 | 2 | |
| `psram_a[7]` | G16 | 2 | |
| `psram_a[8]` | G18 | 2 | |
| `psram_a[9]` | H16 | 2 | |
| `psram_a[10]` | H17 | 2 | |
| `psram_a[11]` | H18 | 2 | |
| `psram_a[12]` | J16 | 2 | |
| `psram_a[13]` | J17 | 2 | |
| `psram_a[14]` | C20 | 2 | |
| `psram_a[15]` | D19 | 2 | |
| `psram_a[16]` | E19 | 2 | |
| `psram_a[17]` | E20 | 2 | |
| `psram_a[18]` | F19 | 2 | |
| `psram_a[19]` | F20 | 2 | |
| `psram_a[20]` | G20 | 2 | |
| `psram_a[21]` | H20 | 2 | |
| `psram_a[22]` | P18 | 3 | always 0 (23-bit byte `ADDR_WIDTH` / 22-bit real word address, §3) — NC on the board |
**PSRAM data:**
| Signal | Ball | Bank | Note |
|---|---|---|---|
| `psram_dq[0]` | K18 | 2 | |
| `psram_dq[1]` | C18 | 2 | dual-function ball (URC_GPLL0T_IN), used here as plain GPIO |
| `psram_dq[2]` | D17 | 2 | dual-function ball (URC_GPLL0C_IN), used here as plain GPIO |
| `psram_dq[3]` | D20 | 2 | dual-function ball (VREF1_2), used here as plain GPIO |
| `psram_dq[4]` | G19 | 2 | dual-function ball (GR_PCLK2_1), used here as plain GPIO |
| `psram_dq[5]` | J18 | 2 | dual-function ball (GR_PCLK2_0), used here as plain GPIO |
| `psram_dq[6]` | J19 | 2 | dual-function ball (PCLKT2_1), used here as plain GPIO |
| `psram_dq[7]` | J20 | 2 | dual-function ball (PCLKT2_0), used here as plain GPIO |
| `psram_dq[8]` | K19 | 2 | dual-function ball (PCLKC2_1), used here as plain GPIO |
| `psram_dq[9]` | K20 | 2 | dual-function ball (PCLKC2_0), used here as plain GPIO |
| `psram_dq[10]` | L17 | 3 | |
| `psram_dq[11]` | M18 | 3 | |
| `psram_dq[12]` | M17 | 3 | |
| `psram_dq[13]` | N16 | 3 | |
| `psram_dq[14]` | N18 | 3 | |
| `psram_dq[15]` | P17 | 3 | |
**PSRAM control:**
| Signal | Ball | Bank | Note |
|---|---|---|---|
| `psram_ce_n` | N17 | 3 | |
| `psram_lb_n` | T16 | 3 | |
| `psram_oe_n` | R16 | 3 | |
| `psram_ub_n` | N19 | 3 | |
| `psram_we_n` | R17 | 3 | |
| `psram_zz_n` | N20 | 3 | |
---
## 8. Bill of materials (draft)
| Ref | Part | Function | Availability |
|---|---|---|---|
| U1 | LFE5U-45F-8BG381C | FPGA | Already the project's confirmed target (see main docs, price/stock table) |
| U2 | ISSI IS66WVE4M16EBLL-70BLI | Parallel PSRAM, 64Mb, 70ns | Verified listed, DigiKey/Mouser |
| U3, U4 | TLV62568 | Buck converter, core + IO rails | Confirmed in production |
| U5 | TLV73325 | LDO, 2.5V aux rail | Confirmed in production |
| U6 | W25Q128JV | SPI NOR flash, config | Confirmed in production, multiple packages |
| Y1 | SiT2001B, 16&nbsp;MHz | System clock oscillator | Confirmed in production |
Not yet specified: exact package/footprint per part, decoupling cap
values, JTAG header, PSRAM address-bus trim if a smaller/cheaper
density than 4M×16 turns out to be sufficient once real network
sizes are decided.
---
## 9. Open items before schematic capture
- [x] `ADDR_WIDTH` set to 23 (full 8&nbsp;MB) across all RTL modules
and testbenches, matching the recommended part's real capacity
(2026-09-02) — see §3.
- [ ] If the fallback true-SRAM part in §3 is used instead (smaller
density), decide whether to shrink `ADDR_WIDTH` back down to
match it or keep 23 with the extra range simply unused.
- [x] Real `.lpf` pin assignment for `clk`/`rst`/application SPI/PSRAM
(§7, `synth/ecp5/spi_neuron_top.lpf`), place&route-verified
(2026-09-03) — 0 constraint errors; cross-checked against the
real Lattice datasheet (§7, matches on 6/7 banks exactly). Fmax
54.58&nbsp;MHz when this pinout was first verified, 75.30&nbsp;MHz
after the `neuron_parallel` timing-closure work, 73.88&nbsp;MHz
after adding the host-attention pins, 66.68&nbsp;MHz after adding
the flash subsystem, and **67.91&nbsp;MHz** for the current full
system after making the flash SPI bus fully independent (Phase F7,
§7, 2026-09-04) — see §7 for the full history and why each change is
pin-placement noise, not a regression.
**Config-SPI and JTAG ball numbers are still not pinned down**
(§7) — confirmed by reading the full real datasheet that it has
no per-ball table at all (only functional descriptions and
summary counts: TAP=4, misc dedicated=7 for this package), so
the actual per-ball assignment remains a separate Lattice
resource not available in this environment. **This does not
block any RTL/synthesis work**: those are dedicated/fixed-
function pins with no corresponding port in `rtl/spi_neuron_top.v`,
so no `.lpf` entry is possible or needed for them, and every
place&route run above already completes with 0 errors without
them. They only matter for **PCB schematic capture** (JTAG
header + config-flash routing) — the user's own separate,
in-progress KiCad work, not something this repo's synthesis
flow needs to resolve.
**Distinct from this open item** (do not conflate the two): the
flash subsystem's own runtime SPI pins (`flash_sclk`, `flash_mosi`,
`flash_miso`, `flash_cs_n` — Phases F1-F6, made fully independent
in Phase F7, `WORKLOG.md`) **are** real, pinned, place&route-
verified ordinary GPIO on bank 7 (`flash_sclk`=E3, `flash_mosi`=D3,
`flash_miso`=D5, `flash_cs_n`=E4), generated the same way as every
other signal in this table — **no pin shared with any ECP5 config
primitive** (Phase F7 removed the earlier `USRMCLK`/CCLK reuse for
SCLK; `USRMCLK` utilisation in the current full-system synthesis
is 0/1, confirming it). §5's PCB-level implication: the
W25Q128JV's DI/DO/CS/CLK pins must be wired to *both* the dedicated
sysCONFIG pins (for boot) *and* these 4 ordinary GPIO balls (for
runtime access after configuration completes) — a board-level
duplication inherent to using one physical chip for both roles,
not yet reflected in a schematic since none exists yet (see the
KiCad item below).
- [ ] Confirm PSRAM/SPI signal integrity at whatever clock is
actually fitted (§4) — no signal integrity analysis done yet
- [ ] JTAG header footprint choice (blocked on the JTAG ball question
above)
- [ ] KiCad (or other) schematic capture — none exists yet for this
device/package combination
Binary file not shown.
File diff suppressed because it is too large Load Diff
-186
View File
@@ -1,186 +0,0 @@
# FPGA-Neural V2 — Datasheet
**Status: DRAFT / PRE-RELEASE.** This datasheet documents the INTENDED
V2 board architecture as of STEP20. It does **not** certify a finished,
release-ready design — see §11 Limitations and
`hardware/v2/docs/OPEN_ITEMS.md` for the current, real blocker list.
Do not read any statement here as "physically validated" unless it
says so explicitly.
## 1. General
FPGA-Neural V2 is an embedded neural-network accelerator built around a
Lattice ECP5 FPGA and a single external SDRAM. It executes small,
dependency-graph-structured INT8 neural networks (dense layers, DAGs)
using a Neural Multiprocessor of parallel MAC engines, streaming
weight/activation tiles from one external SDRAM chip that also holds
results.
Architecture stack (top to bottom): SPI host interface → job
registration → Dependency Manager / Neural Director → N parallel
Neural Processors → Memory Manager / streaming tile delivery → Unified
SDRAM Backend → one physical SDRAM.
## 2. FPGA
| Item | Value | Basis |
|---|---|---|
| Part | Lattice LFE5U-45F | DESIGN DECISION |
| Package | CABGA381 | DESIGN DECISION |
| Speed grade | -8 | DESIGN DECISION |
| Ordering part number | LFE5U-45F-8BG381C | DESIGN DECISION (standard Lattice ordering suffix for this grade/package; not independently cross-checked against a live distributor listing this session) |
| Logic (post-synthesis, N=4, frozen STEP19 compute core) | TRELLIS_FF=6425, TRELLIS_COMB=6023, MULT18X18D=32, DP16KD=0 | VERIFIED (real Yosys synthesis, STEP19) |
| I/O used (frozen STEP19 top, no physical host bus) | 149/245 TRELLIS_IO | VERIFIED (real nextpnr-ecp5 P&R, STEP19) |
| I/O used (this step's new board-level top, SPI + osc + reset + SDRAM) | not yet synthesized this round | OPEN — see §11 |
Operating assumption: single clock domain, no CDC beyond the SPI
bridge's own double-flop synchronizers and the reset synchronizer
(§5).
## 3. Neural accelerator
| Parameter | Value |
|---|---|
| N_PROCESSORS | 4 (frozen reference; N=2 also validated; N=8 is a future evolution) |
| P_IN (MAC width) | 8 |
| Data representation | INT8 operands |
| Accumulator | INT32, ReLU + INT8 saturate on output |
| MAC architecture | 8-wide parallel MAC, balanced adder tree (`neural_processor.v`, unchanged since before this freeze) |
| Processor parallelism | N independent Neural Processors, one dependency-graph node in flight per processor |
| Supported memory traffic | weights (read-only, 64-bit packed fetch, cached), activations (read, byte-maskable), results (write, byte-maskable) — all through the SAME single SDRAM |
**RTL capability vs. software/API capability:** the RTL executes one
pre-compiled dependency graph (nodes with producer/consumer edges,
fixed tile counts) registered via 108 bits of per-job configuration
(node id, dependency list, activation/weight/result base addresses,
tile count). There is no on-chip graph compiler, no floating point, no
training — job graphs and addresses are computed off-chip and loaded
via the host interface (§9).
## 4. Unified memory
```
┌─────────────────────┐
│ FPGA ECP5 │
│ │
│ 4x Neural Engines │
│ │ │
│ v │
│ Unified SDRAM │
│ Backend / Arbiter │
└─────────┬───────────┘
│ 16-bit SDRAM bus
v
┌─────────────────────┐
│ AS4C4M16SA-6TIN │
│ Weights │
│ Activations │
│ Results │
└─────────────────────┘
```
| Item | Value | Basis |
|---|---|---|
| Device | Alliance Memory AS4C4M16SA-6TIN | DESIGN DECISION (STEP16-19) |
| Capacity | 4M x 16 (8MB) | DATASHEET VALUE |
| Data width | 16-bit (DQ[15:0]) + DQM[1:0] byte mask | DATASHEET VALUE |
| Addressing | BA[1:0] (4 banks) + A[11:0] (row/col, multiplexed) | DATASHEET VALUE |
| Clock | shared with FPGA system clock (§5) | DESIGN DECISION |
| Initialization/refresh | real, RTL-implemented power-up wait + mode-register-set + periodic AUTO REFRESH (`sdram_controller.v`) | VERIFIED (real refresh events observed in simulation, STEP16-19) |
| Arbitration | single physical port, 2-way logical split: W (weight, read-only, cached) / AR (activation+result, read/write, byte-maskable), each internally arbitrated across N processors by a generic, reused `slot_mem_arbiter` | VERIFIED (STEP19 bit-exact regression, reconfirmed via Verilator this step — see errors.log ERR-0024) |
| Official V2 memory map | weights @0x010000, activations @0x200000, results @0x300000, all within the single 8MB space, 1MB-aligned | DESIGN DECISION |
PSRAM is **not** part of V2. The V1 PSRAM controller (`hardware/v1/rtl/psram_controller.v`) is not instantiated anywhere in the V2 physical path.
## 5. Clock / PLL
```
16 MHz OSCILLATOR
|
v
ECP5 PLL (EHXPLLL)
CLKI_DIV=1 CLKFB_DIV=4 CLKOP_DIV=9
FEEDBK_PATH=CLKOP VCO=576MHz
|
v
FPGA SYSTEM CLOCK
64 MHz
(real, tool-generated ratio: 16 * 4 / 1, CLKOP_DIV=9 -> 576/9=64)
```
| Item | Value | Basis |
|---|---|---|
| Oscillator | 16 MHz (board-level, prior project record) | DESIGN DECISION (part number: TBD — not selected this session) |
| PLL primitive | EHXPLLL (`ecp5_pll_sys_clk.v`) | VERIFIED design-time via Project Trellis `ecppll` v1.4 (real tool, real parameters) |
| Generated system clock | 64 MHz | DESIGN DECISION, chosen over 80MHz because STEP19's own multi-seed P&R data showed only 1/8 seeds closing timing at >=80MHz on the compute-only core, and the new board-level top adds more logic still; 64MHz is not yet itself confirmed by P&R on the NEW top (see §11) |
| PLL lock | `locked` output, feeds `reset_sync.v` | DESIGN DECISION; NOT simulatable (Lattice EHXPLLL has no open sim model) — real lock behavior is a real-hardware-only characterization, see §10 |
| Timing constraints | none yet written for the new board-level top | OPEN — see §11 |
## 6. Interfaces
### SPI host interface (`spi_host_bridge.v`)
Mode 0 (CPOL=0/CPHA=0), MSB-first, one opcode per CS-low period.
Opcodes: `0x10` WRITE_JOB (job registration, 15-byte payload), `0x01`
WRITE_MEM / `0x02` READ_MEM (raw, word-addressed SDRAM access via a
second arbitrated port), `0x20` STATUS, `0x0F` RESET. Verified in
isolation (18/18, `tb_spi_host_bridge.v`). **Not yet verified
end-to-end under realistic multi-job pacing** — see §11/ERR-0025.
The 110-pin `reg_*` bus used by V2's own internal simulation
testbenches is a testbench-only convenience and is **not** the
physical interface.
### JTAG
Standard ECP5 JTAG (TDI/TDO/TCK/TMS), always available regardless of
configuration boot mode, per Lattice's own standard requirement.
### Configuration
Standard ECP5 PROGRAMN/INITN/DONE/CCLK. Boot-mode/flash-part decision:
OPEN (see §11).
## 7. Electrical
Rail voltage requirements are DATASHEET VALUEs (from real device
datasheets); no regulator part numbers, current budget, or decoupling
values are finalized this round. Full detail:
`hardware/v2/docs/POWER_ARCHITECTURE.md`.
## 8. Pinout
Full table: `hardware/v2/docs/PINOUT.md`. Summary: 37 real SDRAM
signals + clk/rst are ball-assigned and P&R-verified (STEP19, against
the STEP19 compute-only top). The board-level top added this step
(SPI + oscillator + reset pins) has **not** had its own ball
assignment or P&R run yet.
## 9. Mechanical / board assumptions
None assumed beyond the package footprint implied by CABGA381. No PCB
dimensions, connector placement, or stack-up are specified — that is
schematic/PCB-capture work, not yet started (see
`hardware/v2/docs/SCHEMATIC_READINESS.md`).
## 10. Programming / first power-on
JTAG programming is standard. A first-power-on procedure exists at
`hardware/v2/docs/FIRST_POWER_ON.md` (procedure only — not executed
against real hardware, since no board has been fabricated).
## 11. Limitations (real, current, as of this datasheet's own writing)
- **The physical SPI host interface is NOT proven end-to-end
correct.** A real, disclosed defect (errors.log ERR-0025 Part B)
produces wrong results when two jobs are dispatched with realistic
SPI pacing, even though registration itself is confirmed correct.
This is the single largest open item.
- The board-level top (`fpga_neural_v2_top.v`) has not been through
synthesis or P&R this round — deliberately, since running the real
toolchain against RTL known to compute wrong answers would not be a
meaningful result.
- No PCB, schematic capture, or fabricated hardware exists. Nothing in
this document should be read as "physically validated."
- Regulator, configuration-flash, and connector part numbers are not
selected.
- The STEP19 compute+memory core (raw `reg_*` interface, no SPI
bridge) IS bit-exact verified (N=2 and N=4, 256/256, reconfirmed via
Verilator this session) and remains the actual, working reference
design underneath this datasheet's own described board architecture.
-160
View File
@@ -1,160 +0,0 @@
# FPGA-Neural V2 — Reference Schematic (textual)
**No KiCad schematic was generated this session.** No RTL-to-schematic
or netlist-to-KiCad automation tool is available in this environment,
and the project's own separate, pre-existing KiCad PCB directory
(`FPGA-Neural/FPGA-Neural/FPGA-Neural/`) is an unrelated, independently
tracked project (its own nested `.git`, near-empty as of last check) —
it was not touched, and this document does not assume its contents.
This is a textual/ASCII reference schematic: a real starting point for
PCB capture, not a substitute for one. All ball assignments below are
the real, P&R-verified ones from `hardware/v2/constraints/
v2_unified.lpf` (STEP19) unless marked otherwise.
## 1. Top-level block diagram
```
+---------------------------+
| HOST MCU |
| SPI |
+------------+----------------+
|
v
+----------------------------------------------------------+
| ECP5 FPGA (LFE5U-45F-8BG381) |
| |
| +--------------+ +---------------------------+ |
| | SPI Host |---->| Register / Control | |
| | Bridge | | (job registration) | |
| +--------------+ +-------------+-------------+ |
| | |
| +-------------v-------------+ |
| | Neural Accelerator (N=4) | |
| | Processor 0..3 | |
| +-------------+-------------+ |
| | |
| +-------------v-------------+ |
| | Unified SDRAM Backend | |
| +-------------+-------------+ |
+----------------------------------------------------------+
|
16-bit SDRAM bus
v
+----------------------------+
| AS4C4M16SA-6TIN |
| Weights / Activations / |
| Results |
+----------------------------+
16 MHz osc --> ECP5 PLL (EHXPLLL) --> 64 MHz system clock
Power rails --> POR/supervisor --> FPGA reset, SDRAM init
Configuration flash + JTAG connector (see 5/6)
```
## 2. SDRAM connection table (real, P&R-verified balls)
| Signal | Ball | Bank | I/O std (assumed) | Direction |
|---|---|---|---|---|
| CLK (shared w/ system clk) | H5 | — | LVCMOS33 | FPGA -> SDRAM |
| CKE | B5 | 7 | LVCMOS33 | FPGA -> SDRAM |
| CS_N | C5 | 7 | LVCMOS33 | FPGA -> SDRAM |
| RAS_N | C4 | 7 | LVCMOS33 | FPGA -> SDRAM |
| CAS_N | A3 | 7 | LVCMOS33 | FPGA -> SDRAM |
| WE_N | B3 | 7 | LVCMOS33 | FPGA -> SDRAM |
| BA[0] | E4 | 7 | LVCMOS33 | FPGA -> SDRAM |
| BA[1] | C3 | 7 | LVCMOS33 | FPGA -> SDRAM |
| A[0..11] | D5,D3,F4,E5,E3,F5,A2,B1,C2,C1,D2,D1 | 7 | LVCMOS33 | FPGA -> SDRAM |
| DQ[0..15] | E1,G5,H3,J5,K3,K2,H1,J1,K1,K4,L4,L5,M5,M4,N4,N5 | 7/6 | LVCMOS33 | bidirectional |
| DQM[0..1] | P5,N3 | 6 | LVCMOS33 | FPGA -> SDRAM |
Full source: `hardware/v2/constraints/v2_unified.lpf`. LVCMOS33 is
assumed to match the SDRAM's own real 3.3V requirement and matches
banks 6/7's real VCCIO range per `docs/pinouts.md` — not yet
independently cross-checked at the schematic/PCB level (WARNING, not
BLOCKER).
## 3. Clock schematic
```
16MHz OSC ---> CLKI (H5, reused from V1's own real LPF)
|
+-----v------+
| EHXPLLL | CLKI_DIV=1, CLKFB_DIV=4, CLKOP_DIV=9
| (hard IP) | FEEDBK_PATH=CLKOP, VCO=576MHz
+-----+------+
| CLKOP = 64MHz
v
FPGA system clock (feeds compute, SDRAM ctrl, SPI bridge)
|
+-----v------+
| reset_sync | <-- ext POR (active-low) + PLL LOCK
+-----+------+
v
rst (sync-deassert, feeds every synchronous block)
```
Oscillator part number: **TBD** (not selected this session — a real
16MHz, 3.3V HCMOS clock oscillator in a standard SMD package is the
intended class of part; no specific manufacturer/part number is
claimed without a real datasheet lookup performed this session).
## 4. Power schematic (rails only — no regulator parts selected)
```
3.3V/1.1V/2.5V rails (regulators: TBD)
| | |
v v v
VCCIO VCC(core) VCCAUX
(banks (real ball (real ball
6/7=SDRAM cluster, cluster,
I/O, etc) see see
POWER_ARCH POWER_ARCH
.md) .md)
|
v
SDRAM VDD/VDDQ (3.3V, DATASHEET VALUE per AS4C4M16SA-6TIN)
```
Full rail table, decoupling guidance, and current-budget status:
`hardware/v2/docs/POWER_ARCHITECTURE.md` (unchanged this step — no new
power work performed).
## 5. Configuration / JTAG schematic
```
FPGA
|-- TDI/TDO/TCK/TMS --> JTAG connector (standard pinout, always
| available regardless of boot mode)
|-- PROGRAMN/INITN/DONE/CCLK --> configuration flash (part: TBD) or
JTAG-only bring-up (decision: OPEN)
```
No configuration-flash part has been selected; JTAG-only bring-up
remains a valid fallback and is documented as such in
`hardware/v2/docs/CONFIGURATION.md`-equivalent content inside
`OPEN_ITEMS.md` (a dedicated `CONFIGURATION.md` was not created this
round — tracked as an open item, not silently dropped).
## 6. Host interface schematic
```
Host MCU --SPI--> FPGA: spi_sclk, spi_mosi, spi_miso, spi_cs_n
```
No ball assignment exists yet for these 4 signals (the board-level
top was not run through P&R this session — see the datasheet's own
§11 Limitations). Pull-up on `spi_cs_n` (idle-high) is the standard,
expected design decision for a single-master SPI bus; not yet placed
in any real LPF.
## 7. What this schematic deliberately does NOT claim
- No KiCad artifact. No PCB. No fabricated board.
- No ball assignment for the new SPI/oscillator/reset pins (P&R not
run against the new board-level top this session, since the design
has a known, unresolved functional defect — see errors.log
ERR-0025 Part B).
- No regulator, flash, or connector part numbers.
This document is a real, honest starting point for PCB capture, not a
finished schematic.
@@ -1,92 +0,0 @@
# NMS Activation Fill Controller Timing (STEP14 Part B)
Status: fixed, real post-P&R verified, bit-exact, adopted. Full data:
`hardware/v2/reports/step14_activation_timing.csv`. Full narrative:
`hardware/v2/logs/experiments.log` (EXP-0029, 0030, 0031),
`decisions.log` (DEC-0026, DEC-0027).
## B1 — Exact critical path (not assumed)
Mined directly from the real nextpnr-ecp5 P&R report for
`nms_neural_multiprocessor_stream.v` at N_SLOTS=4
(Fmax=55.22 MHz, FAIL @ 80 MHz). Full path, 18.11 ns total (6.25 ns
logic + 11.85 ns routing):
```
SOURCE: u_act_fill.resident_tag[11] (register Q)
-> COMBINATIONAL, chained, NO register in between:
(1) max_n_tiles computation, nms_activation_fill_ctrl.v:92
(N_SLOTS-wide running-max fold, each iteration gated by a
23-bit tag-equality check) -- long CCU2C carry chain
(2) resident_count < max_n_tiles comparison, line 165
(the ST_IDLE refill/continue decision) -- ANOTHER 16-bit
magnitude-comparison carry chain, feeding directly off (1)
in the SAME cycle
(3) into pf_start's own next-state logic
DESTINATION: u_act_fill.pf_addr's clock-enable (CE) pin
```
Two full 16-bit magnitude comparisons sit in **one** combinational
cone across **one** clock edge. This confirms, at the exact RTL-line
level, the failure class DEC-0016/EXP-0022 predicted analytically
("O(N_SLOTS) unpipelined combinational scan feeding directly into a
control decision") — but precisely localizes it to the comparison
logic (lines 92 and 165), *not* the priority-encoder
(`desired_valid`/`desired_x_base`, lines 77-86), which does not appear
in this critical path at all.
## B2 — Scaling behavior
The bottleneck is the `max_n_tiles` running-max fold: an imperative
`for` loop creates a data dependency between iterations (`max_n_tiles`
after iteration *i* depends on iteration *i-1*), which Yosys
synthesizes as a sequentially-chained carry structure — inherently
O(N_SLOTS) deep, not O(log N_SLOTS). At N_SLOTS=4 the chain reached
6.25 ns logic + 11.85 ns routing; at N_SLOTS=8 it doubles again (see
below).
## B3 — Minimum fix (two iterations, evidence-driven)
**v2** (one pipeline stage: register `max_n_tiles` before its use in
the `resident_count` comparison): Fmax 55.22 → 72.78 MHz (+31.8%) —
real improvement, still fails 80 MHz. Re-tracing showed the *remaining*
critical path was entirely inside `max_n_tiles`'s own computation
(now feeding its own register), confirming the fix needed to go one
level deeper.
**v3** (second stage: register each slot's tag-equality/masking result
first — independent per-slot work, no N_SLOTS-dependent chain — *then*
fold the already-registered, already-masked values): Fmax 55.22 →
**106.81 MHz** (+93.4%). **PASSES** 80 MHz with real margin. Resource
cost: LUT4 -5.5%, FF +1.4% (2 added pipeline registers), CCU2C
unchanged.
## B4 — No serialization reintroduced
Verified directly: N_SLOTS=2 bit-exact regression test (D-Stress, real
V1 PSRAM chain) gives **numerically identical** cycle count and
sustained MAC/cycle before and after the fix (185270/185270 cycles,
0.1769/0.1769 MAC/cycle). The 3 total cycles of added latency apply
only to the rare, tile-refill-boundary-only decision — never to the
real-time per-tile consumption path (already fully decoupled by
STEP13's own streaming manager). Higher Fmax, zero throughput cost —
satisfying B4's explicit requirement.
## N=8 (exploratory)
`nms_activation_fill_ctrl_v3.v` at N_SLOTS=8: DSP=64/72 (89%, FEASIBLE),
LUT4=4653, FF=10855 (both comfortably FEASIBLE). **Fmax=52.25 MHz,
FAILS 80 MHz** — the v3 fix's second stage (the max-fold itself) is
still O(N_SLOTS)-deep; at N=8 it is twice as deep as at N=4 and becomes
dominant again. This is expected: v3 shifted the crossover point, it
did not eliminate the underlying dependency. A genuine balanced-tree
reduction (or a pipeline scaling with log₂(N_SLOTS) rather than a flat
2-stage split) would be required for N=8 — not undertaken this round
(N=8 is explicitly exploratory; the limiting resource (Fmax, not
DSP/LUT/FF/BRAM) is precisely identified and quantified, per spec).
## Adoption
`nms_activation_fill_ctrl_v3.v` is adopted as the reference activation
fill controller for N_SLOTS≥4 configurations (DEC-0027). The original
and the insufficient v2 are preserved for reference.
-122
View File
@@ -1,122 +0,0 @@
# NMS Continuous Tile Stream — Memory Manager Redesign (STEP13)
Status: implemented, bit-exact verified, synthesized. **Adopted** as
the new reference NMS memory-manager configuration (DEC-0025). Full
data: `hardware/v2/nms/reports/batch_processor_{sweep.csv,summary.md}`.
Full narrative: `hardware/v2/logs/experiments.log` (EXP-0025 through
EXP-0028), `decisions.log` (DEC-0024, DEC-0025).
## Why this file is not `neural_processor_batch.v`
The governing brief for this STEP asked for a "batch/continuous
neuron execution model" — multiple neurons processed per dispatch, or
a continuous neuron stream — to amortize the ~68.5-cycles/neuron
non-memory floor found in EXP-0024. Before writing any RTL, Step 1
required tracing the actual RTL to find exactly where those cycles
go, rather than assuming.
That trace (EXP-0025, an isolated testbench with `neural_processor.v`
+ `nms_memory_manager_pf.v` driven with zero real memory latency
anywhere) found: **93.4% of the floor is explained by a 4-cycles/tile
serialization bug inside the memory manager's own `ST_RUN` state**,
not by per-job dispatch overhead (only 6.6%). `ST_RUN` implements
operand delivery as a strictly sequential chain —
`read_issued → read_ready → present → consumed` — with zero overlap
between consecutive tiles, even though:
- the local activation/weight SRAMs (`nms_activation_replicated.v`,
`nms_weight_packed.v`) have only a 1-cycle `rd_en`-to-data latency;
- `neural_processor.v`'s own `operand_ready` is held continuously high
through the whole tile-loading phase — its datapath is explicitly
designed (per its own header comment) to accept a new tile every
cycle while previous tiles drain through the adder tree/accumulator.
Neither side of this interface requires 4 cycles/tile. It is purely
an artifact of the memory manager's own un-pipelined FSM. **The fix is
therefore a continuous per-tile streaming redesign of the memory
manager, not a neuron-batching scheme — hence
`nms_memory_manager_stream.v`, not `neural_processor_batch.v`.**
`neural_processor.v` itself required no modification.
## Design: `nms_memory_manager_stream.v`
Drop-in replacement for `nms_memory_manager_pf.v` (identical external
interface, same `weight_prefetch_engine.v` instance, same outer job
FSM `ST_IDLE`/`ST_WAIT_RESULT`/`ST_WRITE_RES`/`ST_DONE`). Only
`ST_RUN`'s internal operand-delivery logic differs:
- `rd_ptr` — the read-**issue** pointer (which tile's SRAM read has
been, or is about to be, issued), independent of and normally one
tile ahead of `tile_idx` (the **consumption** pointer, i.e. how many
tiles `neural_processor.v` has actually accepted).
- A 1-deep skid buffer (`buf_valid`/`buf_input`/`buf_weight`/
`buf_last`) holds one tile's fully-read SRAM data, presented to NP
as `operand_valid`/`input_data`/`weight_data`/`tile_last`.
- Every cycle: if a read issued last cycle is landing now (1-cycle
SRAM latency), it's captured into the skid buffer; independently, a
new read is issued for `rd_ptr` whenever legal (in bounds, weight +
activation ready) **and** the buffer will not overflow (empty, or
being drained this same cycle).
Since `operand_ready` stays high throughout the tile-loading phase,
the skid buffer drains every cycle it's full, so a new read can be
issued every cycle too — sustained ~1 cycle/tile, down from 4.
`tile_idx` (the consumption pointer) is still what feeds
`weight_prefetch_engine.v`'s own `consumed_count` port — its external
contract is unchanged; only the local SRAM read-issue pointer
(`rd_ptr`) is new, and it can run up to one tile ahead of `tile_idx`
(the skid buffer's own depth).
## Verification chain (all real, none assumed)
1. **EXP-0025**: isolated zero-latency trace of the *old* design —
established the 4-cycles/tile floor and its 93.4% share of
EXP-0024's real measured floor.
2. **EXP-0026**: same isolated trace against the *new* design — the
fix works exactly as designed (confirmed cycle-by-cycle), but
total cycles barely move (81→80), because it immediately hits a
*second*, previously-masked bottleneck: `weight_prefetch_engine.v`'s
own word-fetch rate is *also* exactly 4 cycles/tile (P_IN=8 bytes ÷
16-bit bus = 4 word-transactions, 1 cycle/word minimum even at
zero real latency) — a bus-**width** ceiling, structurally
different from an FSM-serialization ceiling, that happens to
coincide numerically today.
3. **EXP-0027**: a direct control experiment — a scratch variant with
weight-fetch bypassed (always-ready) isolates the new design's
*own* ceiling: a clean 1 cycle/tile (100% of `neural_processor.v`'s
theoretical per-tile rate), vs. the old design's hard 4-cycles/tile
cap under the identical bypass. This is the direct proof that the
fix removes a real, 4× architectural ceiling — it was just masked
by a coincidentally-equal second bottleneck.
4. **EXP-0028**: full real-system integration
(`nms_dataflow_core_stream.v``nms_neural_multiprocessor_stream.v`,
real V1 PSRAM chain) — bit-exact PASS, 256/256 neurons, D-Stress
workload identical to EXP-0022/0024. Real cycle count: 185270 vs.
185398 (`_pf` baseline), -0.07% — confirms the "masked, zero net
benefit today" prediction exactly. Real synthesis + P&R: N=1
Fmax=142.92 MHz (+3.7% vs. baseline), N=2 Fmax=92.57 MHz (-2.8%,
still comfortably above 80 MHz), resource cost within ±6%. N=4:
55.22 MHz, FAILS 80 MHz — but for the *pre-existing*,
already-documented `nms_activation_fill_ctrl.v` priority-scan
regression (EXP-0022), unrelated to and unaffected by this fix.
## Outcome and adoption
**Outcome B** (helps, but another bottleneck appears — see
DEC-0025 and `batch_processor_summary.md` for the full nine-question
final decision). `nms_memory_manager_stream.v` is adopted as the new
reference configuration: it is a strict improvement (bit-exact,
resource-neutral, no measured downside) and is **required groundwork**
for any future PSRAM bandwidth increase to actually translate into a
throughput gain — without it, a wider/faster memory would immediately
hit the old FSM's 4-cycles/tile ceiling and realize only 25% of its
potential benefit. The original `nms_memory_manager.v` and
`nms_memory_manager_pf.v` remain preserved, unmodified, for A/B/C
reference. Neuron-batching (the brief's original Model B/C) was not
pursued — evidence showed it addresses only 6.6% of the real floor and
would deliver no measurable benefit today for the identical reason
(weight-fetch-rate-bound). N=4/N=8 viability remains blocked by two
independent issues neither addressed by this STEP: external PSRAM
bandwidth, and the activation fill controller's own Fmax regression —
both flagged as future work.
-145
View File
@@ -1,145 +0,0 @@
# NMS Real Weight Prefetch Engine (STEP11)
Status: implemented, bit-exact verified, benchmarked against the real
V1 PSRAM chain, synthesized. **Not adopted as the default NMS
configuration** — see Outcome/Recommendation below. Full data:
`hardware/v2/nms/reports/nms_prefetch_sweep.csv`,
`nms_prefetch_summary.md`; full narrative:
`hardware/v2/logs/experiments.log` (EXP-0023, EXP-0024),
`decisions.log` (DEC-0023), `errors.log` (ERR-0015).
## Problem
The "Current NMS" baseline (`nms_memory_manager.v`, backed by
`prefetch_engine.v`) measured `prefetch_effectiveness≈0%` and
`weight_stall≈92.5%` at N_SLOTS=2 (EXP-0022). Tracing the actual RTL
(not assuming from filenames) showed the real gap: `prefetch_engine.v`
is a single-shot FSM (`ST_IDLE`/`ST_READ_W`/`ST_DONE`) that can only
have **one fetch in flight at a time**, and `nms_memory_manager.v`'s
own restart logic only re-triggers the next tile's fetch once the
*previous* tile's fetch has fully completed and the FSM has returned
to idle — paying a real per-tile control-plane restart cost on every
tile boundary. The gap was never insufficient lookahead *distance*
(the old design already tried to fetch as far ahead as `n_tiles`
allowed); it was zero *outstanding-request depth*.
## Real backend constraint
`memory_interface.v``psram_controller.v` (V1, reused verbatim,
never modified) is a fire-and-forget, **one-transaction-in-flight**
protocol: a single `mem_req` pulse, wait for `mem_ready`, and that IS
the whole transaction. No wire-level pipelining is physically possible
against a real single PSRAM port. So "multiple outstanding requests"
cannot mean multiple simultaneous word transactions — it means
eliminating the *control-plane* overhead paid at every tile boundary
and letting the fetch stream run continuously across tiles, queueing
up to `PREFETCH_DISTANCE` tiles of lookahead ahead of consumption.
## Design: `weight_prefetch_engine.v`
Two monotonic counters fully describe the engine (tiles are always
fetched in strict sequential order, never reordered or re-fetched, so
no per-tile state array is needed):
- `fetch_tile`/`fetch_word` — the next word to request (or the word
currently in flight).
- `ready_count` — tiles 0..`ready_count`-1 are fully resident in the
weight SRAM.
`consumed_count` (the consumer's own tile index, `nms_memory_manager_pf.v`'s
`tile_idx`) bounds a configurable lookahead window:
`window_limit = consumed_count + PREFETCH_DISTANCE`; the engine may
fetch tile K only if `K < n_tiles` **and** `K < window_limit`.
The core mechanism: on `mem_ready && req_outstanding`, the just-completed
word is committed **and**, in the same cycle, the very next request is
issued — either the same tile's next word, or (at a tile boundary) the
next tile's first word — giving zero-gap streaming across tile
boundaries against a backend that only ever has one word in flight.
(An earlier draft used mutually-exclusive `if/else-if` branches for
"commit" vs. "issue next", which reintroduced a 1-cycle gap between
*every* word, not just tile boundaries; fixed by merging both into one
branch — see `weight_prefetch_engine.v`'s own header comment.)
## Integration: the "_pf" A/B variants
Per the explicit "preserve the current NMS baseline" constraint, the
new engine was integrated into parallel `_pf`-suffixed files, leaving
the originals untouched:
- `nms_memory_manager_pf.v` — drop-in replacement for
`nms_memory_manager.v`'s external interface; internally swaps the
private `prefetch_engine.v` instance for `weight_prefetch_engine.v`,
and changes `can_present`'s weight-ready check from
`tile_idx < wgt_fetched` to `tile_idx < wgt_ready_count`.
- `nms_dataflow_core_pf.v` — mirrors `nms_dataflow_core.v`, adds a
`PREFETCH_DISTANCE` parameter, instantiates `nms_memory_manager_pf`.
- `nms_neural_multiprocessor_pf.v` — mirrors
`nms_neural_multiprocessor.v`, instantiates `nms_dataflow_core_pf`.
Both the baseline (`nms_neural_multiprocessor.v`) and the prefetch
variant (`nms_neural_multiprocessor_pf.v`) remain in the repository
side by side; neither supersedes the other.
## Verification
`hardware/v2/nms/sim/tb_weight_prefetch.v` — isolated correctness
testbench: real `sim_word_mem` (configurable extra latency), real
`nms_weight_packed.v` production SRAM, bit-exact fill-pattern checking.
Covers `n_tiles ∈ {0,1,2,PFD,PFD+1,MAX_TILES-1,MAX_TILES}`, back-to-back
jobs with no explicit reset, a dedicated windowing-cap test (frozen
consumer, confirms `ready_count` stops exactly at
`min(PFD,MAX_TILES)`), and (post-ERR-0015) a large-PFD regression case.
10/10 (9/9 at PFD≥MAX_TILES) tests pass bit-exact across
PFD∈{1,2,4,8,32} and under injected extra memory latency.
`hardware/v2/nms/sim/tb_nms_dstress_pf.v` — full real-integration
benchmark: identical D-Stress workload/golden-model/correctness
criteria as `tb_nms_dstress.v` (EXP-0022), instantiating
`nms_neural_multiprocessor_pf` with a `PFD_CFG` parameter, plus new
testbench-only instrumentation for `weight_stall_cycles` and
`prefetch_effectiveness` (tiles consumed with zero weight-blocking
cycles beforehand / total tiles consumed — the exact STEP11
definition). All runs pass 256/256 neurons bit-exact vs. the golden
model.
## ERR-0015: a real bug found and fixed
The initial `window_limit` computation truncated the
`PREFETCH_DISTANCE` *parameter itself* to `CNTW` bits
(`PREFETCH_DISTANCE[CNTW-1:0]`) before adding it to `consumed_count`.
At `MAX_TILES=16` (`CNTW=5` bits), `PFD=32` truncates to 0, making
`window_limit == consumed_count` forever and deadlocking the engine
completely (0/256 neurons ever completed, 0% PSRAM utilization).
Fixed by computing `window_limit` and its comparisons in a fixed
32-bit width, using the untruncated parameter value. Regression-tested
in `tb_weight_prefetch.v`. Full writeup: `errors.log` ERR-0015.
## Results and outcome
See `nms_prefetch_summary.md` for the full comparison table and the
nine explicitly-answered final-report questions. In short:
- **N_SLOTS=1** (no port contention): a real, reproducible **-10.3%**
cycle-count improvement (PFD=1 → PFD≥2), then a complete plateau —
deeper buffering gives zero further benefit. Sustained MAC/cycle
reaches only 2.8% of the theoretical target.
- **N_SLOTS=2** (this project's own primary reference configuration,
real shared-port contention via `slot_mem_arbiter`): **zero
measurable benefit** at any PREFETCH_DISTANCE from 1 to 16 — all
runs are statistically indistinguishable from each other and from
the pre-STEP11 baseline. The single physical PSRAM port is already
saturated (90.5% busy, unchanged from baseline) by natural two-slot
contention before any lookahead scheme can act.
**Final decision: Outcome B (N_SLOTS=1, partial) / Outcome C
(N_SLOTS=2, failure against the 90% criterion).** The mechanism is
correct and does measurably hide latency when the port has spare
capacity; it cannot manufacture bandwidth out of an already-saturated
single physical port. Reaching the STEP11 target would require ~36×
(N=1) to ~82× (N=2) more real PSRAM bandwidth — a hardware-level
constraint, not an RTL-scheduling one. Per DEC-0023, the new engine is
**not** recommended as the default NMS configuration; both variants
are preserved for reference. The evidence-backed next step (real PSRAM
bandwidth — wider bus, multiple independent banks, or a faster backing
technology) is flagged as future work, not undertaken this round.
@@ -1,91 +0,0 @@
# NMS Weight Datapath Scaling (STEP14 Part A)
Status: architectural requirement established and proven (simulation),
**not realizable on real hardware today** (fixed 16-bit physical
PSRAM). Full data: `hardware/v2/reports/step14_weight_scaling.csv`.
Full narrative: `hardware/v2/logs/experiments.log` (EXP-0032, EXP-0033),
`decisions.log` (DEC-0028).
## Question answered
*At what weight-path width does the processor stop being fundamentally
starved by weight delivery?* **64 bits** — exactly `P_IN × DATA_WIDTH`
(8 × 8). Proven by direct cycle-exact simulation, not assumed.
## What was built
`weight_prefetch_engine_wide.v` — a parameterized (`MEM_DATA_WIDTH`)
generalization of the real `weight_prefetch_engine.v`'s continuous
cross-tile-boundary streaming design, simulation-only/exploratory
(same status as `ideal_memory_model.v`). `WORDS_PER_TILE =
ceil(P_IN*DATA_WIDTH / MEM_DATA_WIDTH)`, clamped to a minimum of 1.
`nms_memory_manager_stream_wide.v` pairs it with STEP13's own streaming
memory manager unchanged (A2's requirement), on a *separate* logical
wide port from the real 16-bit result-write-back port.
A real bug was found and fixed during development: address stepping
initially used `WORDS_PER_TILE × BYTES_PER_WORD` as the inter-tile
byte stride, which over-counts whenever the bus is wider than one full
tile (the 128-bit case, `WORDS_PER_TILE=1` but `BYTES_PER_WORD=16`
while the tile itself is only 8 bytes) — this skips over the next
tile's actual data in the packed backing store. Fixed by defining
`TILE_BYTES = TILE_BITS/8` as the canonical, width-independent stride.
## Results (bit-exact + ideal-memory cycle count)
| Width | Words/tile | Steady-state cycles/tile | 16-tile job total |
|---|---|---|---|
| 16-bit | 4 | 4 | 80 |
| 32-bit | 2 | 2 | 48 |
| **64-bit** | **1** | **1** | **32** |
| 128-bit | 1 | 1 | 32 |
All four widths pass bit-exact correctness (9/9 tests each, including
under injected extra memory latency). 64-bit achieves a clean,
cycle-exact **1 cycle/tile** — 100% of `neural_processor.v`'s own
theoretical per-tile acceptance rate, exactly matching the streaming
memory manager's own ceiling (EXP-0027, STEP13). 128-bit gives **zero**
further benefit: a bus wider than one full tile still delivers exactly
one tile per transaction in this single-tile-per-request design (no
multi-tile bursting was attempted).
## The critical distinction: logical vs. physical bandwidth (A5)
STEP14 explicitly warned against assuming a wider logical interface
means the real memory can deliver it. It cannot, here: **the real V1
PSRAM chain is fixed at 16 bits** — a real chip
(ISSI IS66WVE4M16EBLL-70BLI, x16), not an RTL parameter. The
already-existing, already-verified `weight_prefetch_engine.v` (real,
used throughout STEP11-13) *is* exactly what a "64-bit logical / 16-bit
physical" packing adapter would produce: it assembles one 64-bit
logical tile from 4 real sequential 16-bit word transactions. Its real,
repeatedly-measured result is 4 cycles/tile — identical to the ideal
16-bit row above, because the real transaction count is unchanged
regardless of what the logical interface upstream claims. **A logical
wide interface backed by a physically-narrow bus delivers exactly the
narrow bus's own throughput.** No new "packing adapter" module was
built for this reason — the real engine already demonstrates the
answer, conclusively, without further RTL.
## Answer to the primary research questions
- **Is 16-bit weight delivery fundamentally insufficient for P_IN=8?**
Yes — it costs 4 cycles/tile, 4× the achievable minimum.
- **Is 32-bit enough?** No — still 2× the achievable minimum (2
cycles/tile).
- **Is 64-bit the natural architectural point?** Yes, exactly — proven
cycle-exact, not approximate.
- **Does wider logical delivery actually improve real throughput?**
**Not on this hardware.** Realizing the 64-bit ideal requires a
matching *physical* bandwidth increase (a real 64-bit-wide external
bus, or multiple parallel 16-bit PSRAM chips banked together) — a
board/silicon-level change, outside this project's own RTL scope.
## Recommendation
The 64-bit requirement is now precisely quantified and should inform
any future hardware revision (wider PSRAM, multiple banks). No RTL
change is warranted on the current board: `weight_prefetch_engine.v`
(real, 16-bit) remains the correct, already-optimal implementation
given the fixed physical bus width — STEP13's streaming-manager fix
already extracts everything available from the real interface.
-107
View File
@@ -1,107 +0,0 @@
Perfetto, il CSV è quello giusto: FPGA-SC-02034-3-0-ECP5U-45-Pinout.csv, rev. 3.0. L'ho letto e possiamo finalmente fare il pinout esatto per il LFE5U-45F caBGA381, senza deduzioni.
Ho anche verificato la documentazione Lattice aggiornata: per LFE5U VCC = 1,1 V ±5%, VCCAUX = 2,5 V ±5%, VCCIO da 1,2 a 3,3 V e VCCIO8 è il banco associato alla configurazione.
Pinout essenziale BG381
Ball Funzione Alimentazione / collegamento
H13 VCC 1.1 V
J13 VCC 1.1 V
K13 VCC 1.1 V
L13 VCC 1.1 V
M13 VCC 1.1 V
N13 VCC 1.1 V
H12 VCC 1.1 V
N12 VCC 1.1 V
H11 VCC 1.1 V
N11 VCC 1.1 V
H10 VCC 1.1 V
N10 VCC 1.1 V
H9 VCC 1.1 V
N9 VCC 1.1 V
H8 VCC 1.1 V
J8 VCC 1.1 V
K8 VCC 1.1 V
L8 VCC 1.1 V
M8 VCC 1.1 V
N8 VCC 1.1 V
F6 VCCAUX 2.5 V
P6 VCCAUX 2.5 V
F15 VCCAUX 2.5 V
P15 VCCAUX 2.5 V
F9, F10 VCCIO0 Bank 0
F11, F12 VCCIO1 Bank 1
H14, H15, J15 VCCIO2 Bank 2
L14, L15, M15 VCCIO3 Bank 3
L6, L7, M6 VCCIO6 Bank 6
H6, H7, J6 VCCIO7 Bank 7
P9, P10 VCCIO8 Bank 8 / configuration
Nota: il CSV elenca anche i pin VSS/VSSIO; vanno tutti collegati correttamente al piano GND, senza lasciarne nessuno scollegato.
JTAG / programmazione
Questi sono particolarmente importanti perché sono ball esatti del BG381:
Ball Pin Bank Collegamento
R5 TDI 40 JTAG TDI
T5 TCK 40 JTAG TCK
U5 TMS 40 JTAG TMS
V4 TDO 40 JTAG TDO
W3 PROGRAMN 8 controllo configurazione
V3 INITN 8 stato configurazione
Y3 DONE 8 configurazione completata
Il bank 8 è quello associato all'interfaccia di configurazione; Lattice indica esplicitamente VCCIO8 come alimentazione da dimensionare in funzione dell'interfaccia di configurazione utilizzata.
CCLK
Dal CSV:
Ball Pin Bank
U3 CCLK 8
Attenzione però: CCLK non è automaticamente il clock di sistema della nostra rete neurale. È il clock associato alla configurazione; il clock operativo della FPGA va identificato separatamente nel percorso dell'oscillatore/PLL.
Decoupling
Qui preferisco essere molto preciso: non voglio inventare una quantità di condensatori per “ogni VCC”.
La Hardware Checklist ufficiale Lattice richiede esplicitamente:
VCC 1,1 V con PCB plane e adeguato decoupling;
VCCAUX 2,5 V;
VCCIO 1,23,3 V;
VCCIO8 coerente con l'interfaccia di configurazione.
Per la nostra scheda farei quindi una rete di decoupling distribuita, non un condensatore unico per ogni ball:
LFE5U-45F
┌──────────────┐
1V1_CORE ─────┤ VCC │
│ │
2V5_AUX ──────┤ VCCAUX │
│ │
3V3/2V5/etc ──┤ VCCIOx │
│ │
GND ──────────┤ VSS │
└──────────────┘
con i condensatori fisicamente distribuiti attorno al BGA, con quelli ad alta frequenza il più vicino possibile alle rispettive vie/pad di alimentazione.
Una cosa importante
Adesso che abbiamo il CSV ufficiale, posso fare un lavoro ancora più utile: estrarre tutti i 381 ball del BG381 e generarti una tabella Markdown completa divisa in:
VCC
VCCAUX
VCCIO08
VSS/VSSIO
JTAG
PROGRAMN/INITN/DONE
clock
GPIO utilizzabili
pin da NON utilizzare
BOM decoupling consigliata
header JTAG completo
Quella diventerebbe praticamente la nostra scheda di riferimento hardware per il PCB V2.
-203
View File
@@ -1,203 +0,0 @@
# 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
@@ -1,162 +0,0 @@
# 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.
-163
View File
@@ -1,163 +0,0 @@
# 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
@@ -1,98 +0,0 @@
# 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
@@ -1,75 +0,0 @@
# 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).
-75
View File
@@ -1,75 +0,0 @@
# 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) |
-70
View File
@@ -1,70 +0,0 @@
# 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
@@ -1,62 +0,0 @@
# 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.
-67
View File
@@ -1,67 +0,0 @@
# 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
@@ -1,34 +0,0 @@
# 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
@@ -1,30 +0,0 @@
# 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.
-27
View File
@@ -1,27 +0,0 @@
# 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
@@ -1,28 +0,0 @@
# 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.
-40
View File
@@ -1,40 +0,0 @@
# 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.
-26
View File
@@ -1,26 +0,0 @@
# 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.
-143
View File
@@ -1,143 +0,0 @@
# 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).
-122
View File
@@ -1,122 +0,0 @@
# 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
@@ -1,328 +0,0 @@
# 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.