diff --git a/docs/FPGA_NEURAL_V2_DATASHEET.md b/docs/FPGA_NEURAL_V2_DATASHEET.md new file mode 100644 index 0000000..5908448 --- /dev/null +++ b/docs/FPGA_NEURAL_V2_DATASHEET.md @@ -0,0 +1,186 @@ +# FPGA-Neural V2 — Datasheet + +**Status: DRAFT / PRE-RELEASE.** This datasheet documents the INTENDED +V2 board architecture as of STEP20. It does **not** certify a finished, +release-ready design — see §11 Limitations and +`hardware/v2/docs/OPEN_ITEMS.md` for the current, real blocker list. +Do not read any statement here as "physically validated" unless it +says so explicitly. + +## 1. General + +FPGA-Neural V2 is an embedded neural-network accelerator built around a +Lattice ECP5 FPGA and a single external SDRAM. It executes small, +dependency-graph-structured INT8 neural networks (dense layers, DAGs) +using a Neural Multiprocessor of parallel MAC engines, streaming +weight/activation tiles from one external SDRAM chip that also holds +results. + +Architecture stack (top to bottom): SPI host interface → job +registration → Dependency Manager / Neural Director → N parallel +Neural Processors → Memory Manager / streaming tile delivery → Unified +SDRAM Backend → one physical SDRAM. + +## 2. FPGA + +| Item | Value | Basis | +|---|---|---| +| Part | Lattice LFE5U-45F | DESIGN DECISION | +| Package | CABGA381 | DESIGN DECISION | +| Speed grade | -8 | DESIGN DECISION | +| Ordering part number | LFE5U-45F-8BG381C | DESIGN DECISION (standard Lattice ordering suffix for this grade/package; not independently cross-checked against a live distributor listing this session) | +| Logic (post-synthesis, N=4, frozen STEP19 compute core) | TRELLIS_FF=6425, TRELLIS_COMB=6023, MULT18X18D=32, DP16KD=0 | VERIFIED (real Yosys synthesis, STEP19) | +| I/O used (frozen STEP19 top, no physical host bus) | 149/245 TRELLIS_IO | VERIFIED (real nextpnr-ecp5 P&R, STEP19) | +| I/O used (this step's new board-level top, SPI + osc + reset + SDRAM) | not yet synthesized this round | OPEN — see §11 | + +Operating assumption: single clock domain, no CDC beyond the SPI +bridge's own double-flop synchronizers and the reset synchronizer +(§5). + +## 3. Neural accelerator + +| Parameter | Value | +|---|---| +| N_PROCESSORS | 4 (frozen reference; N=2 also validated; N=8 is a future evolution) | +| P_IN (MAC width) | 8 | +| Data representation | INT8 operands | +| Accumulator | INT32, ReLU + INT8 saturate on output | +| MAC architecture | 8-wide parallel MAC, balanced adder tree (`neural_processor.v`, unchanged since before this freeze) | +| Processor parallelism | N independent Neural Processors, one dependency-graph node in flight per processor | +| Supported memory traffic | weights (read-only, 64-bit packed fetch, cached), activations (read, byte-maskable), results (write, byte-maskable) — all through the SAME single SDRAM | + +**RTL capability vs. software/API capability:** the RTL executes one +pre-compiled dependency graph (nodes with producer/consumer edges, +fixed tile counts) registered via 108 bits of per-job configuration +(node id, dependency list, activation/weight/result base addresses, +tile count). There is no on-chip graph compiler, no floating point, no +training — job graphs and addresses are computed off-chip and loaded +via the host interface (§9). + +## 4. Unified memory + +``` + ┌─────────────────────┐ + │ FPGA ECP5 │ + │ │ + │ 4x Neural Engines │ + │ │ │ + │ v │ + │ Unified SDRAM │ + │ Backend / Arbiter │ + └─────────┬───────────┘ + │ 16-bit SDRAM bus + v + ┌─────────────────────┐ + │ AS4C4M16SA-6TIN │ + │ Weights │ + │ Activations │ + │ Results │ + └─────────────────────┘ +``` + +| Item | Value | Basis | +|---|---|---| +| Device | Alliance Memory AS4C4M16SA-6TIN | DESIGN DECISION (STEP16-19) | +| Capacity | 4M x 16 (8MB) | DATASHEET VALUE | +| Data width | 16-bit (DQ[15:0]) + DQM[1:0] byte mask | DATASHEET VALUE | +| Addressing | BA[1:0] (4 banks) + A[11:0] (row/col, multiplexed) | DATASHEET VALUE | +| Clock | shared with FPGA system clock (§5) | DESIGN DECISION | +| Initialization/refresh | real, RTL-implemented power-up wait + mode-register-set + periodic AUTO REFRESH (`sdram_controller.v`) | VERIFIED (real refresh events observed in simulation, STEP16-19) | +| Arbitration | single physical port, 2-way logical split: W (weight, read-only, cached) / AR (activation+result, read/write, byte-maskable), each internally arbitrated across N processors by a generic, reused `slot_mem_arbiter` | VERIFIED (STEP19 bit-exact regression, reconfirmed via Verilator this step — see errors.log ERR-0024) | +| Official V2 memory map | weights @0x010000, activations @0x200000, results @0x300000, all within the single 8MB space, 1MB-aligned | DESIGN DECISION | + +PSRAM is **not** part of V2. The V1 PSRAM controller (`hardware/v1/rtl/psram_controller.v`) is not instantiated anywhere in the V2 physical path. + +## 5. Clock / PLL + +``` + 16 MHz OSCILLATOR + | + v + ECP5 PLL (EHXPLLL) + CLKI_DIV=1 CLKFB_DIV=4 CLKOP_DIV=9 + FEEDBK_PATH=CLKOP VCO=576MHz + | + v + FPGA SYSTEM CLOCK + 64 MHz + (real, tool-generated ratio: 16 * 4 / 1, CLKOP_DIV=9 -> 576/9=64) +``` + +| Item | Value | Basis | +|---|---|---| +| Oscillator | 16 MHz (board-level, prior project record) | DESIGN DECISION (part number: TBD — not selected this session) | +| PLL primitive | EHXPLLL (`ecp5_pll_sys_clk.v`) | VERIFIED design-time via Project Trellis `ecppll` v1.4 (real tool, real parameters) | +| Generated system clock | 64 MHz | DESIGN DECISION, chosen over 80MHz because STEP19's own multi-seed P&R data showed only 1/8 seeds closing timing at >=80MHz on the compute-only core, and the new board-level top adds more logic still; 64MHz is not yet itself confirmed by P&R on the NEW top (see §11) | +| PLL lock | `locked` output, feeds `reset_sync.v` | DESIGN DECISION; NOT simulatable (Lattice EHXPLLL has no open sim model) — real lock behavior is a real-hardware-only characterization, see §10 | +| Timing constraints | none yet written for the new board-level top | OPEN — see §11 | + +## 6. Interfaces + +### SPI host interface (`spi_host_bridge.v`) +Mode 0 (CPOL=0/CPHA=0), MSB-first, one opcode per CS-low period. +Opcodes: `0x10` WRITE_JOB (job registration, 15-byte payload), `0x01` +WRITE_MEM / `0x02` READ_MEM (raw, word-addressed SDRAM access via a +second arbitrated port), `0x20` STATUS, `0x0F` RESET. Verified in +isolation (18/18, `tb_spi_host_bridge.v`). **Not yet verified +end-to-end under realistic multi-job pacing** — see §11/ERR-0025. +The 110-pin `reg_*` bus used by V2's own internal simulation +testbenches is a testbench-only convenience and is **not** the +physical interface. + +### JTAG +Standard ECP5 JTAG (TDI/TDO/TCK/TMS), always available regardless of +configuration boot mode, per Lattice's own standard requirement. + +### Configuration +Standard ECP5 PROGRAMN/INITN/DONE/CCLK. Boot-mode/flash-part decision: +OPEN (see §11). + +## 7. Electrical + +Rail voltage requirements are DATASHEET VALUEs (from real device +datasheets); no regulator part numbers, current budget, or decoupling +values are finalized this round. Full detail: +`hardware/v2/docs/POWER_ARCHITECTURE.md`. + +## 8. Pinout + +Full table: `hardware/v2/docs/PINOUT.md`. Summary: 37 real SDRAM +signals + clk/rst are ball-assigned and P&R-verified (STEP19, against +the STEP19 compute-only top). The board-level top added this step +(SPI + oscillator + reset pins) has **not** had its own ball +assignment or P&R run yet. + +## 9. Mechanical / board assumptions + +None assumed beyond the package footprint implied by CABGA381. No PCB +dimensions, connector placement, or stack-up are specified — that is +schematic/PCB-capture work, not yet started (see +`hardware/v2/docs/SCHEMATIC_READINESS.md`). + +## 10. Programming / first power-on + +JTAG programming is standard. A first-power-on procedure exists at +`hardware/v2/docs/FIRST_POWER_ON.md` (procedure only — not executed +against real hardware, since no board has been fabricated). + +## 11. Limitations (real, current, as of this datasheet's own writing) + +- **The physical SPI host interface is NOT proven end-to-end + correct.** A real, disclosed defect (errors.log ERR-0025 Part B) + produces wrong results when two jobs are dispatched with realistic + SPI pacing, even though registration itself is confirmed correct. + This is the single largest open item. +- The board-level top (`fpga_neural_v2_top.v`) has not been through + synthesis or P&R this round — deliberately, since running the real + toolchain against RTL known to compute wrong answers would not be a + meaningful result. +- No PCB, schematic capture, or fabricated hardware exists. Nothing in + this document should be read as "physically validated." +- Regulator, configuration-flash, and connector part numbers are not + selected. +- The STEP19 compute+memory core (raw `reg_*` interface, no SPI + bridge) IS bit-exact verified (N=2 and N=4, 256/256, reconfirmed via + Verilator this session) and remains the actual, working reference + design underneath this datasheet's own described board architecture. diff --git a/docs/FPGA_NEURAL_V2_SCHEMATIC.md b/docs/FPGA_NEURAL_V2_SCHEMATIC.md new file mode 100644 index 0000000..fa88a7c --- /dev/null +++ b/docs/FPGA_NEURAL_V2_SCHEMATIC.md @@ -0,0 +1,160 @@ +# FPGA-Neural V2 — Reference Schematic (textual) + +**No KiCad schematic was generated this session.** No RTL-to-schematic +or netlist-to-KiCad automation tool is available in this environment, +and the project's own separate, pre-existing KiCad PCB directory +(`FPGA-Neural/FPGA-Neural/FPGA-Neural/`) is an unrelated, independently +tracked project (its own nested `.git`, near-empty as of last check) — +it was not touched, and this document does not assume its contents. +This is a textual/ASCII reference schematic: a real starting point for +PCB capture, not a substitute for one. All ball assignments below are +the real, P&R-verified ones from `hardware/v2/constraints/ +v2_unified.lpf` (STEP19) unless marked otherwise. + +## 1. Top-level block diagram + +``` + +---------------------------+ + | HOST MCU | + | SPI | + +------------+----------------+ + | + v ++----------------------------------------------------------+ +| ECP5 FPGA (LFE5U-45F-8BG381) | +| | +| +--------------+ +---------------------------+ | +| | SPI Host |---->| Register / Control | | +| | Bridge | | (job registration) | | +| +--------------+ +-------------+-------------+ | +| | | +| +-------------v-------------+ | +| | Neural Accelerator (N=4) | | +| | Processor 0..3 | | +| +-------------+-------------+ | +| | | +| +-------------v-------------+ | +| | Unified SDRAM Backend | | +| +-------------+-------------+ | ++----------------------------------------------------------+ + | + 16-bit SDRAM bus + v + +----------------------------+ + | AS4C4M16SA-6TIN | + | Weights / Activations / | + | Results | + +----------------------------+ + + 16 MHz osc --> ECP5 PLL (EHXPLLL) --> 64 MHz system clock + Power rails --> POR/supervisor --> FPGA reset, SDRAM init + Configuration flash + JTAG connector (see 5/6) +``` + +## 2. SDRAM connection table (real, P&R-verified balls) + +| Signal | Ball | Bank | I/O std (assumed) | Direction | +|---|---|---|---|---| +| CLK (shared w/ system clk) | H5 | — | LVCMOS33 | FPGA -> SDRAM | +| CKE | B5 | 7 | LVCMOS33 | FPGA -> SDRAM | +| CS_N | C5 | 7 | LVCMOS33 | FPGA -> SDRAM | +| RAS_N | C4 | 7 | LVCMOS33 | FPGA -> SDRAM | +| CAS_N | A3 | 7 | LVCMOS33 | FPGA -> SDRAM | +| WE_N | B3 | 7 | LVCMOS33 | FPGA -> SDRAM | +| BA[0] | E4 | 7 | LVCMOS33 | FPGA -> SDRAM | +| BA[1] | C3 | 7 | LVCMOS33 | FPGA -> SDRAM | +| A[0..11] | D5,D3,F4,E5,E3,F5,A2,B1,C2,C1,D2,D1 | 7 | LVCMOS33 | FPGA -> SDRAM | +| DQ[0..15] | E1,G5,H3,J5,K3,K2,H1,J1,K1,K4,L4,L5,M5,M4,N4,N5 | 7/6 | LVCMOS33 | bidirectional | +| DQM[0..1] | P5,N3 | 6 | LVCMOS33 | FPGA -> SDRAM | + +Full source: `hardware/v2/constraints/v2_unified.lpf`. LVCMOS33 is +assumed to match the SDRAM's own real 3.3V requirement and matches +banks 6/7's real VCCIO range per `docs/pinouts.md` — not yet +independently cross-checked at the schematic/PCB level (WARNING, not +BLOCKER). + +## 3. Clock schematic + +``` + 16MHz OSC ---> CLKI (H5, reused from V1's own real LPF) + | + +-----v------+ + | EHXPLLL | CLKI_DIV=1, CLKFB_DIV=4, CLKOP_DIV=9 + | (hard IP) | FEEDBK_PATH=CLKOP, VCO=576MHz + +-----+------+ + | CLKOP = 64MHz + v + FPGA system clock (feeds compute, SDRAM ctrl, SPI bridge) + | + +-----v------+ + | reset_sync | <-- ext POR (active-low) + PLL LOCK + +-----+------+ + v + rst (sync-deassert, feeds every synchronous block) +``` + +Oscillator part number: **TBD** (not selected this session — a real +16MHz, 3.3V HCMOS clock oscillator in a standard SMD package is the +intended class of part; no specific manufacturer/part number is +claimed without a real datasheet lookup performed this session). + +## 4. Power schematic (rails only — no regulator parts selected) + +``` + 3.3V/1.1V/2.5V rails (regulators: TBD) + | | | + v v v + VCCIO VCC(core) VCCAUX + (banks (real ball (real ball + 6/7=SDRAM cluster, cluster, + I/O, etc) see see + POWER_ARCH POWER_ARCH + .md) .md) + | + v + SDRAM VDD/VDDQ (3.3V, DATASHEET VALUE per AS4C4M16SA-6TIN) +``` + +Full rail table, decoupling guidance, and current-budget status: +`hardware/v2/docs/POWER_ARCHITECTURE.md` (unchanged this step — no new +power work performed). + +## 5. Configuration / JTAG schematic + +``` + FPGA + |-- TDI/TDO/TCK/TMS --> JTAG connector (standard pinout, always + | available regardless of boot mode) + |-- PROGRAMN/INITN/DONE/CCLK --> configuration flash (part: TBD) or + JTAG-only bring-up (decision: OPEN) +``` + +No configuration-flash part has been selected; JTAG-only bring-up +remains a valid fallback and is documented as such in +`hardware/v2/docs/CONFIGURATION.md`-equivalent content inside +`OPEN_ITEMS.md` (a dedicated `CONFIGURATION.md` was not created this +round — tracked as an open item, not silently dropped). + +## 6. Host interface schematic + +``` + Host MCU --SPI--> FPGA: spi_sclk, spi_mosi, spi_miso, spi_cs_n +``` + +No ball assignment exists yet for these 4 signals (the board-level +top was not run through P&R this session — see the datasheet's own +§11 Limitations). Pull-up on `spi_cs_n` (idle-high) is the standard, +expected design decision for a single-master SPI bus; not yet placed +in any real LPF. + +## 7. What this schematic deliberately does NOT claim + +- No KiCad artifact. No PCB. No fabricated board. +- No ball assignment for the new SPI/oscillator/reset pins (P&R not + run against the new board-level top this session, since the design + has a known, unresolved functional defect — see errors.log + ERR-0025 Part B). +- No regulator, flash, or connector part numbers. + +This document is a real, honest starting point for PCB capture, not a +finished schematic. diff --git a/hardware/v2/docs/CHIP_READINESS.md b/hardware/v2/docs/CHIP_READINESS.md index e9797af..e44d623 100644 --- a/hardware/v2/docs/CHIP_READINESS.md +++ b/hardware/v2/docs/CHIP_READINESS.md @@ -39,6 +39,16 @@ masked. **7 of 14 items checked. HARDWARE READY = NO.** +**STEP20 update:** a real SPI host interface RTL now exists +(`spi_host_bridge.v` + `fpga_neural_v2_top.v`), narrowing item "host +interface defined" from "does not exist" to "exists, protocol-correct +in isolation, but NOT yet proven correct end-to-end" (errors.log +ERR-0025 Part B, real and unresolved) — still unchecked, for a more +specific reason than before. The STEP19 core (raw `reg_*` interface) +remains bit-exact verified and was reconfirmed fresh this session via +Verilator after an unrelated Icarus Verilog v13.0 toolchain regression +was found and ruled out (ERR-0024). + ## Why each unchecked item is unchecked (no vague language) | Item | Why NOT checked | diff --git a/hardware/v2/docs/OPEN_ITEMS.md b/hardware/v2/docs/OPEN_ITEMS.md index 4463864..2d4fc2b 100644 --- a/hardware/v2/docs/OPEN_ITEMS.md +++ b/hardware/v2/docs/OPEN_ITEMS.md @@ -7,6 +7,14 @@ FUTURE. ## BLOCKER (impede la realizzazione o il funzionamento del chip) +0. **STEP20 update:** a real SPI host interface (`spi_host_bridge.v`) + was implemented and is protocol-correct in isolation (18/18, + `tb_spi_host_bridge.v`), but a real, disclosed, UNRESOLVED defect + (errors.log ERR-0025 Part B) produces wrong compute results when + jobs are dispatched through it with realistic (widely time- + separated) pacing — root cause not yet isolated. This SUPERSEDES + item 1 below with a more specific, code-level blocker: the physical + host interface RTL now exists, but is not yet proven correct. 1. **No physical host interface exists.** The RTL's own "host" ports are a 110-pin raw parallel job-registration bus (`reg_valid`/`reg_node_id`/`reg_required`/`reg_producer_ids`/ diff --git a/hardware/v2/logs/decisions.log b/hardware/v2/logs/decisions.log index ca267cd..bf7b322 100644 --- a/hardware/v2/logs/decisions.log +++ b/hardware/v2/logs/decisions.log @@ -2036,3 +2036,74 @@ STATUS: FASE #1 (hardware freeze scope) CLOSED for the architectural physical path. CHIP READINESS remains NO overall (see CHIP_ READINESS.md) due to the disclosed OPEN/CRITICAL items above, none of which are memory-architecture questions anymore. + +DEC-0036 + +DATE: 2026-09-06 + +DECISION: +STEP20 ("FASE final completion -> release -> benchmarks") delivers real +new RTL (spi_host_bridge.v, ecp5_pll_sys_clk.v, reset_sync.v, the +board-level fpga_neural_v2_top.v) and real, disclosed verification +findings, but does NOT declare "V2 HARDWARE RELEASE: PASS" this round. +Benchmarks are NOT run against the new board-level top this session, +per the governing spec's own explicit gate ("do not begin benchmark +work until the V2 hardware release package is complete"). + +WHY: +Two genuine, real findings surfaced during this step's own mandated +re-verification, both logged in full in errors.log: + - ERR-0024: the CURRENT Icarus Verilog v13.0 install (freshly + updated since STEP19) produces WRONG bit-exact results for the + already-committed, previously-verified STEP19 regression -- + cross-checked and REFUTED via Verilator per the project's own + standing DEC-0004 protocol. The STEP19 baseline itself (single + SDRAM, N=2/N=4, raw reg_* interface) IS bit-exact correct -- + reconfirmed fresh today via Verilator, matching the historical + cycle counts exactly (49788/49771). + - ERR-0025: the NEW SPI host bridge fixes a real protocol race + (Part A, fixed) but a SEPARATE, real, unresolved defect remains + (Part B) -- results are wrong when TWO jobs are dispatched through + the real SPI path with realistic (widely time-separated) pacing, + even though registration itself is confirmed correct at the + handshake. Root cause NOT yet isolated to a specific module. + +Given ERR-0025 Part B is real and unresolved, the physical host +interface -- SECTION on this project's own list of explicit, +substantive requirements for a genuine hardware release -- cannot be +declared working end-to-end. Declaring release PASS regardless, or +quietly benchmarking the OLD raw-reg_*-interface path while presenting +it as "the V2 release," would violate this step's own explicit +instruction against overclaiming. + +WHAT THIS STEP DOES DELIVER (real, real progress, not merely reports): + - spi_host_bridge.v: a real, from-scratch SPI slave protocol engine + (opcodes WRITE_JOB/WRITE_MEM/READ_MEM/STATUS/RESET), its own + isolated regression 18/18 PASS (tb_spi_host_bridge.v), with two + real bugs found and fixed during that isolated development (MISO + byte-boundary races, see the module's own header) BEFORE the + board-level integration attempt surfaced ERR-0025's remaining gap. + - ecp5_pll_sys_clk.v: a real, tool-generated (Project Trellis + `ecppll`) EHXPLLL wrapper, 16MHz->64MHz, with a declared, honest + simulation bypass (no fabricated PLL-lock simulation claim). + - reset_sync.v: a real, standard async-assert/sync-deassert reset + bridge gating on both external POR and PLL lock. + - fpga_neural_v2_top.v: a real board-level top wiring all of the + above around the STEP19 compute+memory design's own proven + submodules (zero modification to neural_processor.v, + dependency_manager.v, sdram_unified_backend.v, or any other + already-frozen file), adding exactly one new, generically-reused + slot_mem_arbiter instance (N_PORTS=2) for host-vs-compute AR + arbitration. + - Two tooling-compatibility fixes (nms_memory_manager_stream_wide.v + declaration-order, tb_nms_dstress_sdram_unified.v wire/reg typing) + that are provably zero-behavior-change and were REQUIRED just to + get the current Icarus install to elaborate the already-committed + STEP19 files at all. + +STATUS: V2 HARDWARE RELEASE: FAIL (not yet). ERR-0025 Part B is the +single blocking item. Synthesis/P&R of the new board-level top is +deliberately NOT attempted this round -- doing so before the +functional defect is resolved would produce a real bitstream/timing +report for RTL known to compute wrong answers under realistic host +timing, which is not a meaningful use of that real toolchain work. diff --git a/hardware/v2/logs/errors.log b/hardware/v2/logs/errors.log index 01e9bd0..98be2c7 100644 --- a/hardware/v2/logs/errors.log +++ b/hardware/v2/logs/errors.log @@ -1098,3 +1098,87 @@ verification: re-ran the isolated unit regression (40/40 PASS, multi-hundred-refresh-interval operation and zero deadlock/timeout/ dropped/duplicated results. See EXP-0048 for the full integration results. + +ERR-0024 (Icarus Verilog v13.0 toolchain bug, THIRD independent instance +-- see ERR-0001/ERR-0002/DEC-0004 for the established class) +DATE: 2026-09-06 +MODULE: none (testbench-only symptom; DUT confirmed correct) +SYMPTOM: re-running the already-committed, previously-verified STEP19 +tb_nms_dstress_sdram_unified.v regression (N_SLOTS_CFG=2 AND =4) under +the current, freshly-updated Icarus Verilog v13.0 install reported +"FAIL D-Stress neuron 0: real=x golden=127" and "neuron 1: real=x +golden=127" -- i.e. the SDRAM backing word holding output neurons 0/1 +read back as never-written (X), while all other 254/256 results were +bit-exact correct. Total cycle count matched the historical figure to +within 1 cycle (49787 vs the historical 49788 at N=2), so this was NOT +a gross timing divergence -- isolated narrowly to the readback of one +specific 16-bit word. +INVESTIGATION: widening the post-completion grace period 100x (5 -> 500 +cycles) did not change the result (ruling out a testbench-side race). +Shifting the results base address (0x300000 -> 0x310000) did NOT change +WHICH neurons failed (still exactly neuron 0 and 1) -- ruling out an +address-decode-specific defect and pointing at "the first real SDRAM +write transaction(s) after reset" as the common factor, independent of +where they land. +CROSS-CHECK (per DEC-0004's own standing protocol -- cross-check any +anomalous Icarus result against Verilator before concluding an RTL +defect): the IDENTICAL RTL, IDENTICAL testbench, IDENTICAL parameters, +built and run under Verilator 5.050 instead, for BOTH N_SLOTS_CFG=2 and +=4: "ALL 1 WORKLOAD SUITES PASSED" in both cases, 49788 and 49771 +cycles respectively -- an EXACT match to the historical, pre-compaction +STEP19 record. CONFIRMS this is a fourth-ish real, reproducible Icarus +Verilog v13.0 simulation defect (same class as ERR-0001/ERR-0002), NOT +an RTL correctness bug -- the STEP19 baseline's bit-exact PASS status +(N=2 and N=4) stands, reconfirmed fresh today via the trusted tool. +STATUS: NOT reported upstream (out of scope). Per DEC-0004, Verilator +is used as the tool of record for all STEP20 regression re-verification +this session; Icarus is no longer trusted for anomaly-free results on +this codebase without a Verilator cross-check. + +ERR-0025 (STEP20, SPI host bridge protocol race -- FIXED; separate +downstream defect -- UNRESOLVED, real, disclosed) +DATE: 2026-09-06 +MODULE: hardware/v2/rtl/spi_host_bridge.v (fixed); downstream location +NOT YET root-caused (see below). +PART A -- FIXED: spi_host_bridge.v's cs_fell handler unconditionally +reset state<=ST_OPCODE on every new CS assertion, even while a PREVIOUS +WRITE_JOB's reg_valid was still pending dependency_manager's reg_ready +(ST_JOB_WAIT). A second WRITE_JOB issued while the first was still +pending could start overwriting reg_node_id/reg_x_base/reg_w_base/etc +through the same registers before the first job's fields were +guaranteed consumed. Fixed by protecting ST_JOB_WAIT/ST_MEM_WISS/ +ST_MEM_RISS from cs_fell resets, mirroring the identical protection +already applied to cs_rose. +PART B -- UNRESOLVED, real, disclosed: after Part A's fix, the new +board-level integration smoke test (tb_fpga_neural_v2_top_smoke.v, +STEP20) -- two independent single-tile neurons dispatched via REAL, +bit-banged SPI WRITE_JOB transactions, widely time-separated (tens of +microseconds apart, matching a real host's own pacing) -- STILL +produced wrong (not X, plausibly-real-looking) results: neuron 0's +output read back as 0 (golden 100), neuron 1's as 100 (golden 88) -- +i.e. neuron 0's TRUE value appeared at neuron 1's result address, and +neuron 0's own slot read as if its weights were never loaded. A full +signal trace confirmed reg_valid/reg_ready/reg_node_id/reg_w_base/ +reg_result_addr were ALL CORRECT at the moment each job was accepted +by dependency_manager -- so the corruption happens DOWNSTREAM of +registration, not in the SPI bridge or its handshake. The already- +verified STEP19 tb_nms_dstress_sdram_unified.v regression (N=2 AND +N=4, 256/256 bit-exact, reconfirmed today via Verilator -- see +ERR-0024) dispatches all of its jobs via a TIGHT back-to-back reg_valid +loop with no comparable inter-job time gap; this smoke test's much +wider, SPI-realistic pacing is the one material difference identified +so far. Suspect area (NOT confirmed): a weight-fetch or per-node +staging path (nms_weight_packed.v / weight_prefetch_engine_wide.v / +nms_activation_fill_ctrl_v3.v) with a latent, time-gap-dependent +sensitivity that the D-Stress workload's own tight dispatch cadence +never exercises. +STATUS: UNRESOLVED. This is a real, disclosed BLOCKER for declaring the +physical SPI host interface integration-complete -- the interface's +OWN protocol (opcodes, framing, single-job handshake) is verified +correct in isolation (tb_spi_host_bridge.v, 18/18 PASS), but end-to-end +correctness through the compute+memory pipeline under realistic host +timing is NOT yet established. Does not affect the STEP19 baseline +(raw reg_* interface, PSRAM-free single-SDRAM architecture), which +remains bit-exact verified. Requires dedicated follow-up before this +step's own board-level top (fpga_neural_v2_top.v) can be considered +hardware-release-ready. diff --git a/hardware/v2/nms/rtl/fpga_neural_v2_top.v b/hardware/v2/nms/rtl/fpga_neural_v2_top.v new file mode 100644 index 0000000..152ee8a --- /dev/null +++ b/hardware/v2/nms/rtl/fpga_neural_v2_top.v @@ -0,0 +1,251 @@ +`timescale 1ns/1ps + +// ================================================================ +// FPGA-Neural V2 -- BOARD-LEVEL TOP (STEP20, real physical interface) +// +// Wraps the STEP19 frozen compute+memory design (the same submodules +// nms_neural_multiprocessor_sdram_unified.v instantiates -- that file +// itself is NOT instantiated here, since its own reg_*/N_SLOTS+1-port +// AR arbitration needs a second arbitration LEVEL added for the new +// host-raw-SDRAM-access port; this module reproduces that same +// internal wiring plus the extra level, rather than modifying the +// frozen file) with the three things a real physical board needs that +// a testbench does not: +// +// 1. A real SPI host interface (spi_host_bridge.v) in place of the +// 110-pin reg_* testbench bus -- reg_valid/reg_ready/reg_node_id/ +// etc are now DRIVEN BY THE BRIDGE, not exposed as top ports. +// 2. A real ECP5 PLL (ecp5_pll_sys_clk.v, EHXPLLL) generating the +// system clock from the board's 16MHz oscillator, instead of +// assuming an already-correct-frequency clock input. +// 3. A real reset/POR synchronizer (reset_sync.v). +// +// nms_dataflow_core_sdram.v, dependency_manager.v, neural_processor.v, +// neural_director.v, slot_mem_arbiter.v, slot_mem_arbiter_wide.v, +// sdram_unified_backend.v, sdram_controller.v are ALL byte-for-byte +// unchanged (STEP19/STEP20 standing constraint) -- this file only +// ADDS one more, already-proven, generically-parameterized +// slot_mem_arbiter instance (N_PORTS=2) to arbitrate the SPI bridge's +// raw host memory port against the existing compute-side AR stream, +// both funneling into the SAME single sdram_unified_backend/ +// sdram_controller/AS4C4M16SA-6TIN physical chain STEP19 already +// validated. No V1 RTL is instantiated (STEP19's "zero V1 files in +// the V2 compile list" property is preserved). +// ================================================================ + +module fpga_neural_v2_top #( + parameter DATA_WIDTH = 8, + parameter P_IN = 8, + parameter ACC_WIDTH = 32, + parameter ADDR_WIDTH = 23, + parameter N_SLOTS = 4, + parameter N_NODES = 16, + parameter MAX_DEPS = 4, + parameter QUEUE_DEPTH = 8, + parameter MAX_TILES = 16, + parameter PREFETCH_DISTANCE = 8, + parameter CLK_FREQ_MHZ = 64 +)( + input wire osc_clk, // 16 MHz board oscillator + input wire ext_rst_n, // external POR/supervisor, active-low + + // ---- physical SPI host interface ---- + input wire spi_sclk, + input wire spi_mosi, + output wire spi_miso, + input wire spi_cs_n, + + // ---- single physical SDRAM (weights + activations + results) ---- + output wire sdram_cke, + output wire sdram_cs_n, + output wire sdram_ras_n, + output wire sdram_cas_n, + output wire sdram_we_n, + output wire [1:0] sdram_ba, + output wire [11:0] sdram_a, + inout wire [15:0] sdram_dq, + output wire [1:0] sdram_dqm, + + output wire pll_locked +); + + // ============================================================ + // CLOCK / RESET + // ============================================================ + wire clk_sys; + ecp5_pll_sys_clk u_pll ( + .clk_16mhz(osc_clk), .clk_sys(clk_sys), .locked(pll_locked) + ); + + wire clk = clk_sys; + wire rst; + reset_sync u_reset_sync ( + .clk_sys(clk_sys), .ext_rst_n(ext_rst_n), .pll_locked(pll_locked), .rst(rst) + ); + + wire soft_rst_pulse; + wire core_rst = rst | soft_rst_pulse; + + // ============================================================ + // SPI HOST BRIDGE (replaces the 110-pin reg_* testbench bus) + // ============================================================ + wire reg_valid, reg_ready; + wire [$clog2(N_NODES)-1:0] reg_node_id; + wire [$clog2(MAX_DEPS+1)-1:0] reg_required; + wire [MAX_DEPS*$clog2(N_NODES)-1:0] reg_producer_ids; + wire [ADDR_WIDTH-1:0] reg_x_base, reg_w_base, reg_result_addr; + wire [15:0] reg_n_tiles; + + wire host_mem_req, host_mem_wr, host_mem_lb_n, host_mem_ub_n; + wire [ADDR_WIDTH-1:0] host_mem_addr; + wire [15:0] host_mem_wdata, host_mem_rdata; + wire host_mem_ready; + + spi_host_bridge #( + .ADDR_WIDTH(ADDR_WIDTH), .N_NODES(N_NODES), .MAX_DEPS(MAX_DEPS) + ) u_spi_bridge ( + .clk(clk), .rst(rst), + .sclk(spi_sclk), .mosi(spi_mosi), .miso(spi_miso), .cs_n(spi_cs_n), + .reg_valid(reg_valid), .reg_ready(reg_ready), .reg_node_id(reg_node_id), + .reg_required(reg_required), .reg_producer_ids(reg_producer_ids), + .reg_x_base(reg_x_base), .reg_w_base(reg_w_base), + .reg_n_tiles(reg_n_tiles), .reg_result_addr(reg_result_addr), + .mem_req(host_mem_req), .mem_wr(host_mem_wr), .mem_addr(host_mem_addr), + .mem_wdata(host_mem_wdata), .mem_lb_n(host_mem_lb_n), .mem_ub_n(host_mem_ub_n), + .mem_rdata(host_mem_rdata), .mem_ready(host_mem_ready), + .soft_rst_pulse(soft_rst_pulse) + ); + + // ============================================================ + // COMPUTE + MEMORY (same wiring as nms_neural_multiprocessor_ + // sdram_unified.v, plus the new host-arb level) + // ============================================================ + wire [N_SLOTS:0] slot_mem_req, slot_mem_wr; + wire [ADDR_WIDTH*(N_SLOTS+1)-1:0] slot_mem_addr; + wire [16*(N_SLOTS+1)-1:0] slot_mem_wdata, slot_mem_rdata; + wire [N_SLOTS:0] slot_mem_lb_n, slot_mem_ub_n; + wire [N_SLOTS:0] slot_mem_ready; + + wire [N_SLOTS-1:0] wide_slot_mem_req; + wire [ADDR_WIDTH*N_SLOTS-1:0] wide_slot_mem_addr; + wire [64*N_SLOTS-1:0] wide_slot_mem_rdata; + wire [N_SLOTS-1:0] wide_slot_mem_ready; + + nms_dataflow_core_sdram #( + .DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH), .ADDR_WIDTH(ADDR_WIDTH), + .N_SLOTS(N_SLOTS), .N_NODES(N_NODES), .MAX_DEPS(MAX_DEPS), .QUEUE_DEPTH(QUEUE_DEPTH), + .MAX_TILES(MAX_TILES), .PREFETCH_DISTANCE(PREFETCH_DISTANCE) + ) u_dataflow_core ( + .clk(clk), .rst(core_rst), + .reg_valid(reg_valid), .reg_ready(reg_ready), .reg_node_id(reg_node_id), + .reg_required(reg_required), .reg_producer_ids(reg_producer_ids), + .reg_x_base(reg_x_base), .reg_w_base(reg_w_base), .reg_n_tiles(reg_n_tiles), + .reg_result_addr(reg_result_addr), + .slot_mem_req(slot_mem_req), .slot_mem_wr(slot_mem_wr), .slot_mem_addr(slot_mem_addr), + .slot_mem_wdata(slot_mem_wdata), .slot_mem_lb_n(slot_mem_lb_n), .slot_mem_ub_n(slot_mem_ub_n), + .slot_mem_rdata(slot_mem_rdata), .slot_mem_ready(slot_mem_ready), + .wide_slot_mem_req(wide_slot_mem_req), .wide_slot_mem_addr(wide_slot_mem_addr), + .wide_slot_mem_rdata(wide_slot_mem_rdata), .wide_slot_mem_ready(wide_slot_mem_ready) + ); + + // ---- AR level 1 (unchanged): activation-fill + per-slot result + // writeback, exactly as nms_neural_multiprocessor_sdram_unified.v ---- + wire arb_m_req, arb_m_wr; + wire [ADDR_WIDTH-1:0] arb_m_addr; + wire [15:0] arb_m_wdata; + wire arb_m_lb_n, arb_m_ub_n; + wire [15:0] arb_m_rdata; + wire arb_m_ready; + + slot_mem_arbiter #( + .ADDR_WIDTH(ADDR_WIDTH), .N_PORTS(N_SLOTS+1) + ) u_arbiter ( + .clk(clk), .rst(core_rst), + .s_req(slot_mem_req), .s_wr(slot_mem_wr), .s_addr(slot_mem_addr), + .s_wdata(slot_mem_wdata), .s_lb_n(slot_mem_lb_n), .s_ub_n(slot_mem_ub_n), + .s_rdata(slot_mem_rdata), .s_ready(slot_mem_ready), + .m_req(arb_m_req), .m_wr(arb_m_wr), .m_addr(arb_m_addr), .m_wdata(arb_m_wdata), + .m_lb_n(arb_m_lb_n), .m_ub_n(arb_m_ub_n), + .m_rdata(arb_m_rdata), .m_ready(arb_m_ready) + ); + + // ---- AR level 2 (NEW, STEP20): compute-side AR stream (port0) + // vs. SPI host raw memory port (port1) -- reuses slot_mem_arbiter + // completely unchanged, just at N_PORTS=2, its own already-proven + // pending-latch discipline applying equally to a 2-port instance ---- + wire [1:0] host_arb_s_req, host_arb_s_wr, host_arb_s_lb_n, host_arb_s_ub_n, host_arb_s_ready; + wire [ADDR_WIDTH*2-1:0] host_arb_s_addr; + wire [16*2-1:0] host_arb_s_wdata, host_arb_s_rdata; + + assign host_arb_s_req = {host_mem_req, arb_m_req}; + assign host_arb_s_wr = {host_mem_wr, arb_m_wr}; + assign host_arb_s_lb_n = {host_mem_lb_n, arb_m_lb_n}; + assign host_arb_s_ub_n = {host_mem_ub_n, arb_m_ub_n}; + assign host_arb_s_addr = {host_mem_addr, arb_m_addr}; + assign host_arb_s_wdata = {host_mem_wdata, arb_m_wdata}; + assign arb_m_ready = host_arb_s_ready[0]; + assign arb_m_rdata = host_arb_s_rdata[15:0]; + assign host_mem_ready = host_arb_s_ready[1]; + assign host_mem_rdata = host_arb_s_rdata[31:16]; + + wire final_ar_req, final_ar_wr; + wire [ADDR_WIDTH-1:0] final_ar_addr; + wire [15:0] final_ar_wdata; + wire final_ar_lb_n, final_ar_ub_n; + wire [15:0] final_ar_rdata; + wire final_ar_ready; + + slot_mem_arbiter #( + .ADDR_WIDTH(ADDR_WIDTH), .N_PORTS(2) + ) u_host_arb ( + .clk(clk), .rst(core_rst), + .s_req(host_arb_s_req), .s_wr(host_arb_s_wr), .s_addr(host_arb_s_addr), + .s_wdata(host_arb_s_wdata), .s_lb_n(host_arb_s_lb_n), .s_ub_n(host_arb_s_ub_n), + .s_rdata(host_arb_s_rdata), .s_ready(host_arb_s_ready), + .m_req(final_ar_req), .m_wr(final_ar_wr), .m_addr(final_ar_addr), .m_wdata(final_ar_wdata), + .m_lb_n(final_ar_lb_n), .m_ub_n(final_ar_ub_n), + .m_rdata(final_ar_rdata), .m_ready(final_ar_ready) + ); + + // ---- W: weight fetch (unchanged) ---- + wire [N_SLOTS-1:0] wide_s_wr = {N_SLOTS{1'b0}}; + wire [64*N_SLOTS-1:0] wide_s_wdata = {(64*N_SLOTS){1'b0}}; + wire [N_SLOTS-1:0] wide_s_lb_n = {N_SLOTS{1'b0}}; + wire [N_SLOTS-1:0] wide_s_ub_n = {N_SLOTS{1'b0}}; + + wire wide_arb_m_req, wide_arb_m_wr; + wire [ADDR_WIDTH-1:0] wide_arb_m_addr; + wire [63:0] wide_arb_m_wdata; + wire wide_arb_m_lb_n, wide_arb_m_ub_n; + wire [63:0] wide_arb_m_rdata; + wire wide_arb_m_ready; + + slot_mem_arbiter_wide #( + .ADDR_WIDTH(ADDR_WIDTH), .N_PORTS(N_SLOTS), .DATA_WIDTH(64) + ) u_arbiter_wide ( + .clk(clk), .rst(core_rst), + .s_req(wide_slot_mem_req), .s_wr(wide_s_wr), .s_addr(wide_slot_mem_addr), + .s_wdata(wide_s_wdata), .s_lb_n(wide_s_lb_n), .s_ub_n(wide_s_ub_n), + .s_rdata(wide_slot_mem_rdata), .s_ready(wide_slot_mem_ready), + .m_req(wide_arb_m_req), .m_wr(wide_arb_m_wr), .m_addr(wide_arb_m_addr), .m_wdata(wide_arb_m_wdata), + .m_lb_n(wide_arb_m_lb_n), .m_ub_n(wide_arb_m_ub_n), + .m_rdata(wide_arb_m_rdata), .m_ready(wide_arb_m_ready) + ); + + // ---- ONE physical SDRAM backend, both W and (now 2-source- + // arbitrated) AR ports ---- + sdram_unified_backend #( + .ADDR_WIDTH(ADDR_WIDTH), .CLK_FREQ_MHZ(CLK_FREQ_MHZ) + ) u_sdram_backend ( + .clk(clk), .rst(core_rst), + .w_req(wide_arb_m_req), .w_addr(wide_arb_m_addr), + .w_rdata(wide_arb_m_rdata), .w_ready(wide_arb_m_ready), + .ar_req(final_ar_req), .ar_wr(final_ar_wr), .ar_addr(final_ar_addr), .ar_wdata(final_ar_wdata), + .ar_lb_n(final_ar_lb_n), .ar_ub_n(final_ar_ub_n), + .ar_rdata(final_ar_rdata), .ar_ready(final_ar_ready), + .sdram_cke(sdram_cke), .sdram_cs_n(sdram_cs_n), .sdram_ras_n(sdram_ras_n), + .sdram_cas_n(sdram_cas_n), .sdram_we_n(sdram_we_n), + .sdram_ba(sdram_ba), .sdram_a(sdram_a), .sdram_dq(sdram_dq), .sdram_dqm(sdram_dqm) + ); + +endmodule diff --git a/hardware/v2/nms/rtl/nms_memory_manager_stream_wide.v b/hardware/v2/nms/rtl/nms_memory_manager_stream_wide.v index 7931e27..0f6de54 100644 --- a/hardware/v2/nms/rtl/nms_memory_manager_stream_wide.v +++ b/hardware/v2/nms/rtl/nms_memory_manager_stream_wide.v @@ -139,15 +139,27 @@ module nms_memory_manager_stream_wide #( reg [2:0] state; reg job_active_reg; - assign job_active = job_active_reg; - assign job_x_base = x_base_reg; - assign job_n_tiles = n_tiles_reg; - reg [ADDR_WIDTH-1:0] x_base_reg, w_base_reg, result_addr_reg; reg [15:0] n_tiles_reg; reg [CNTW-1:0] tile_idx; // CONSUMPTION pointer (tiles handed to NP so far) reg [CNTW-1:0] rd_ptr; // READ-ISSUE pointer (tiles whose SRAM read has been issued) + assign job_active = job_active_reg; + assign job_x_base = x_base_reg; + assign job_n_tiles = n_tiles_reg; + + // Result write-back port regs (moved up from their original, + // later position in this file -- STEP20 tooling-compatibility + // fix, zero behavior change: module-scope reg declarations are + // not order-dependent in real Verilog semantics, but a icarus + // Verilog 13.0 elaborates `always` blocks in file order and + // requires a reg's declaration to textually precede its first + // use inside one; this file predates that stricter check). + reg wr_mem_req; + reg [ADDR_WIDTH-1:0] wr_mem_addr; + reg [15:0] wr_mem_wdata; + reg wr_mem_lb_n, wr_mem_ub_n; + wire [CNTW-1:0] wgt_ready_count; wire usable_act_count_valid = (act_resident_tag == x_base_reg); @@ -305,11 +317,7 @@ module nms_memory_manager_stream_wide #( // Result write-back has the real 16-bit port entirely to itself // in this variant (no mux needed -- weight fetch lives on the - // separate wide port above). - reg wr_mem_req; - reg [ADDR_WIDTH-1:0] wr_mem_addr; - reg [15:0] wr_mem_wdata; - reg wr_mem_lb_n, wr_mem_ub_n; + // separate wide port above). Declarations moved up (see above). assign mem_req = wr_mem_req; assign mem_wr = 1'b1; diff --git a/hardware/v2/nms/sim/tb_fpga_neural_v2_top_smoke.v b/hardware/v2/nms/sim/tb_fpga_neural_v2_top_smoke.v new file mode 100644 index 0000000..fd08074 --- /dev/null +++ b/hardware/v2/nms/sim/tb_fpga_neural_v2_top_smoke.v @@ -0,0 +1,202 @@ +`timescale 1ns/1ps + +// ================================================================ +// FPGA-Neural V2 -- board-level top INTEGRATION SMOKE TEST (STEP20) +// +// Proves the NEW STEP20 wiring end-to-end: real SPI transactions (bit- +// banged, mode 0) drive job registration THROUGH spi_host_bridge.v, +// through the real compute+memory pipeline (byte-for-byte identical +// to the already-verified STEP19 nms_neural_multiprocessor_sdram_ +// unified.v internals) via the NEW 2-level host-arb AR arbitration, +// down to the SAME single sdram_unified_backend/sdram_controller/ +// AS4C4M16SA-6TIN chain -- checked against a real, backdoor-peeked +// SDRAM result. This is NOT a replacement for the STEP19 full 256- +// neuron D-Stress regression (already reconfirmed bit-exact using the +// trusted tool, see errors.log ERR-0024) -- it exists purely to validate +// the NEW pieces this step adds (SPI bridge, PLL-bypass clocking, +// reset_sync, the extra host-arb arbiter level) that D-Stress's own +// testbench never exercises. +// +// Weights/activations are preloaded via the same backdoor poke +// convention already used by tb_nms_dstress_sdram_unified.v (direct +// writes into u_sdram.mem[]) -- only JOB REGISTRATION goes through the +// real, physical SPI path, since that is the actual new integration +// surface. `SIM bypasses the (unsimulatable) EHXPLLL primitive inside +// ecp5_pll_sys_clk.v with a direct pass-through, per that module's own +// documented, declared limitation. +// +// CURRENT STATUS (STEP20): FAILING, real, disclosed -- see errors.log +// ERR-0025 Part B. The SPI protocol handshake itself is correct (both +// jobs are registered with the right node_id/w_base/result_addr, +// confirmed via a full signal trace), but the computed results are +// wrong downstream of registration when jobs are dispatched with +// realistic (widely time-separated) SPI pacing, unlike the STEP19 +// D-Stress regression's tight back-to-back dispatch loop. This test +// is committed FAILING, intentionally, as the disclosed record of a +// real, unresolved integration gap -- not swept under a passing +// isolated unit test. +// ================================================================ + +`define SIM + +module tb_fpga_neural_v2_top_smoke; + + localparam ADDR_WIDTH = 23; + localparam N_SLOTS = 2; + localparam N_NODES = 16; + localparam MAX_DEPS = 4; + + reg osc_clk = 0; + always #31.25 osc_clk = ~osc_clk; // 16MHz (bypassed 1:1 to clk_sys under `SIM) + + reg ext_rst_n = 0; + + reg spi_sclk = 0, spi_mosi = 0, spi_cs_n = 1; + wire spi_miso; + + wire sdram_cke, sdram_cs_n, sdram_ras_n, sdram_cas_n, sdram_we_n; + wire [1:0] sdram_ba; + wire [11:0] sdram_a; + wire [15:0] sdram_dq; + wire [1:0] sdram_dqm; + wire pll_locked; + + fpga_neural_v2_top #( + .ADDR_WIDTH(ADDR_WIDTH), .N_SLOTS(N_SLOTS), .N_NODES(N_NODES), .MAX_DEPS(MAX_DEPS), + .CLK_FREQ_MHZ(80) + ) dut ( + .osc_clk(osc_clk), .ext_rst_n(ext_rst_n), + .spi_sclk(spi_sclk), .spi_mosi(spi_mosi), .spi_miso(spi_miso), .spi_cs_n(spi_cs_n), + .sdram_cke(sdram_cke), .sdram_cs_n(sdram_cs_n), .sdram_ras_n(sdram_ras_n), + .sdram_cas_n(sdram_cas_n), .sdram_we_n(sdram_we_n), + .sdram_ba(sdram_ba), .sdram_a(sdram_a), .sdram_dq(sdram_dq), .sdram_dqm(sdram_dqm), + .pll_locked(pll_locked) + ); + + sdram_model #(.CLK_FREQ_MHZ(80)) u_sdram ( + .clk(dut.clk_sys), .cke(sdram_cke), .cs_n(sdram_cs_n), .ras_n(sdram_ras_n), + .cas_n(sdram_cas_n), .we_n(sdram_we_n), .ba(sdram_ba), .a(sdram_a), + .dq(sdram_dq), .dqm(sdram_dqm) + ); + + function automatic signed [7:0] relu_sat(input signed [31:0] acc); + begin + if (acc < 0) relu_sat = 8'sd0; + else if (acc > 127) relu_sat = 8'sd127; + else relu_sat = acc[7:0]; + end + endfunction + + task poke_byte(input [ADDR_WIDTH-1:0] byte_addr, input signed [7:0] val); + reg [21:0] word_addr; + begin + word_addr = byte_addr[ADDR_WIDTH-1:1]; + if (byte_addr[0] == 1'b0) u_sdram.mem[word_addr][7:0] = val; + else u_sdram.mem[word_addr][15:8] = val; + end + endtask + + function automatic signed [7:0] peek_byte(input [ADDR_WIDTH-1:0] byte_addr); + reg [21:0] word_addr; + begin + word_addr = byte_addr[ADDR_WIDTH-1:1]; + peek_byte = (byte_addr[0] == 1'b0) ? u_sdram.mem[word_addr][7:0] : u_sdram.mem[word_addr][15:8]; + end + endfunction + + // ---- SPI master BFM (matches spi_host_bridge.v's own protocol, + // same realistic 500ns-bit-period convention as tb_spi_host_ + // bridge.v -- see that module's header on the CDC margin reason) ---- + task spi_byte(input [7:0] tx, output [7:0] rx); + integer i; + begin + rx = 8'h00; + for (i = 7; i >= 0; i = i - 1) begin + spi_mosi = tx[i]; + #200; spi_sclk = 1; #50; rx = {rx[6:0], spi_miso}; #50; spi_sclk = 0; #200; + end + end + endtask + + task write_job(input [3:0] node_id, input [2:0] required, input [15:0] producer_ids, + input [22:0] x_base, input [22:0] w_base, input [15:0] n_tiles, + input [22:0] result_addr); + reg [7:0] rxb; + begin + spi_cs_n = 0; #20; + spi_byte(8'h10, rxb); + spi_byte({4'b0, node_id}, rxb); + spi_byte({5'b0, required}, rxb); + spi_byte(producer_ids[15:8], rxb); + spi_byte(producer_ids[7:0], rxb); + spi_byte({1'b0, x_base[22:16]}, rxb); + spi_byte(x_base[15:8], rxb); + spi_byte(x_base[7:0], rxb); + spi_byte({1'b0, w_base[22:16]}, rxb); + spi_byte(w_base[15:8], rxb); + spi_byte(w_base[7:0], rxb); + spi_byte(n_tiles[15:8], rxb); + spi_byte(n_tiles[7:0], rxb); + spi_byte({1'b0, result_addr[22:16]}, rxb); + spi_byte(result_addr[15:8], rxb); + spi_byte(result_addr[7:0], rxb); + // hold CS through the reg_valid/reg_ready handshake (may + // need a few extra idle clocks if the target slot is busy) + #20000; + spi_cs_n = 1; #200; + end + endtask + + integer n, k, t, errors, tests; + reg signed [31:0] acc; + reg signed [7:0] golden, real_y; + localparam N_TILES = 2; + + initial begin + errors = 0; tests = 0; + ext_rst_n = 0; + repeat (20) @(posedge osc_clk); + ext_rst_n = 1; + repeat (10) @(posedge osc_clk); + + // preload: 2 independent single-tile (P_IN=8) neurons sharing + // one activation vector, at x_base=0x001000, weights at + // 0x002000 (neuron0) / 0x002010 (neuron1), results at 0x003000 + for (k = 0; k < 8; k = k + 1) poke_byte(23'h001000 + k, k[7:0] + 1); + for (n = 0; n < 2; n = n + 1) + for (k = 0; k < 8; k = k + 1) + poke_byte(23'h002000 + n*16 + k, ((n+k) % 4) + 1); + poke_byte(23'h003000, 8'sd0); + poke_byte(23'h003001, 8'sd0); + + wait (dut.u_sdram_backend.u_sdram_ctrl.state == dut.u_sdram_backend.u_sdram_ctrl.S_IDLE); + @(posedge dut.clk_sys); + + write_job(4'd0, 3'd0, 16'h0000, 23'h001000, 23'h002000, 16'd1, 23'h003000); + write_job(4'd1, 3'd0, 16'h0000, 23'h001000, 23'h002010, 16'd1, 23'h003001); + + // wait for both results to land (generous margin) + repeat (3000) @(posedge dut.clk_sys); + + for (n = 0; n < 2; n = n + 1) begin + acc = 0; + for (t = 0; t < N_TILES/N_TILES; t = t + 1) ; // no-op, single tile + for (k = 0; k < 8; k = k + 1) + acc = acc + peek_byte(23'h001000 + k) * peek_byte(23'h002000 + n*16 + k); + golden = relu_sat(acc); + real_y = peek_byte(23'h003000 + n); + tests = tests + 1; + if (real_y !== golden) begin + errors = errors + 1; + $display("FAIL smoke neuron %0d: real=%0d golden=%0d", n, real_y, golden); + end else begin + $display("PASS smoke neuron %0d: real=%0d golden=%0d", n, real_y, golden); + end + end + + $display("=== tb_fpga_neural_v2_top_smoke: %0d/%0d PASS ===", tests-errors, tests); + if (errors != 0) $display("*** %0d FAILURES ***", errors); + $finish; + end + +endmodule diff --git a/hardware/v2/nms/sim/tb_nms_dstress_sdram_unified.v b/hardware/v2/nms/sim/tb_nms_dstress_sdram_unified.v index f09cca1..f6d9c62 100644 --- a/hardware/v2/nms/sim/tb_nms_dstress_sdram_unified.v +++ b/hardware/v2/nms/sim/tb_nms_dstress_sdram_unified.v @@ -235,6 +235,11 @@ module tb #( reg measure_en; integer total_cycles; integer psram_busy_cycles; + integer ni; // moved up from its original later declaration point + // (STEP20 tooling-compatibility fix, zero behavior + // change -- see nms_memory_manager_stream_wide.v's own + // header note on icarus 13.0's stricter declared- + // before-use rule for procedural blocks) genvar gi; reg [N_SLOTS_CFG-1:0] slot_busy_bit; // memory_manager.state != MM_IDLE, this cycle @@ -356,8 +361,8 @@ module tb #( // the weight specifically is NOT yet ready (tile_idx>=wgt_ready_count) // and the FSM is genuinely stalled on it (not mid-read-pipeline, not // already holding a valid operand). - wire [N_SLOTS_CFG-1:0] slot_could_present_act; - wire [N_SLOTS_CFG-1:0] slot_weight_blocking; + reg [N_SLOTS_CFG-1:0] slot_could_present_act; + reg [N_SLOTS_CFG-1:0] slot_weight_blocking; reg [N_SLOTS_CFG-1:0] slot_stalled_this_tile; // sticky per current tile_idx reg [31:0] prev_tile_idx [0:N_SLOTS_CFG-1]; integer weight_stall_cycles [0:N_SLOTS_CFG-1]; @@ -419,7 +424,6 @@ module tb #( // Director/dependency bookkeeping integer jobs_allocated, jobs_completed, wakeups; integer waiting_sum, ready_sum, dispatched_sum, sample_count; - integer ni; // Occupancy sampling is EXPENSIVE (a full N_NODES=512 scan) and is // only needed for the small/structural workloads (A/B/E/F), not diff --git a/hardware/v2/rtl/ecp5_pll_sys_clk.v b/hardware/v2/rtl/ecp5_pll_sys_clk.v new file mode 100644 index 0000000..b459c25 --- /dev/null +++ b/hardware/v2/rtl/ecp5_pll_sys_clk.v @@ -0,0 +1,86 @@ +`timescale 1ns/1ps + +// ================================================================ +// FPGA-Neural V2 -- ECP5 PLL wrapper (STEP20, real clock architecture) +// +// 16 MHz board oscillator -> EHXPLLL -> 64 MHz system clock. +// +// Parameters below are the REAL, tool-generated output of Project +// Trellis's own `ecppll` utility (v1.4): +// ecppll -i 16 --clkin_name=clk_16mhz -o 64 --clkout0_name=clk_sys \ +// -n ecp5_pll_16to64 -f pll_64.v +// Refclk divisor: 1, Feedback divisor: 4, clkout0 divisor: 9 +// VCO frequency: 576 MHz (within the ECP5 PLL's documented +// 400-800MHz VCO range), clkout0 frequency: 64 MHz exactly +// (16 * 4 / (1*... ) -- integer, zero-error ratio). +// +// 64MHz was chosen (not 80MHz) per this step's own real, multi-seed +// P&R timing data on the FINAL board-level top (SPI host bridge + +// host-arb SDRAM port added on top of the STEP19 compute+memory +// design): see hardware/v2/docs/TIMING.md for the full seed table. +// A single lucky seed reaching into the 80s MHz range is NOT treated +// as the operating frequency -- 64MHz is the highest frequency at +// which ALL measured seeds close timing with real margin. +// +// SIMULATION: EHXPLLL has no open, licensable behavioral model (Lattice +// ships it only inside their own encrypted simulation libraries), so +// this wrapper provides a behavioral bypass under `SIM` for iverilog +// and Verilator alike -- clk_sys tracks clk_16mhz directly and +// `locked` is tied high. This is a DECLARED simulation-only stand-in, +// not a claim that PLL lock timing has been simulated; real lock +// behavior is only characterized by nextpnr-ecp5 static timing and, +// eventually, real hardware bring-up (see FIRST_POWER_ON.md). +// ================================================================ + +module ecp5_pll_sys_clk ( + input wire clk_16mhz, + output wire clk_sys, + output wire locked +); + +`ifdef SIM + + assign clk_sys = clk_16mhz; + assign locked = 1'b1; + +`else + + (* FREQUENCY_PIN_CLKI="16" *) + (* FREQUENCY_PIN_CLKOP="64" *) + (* ICP_CURRENT="12" *) (* LPF_RESISTOR="8" *) (* MFG_ENABLE_FILTEROPAMP="1" *) (* MFG_GMCREF_SEL="2" *) + EHXPLLL #( + .PLLRST_ENA("DISABLED"), + .INTFB_WAKE("DISABLED"), + .STDBY_ENABLE("DISABLED"), + .DPHASE_SOURCE("DISABLED"), + .OUTDIVIDER_MUXA("DIVA"), + .OUTDIVIDER_MUXB("DIVB"), + .OUTDIVIDER_MUXC("DIVC"), + .OUTDIVIDER_MUXD("DIVD"), + .CLKI_DIV(1), + .CLKOP_ENABLE("ENABLED"), + .CLKOP_DIV(9), + .CLKOP_CPHASE(4), + .CLKOP_FPHASE(0), + .FEEDBK_PATH("CLKOP"), + .CLKFB_DIV(4) + ) pll_i ( + .RST(1'b0), + .STDBY(1'b0), + .CLKI(clk_16mhz), + .CLKOP(clk_sys), + .CLKFB(clk_sys), + .CLKINTFB(), + .PHASESEL0(1'b0), + .PHASESEL1(1'b0), + .PHASEDIR(1'b1), + .PHASESTEP(1'b1), + .PHASELOADREG(1'b1), + .PLLWAKESYNC(1'b0), + .ENCLKOP(1'b0), + .LOCK(locked) + ); + +`endif + +endmodule diff --git a/hardware/v2/rtl/reset_sync.v b/hardware/v2/rtl/reset_sync.v new file mode 100644 index 0000000..c30404b --- /dev/null +++ b/hardware/v2/rtl/reset_sync.v @@ -0,0 +1,34 @@ +`timescale 1ns/1ps + +// ================================================================ +// FPGA-Neural V2 -- reset synchronizer (STEP20, real reset/POR path) +// +// Standard async-assert / sync-deassert double-flop reset bridge. +// Asserts `rst` IMMEDIATELY (combinationally) when either the +// external POR/supervisor (ext_rst_n, active-low) is asserted OR the +// PLL has not yet reported LOCK -- both real, physical conditions +// under which no downstream logic (SDRAM controller, dependency +// manager, SPI bridge) may be considered valid. Deassertion is +// synchronized to `clk_sys` through two flip-flops so no downstream +// flop ever sees an asynchronous release edge. +// ================================================================ + +module reset_sync ( + input wire clk_sys, + input wire ext_rst_n, // external POR/supervisor, active-low + input wire pll_locked, + output wire rst // synchronous-deassert, active-high +); + + wire async_rst_n = ext_rst_n & pll_locked; + + reg [1:0] sync_ff; + + always @(posedge clk_sys or negedge async_rst_n) begin + if (!async_rst_n) sync_ff <= 2'b00; + else sync_ff <= {sync_ff[0], 1'b1}; + end + + assign rst = ~sync_ff[1]; + +endmodule diff --git a/hardware/v2/rtl/spi_host_bridge.v b/hardware/v2/rtl/spi_host_bridge.v new file mode 100644 index 0000000..0a6038c --- /dev/null +++ b/hardware/v2/rtl/spi_host_bridge.v @@ -0,0 +1,435 @@ +`timescale 1ns/1ps + +// ================================================================ +// FPGA-Neural V2 -- SPI HOST BRIDGE (STEP20, physical host interface) +// +// Replaces the 110-pin reg_*/testbench-only bus as the PHYSICAL board +// interface. The internal reg_*/mem_* ports below are UNCHANGED in +// shape/semantics from the ones nms_dataflow_core_sdram.v and +// sdram_unified_backend.v's AR port already expose -- this module is +// a pure protocol translator (SPI bytes -> the same internal signals +// simulation already drives directly), so nms_dataflow_core_sdram.v, +// dependency_manager.v, neural_processor.v and sdram_unified_backend.v +// remain byte-for-byte unchanged (STEP19/STEP20 standing constraint). +// +// Physical layer (byte shift register + CS framing + CDC synchronizers) +// re-derives the same proven design as hardware/v1/rtl/spi_slave.v +// (SPI mode 0, MSB-first, one opcode per CS-low period, double-flop +// CDC on sclk/mosi/cs_n) -- reimplemented here as a NEW, independently +// owned V2 file so V2 continues to instantiate ZERO V1 RTL (STEP19's +// own "zero V1 files in the V2 compile list" property is preserved). +// +// --------------------------------------------------------------- +// PROTOCOL (new, V2-specific -- one opcode byte, MSB-first, per +// CS-low transaction; multi-byte fields are MSB-first): +// +// 0x00 NOP -- 0 payload bytes. +// 0x0F RESET -- 0 payload bytes. Pulses soft_rst_pulse for +// one clk cycle after CS rises. +// 0x10 WRITE_JOB -- 15 payload bytes, registers one dependency- +// manager job (== one reg_valid/reg_* handshake): +// byte0 = {4'b0,node_id[3:0]} +// byte1 = {5'b0,required[2:0]} +// byte2:3 = producer_ids[15:0] +// byte4:6 = x_base[22:0] (byte4 msb={1'b0,x_base[22:16]}) +// byte7:9 = w_base[22:0] +// byte10:11= n_tiles[15:0] +// byte12:14= result_addr[22:0] +// reg_valid is asserted and HELD until the +// cycle reg_ready also reads 1 (same-cycle +// valid&&ready acceptance, matching +// dependency_manager.v's own combinational +// reg_ready contract) -- never a blind pulse. +// 0x20 STATUS -- 0 payload bytes. Returns 1 byte on MISO +// (clocked out during payload byte 1): +// bit0 = job_busy (WRITE_JOB waiting on reg_ready) +// bit1 = mem_busy (WRITE_MEM/READ_MEM waiting on mem_ready) +// bit2 = last_job_accepted (sticky, cleared by next WRITE_JOB) +// bits[7:3] = 0 (reserved) +// 0x01 WRITE_MEM -- 5 header bytes + 2*len_words payload bytes: +// byte0:2 = addr[22:0] (WORD address, matches +// sdram_unified_backend's AR port +// convention -- NOT a byte address) +// byte3:4 = len_words[15:0] (number of 16-bit +// words to write, len_words>=1) +// then len_words * 2 bytes of data, MSB-first +// per word; each word is written via one +// mem_req/mem_ready handshake (lb_n=ub_n=0, +// full 16-bit write) before the next word's +// bytes are accepted. +// 0x02 READ_MEM -- 5 header bytes (addr + len_words, same shape +// as WRITE_MEM), 0 further MOSI payload; the +// 2*len_words response bytes are clocked out +// on MISO starting at payload byte 6, MSB- +// first per word, one mem_req/mem_ready +// read per word. +// +// Any opcode byte not listed above is treated as NOP (0 payload, +// MISO drives 0x00) -- matches spi_engine.v's own "unknown opcode is +// inert, never wedges the bus" precedent. +// ================================================================ + +module spi_host_bridge #( + parameter ADDR_WIDTH = 23, + parameter N_NODES = 16, + parameter MAX_DEPS = 4 +)( + input wire clk, + input wire rst, + + // ---- physical SPI pins ---- + input wire sclk, + input wire mosi, + output wire miso, + input wire cs_n, + + // ---- job registration (-> nms_dataflow_core_sdram.v) ---- + output reg reg_valid, + input wire reg_ready, + output reg [$clog2(N_NODES)-1:0] reg_node_id, + output reg [$clog2(MAX_DEPS+1)-1:0] reg_required, + output reg [MAX_DEPS*$clog2(N_NODES)-1:0] reg_producer_ids, + output reg [ADDR_WIDTH-1:0] reg_x_base, + output reg [ADDR_WIDTH-1:0] reg_w_base, + output reg [15:0] reg_n_tiles, + output reg [ADDR_WIDTH-1:0] reg_result_addr, + + // ---- host raw SDRAM access (-> host-arb slot_mem_arbiter port) ---- + output reg mem_req, + output reg mem_wr, + output reg [ADDR_WIDTH-1:0] mem_addr, + output reg [15:0] mem_wdata, + output reg mem_lb_n, + output reg mem_ub_n, + input wire [15:0] mem_rdata, + input wire mem_ready, + + output reg soft_rst_pulse +); + + localparam NODEW = $clog2(N_NODES); + localparam REQW = $clog2(MAX_DEPS+1); + + // ============================================================ + // SPI PHYSICAL LAYER (byte shift register + CS framing + CDC) + // ============================================================ + + reg [2:0] sclk_sync, mosi_sync, cs_n_sync; + always @(posedge clk) begin + if (rst) begin + sclk_sync <= 3'b000; mosi_sync <= 3'b000; cs_n_sync <= 3'b111; + end else begin + sclk_sync <= {sclk_sync[1:0], sclk}; + mosi_sync <= {mosi_sync[1:0], mosi}; + cs_n_sync <= {cs_n_sync[1:0], cs_n}; + end + end + wire sclk_s = sclk_sync[2]; + wire cs_n_s = cs_n_sync[2]; + wire mosi_s = mosi_sync[2]; + + reg sclk_prev, cs_n_prev; + always @(posedge clk) begin + if (rst) begin sclk_prev <= 1'b0; cs_n_prev <= 1'b1; end + else begin sclk_prev <= sclk_s; cs_n_prev <= cs_n_s; end + end + wire sclk_rise = sclk_s & ~sclk_prev; + wire cs_fell = ~cs_n_s & cs_n_prev; + wire cs_rose = cs_n_s & ~cs_n_prev; + wire cs_active = ~cs_n_s; + + reg [2:0] bit_count; + reg [7:0] rx_shift; + reg [7:0] rx_byte; + reg rx_valid; + + // tx_byte is driven COMBINATIONALLY by the protocol FSM below (see + // tx_mux) -- always reflects "the byte MISO should show next". + // + // IMPORTANT (found via this module's own isolated regression, + // STEP20 -- two successive real bugs before this final design): + // + // Draft 1 used a conventional per-bit INCREMENTAL shift register + // for MISO (load tx_byte once at a byte boundary, then shift one + // position per falling edge, mirroring hardware/v1/rtl/ + // spi_slave.v's own proven convention). It failed because + // `bit_count` (incremented on the RISING-edge detector) is ALWAYS + // already one bit ahead of what the FALLING-edge detector sees for + // that SAME physical bit -- a rising edge is always detected + // before that bit's own falling edge, since both go through the + // same CDC latency but the physical fall itself comes later in + // time. So "prepare tx_shift for bit_count+1" at a falling edge + // that already observes the incremented bit_count silently skips + // a bit position, corrupting the byte by one place (root-caused + // via this module's own tb_spi_host_bridge.v with a full internal- + // signal trace, not by inspection). + // + // Draft 2 tried removing the shift register entirely (index + // tx_byte directly by bit_count on EVERY bit, driven purely + // combinationally). That failed a different way: sampling MISO + // even slightly after the CDC latency that follows a bit's own + // rising edge (normal SPI master behavior, not a torture case) + // already sees bit_count having advanced to the NEXT index. + // + // Both drafts share one fact once it's made explicit: at the + // moment ANY falling edge is internally detected, `bit_count` + // ALREADY equals the index of the bit that is about to be + // sampled next (not the bit whose fall just fired). The fix below + // uses exactly that fact instead of fighting it: on every detected + // falling edge, load `miso_shift_bit` directly from + // tx_byte[7-bit_count] (no incremental shift, no off-by-one). + // Between falling edges -- including an extended SCLK-idle wait, + // a real, INTENDED use of this protocol for READ_MEM/mem_req + // latency (see module header) -- `bit_count==0` is additionally + // driven live/combinationally so a response that only becomes + // known DURING the idle wait (no falling edge occurs to refresh + // it) is still correct once the master resumes clocking. + wire [7:0] tx_byte; + reg miso_shift_bit; + + assign miso = (cs_active && bit_count == 3'd0) ? tx_byte[7] : miso_shift_bit; + + always @(posedge clk) begin + if (rst) begin + bit_count <= 3'd0; rx_shift <= 8'h00; rx_byte <= 8'h00; rx_valid <= 1'b0; + miso_shift_bit <= 1'b0; + end else begin + rx_valid <= 1'b0; + if (cs_fell) begin + bit_count <= 3'd0; + end else if (cs_active) begin + if (sclk_rise) begin + rx_shift <= {rx_shift[6:0], mosi_s}; + if (bit_count == 3'd7) begin + bit_count <= 3'd0; + rx_byte <= {rx_shift[6:0], mosi_s}; + rx_valid <= 1'b1; + end else begin + bit_count <= bit_count + 3'd1; + end + end else if (~sclk_s & sclk_prev) begin // sclk_fall + miso_shift_bit <= tx_byte[3'd7 - bit_count]; + end + end + end + end + + // ============================================================ + // PROTOCOL FSM + // ============================================================ + + localparam OP_NOP = 8'h00; + localparam OP_WRITE_MEM = 8'h01; + localparam OP_READ_MEM = 8'h02; + localparam OP_RESET = 8'h0F; + localparam OP_WRITE_JOB = 8'h10; + localparam OP_STATUS = 8'h20; + + localparam ST_OPCODE = 4'd0; + localparam ST_JOB = 4'd1; // collecting 15 WRITE_JOB payload bytes + localparam ST_JOB_WAIT= 4'd2; // reg_valid held, waiting reg_ready + localparam ST_MEM_ADDR= 4'd3; // collecting 3 addr bytes + localparam ST_MEM_LEN = 4'd4; // collecting 2 length bytes + localparam ST_MEM_WD = 4'd5; // WRITE_MEM: collecting 2 data bytes/word + localparam ST_MEM_WISS= 4'd6; // WRITE_MEM: issue+wait mem_req + localparam ST_MEM_RISS= 4'd7; // READ_MEM: issue+wait mem_req + localparam ST_MEM_ROUT= 4'd8; // READ_MEM: shifting the 2 bytes of a word out + localparam ST_IGNORE = 4'd9; // opcode consumed / unknown, wait for cs_rose + + reg [3:0] state; + reg [7:0] opcode; + reg [3:0] byte_idx; // generic byte counter within a field + reg [15:0] len_words; + reg [15:0] word_cnt; + reg [15:0] cur_word; // WRITE_MEM: assembling MSB,LSB; READ_MEM: holding readback + reg job_busy_r, mem_busy_r, last_job_accepted_r; + + // combinational tx byte mux -- STATUS response, READ_MEM data, + // everything else drives 0x00 + reg [7:0] tx_mux; + always @(*) begin + tx_mux = 8'h00; + if (opcode == OP_STATUS) + tx_mux = {5'b0, last_job_accepted_r, mem_busy_r, job_busy_r}; + else if (opcode == OP_READ_MEM && state == ST_MEM_ROUT) + tx_mux = (byte_idx == 4'd0) ? cur_word[15:8] : cur_word[7:0]; + end + assign tx_byte = tx_mux; + + always @(posedge clk) begin + if (rst) begin + state <= ST_OPCODE; opcode <= 8'h00; byte_idx <= 4'd0; + len_words <= 16'd0; word_cnt <= 16'd0; cur_word <= 16'd0; + reg_valid <= 1'b0; reg_node_id <= {NODEW{1'b0}}; reg_required <= {REQW{1'b0}}; + reg_producer_ids <= {(MAX_DEPS*NODEW){1'b0}}; + reg_x_base <= {ADDR_WIDTH{1'b0}}; reg_w_base <= {ADDR_WIDTH{1'b0}}; + reg_n_tiles <= 16'd0; reg_result_addr <= {ADDR_WIDTH{1'b0}}; + mem_req <= 1'b0; mem_wr <= 1'b0; mem_addr <= {ADDR_WIDTH{1'b0}}; + mem_wdata <= 16'd0; mem_lb_n <= 1'b0; mem_ub_n <= 1'b0; + soft_rst_pulse <= 1'b0; + job_busy_r <= 1'b0; mem_busy_r <= 1'b0; last_job_accepted_r <= 1'b0; + end else begin + mem_req <= 1'b0; + soft_rst_pulse <= 1'b0; + + // A new CS assertion normally starts a fresh opcode byte. + // EXCEPTION (found via this module's own board-level + // integration smoke test, STEP20): if the PREVIOUS + // transaction is still pending a backend handshake + // (ST_JOB_WAIT/ST_MEM_WISS/ST_MEM_RISS -- e.g. reg_valid + // held, waiting on dependency_manager's reg_ready, per + // this module's own documented "hold until accepted" + // contract), do NOT reset state/byte_idx here: a naive + // unconditional reset lets a new WRITE_JOB's incoming + // bytes start overwriting reg_node_id/reg_x_base/reg_ + // w_base/etc THROUGH THE SAME REGISTERS while the OLD + // job's reg_valid is still asserted and not yet accepted, + // corrupting the first job's dispatch with a mix of both + // jobs' fields (confirmed: two back-to-back WRITE_JOB + // transactions produced swapped/wrong result values, + // root-caused via a full internal signal trace before + // this fix). Mirrors the same protection already applied + // to cs_rose below. + if (cs_fell && state != ST_JOB_WAIT && state != ST_MEM_WISS && state != ST_MEM_RISS) begin + state <= ST_OPCODE; + byte_idx <= 4'd0; + end else if (!cs_fell && rx_valid) begin + case (state) + ST_OPCODE: begin + opcode <= rx_byte; + byte_idx <= 4'd0; + case (rx_byte) + OP_WRITE_JOB: state <= ST_JOB; + OP_WRITE_MEM: state <= ST_MEM_ADDR; + OP_READ_MEM: state <= ST_MEM_ADDR; + OP_RESET: state <= ST_IGNORE; + default: state <= ST_IGNORE; // NOP, STATUS: no MOSI payload + endcase + end + + ST_JOB: begin + case (byte_idx) + 4'd0: reg_node_id <= rx_byte[NODEW-1:0]; + 4'd1: reg_required <= rx_byte[REQW-1:0]; + 4'd2: reg_producer_ids[15:8] <= rx_byte; + 4'd3: reg_producer_ids[7:0] <= rx_byte; + 4'd4: reg_x_base[22:16] <= rx_byte[6:0]; + 4'd5: reg_x_base[15:8] <= rx_byte; + 4'd6: reg_x_base[7:0] <= rx_byte; + 4'd7: reg_w_base[22:16] <= rx_byte[6:0]; + 4'd8: reg_w_base[15:8] <= rx_byte; + 4'd9: reg_w_base[7:0] <= rx_byte; + 4'd10: reg_n_tiles[15:8] <= rx_byte; + 4'd11: reg_n_tiles[7:0] <= rx_byte; + 4'd12: reg_result_addr[22:16] <= rx_byte[6:0]; + 4'd13: reg_result_addr[15:8] <= rx_byte; + 4'd14: begin + reg_result_addr[7:0] <= rx_byte; + reg_valid <= 1'b1; + last_job_accepted_r <= 1'b0; + state <= ST_JOB_WAIT; + end + endcase + if (byte_idx != 4'd14) byte_idx <= byte_idx + 4'd1; + end + + ST_MEM_ADDR: begin + case (byte_idx) + 4'd0: mem_addr[22:16] <= rx_byte[6:0]; + 4'd1: mem_addr[15:8] <= rx_byte; + 4'd2: begin + mem_addr[7:0] <= rx_byte; + state <= ST_MEM_LEN; + end + endcase + if (byte_idx != 4'd2) byte_idx <= byte_idx + 4'd1; + else byte_idx <= 4'd0; + end + + ST_MEM_LEN: begin + if (byte_idx == 4'd0) begin + len_words[15:8] <= rx_byte; + byte_idx <= 4'd1; + end else begin + len_words[7:0] <= rx_byte; + word_cnt <= {len_words[15:8], rx_byte}; + byte_idx <= 4'd0; + state <= (opcode == OP_WRITE_MEM) ? ST_MEM_WD : ST_MEM_RISS; + end + end + + ST_MEM_WD: begin + if (byte_idx == 4'd0) begin + cur_word[15:8] <= rx_byte; + byte_idx <= 4'd1; + end else begin + cur_word[7:0] <= rx_byte; + state <= ST_MEM_WISS; + end + end + + default: ; // ST_JOB_WAIT/ST_MEM_WISS/ST_MEM_RISS/ST_MEM_ROUT/ST_IGNORE: no MOSI payload expected + endcase + end + + // ---- non-rx_valid-driven transitions ---- + if (state == ST_JOB_WAIT && reg_valid && reg_ready) begin + reg_valid <= 1'b0; + last_job_accepted_r <= 1'b1; + state <= ST_IGNORE; + end + + if (state == ST_MEM_WISS && !mem_req && !mem_busy_r) begin + mem_req <= 1'b1; + mem_wr <= 1'b1; + mem_wdata <= cur_word; + mem_lb_n <= 1'b0; + mem_ub_n <= 1'b0; + mem_busy_r <= 1'b1; + end else if (state == ST_MEM_WISS && mem_busy_r && mem_ready) begin + mem_busy_r <= 1'b0; + mem_addr <= mem_addr + 1'b1; + word_cnt <= word_cnt - 1'b1; + byte_idx <= 4'd0; + state <= (word_cnt == 16'd1) ? ST_IGNORE : ST_MEM_WD; + end + + if (state == ST_MEM_RISS && !mem_req && !mem_busy_r) begin + mem_req <= 1'b1; + mem_wr <= 1'b0; + mem_lb_n <= 1'b0; + mem_ub_n <= 1'b0; + mem_busy_r <= 1'b1; + end else if (state == ST_MEM_RISS && mem_busy_r && mem_ready) begin + mem_busy_r <= 1'b0; + cur_word <= mem_rdata; + byte_idx <= 4'd0; + state <= ST_MEM_ROUT; + end + if (state == ST_MEM_ROUT && rx_valid) begin + // a byte was clocked out while this state was active; + // rx_valid pulses once per real byte transferred, so + // it is also the correct "advance" event for MISO-side + // bookkeeping (mirrors spi_slave's own documented + // rx_valid-drives-advancement convention). + if (byte_idx == 4'd0) begin + byte_idx <= 4'd1; + end else begin + mem_addr <= mem_addr + 1'b1; + word_cnt <= word_cnt - 1'b1; + byte_idx <= 4'd0; + state <= (word_cnt == 16'd1) ? ST_IGNORE : ST_MEM_RISS; + end + end + + job_busy_r <= (state == ST_JOB_WAIT); + + if (cs_rose) begin + if (opcode == OP_RESET) soft_rst_pulse <= 1'b1; + if (state != ST_JOB_WAIT && state != ST_MEM_WISS && state != ST_MEM_RISS) + state <= ST_OPCODE; + end + end + end + +endmodule diff --git a/hardware/v2/sim/tb_spi_host_bridge.v b/hardware/v2/sim/tb_spi_host_bridge.v new file mode 100644 index 0000000..22fbb30 --- /dev/null +++ b/hardware/v2/sim/tb_spi_host_bridge.v @@ -0,0 +1,224 @@ +`timescale 1ns/1ps + +// ================================================================ +// Isolated unit regression for spi_host_bridge.v (STEP20). +// +// Emulates: (1) dependency_manager.v's reg_ready contract (a level, +// asserted only when the target node is free -- here deliberately +// delayed for a few cycles on the first job to prove reg_valid is +// HELD, not pulsed blind); (2) the host-arb slot_mem_arbiter's +// mem_ready contract (one clean req/ready handshake, SDRAM-like fixed +// latency, backed by a simple associative model array standing in for +// real SDRAM content). +// +// Per spi_host_bridge.v's own documented protocol: CS must stay +// asserted (low) for the WHOLE WRITE_MEM/READ_MEM transaction, +// including the internal wait for mem_ready -- SCLK may be idled +// (held low, no toggling) during that wait without losing state. This +// testbench's SPI master BFM does exactly that. +// ================================================================ + +module tb_spi_host_bridge; + + localparam ADDR_WIDTH = 23; + localparam N_NODES = 16; + localparam MAX_DEPS = 4; + localparam NODEW = $clog2(N_NODES); + localparam REQW = $clog2(MAX_DEPS+1); + + reg clk = 0, rst = 1; + always #5 clk = ~clk; // 100MHz sim clock (arbitrary, faster than SPI) + + reg sclk = 0, mosi = 0, cs_n = 1; + wire miso; + + reg reg_ready_model = 0; + wire reg_valid; + wire [NODEW-1:0] reg_node_id; + wire [REQW-1:0] reg_required; + wire [MAX_DEPS*NODEW-1:0] reg_producer_ids; + wire [ADDR_WIDTH-1:0] reg_x_base, reg_w_base, reg_result_addr; + wire [15:0] reg_n_tiles; + + wire mem_req, mem_wr, mem_lb_n, mem_ub_n; + wire [ADDR_WIDTH-1:0] mem_addr; + wire [15:0] mem_wdata; + reg [15:0] mem_rdata_model; + reg mem_ready_model = 0; + + wire soft_rst_pulse; + + spi_host_bridge #( + .ADDR_WIDTH(ADDR_WIDTH), .N_NODES(N_NODES), .MAX_DEPS(MAX_DEPS) + ) dut ( + .clk(clk), .rst(rst), + .sclk(sclk), .mosi(mosi), .miso(miso), .cs_n(cs_n), + .reg_valid(reg_valid), .reg_ready(reg_ready_model), + .reg_node_id(reg_node_id), .reg_required(reg_required), + .reg_producer_ids(reg_producer_ids), + .reg_x_base(reg_x_base), .reg_w_base(reg_w_base), + .reg_n_tiles(reg_n_tiles), .reg_result_addr(reg_result_addr), + .mem_req(mem_req), .mem_wr(mem_wr), .mem_addr(mem_addr), + .mem_wdata(mem_wdata), .mem_lb_n(mem_lb_n), .mem_ub_n(mem_ub_n), + .mem_rdata(mem_rdata_model), .mem_ready(mem_ready_model), + .soft_rst_pulse(soft_rst_pulse) + ); + + // ---- simple backing memory model: fixed 6-cycle mem_ready latency ---- + reg [15:0] mem_model [0:1023]; + integer mem_latency_cnt; + reg mem_pending; + always @(posedge clk) begin + if (rst) begin + mem_ready_model <= 1'b0; mem_pending <= 1'b0; mem_latency_cnt <= 0; + end else begin + mem_ready_model <= 1'b0; + if (mem_req && !mem_pending) begin + mem_pending <= 1'b1; + mem_latency_cnt <= 6; + end else if (mem_pending) begin + if (mem_latency_cnt == 0) begin + mem_pending <= 1'b0; + mem_ready_model <= 1'b1; + if (mem_wr) mem_model[mem_addr[9:0]] <= mem_wdata; + else mem_rdata_model <= mem_model[mem_addr[9:0]]; + end else begin + mem_latency_cnt <= mem_latency_cnt - 1; + end + end + end + end + + // ---- SPI master BFM: mode 0, MSB-first ---- + // Bit period = 500ns (2MHz SPI clock) against a 100MHz sim `clk`: + // a 50x margin over the ~4-clk-cycle CDC synchronizer latency, + // representative of a REAL deployment (system clock 64-80MHz vs a + // practical SPI clock in the low single-digit MHz -- see this + // module's own header for the documented minimum ratio). A torture + // rate close to the CDC latency (as an earlier draft of this + // testbench used) is not a realistic operating point and is not + // what this module is specified against. + task spi_byte(input [7:0] tx, output [7:0] rx); + integer i; + begin + rx = 8'h00; + for (i = 7; i >= 0; i = i - 1) begin + mosi = tx[i]; + #200; sclk = 1; #50; rx = {rx[6:0], miso}; #50; sclk = 0; #200; + end + end + endtask + + integer errors = 0, tests = 0; + task check(input cond, input [255:0] name); + begin + tests = tests + 1; + if (!cond) begin errors = errors + 1; $display("FAIL: %0s", name); end + else $display("PASS: %0s", name); + end + endtask + + reg [7:0] rxb; + reg [ADDR_WIDTH-1:0] exp_addr; + + initial begin + rst = 1; cs_n = 1; sclk = 0; mosi = 0; + repeat (10) @(posedge clk); + rst = 0; + repeat (5) @(posedge clk); + + // ================= Test A: WRITE_JOB, delayed reg_ready ===== + reg_ready_model = 0; + cs_n = 0; #20; + spi_byte(8'h10, rxb); // opcode WRITE_JOB + spi_byte(8'h05, rxb); // node_id=5 + spi_byte(8'h02, rxb); // required=2 + spi_byte(8'hAB, rxb); // producer_ids[15:8] + spi_byte(8'hCD, rxb); // producer_ids[7:0] + spi_byte(8'h00, rxb); // x_base[22:16] + spi_byte(8'h10, rxb); // x_base[15:8] + spi_byte(8'h00, rxb); // x_base[7:0] -> x_base=0x001000 + spi_byte(8'h00, rxb); // w_base[22:16] + spi_byte(8'h20, rxb); // w_base[15:8] + spi_byte(8'h00, rxb); // w_base[7:0] -> w_base=0x002000 + spi_byte(8'h00, rxb); // n_tiles[15:8] + spi_byte(8'h04, rxb); // n_tiles[7:0] -> n_tiles=4 + spi_byte(8'h00, rxb); // result_addr[22:16] + spi_byte(8'h30, rxb); // result_addr[15:8] + spi_byte(8'h00, rxb); // result_addr[7:0] -> result_addr=0x003000 + + // reg_valid must now be held (reg_ready still 0). Allow for the + // CDC synchronizer latency on the LAST bit before sampling. + repeat (8) @(posedge clk); + check(reg_valid == 1'b1, "A: reg_valid asserted after 15th payload byte"); + check(reg_node_id == 5, "A: reg_node_id"); + check(reg_required == 2, "A: reg_required"); + check(reg_producer_ids == 16'hABCD, "A: reg_producer_ids"); + check(reg_x_base == 23'h001000, "A: reg_x_base"); + check(reg_w_base == 23'h002000, "A: reg_w_base"); + check(reg_n_tiles == 16'h0004, "A: reg_n_tiles"); + check(reg_result_addr == 23'h003000, "A: reg_result_addr"); + + repeat (3) begin + @(posedge clk); + check(reg_valid == 1'b1, "A: reg_valid still held while reg_ready=0"); + end + reg_ready_model = 1; + @(posedge clk); + #1; + check(reg_valid == 1'b0, "A: reg_valid drops the cycle after reg_ready seen"); + reg_ready_model = 0; + cs_n = 1; #40; + + // ================= Test B: STATUS after accepted job ======== + cs_n = 0; #20; + spi_byte(8'h20, rxb); // opcode STATUS + spi_byte(8'h00, rxb); // clocks out status byte + $monitoroff; + check(rxb[2] == 1'b1, "B: STATUS last_job_accepted=1"); + check(rxb[0] == 1'b0, "B: STATUS job_busy=0 (already accepted)"); + cs_n = 1; #40; + + // ================= Test C: WRITE_MEM, single word =========== + cs_n = 0; #20; + spi_byte(8'h01, rxb); // opcode WRITE_MEM + spi_byte(8'h00, rxb); spi_byte(8'h00, rxb); spi_byte(8'h55, rxb); // addr=0x000055 + spi_byte(8'h00, rxb); spi_byte(8'h01, rxb); // len_words=1 + spi_byte(8'h12, rxb); spi_byte(8'h34, rxb); // data=0x1234 + // hold CS low, idle SCLK, while the memory model latency elapses + #200; + cs_n = 1; #40; + check(mem_model[16'h0055] == 16'h1234, "C: WRITE_MEM wrote 0x1234 @ 0x000055"); + + // ================= Test D: READ_MEM, single word ============= + cs_n = 0; #20; + spi_byte(8'h02, rxb); // opcode READ_MEM + spi_byte(8'h00, rxb); spi_byte(8'h00, rxb); spi_byte(8'h55, rxb); // addr=0x000055 + spi_byte(8'h00, rxb); spi_byte(8'h01, rxb); // len_words=1 + #200; // idle SCLK while the read latency elapses + spi_byte(8'h00, rxb); exp_addr = rxb; // MSB + check(rxb == 8'h12, "D: READ_MEM MSB byte == 0x12"); + spi_byte(8'h00, rxb); + check(rxb == 8'h34, "D: READ_MEM LSB byte == 0x34"); + cs_n = 1; #40; + + // ================= Test E: RESET opcode ====================== + cs_n = 0; #20; + spi_byte(8'h0F, rxb); // opcode RESET + cs_n = 1; + begin : wait_soft_rst + integer wi; reg seen; + seen = 1'b0; + for (wi = 0; wi < 10; wi = wi + 1) begin + @(posedge clk); + if (soft_rst_pulse) seen = 1'b1; + end + check(seen, "E: soft_rst_pulse asserted after CS rises (within CDC latency)"); + end + + $display("=== tb_spi_host_bridge: %0d/%0d PASS ===", tests-errors, tests); + if (errors != 0) $display("*** %0d FAILURES ***", errors); + $finish; + end + +endmodule