Commit Graph
8 Commits
Author SHA1 Message Date
micheleandClaude Sonnet 5 a918c3f1e9 feat: configurable activation functions + runtime-configurable network topology
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
2026-09-02 20:18:24 +02:00
michele 233d6ff7fb feat: complete Phase 5 multi-layer network (RUN_NETWORK) + fix STATUS race
Wires the already-present layer_sequencer.v into the SPI stack:

- spi_engine.v: RUN_NETWORK opcode (0x23) + SET_BASE selectors for
  table_base/buf_a_base/buf_b_base; STATUS.busy/done extended to
  track the sequencer (seq_busy/seq_done) alongside neuron_memory
  directly, so done latches on the last layer only.
- spi_neuron_top.v: instantiates layer_sequencer, muxes
  neuron_memory's control inputs between it (while seq_busy) and
  spi_engine's direct-drive path (legacy single-layer mode), wires
  the sequencer's own RAM master to mem_arbiter's Port C.

Found and fixed a real race while writing the end-to-end test: STATUS's
sticky/clear-on-read done bit read its value live/combinationally
during transmission and cleared unconditionally on any STATUS read.
A done_event landing mid-transmission of a STATUS response byte could
be silently dropped -- the host would receive a stale byte while the
sticky bit was cleared regardless, hanging any host polling STATUS in
a loop. Present since Phase 4, not RUN_NETWORK-specific; only
surfaced under this test's continuous polling. Fixed by latching a
status_snapshot at opcode-accept time and gating the clear on what
was actually transmitted.

Tests: spi_engine_tb.v gains RUN_NETWORK/SET_BASE opcode tests (K/L);
new layer_sequencer_tb.v unit-tests the sequencer FSM directly
(descriptor table, ping-pong buffer addressing, byte-exact copy-out);
new spi_neuron_top_runnetwork_tb.v drives a real 2-layer network over
simulated SPI end to end (real neuron_memory + PSRAM, hand-computed
expected output) and confirms the legacy single-layer path still
works afterward. All existing testbenches still pass.
2026-09-02 19:47:36 +02:00
micheleandClaude Sonnet 5 a2bd60e305 feat: complete Phase 4 SPI RTL (engine, arbiter, top) + real-RAM e2e test
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
2026-09-02 15:44:03 +02:00
micheleandClaude Sonnet 5 87efce3d9b docs: draft SPI protocol v1 for Phase 4 (opcodes, register map)
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
2026-09-02 15:04:37 +02:00
micheleandClaude Sonnet 5 661363f637 feat: extend neuron_memory to support N_NEURONS>1 (Phase 3)
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
2026-09-02 14:50:45 +02:00
micheleandClaude Sonnet 5 1a6f0ba2ef fix: guard neuron_parallel against invalid N_INPUTS/PARALLEL combos
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
2026-09-02 14:41:49 +02:00
micheleandClaude Sonnet 5 9b9859a104 test: add Phase 2 parameter sweep for neuron_parallel, update roadmap
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
2026-09-02 14:24:43 +02:00
michele 16769eaa4b working with memory 2026-09-02 13:09:20 +02:00