Adds neural_director_grouped.v, a direct extension of neural_director_ packed.v's own already-proven 2-position pairing discipline to 8-position octets (matching systolic_group.v's fixed 4 PEs x 2 lanes). Real, deliberate finding: the host-facing SPI/WRITE_JOB submission protocol needs zero changes -- the host just submits 8 jobs sharing a weight base instead of 2, the same real pattern already required today. Real out-of-context synthesis of one systolic_group.v: 32 DSP48E1 (13.3%), confirming the original brainstorm's own DSP projection exactly. Found and fixed two real bugs: (1) a wraparound-arithmetic width bug in the octet index computation (same class already flagged for address math elsewhere in this project -- needs N+1 bits before the mod-reduce compare, not N); (2) a real, generalizable testbench race -- driving stimulus on the same clock edge the DUT samples on works fine with a natural gap between pulses (every prior testbench in this project has one) but silently double-registers data when called back-to-back with zero gap, confirmed via real signal tracing. Fixed with @(negedge clk) stimulus; CLAUDE.md's existing blocking/nonblocking testbench-race lesson extended to cover this new trigger. Verified via tb_neural_director_grouped.v: 4/4 PASS (octet dispatch + per-PE addressing, stall-not-mis-dispatch on a mismatched octet, queue wraparound). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MUG92aM9m68TRc4rG55BcC
139 lines
8.6 KiB
Markdown
139 lines
8.6 KiB
Markdown
# FPGA-Neural — project instructions for Claude Code
|
|
|
|
Hardware neural accelerator for a custom PCB: bare **Xilinx XC7A100T-CSG324-2**
|
|
(Artix-7) chip + real DDR3, designed and assembled by the user themselves —
|
|
never a Digilent/dev-board purchase. An ESP32 is the host/central processor,
|
|
talking to the FPGA over a dedicated SPI bus (FPGA is slave there) and, via
|
|
the FPGA, through to a separate config flash used only for FPGA bootstrapping
|
|
(FPGA is master on that second, physically distinct SPI bus).
|
|
|
|
Active branch: **`v3-artix7`**. `hardware/v3/` is the current, real target.
|
|
`hardware/v2/` is the archived ECP5 baseline (frozen, DSP-count-limited,
|
|
superseded — do not build on it, some of its RTL is still *reused*
|
|
unmodified by v3, e.g. `layer_prefetch_ctrl.v`/`layer_weight_buffer.v`).
|
|
`hardware/v1/` is older still, reference only.
|
|
|
|
## Read first
|
|
|
|
- `docs/PHYSICAL_REALIZATION.md` — every real pin assignment, part number,
|
|
timing number, and memory-layout convention needed for the physical board
|
|
and for host (ESP32) firmware. Keep it in sync with reality — if a pin
|
|
assignment or timing number changes, update this file in the same commit.
|
|
- `docs/ARCHITECTURE_ANALYSIS.md` — real, measured bottleneck analysis
|
|
(DDR3 bandwidth is the real ceiling, not DSP count — see it before
|
|
proposing to scale core count) and ranked recommended interventions.
|
|
Update it whenever a recommendation from it gets built or a new real
|
|
bottleneck is found.
|
|
- `hardware/v2/logs/experiments.log` — the real project history, one
|
|
`EXP-NNNN` entry per real experiment/change (context/method/result/
|
|
decision/next_action). Read the tail before starting new work; append a
|
|
new entry for anything non-trivial you do. This log — not memory, not
|
|
chat history — is the authoritative record of what's been tried and why.
|
|
|
|
## Toolchains (real paths, already working — don't re-diagnose from scratch)
|
|
|
|
- **Vivado 2026.1**: `source /home/michele/tools_cache/Xilinx/2026.1/Vivado/settings64.sh`,
|
|
then `export LD_LIBRARY_PATH="/home/michele/tools_cache/Xilinx/2026.1/Vivado/lib/lnx64.o/Ubuntu/24:$LD_LIBRARY_PATH"`
|
|
(this machine's Ubuntu is too new for Vivado's own OS detection; the
|
|
LD_LIBRARY_PATH points at Vivado's own bundled compat libs — not a real
|
|
distro package, must be set every session).
|
|
- **OSS CAD Suite** (iverilog/vvp for fast plain-Verilog sims, no Xilinx
|
|
primitives): `source /home/michele/tools_cache/oss-cad-suite/environment`.
|
|
- Real Vivado project: `Vivado/NeuralProcessor/NeuralProcessor.xpr` — the
|
|
MIG DDR3 IP lives there, real, already generated for the exact part.
|
|
|
|
## Hard-won lessons (do not re-derive these the slow way)
|
|
|
|
- **Vivado's imported source copies go stale silently — RTL *and*
|
|
constraint (.xdc) files alike.** If a project file under
|
|
`NeuralProcessor.srcs/sources_1/imports/...` (RTL) or
|
|
`NeuralProcessor.srcs/constrs_1/imports/...` (XDC) was ever edited on
|
|
disk *after* being added to the project, diff it against the live
|
|
`hardware/v3/...` source before trusting any P&R result — `add_files`/
|
|
`update_compile_order` do NOT auto-refresh it, and a stale copy produces
|
|
no error, just silently wrong (old) synthesis/implementation results
|
|
(EXP-0078 for RTL; EXP-0084 for the XDC — the stale constraints file was
|
|
old enough to still have PRE-EXP-0077 pin assignments, predating the
|
|
flash bridge entirely, and its own real bug fixes silently didn't take
|
|
effect across multiple P&R re-runs until the staleness itself was
|
|
diagnosed and fixed). Prefer adding new files so they stay a direct
|
|
reference (check `IS_GLOBAL_INCLUDE`/the file's own path isn't under
|
|
`imports/`) rather than get copied — for BOTH the `sources_1` and
|
|
`constrs_1` filesets. **A real MIG IP regeneration (re-running the
|
|
wizard, even just to change one field like Clock Period) can re-trigger
|
|
a WHOLESALE project source rescan that silently re-imports the ENTIRE
|
|
RTL tree plus constraints back to stale copies in one shot — not just
|
|
the file(s) the regeneration itself touched** (EXP-0086: a second
|
|
wizard run, needed only to revert Clock Period, silently reverted 9
|
|
already-fixed RTL files plus the top XDC all at once). After ANY IP
|
|
regeneration, check every fileset (`get_files -of_objects
|
|
[get_filesets sources_1]` and `constrs_1`) for `*/imports/*` paths
|
|
before trusting the next P&R — don't assume only the previously-fixed
|
|
files are still direct references.
|
|
- **Testbench stimulus must use nonblocking assignment (`<=`), not blocking
|
|
(`=`), when driving a DUT's inputs from a separate `always`/`initial`
|
|
block.** Blocking assignment races the DUT's own `posedge`-triggered
|
|
always block under Icarus and can silently corrupt data OR miss a one-shot
|
|
pulse entirely (causing a real hang) — hit and fixed repeatedly (EXP-0073,
|
|
0075, 0077) before this became standing practice. If a new Icarus
|
|
testbench shows shuffled/duplicated fields or an inexplicable hang,
|
|
suspect this class of bug before assuming the RTL is wrong.
|
|
**Same race family, a DIFFERENT real trigger (EXP-0090)**: driving
|
|
stimulus on `@(posedge clk)` — even with the SAME `=`/handshake shape
|
|
already proven safe elsewhere in this project (e.g. `tb_packed_slot.v`'s
|
|
own `job_start` pulse) — still races the DUT's own posedge-triggered
|
|
sampling when a task issuing that pulse is called BACK-TO-BACK with ZERO
|
|
real simulated gap (no natural `while(!done)`-style polling delay between
|
|
calls, e.g. a tight submission loop). Confirmed via real signal tracing:
|
|
every logical push registered as TWO real, identical DUT-side writes.
|
|
Every prior working example of this pulse pattern happened to always have
|
|
a real gap between calls, so the race was never exercised until a tight
|
|
back-to-back loop (`neural_director_grouped.v`'s own test) hit it. Fix:
|
|
drive stimulus changes on `@(negedge clk)` instead — the DUT still
|
|
samples on `posedge`, so a negedge-driven change can never race it,
|
|
regardless of how tightly consecutive pulses are issued.
|
|
- **Never use a runtime-indexed part-select** (`data[idx*W +: W]` where `idx`
|
|
is a signal, not a constant) on a wide bus in anything synthesizable — a
|
|
known real Fmax killer (`weight_tile_gather.v`'s own header, EXP-0061).
|
|
Use fixed shift-concat, or (as `act_tile_fetch.v` does, EXP-0079) design
|
|
the memory layout so only a fixed slice is ever needed. The current real
|
|
P&R timing margin is thin (WNS +0.013ns, EXP-0078) — there is no slack to
|
|
absorb a new critical path.
|
|
- **A one-shot-pulse requester on a shared/arbitrated bus must see its own
|
|
grant the SAME cycle its own `active` signal first asserts** — a
|
|
registered/one-cycle-late grant silently loses the request forever
|
|
(EXP-0066's own real bug, now a standing design rule for every arbiter/
|
|
requester pair in this project).
|
|
- **`xvlog`/`iverilog` need `-sv`/`-g2012`** respectively to accept
|
|
SystemVerilog-only syntax (e.g. `'0`) even in a plain `.v` file — prefer
|
|
just not using SV-only syntax in synthesizable RTL (Vivado's `synth_design`
|
|
has no such escape hatch at all).
|
|
- **Verify real component availability (LCSC) before committing to a part**
|
|
— the user has asked for this explicitly more than once. Don't guess
|
|
availability or specs from training data; search when it matters.
|
|
- **Real, measured numbers only — never estimate/guess a timing or
|
|
performance figure and present it as fact.** Out-of-context synthesis is
|
|
not a real signoff; only a real in-context `place_design`/`route_design`
|
|
run on the actual top-level module counts. If a number is a projection
|
|
(not measured), say so explicitly and show the real numbers it's built
|
|
from.
|
|
|
|
## Working discipline
|
|
|
|
- Fork before promote: don't edit an already-verified, in-use RTL file in
|
|
place for a new experiment — copy/fork it, verify the fork, then decide
|
|
whether to promote it. (Established V2-era convention, still followed in
|
|
V3.)
|
|
- One variable at a time: verify a new module in isolation before wiring it
|
|
into a larger system; verify the larger system before trusting a P&R
|
|
number built on top of it.
|
|
- Root-cause every anomaly via hierarchical signal tracing — never guess or
|
|
paper over an unexplained result. Several real bugs in this project were
|
|
found exactly this way, not by inspection.
|
|
- After ANY RTL change to logic that's part of the real synthesis target
|
|
(`hardware/v3/rtl/n2_system_ddr3_top.v` and its dependents), re-run a real
|
|
P&R before claiming it's still timing-clean — the margin is thin enough
|
|
that this is not optional caution, it's load-bearing.
|
|
- Commit messages end with the attribution lines already configured for this
|
|
session (Co-Authored-By + Claude-Session) — keep using them.
|