Gather/padding/src_id<out_id guard certified via existing solid
pre-session tests (graph_engine_tb.v checks act_buffer contents via
hierarchical reference, not just final output; graph_engine_guard_tb.v
covers 4 adversarial cases incl. recovery).
BUG-006 (LOW severity): num_neurons_graph=0 shares BUG-005's exact
root cause (neuron_idx is a full 16-bit register, no guard), but
graph_engine's existing per-edge src_id<out_id guard incidentally
catches most garbage-data patterns fast (err at cycle 58 for a
non-trivial test pattern, vs. layer_sequencer's 21761-cycle full run
in BUG-005) -- not a designed protection for this case, so not closed
as a non-issue, but lower severity given the observed practical risk.
Not run to full 65536-iteration completion (impractical for this
campaign's time budget) -- limitation stated explicitly.
Full regression: 40/40 real tests pass, 1 new observational test.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
Layer chain / ping-pong / busy-done certified via the existing
pre-session test, which already verifies the actual ping-pong buffer
address used (not just the value) and correct busy/done timing.
New finding, BUG-005 (CRITICAL): run_num_layers=0 has no guard at
compile time or runtime, and unlike BUG-002's 1-bit group_index (which
can never represent the wraparound value), layer_idx here is a full
8-bit register that naturally reaches it. Confirmed empirically with a
minimal neuron_memory stub: RUN_NETWORK(0) runs through all 256
possible layer indices (21761 cycles), reading arbitrary PSRAM bytes
far past the real descriptor table as if they were valid layer
descriptors, running real neuron_memory passes on them, and writing
results to ping-pong buffer addresses derived from that arbitrary
data. More severe than BUG-002/003/004: reachable via a single
documented SPI opcode (RUN_NETWORK), real PSRAM corruption risk rather
than just a hang or wrong result. Root cause fully isolated, not just
the symptom.
Full regression: 40/40 real tests pass, 1 new observational test
(no pass/fail by design) deterministically reproduces BUG-005.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
Priority B>C>A>D confirmed correct with distinguishable per-port data
(not just "someone got served" but "the right requester got its own
data back") across 4 contention scenarios. D-alone case confirms low
priority does not mean never granted.
Found and fixed a real race in the test harness itself: blocking
assignments withdrew loser requests in the same clock edge meant to
grant the winner, racing the DUT's own synchronous block -- dut.owner
never left SEL_NONE, every wait() blocked forever. Fixed by switching
request-signal drives to non-blocking assignments throughout.
Documented (not filed as a bug) that D can starve indefinitely under
sustained continuous B contention -- standard behavior for a
fixed-priority arbiter with no aging, and explicitly outside the
header's own stated operating assumption (B/C temporally disjoint in
normal operation). Flagged the header's "never starves or corrupts
A/B/C" wording as ambiguous about whether it promises D's own
progress.
Full regression: 40/40 real tests pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
int8_memory_access.v byte<->word conversion and byte-lane selection:
exhaustive 2048-address test + 6 real read/write round-trips through
the FSM handshake. 2054/2054 checks, 0 mismatches, after fixing two
bugs in the test harness itself (a same-timestep race reading a
non-blocking update one iteration late, and a behavioral memory stub
that ignored byte-lane enables on write) -- both documented as
test-side, not RTL, issues.
memory_interface.v and psram_controller.v not re-verified from
scratch: cited against coverage already established/re-confirmed
earlier in this same session (page-mode/tCEM against the ISSI
datasheet, a real pre-existing power-up request-loss bug found and
fixed), re-run clean via the Phase 0 regression harness rather than
trusted from WORKLOG text alone.
Full regression: 39/39 real tests pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
n_inputs_real/n_neurons_real early termination for valid values is
certified real: a "poison" region (data that would saturate the result
if read past the claimed limit) confirms no over-read, cycle counts
scale proportionally. n_inputs_real non-multiple-of-PARALLEL at runtime
matches the documented silent-truncation risk exactly.
n_inputs_real=0 / n_neurons_real=0 (BUG-003/004): confirmed incorrect
behavior in every repetition, but the exact triggering mechanism was
NOT fully isolated -- nearly-identical repeated tests produced
different symptoms (clean hang vs. silently processing the full build
width vs. a third cycle count matching neither). Reported in full,
including the inconsistency itself, rather than picking the cleanest
result. The two new permanent testbenches reflect this honestly: the
solid early-termination checks are hard assertions, the n_*_real=0
probe is deliberately observe-only given the non-deterministic result.
Full regression: 38/38 real tests pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
mac_unit.v: exhaustive unit test (all 65536 (x,w) combinations at
DATA_WIDTH=8, plus 486 boundary acc_in vectors) against an independent
Python oracle (tools/validation/mac_oracle.py). 66022/66022 match, 0
reserves.
mac8.v: first-ever dedicated unit test (previously only indirect
coverage at whatever single PARALLEL neuron_parallel_tb.v happens to
use). Verified at PARALLEL=2/8/32 with structural adversarial vectors
(catches swapped/duplicated tree wiring), 300 random INT8 pairs per
PARALLEL with realistic accumulating acc_in, and worst-case magnitude
adversarial vectors. 939/939 match.
Confirms BUG-002 (N_INPUTS=0 bypasses the N_INPUTS%PARALLEL elaboration
guard) is real, on both simulation and real Yosys synthesis -- root
cause: [DATA_WIDTH*N_INPUTS-1:0] becomes [-1:0] for N_INPUTS=0, which
both tools treat as a genuine 2-bit undriven vector rather than
collapsing to zero width. Includes a documented self-correction: the
first verification attempt produced a false "hang" using an invalid
one-shot late check of a single-cycle done pulse -- caught by
reproducing the same false result on a known-good sanity config before
trusting it.
Full regression re-run clean after adding 3 new testbenches: 36/36 real
tests pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
Builds tools/run_regression.py (no reproducible regression script existed
before -- every prior "N testbenches, all pass" claim was assembled by
hand). Resolves each testbench's dependencies by static analysis of
instantiation sites, not from memory, then compiles+runs everything fresh.
Independently re-verifies the 33-testbench regression clean (0 real
failures) after fixing two false negatives in the harness itself (two
tests are deliberate compile-time-failure negative tests, one file is a
benchmark with no pass/fail verdict by design -- confirmed by reading
each file's own header, not assumed).
Findings recorded in docs/validation/: sim/top.v is dead code (references
a removed FRAC_BITS parameter from the pre-INT8 Q8.8 era); mac_unit.v/
mac8.v have no dedicated unit testbench, only indirect coverage; the
N_INPUTS%PARALLEL elaboration guard does not mathematically cover
N_INPUTS=0 (open finding, not yet confirmed reachable -- BUG-002).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
The flash subsystem's SCLK previously reused the boot config-SPI's CCLK
pad via the ECP5 USRMCLK primitive to save one pin. This made the
"exclusive flash bus" claim misleading (SCLK still depended on the
config engine's own pad electrically) and carried an unresolved
verification gap (USRMCLKTS pad-enable timing never checked against
the primary Lattice sysCONFIG Usage Guide).
flash_sclk is now a genuine 4th ordinary GPIO pin (E3, bank 7), added
purely additively to the real .lpf (git diff: one new line, no existing
ball moved). The flash bus is now 4 fully independent wires
(sclk/mosi/miso/cs_n), zero pins shared with any ECP5 config primitive
-- confirmed by the full-system synthesis reporting USRMCLK 0/1 (0%)
utilisation.
All 33 project testbenches re-run clean after the port rename (no
functional change, only sclk_sim -> sclk). Full-system real synthesis
re-verified: 0 constraint errors, Fmax 67.91MHz (up slightly from
66.68MHz, same critical path, not a regression).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
Adds FPGA-exclusive access to the onboard W25Q128JV SPI NOR flash for
weights/bias/network persistence, layered as spi_flash_master (raw SPI,
USRMCLK-driven) -> flash_copy_engine (flash<->PSRAM streaming, erase-
before-write, Page Program loop) -> flash_slot_manager (16-slot catalog
with CRC32), exposed via 8 new SPI opcodes (0x40-0x47). Fixes two
pre-existing bugs found during bring-up: a psram_controller.v request
lost during power-up, and a one-cycle-pulse race in the PSRAM arbiter
request handshake. Full simulation + real Yosys/nextpnr-ecp5 synthesis
verification (0 errors, Fmax 66.68MHz) in WORKLOG.md and
docs/FPGA-Neural-Flash-Subsystem-Verification.md.
Also updates docs/pinout to reflect the 56-signal real .lpf (3 new
flash pins) and documents the WRITE_RAM/READ_RAM host backpressure
risk found while testing this subsystem.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
PSRAM page-mode read burst support in psram_controller.v: enables the
ISSI IS66WVE4M16EBLL-70BLI's page mode via its configuration-register
software-access sequence at boot (disabled by default on the real
chip), then keeps CE#/OE# asserted after a read so a same-page
continuation only pays tAPA (20ns) instead of a full tAA (70ns)
random access, with automatic tCEM-safe session closing. Only a WRITE
closes the page -- byte-enable changes do not, since
int8_memory_access.v alternates them on nearly every access and an
early implementation attempt that treated them as a close condition
measured a real regression (53.25->61.25 cycles/edge) before being
corrected (53.25->37.53 cycles/edge, +42% gather bandwidth).
sim/psram_model.v gained independent tAPA/tAA and tCEM enforcement
(with a real Verilog same-timestep event-ordering race found and
fixed via a #0 sync) so the regression proves real timing compliance,
not just data correctness. New sim/psram_page_mode_tb.v; full 26-file
regression suite re-run clean. Real nextpnr-ecp5 Fmax re-measured on
the full spi_neuron_top system: 75.73MHz (P2, up from 55.59MHz) and
65.13MHz (P8) -- still under the 80MHz target but not regressed, with
the critical path confirmed (not assumed) to remain entirely inside
neuron_parallel's accumulate chain, never psram_controller.
Also includes this session's other already-validated work: the graph
engine (Type #2 sparse-graph network: act_buffer, graph_engine,
netasm host assembler), real CABGA381 pinout (.lpf, place&route
verified) and physical IRQ_N/DATA_READY_N pins, and Phase 7 timing
closure logs -- all previously uncommitted, documented in WORKLOG.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LH3jPeJ3eFMfF2v8SQhpkk
Implements the rest of the SPI interface (docs §8.1) on top of
spi_slave.v from the previous commit:
- rtl/spi_engine.v: opcode FSM + register bank, all 8 opcodes (NOP,
WRITE_RAM, READ_RAM, RESET, SET_BASE, START, STATUS, READ_OUTPUT,
READ_CONFIG). tx_byte is driven combinationally from live state
(not reactively on tx_byte_req), applying the prefetch-vs-consume
contract documented on spi_slave.v. STATUS.done is a sticky,
clear-on-read latch. RAM master port uses the same byte-level
convention as neuron_memory.v's external mem_* port.
- rtl/mem_arbiter.v: fixed-priority (neuron_memory > spi_engine)
grant-and-forward arbiter sharing one byte-level memory port
between spi_engine's WRITE_RAM/READ_RAM and neuron_memory's own
X/W/bias reads during a run.
- rtl/spi_neuron_top.v: full integration -- spi_slave -> spi_engine
-> mem_arbiter -> a single shared int8_memory_access ->
memory_interface -> psram_controller -> PSRAM pins. neuron_memory's
rst is global rst OR'd with the RESET opcode's soft-reset pulse.
The host has no direct electrical path to the RAM, only through
this chain.
Testing:
- sim/spi_engine_tb.v: 10 tests (one per opcode + WRITE_RAM/READ_RAM,
START idle-vs-busy, STATUS sticky/clear-on-read, extra-MOSI-bytes-
ignored, back-to-back transactions) against a synthetic 2-cycle-
latency RAM model, isolating the opcode FSM from PSRAM timing.
Found and fixed two testbench-only bugs (RTL needed no change):
the same delta-zero clock-edge race as spi_slave_tb.v (blocking
`nm_done=1` landing on the same sim time as a posedge -- fixed via
negedge-based pulsing) and a missing RAM sentinel initialization.
- sim/spi_neuron_top_tb.v: end-to-end test against the **real**
psram_model.v (not a mock) -- RESET/READ_CONFIG/WRITE_RAM/
READ_RAM/SET_BASE/START/STATUS/READ_OUTPUT all driven purely over
simulated SPI. 3/3 scenarios (sum, saturation, ReLU) pass on the
first attempt; confirms the arbiter and shared byte<->word bridge
are correct against real PSRAM timing, not just a synthetic mock.
Real-toolchain verification (Yosys + nextpnr-ecp5 + ecppack):
spi_slave.v and spi_engine.v synthesize clean and comfortably clear
80 MHz in isolation (403 MHz / 191 MHz, no DSP usage). The full
spi_neuron_top.v integration, however, does NOT meet 80 MHz
(~52-56 MHz depending on PARALLEL) -- the critical path is entirely
inside neuron_parallel.v's existing saturation comparator (no
contribution from the new SPI/arbiter logic), but its routed delay
is ~57% worse than in the isolated benchmark due to placement/
routing congestion once SPI + PSRAM logic shares the fabric with
it, not resource exhaustion (2% DSP usage). Documented as a Phase
4/7 finding in docs/FPGA-NeuralNetwork-Engine.md -- a floorplanning/
pipelining problem for Phase 7, not a functional-correctness issue
(verified independently in simulation against real PSRAM timing).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WQV3vS9TXaGDJ5cRfnfidt
First real RTL piece of the SPI interface (docs §8.1 protocol
draft): the physical layer only -- Mode 0 (CPOL=0, CPHA=0),
MSB-first, byte-level shift register with a 3-stage CDC synchronizer
for SCLK/MOSI/CS_N (the SPI master clock is asynchronous to the
FPGA system clock). Exposes rx_byte/rx_valid, tx_byte/tx_byte_req,
and cs_active/cs_start/cs_end to the (not yet written) protocol
engine.
Documented an important consumer contract on tx_byte_req: it is a
prefetch hint (fires once extra after the last byte of every
transaction, since the slave cannot know in advance whether the
master will keep clocking), not a "byte consumed" event -- a
consumer must advance any stateful pointer (e.g. a RAM read address)
on rx_valid instead, which fires exactly once per real byte
transferred.
sim/spi_slave_tb.v: bit-banged SPI master BFM (4 tests: single byte,
multi-byte in one CS period, back-to-back transactions, slower
SCLK). Two testbench-only bugs found and fixed during bring-up (RTL
itself needed no functional change beyond the tx_byte_req contract
comment): the BFM was advancing its tx queue on tx_byte_req instead
of rx_valid (see contract above), and inter-test reset pulses raced
against posedge clk (blocking `rst=1` landing on the same simulation
time as a clock edge) -- fixed by asserting/deasserting reset on
negedge clk instead.
Verified two ways: Icarus Verilog (4/4 tests pass) and the real
ECP5 toolchain used for prior benchmarks (Yosys 0.68 synth: 0
problems, 41 FF / 55 LUT4, no latches; nextpnr-ecp5 --45k --package
CABGA381 --speed 8 --freq 80: PASS, Fmax 403.23 MHz; ecppack:
bitstream generated with no errors).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WQV3vS9TXaGDJ5cRfnfidt
Phase 4 (SPI Interface) only had a high-level conceptual sequence
(RESET/CONFIGURE/LOAD.../START/WAIT/READ) with no concrete opcodes,
framing, or register map -- not enough to start RTL from. Added
docs/FPGA-NeuralNetwork-Engine.md §8.1 with a concrete v1 draft:
- SPI Mode 0, MSB-first, one opcode byte per CS-low transaction.
- Explicit length field on WRITE_RAM/READ_RAM (chosen over
CS-edge-delimited streaming: simpler controller, just a byte
counter).
- READ_CONFIG opcode exposing N_INPUTS/N_NEURONS/PARALLEL/
ADDR_WIDTH/DATA_WIDTH at runtime, so one host firmware build can
target different bitstreams.
- RESET kept as its own opcode (0x0F), distinct from NOP.
- STATUS.done documented as required to be a STICKY, clear-on-read
bit in the SPI register bank: neuron_memory.done is a one-cycle
pulse that a slow SPI poll would almost certainly miss otherwise.
Opcode values themselves are marked explicitly as draft/example,
not frozen -- only the framing rules and the two decisions above are
meant to stick going into Phase 4 RTL work.
No RTL or testbench changes in this commit; design-only.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WQV3vS9TXaGDJ5cRfnfidt
neuron_memory.v only handled a single neuron. Added an N_NEURONS
parameter (default 1, fully backward compatible) and a memory-bound
neuron loop: X is read once (shared layer input), and for each
neuron in turn W and bias are re-read from PSRAM and fed to a
single, reused neuron_parallel instance -- no change to the
validated compute datapath (neuron_parallel/mac8/mac_unit).
Addressing follows layer.v's neuron-major convention: neuron n's
weights live at w_base + n*N_INPUTS bytes, its bias at
bias_addr + n. Output changed from a single `y` port to a packed
`y_bus` (DATA_WIDTH*N_NEURONS bits, neuron-major), matching
layer.v's y_bus.
- rtl/neuron_memory.v: N_NEURONS parameter, neuron_index/
w_group_base/bias_group_addr tracking, y_reg[] array assembled
into y_bus, STATE_WAIT_N now loops back to STATE_READ_W for the
next neuron instead of finishing after one.
- sim/neuron_memory_tb.v: updated to the new y_bus port
(N_NEURONS=1 explicit); all 5 existing tests still pass unchanged,
confirming backward compatibility.
- sim/neuron_memory_multi_tb.v: new end-to-end test (full
memory_interface + psram_controller + psram_model stack) with
N_NEURONS=3, validating per-neuron addressing and a single done
pulse at the end of the sequence (scale, larger value, ReLU).
- Full regression re-run: all existing testbenches still pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WQV3vS9TXaGDJ5cRfnfidt
Both Phase 2 findings (docs/FPGA-NeuralNetwork-Engine.md) shared one
root cause: GROUPS = N_INPUTS / PARALLEL is integer division. When
N_INPUTS is not an exact multiple of PARALLEL, the remainder inputs
were silently dropped from the accumulation (wrong result, no
error); when PARALLEL > N_INPUTS, GROUPS = 0 and the controller's
terminal condition was never met, hanging the neuron forever.
Added a single elaboration-time guard to rtl/neuron_parallel.v: a
`generate` block instantiates a deliberately undefined module when
N_INPUTS % PARALLEL != 0, forcing a hard failure in both simulation
and synthesis instead of a silent wrong answer or a deadlock. Valid
configurations are unaffected (the branch is never elaborated). The
validated datapath (mac8/mac_unit/accumulation/ReLU/saturation) is
untouched -- this is authorized as a scoped exception to the
"core is fixed, do not touch" project policy, for this guard only.
- sim/neuron_parallel_guard_negative_nonmultiple_tb.v and
sim/neuron_parallel_guard_negative_degenerate_tb.v: negative tests
that must fail to elaborate; verified both fail with the expected
"Unknown module type" error.
- sim/parameter_sweep_tb.v: rewritten to valid-configs-only (the
three configs that used to demonstrate truncation/hang no longer
compile, by design); added PARALLEL=2 and PARALLEL=4 configs,
the two best-performing values from
docs/FPGA-Neural-Datapatch-Benchmark.md.
- Full regression re-run after the RTL change: all existing
testbenches still pass unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WQV3vS9TXaGDJ5cRfnfidt
Roadmap Phase 2 asks to validate N_INPUTS/N_NEURONS/PARALLEL
combinations, including non-exact-multiple configurations. Added
sim/parameter_sweep_tb.v with 5 configs (two exact-multiple sanity
checks, two non-exact-multiple, one degenerate PARALLEL>N_INPUTS),
using a cycle-count watchdog instead of a blocking wait so a hanging
config is reported rather than hanging the simulation.
Findings (RTL unchanged, core datapath left untouched):
- GROUPS = N_INPUTS / PARALLEL truncates: when N_INPUTS is not an
exact multiple of PARALLEL, the remainder inputs are silently
never summed (confirmed 30/8 -> 6 dropped, 20/16 -> 4 dropped).
- PARALLEL > N_INPUTS gives GROUPS=0, and the controller's
group_index == GROUPS-1 terminal condition is never met: the
neuron hangs forever (confirmed via watchdog timeout).
Documented both as findings under Phase 2 in
docs/FPGA-NeuralNetwork-Engine.md for follow-up in Phase 3/7.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WQV3vS9TXaGDJ5cRfnfidt
Both testbenches instantiated their DUTs with a FRAC_BITS parameter and
Q8.8 fixed-point 16-bit values, which no longer exist in rtl/neuron_parallel.v
(now plain INT8, DATA_WIDTH=8, hardcoded +127 saturation, ReLU-only clamp).
This made both tests fail elaboration ("parameter FRAC_BITS not found").
Rewrote both benches with integer INT8 stimuli and expectations matching
the current core (no RTL changes): neuron_parallel_tb covers a mixed
vector, ReLU, positive saturation, and mixed values with a boundary
negative bias; layer_tb covers 8 neurons exercising scale, ReLU,
saturation, bias-only, and a sparse weight pattern across groups.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WQV3vS9TXaGDJ5cRfnfidt
preload_vector and preload_weights wrote using base as a word address
instead of a byte address (base + (k>>1) instead of (base>>1) + (k>>1)),
misaligning X/W data in PSRAM relative to what int8_memory_access expects.
Also adds a PATTERN test (X=1..32) to exercise mixed even/odd byte reads
and catch this class of bug going forward.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WQV3vS9TXaGDJ5cRfnfidt