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
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
Bumps ADDR_WIDTH's default from 22 to 23 bits across every RTL
module (neuron_memory, layer_sequencer, spi_engine, spi_neuron_top,
mem_arbiter, int8_memory_access, memory_interface, psram_controller,
memory_model) and every testbench that mirrors it, so the system's
byte-address space reaches the full 8 MiB the recommended PSRAM part
(ISSI IS66WVE4M16EBLL-70BLI, docs/FPGA-Neural-Hardware-Design.md §3)
actually provides -- previously only 4 MiB (half the chip) was
reachable, since int8_memory_access.v's byte->word address shift
(addr >> 1) turned the old 22-bit byte address into only 21 real word
bits, one short of the chip's real 22-bit word address (A0-A21). At
23 bits, that same shift lands exactly on all 22 chip address lines,
so the whole part is usable now instead of deferred to a future
widening.
Also fixes a stray 22'd11-sized literal in layer_sequencer.v's
descriptor-table address increment (numerically already safe via
Verilog's zero-extension, but now correctly unsized so it always
matches ADDR_WIDTH instead of silently assuming 22).
Updated docs/FPGA-NeuralNetwork-Engine.md's SPI protocol address-field
note (23 bits, top 1 reserved bit instead of 2) and
docs/FPGA-Neural-Hardware-Design.md's PSRAM section (the "chip has
one spare address line" framing is gone now that all 22 are wired
and used).
Full regression (all 11 ADDR_WIDTH-touching testbenches, plus a
Yosys elaboration check of spi_neuron_top with the new default and
no override) passes clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WQV3vS9TXaGDJ5cRfnfidt
Two related Phase 5 additions, both threaded the same way (a new
runtime field defaulting to the pre-existing behavior, settable
per-layer via the descriptor table or per-run via SET_BASE):
Configurable activation functions:
- neuron_parallel.v gains a 2-bit `activation` port (ACT_NONE =
linear + two-sided INT8 saturate, ACT_RELU = the original
hardwired behavior, kept as the default so every pre-existing
caller/testbench is unaffected), threaded through neuron_memory.v.
- spi_engine.v: SET_BASE sel=6 (single-layer path); the descriptor
table gains a 7th byte (multi-layer path).
- Verified in neuron_parallel_tb.v (negative pass-through + negative
saturation to -128) and end-to-end in
spi_neuron_top_runnetwork_tb.v (a real negative accumulator that
ACT_RELU would clamp to 0 comes through unclamped under ACT_NONE,
over real SPI/RAM).
Runtime network width (one bitstream, any topology up to its
build-time max, entirely host-configured over SPI):
- neuron_parallel.v gains n_inputs_real, bounding its MAC group loop
(n_inputs_real/PARALLEL groups instead of the fixed build-time
count). neuron_memory.v gains n_inputs_real/n_neurons_real,
bounding its X/W RAM-read loop and its neuron loop. All default to
the build-time max, so unconnected callers are unaffected.
n_inputs_real must stay a multiple of PARALLEL (same constraint
N_INPUTS itself is held to at elaboration time, now the caller's
runtime responsibility).
- spi_engine.v: SET_BASE sel=7/8 (single-layer path); the descriptor
table grows to 11 bytes/layer (+n_inputs_real +n_neurons_real,
multi-layer path) -- layer_sequencer.v also now copies only
n_neurons_real bytes into the ping-pong buffer, not the full
build width.
- This is real early termination, not bookkeeping: no RAM
zero-padding needed for the unused tail, and it measurably
completes faster. neuron_parallel_tb.v TEST 7: 3 cycles vs 6 for a
reduced-vs-full run, with garbage loaded into the skipped lanes to
prove they're never read. neuron_memory_tb.v TEST 5: through the
real PSRAM stack, 209 cycles vs 788. layer_sequencer_tb.v proves a
reduced n_neurons_real shortens the ping-pong copy-out itself
(bytes beyond the real count stay untouched, not just differing).
docs/FPGA-NeuralNetwork-Engine.md: §8.1 opcode/SET_BASE table, new
"Runtime network width" subsection, Phase 5 checklist, Current
Status table, and the "Core architectural principle" statement
updated to reflect that topology (not just trained parameters) is
now host-configured at runtime up to a build-time ceiling.
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
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