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
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
Phase 7 (docs/FPGA-NeuralNetwork-Engine.md): re-ran nextpnr-ecp5 on
the already-synthesized Phase 5 spi_neuron_top netlists (top.json
reused, only placement re-seeded) at --seed 1/2/3 for both P8 and P2.
Both land in a tight band regardless of seed (P8: 39.5-40.6 MHz,
2.6% spread; P2: 42.5-45.0 MHz, 5.8% spread) -- confirms the Phase 5
timing shortfall is a real structural bottleneck, not placement
noise, unlike the much smaller same-tier benchmark design (<2%
utilization, huge placer freedom, genuinely noisy). Corrected the
earlier "pipeline the saturate stage" candidate fix, which targeted
Phase 4's critical path and not the one Phase 5's logic actually
shifted to; block RAM for x_mem/w_mem remains the leading candidate,
not yet implemented.
New docs/FPGA-Neural-Hardware-Design.md: draft hardware design doc
for a board carrying the project's actual target device
(LFE5U-45F-8BG381C) plus the parallel PSRAM rtl/psram_controller.v
is written for. Covers: why not the basic-ecp5-pcb reference board
(wrong package/speed grade, no RAM), a real I/O pin budget from
Lattice's own CABGA381 pinout table, a researched PSRAM part
(ISSI IS66WVE4M16EBLL-70BLI -- 70ns access matches the controller's
timing assumption exactly, with a note on the byte/word address
shift in int8_memory_access.v so the chip's top address line is
correctly left as spare headroom, not a wiring error), clock
(16 MHz, no PLL exists yet so CLK_FREQ_MHZ must match whatever
oscillator is fitted), power/config reusing the reference board's
proven circuitry and errata (config-SPI pin can't double as the
application SPI interface), and a BOM/open-items list.
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
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.
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
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