feat: MILESTONE - real activation-fetch engine, full N=2 system verified on real DDR3 (EXP-0079)

Closes the last major disclosed functional gap: packed_slot.v's
activation data was read through a combinational stand-in since
EXP-0062. New act_tile_fetch.v reads activation tiles directly from
DDR3 (no on-chip buffering needed, unlike weights -- activation data
has no reuse), sharing each slot's existing ctrl port with its own
weight-prefetch engine. Real memory layout: one full BURST_LEN=8-word
burst per tile, deliberately avoiding any runtime-indexed part-select
given this project's thin P&R timing margin (EXP-0078).

Verified at three levels: act_tile_fetch.v alone (6/6), packed_slot.v
with real preloaded activation data (9/9), and the full N=2 system
against real DDR3 via xsim (8/8, 0 errors) -- the first time this
project's compute path has been verified end-to-end with real DDR3
for both weights and activations.

Retired hardware/v3/rtl/n2_system_top.v and its testbench (pre-DDR3
SDR-placeholder era, fully superseded by n2_system_ddr3_top.v).

Also: docs/PHYSICAL_REALIZATION.md (real pinout/parts/timing/protocol
reference for the physical board) and CLAUDE.md (persistent project
instructions for future Claude Code sessions).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUG92aM9m68TRc4rG55BcC
This commit is contained in:
2026-09-20 08:13:15 +02:00
co-authored by Claude Sonnet 5
parent 78577dde59
commit 43a12379a5
11 changed files with 946 additions and 682 deletions
+101
View File
@@ -0,0 +1,101 @@
# 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.
- `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.** If a project file
under `NeuralProcessor.srcs/sources_1/imports/...` 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 results (EXP-0078). 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.
- **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.
- **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.
+225
View File
@@ -0,0 +1,225 @@
# FPGA-Neural V3 — Physical Realization Reference
Real, verified data for the custom PCB (bare XC7A100T-CSG324-2 + DDR3, no dev
board). Every pin/part/setting below comes from a real Vivado-generated
constraint file, a real datasheet, or a real place-and-route run — none of it
is guessed. See `hardware/v2/logs/experiments.log` (EXP-0059 onward) for the
full derivation history.
## 1. Core components (real, verified availability)
| Component | Part | Notes |
|---|---|---|
| FPGA | **XC7A100T-CSG324-2** | Speed grade **-2** (corrected from an initial -1 assumption, EXP-0074) — same die/package/footprint as -1, strictly better timing margin. |
| DDR3 SDRAM | **Micron MT41J128M16JT-125:K** | 2Gb, x16, DDR3-1600-rated (run at 310.078MHz here due to -2 timing closure, see §3). Verified in-stock on LCSC. |
| Config flash | **Winbond W25Q32JVSSIQ** | 32Mbit/4MB, SOIC-8. Comfortably fits the ~30.5Mbit full XC7A100T bitstream. Verified in-stock on LCSC. Wired **exclusively** to the FPGA (see §5). |
## 2. FPGA pin assignments (real, from the routed design)
### 2.1 DDR3 (fixed by the FPGA's own PHY hardware — not a free choice)
Generated by the Vivado MIG wizard (`mig_7series_0.xdc`), all `SSTL15` /
`DIFF_SSTL15` (1.5V), banks 34/35:
| Signal | Pin | Signal | Pin | Signal | Pin |
|---|---|---|---|---|---|
| ddr3_dq[0] | G4 | ddr3_dq[8] | M1 | ddr3_addr[0] | B1 |
| ddr3_dq[1] | G3 | ddr3_dq[9] | K3 | ddr3_addr[1] | A3 |
| ddr3_dq[2] | J3 | ddr3_dq[10] | L3 | ddr3_addr[2] | A4 |
| ddr3_dq[3] | J2 | ddr3_dq[11] | M3 | ddr3_addr[3] | B4 |
| ddr3_dq[4] | K2 | ddr3_dq[12] | M2 | ddr3_addr[4] | C4 |
| ddr3_dq[5] | K1 | ddr3_dq[13] | K5 | ddr3_addr[5] | E7 |
| ddr3_dq[6] | H6 | ddr3_dq[14] | L4 | ddr3_addr[6] | E5 |
| ddr3_dq[7] | H5 | ddr3_dq[15] | L6 | ddr3_addr[7] | E6 |
| ddr3_addr[8] | C7 | ddr3_addr[9] | D8 | ddr3_addr[10] | B6 |
| ddr3_addr[11] | B7 | ddr3_addr[12] | C5 | ddr3_addr[13] | C6 |
| ddr3_ba[0] | B2 | ddr3_ba[1] | B3 | ddr3_ba[2] | A1 |
| ddr3_ras_n | D5 | ddr3_cas_n | D4 | ddr3_we_n | E3 |
| ddr3_reset_n | F6 (LVCMOS15) | ddr3_cke[0] | D7 | ddr3_odt[0] | H2 |
| ddr3_cs_n[0] | D3 | ddr3_dm[0] | G6 | ddr3_dm[1] | L1 |
| ddr3_dqs_p[0] | J4 (DIFF) | ddr3_dqs_n[0] | H4 (DIFF) | | |
| ddr3_dqs_p[1] | N2 (DIFF) | ddr3_dqs_n[1] | N1 (DIFF) | | |
| ddr3_ck_p[0] | A6 (DIFF) | ddr3_ck_n[0] | A5 (DIFF) | | |
| sys_clk_i | E2 (SSTL15, bank 35) | clk_ref_i | C9 (LVCMOS25, bank 16) | | |
**Bank voltage requirements**: bank 34/35 → **1.5V** (DDR3 SSTL15), bank 16 →
**2.5V** (clk_ref_i, LVCMOS25).
`INTERNAL_VREF` for banks 34/35 is set to 0.750V by the MIG constraints
(required for SSTL15 single-ended inputs) — this is a Vivado-side setting,
not a board component, but note it if you ever inspect bitstream generation
warnings about VREF.
### 2.2 Neural-processor management SPI (ESP32 ↔ FPGA, FPGA is **slave**)
Bank 15, package edge column A/B, physically adjacent (short traces), `LVCMOS33`:
| Signal | Pin | Direction (FPGA side) |
|---|---|---|
| sclk | A15 | input |
| mosi | B16 | input |
| miso | B17 | output |
| cs_n | A16 | input |
**Bank 15 VCCO**: assumed **3.3V** — change the XDC's IOSTANDARD if your
board power plan uses a different rail for this bank.
### 2.3 Config flash SPI (FPGA ↔ flash, FPGA is **master**)
These are the FPGA's own dedicated Master-SPI configuration pins, **reclaimed
as ordinary fabric I/O after configuration completes** (requires
`BITSTREAM.CONFIG.PERSIST = FALSE`, the Vivado default — already set
explicitly in the project XDC). Bank 14, `LVCMOS33`:
| Signal | Pin | Direction (FPGA side) | Notes |
|---|---|---|---|
| flash_mosi | K17 | output | = D00_MOSI (config pin, reclaimed) |
| flash_miso | K18 | input | = D01_DIN (config pin, reclaimed) |
| flash_cs_n | L13 | output | = FCS_B (config pin, reclaimed) |
| (CCLK) | E9 | output | **Not a top-level port** — driven internally via the `STARTUPE2` primitive. Wire the flash's own CLK pin to package pin **E9**. |
**Bank 14 VCCO**: assumed **3.3V** (matches the flash's own VCC, typically
1.83.6V for the W25Q32JV — check its datasheet's exact operating range
against whatever VCCO you choose for bank 14).
**Reserved, do not use** (bank 14, same reasons as above but unused by this
design — kept clear for any future Quad-SPI/BPI expansion): `L16` (EMCCLK),
`R16` (RDWR_B), `V15` (CSI_B). The project's own XDC `PROHIBIT`s these so
Vivado's auto-placement never claims them by accident.
### 2.4 FPGA configuration control (dedicated, bank 0, not negotiable)
| Signal | Pin | Purpose |
|---|---|---|
| PROGRAM_B | P9 | pulse low to force a full reconfiguration from flash |
| INIT_B | P7 | goes low during config; can indicate a config error if it re-asserts |
| DONE | P10 | goes high once configuration succeeds — wire to a status LED if desired |
| M0 | P12 | mode select |
| M1 | P13 | mode select |
| M2 | P11 | mode select |
| CFGBVS | P8 | tie to match bank 0's VCCO logic level (see UG470) |
**Mode pin setting for Master SPI boot** (the flash-based autonomous boot
path, see §5): `M[2:0] = 001` (per UG470's mode pin table) — tie via pull-up/
pull-down resistors on the board, not driven dynamically.
### 2.5 JTAG (always available, independent of flash content)
| Signal | Pin |
|---|---|
| TCK | E10 |
| TDI | E11 |
| TMS | E12 |
| TDO | E13 |
Used for: (a) first-ever/factory programming when the flash is blank (see
§5), (b) recovery, (c) development/debug. This project's own plan drives
these from an ESP32 doing real JTAG bit-banging (TAP state machine, IR/DR
shifting) rather than a bench programmer — that firmware is separate,
software-side work, not covered here.
## 3. Real timing signoff (EXP-0078, the current, trustworthy number)
Real in-context Vivado place-and-route (not out-of-context, not estimated):
| Metric | Value |
|---|---|
| DDR3 PHY clock (sys_clk_i) | **310.078 MHz** (3.225ns period) |
| Compute domain clock (ui_clk, PLL-derived 2:1 from sys_clk_i) | **155.039 MHz** |
| WNS (setup slack) | **+0.013 ns** — real, but very thin. Re-verify with a fresh P&R after ANY further logic addition. |
| WHS (hold slack) | +0.032 ns |
| Failing endpoints | 0 / 17473 (setup), 0 / 17470 (hold) |
| LUTs used | 5213 / 63400 (8.22%) |
| DSP48E1 used | 16 / 240 (6.67%) — 8 per compute core × 2 cores |
| Block RAM used | 0 |
| STARTUPE2 used | 1 / 1 (100%) — the config-flash bridge |
## 4. Real DDR3 memory layout convention
Both weight data and activation data share the same DDR3 address space
(word-addressed, 16-bit words, `BURST_LEN=8` per transaction = 128 bits/burst).
- **Weights**: one layer's weight set starts at word address `layer_index *
WORDS_PER_LAYER` (`WORDS_PER_LAYER = LAYER_BYTES/2`). Densely packed —
`layer_prefetch_ctrl.v` reads full bursts sequentially into the on-chip
weight buffer once per job.
- **Activations** (real engine since EXP-0079, `act_tile_fetch.v`): **each
tile (P_IN=8 INT8 values) occupies its own full `BURST_LEN=8`-word
(128-bit) burst slot** — the 8 useful bytes sit in the low 64 bits, the
upper 64 bits are unused padding. This is deliberately 2× wasteful of DDR3
capacity, in exchange for needing zero runtime-indexed bit-selects in the
fetch logic (a real Fmax risk this project's thin P&R margin, §3, can't
currently afford). Tile `t`'s word address is `base + t*BURST_LEN`, always
burst-aligned by construction.
- `base` (a job's own `x_base_a`/`x_base_b`) is chosen freely by whoever
submits jobs (the SPI host) — just keep each position's own activation
array in its own non-overlapping `N_TILES * BURST_LEN`-word region.
## 5. FPGA configuration (boot) procedure
Two complementary paths, both present on this board by design:
1. **Factory-first / recovery (JTAG, ESP32-driven)**: the flash starts
blank on a fresh board — no other path can bootstrap it (a real chicken-
and-egg constraint: the FPGA can't relay flash-programming commands over
SPI, §5.2, until it's already running logic that does that). The ESP32
bit-bangs JTAG (§2.5) to load a bitstream directly, or to run Vivado's own
"indirect SPI flash programming" sequence to write the flash for the
first time. One-time (or rare/recovery-only) step.
2. **Normal boot (Master SPI, autonomous)**: every subsequent power-on, the
FPGA self-configures from the flash via its own dedicated hardware (mode
pins set to Master SPI, §2.4) — no ESP32 involvement needed.
3. **Field firmware updates (SPI-through-FPGA, `FLASH_XFER` opcode 0x40)**:
once the FPGA is running, the ESP32 can rewrite the flash by relaying raw
SPI-NOR bytes through the FPGA over the management SPI bus (§2.2) — the
FPGA then re-transmits them as master on the flash bus (§2.3). This is the
**only** electrical path from ESP32 to the flash; there is no direct
connection (by design, per explicit requirement).
- **Real SPI-NOR opcodes** (verified against the actual W25Q32JV
datasheet, for whoever writes the ESP32-side flashing routine):
`0x06` Write Enable, `0x04` Write Disable, `0x05` Read Status Register-1
(bit0=BUSY, bit1=WEL), `0x02` Page Program, `0x03` Read Data, `0x20`
Sector Erase (4KB), `0x52` 32KB Block Erase, `0xD8` 64KB Block Erase,
`0xC7`/`0x60` Chip Erase.
- **Protocol timing note**: `FLASH_XFER` relays are NOT instantaneous —
each relayed byte's real flash response is only stable starting **two**
host-clocked bytes later (not one), so the host must clock 2 trailing
dummy bytes after its last real command byte to safely receive the
final response. See `spi_host_bridge_v3.v`'s own header for the full
real-measured reasoning (EXP-0077).
- After writing a new bitstream to the flash, reconfigure either by
pulsing `PROGRAM_B` externally, or (future work, not built yet) via a
`ICAPE2`-based warm self-reconfiguration triggered over the same SPI bus.
## 6. Management SPI protocol summary (for ESP32 firmware)
One opcode byte (MSB-first) per CS-low transaction, driven by
`spi_host_bridge_v3.v`:
| Opcode | Name | Payload | Purpose |
|---|---|---|---|
| 0x00 | NOP | 0 bytes | inert |
| 0x0F | RESET | 0 bytes | pulses a soft-reset |
| 0x10 | WRITE_JOB | 16 bytes | submit one inference job (node_id, x_base, w_base, n_tiles, result_addr) |
| 0x20 | STATUS | 0 bytes → 1 byte out | job_busy / mem_busy / last_job_accepted bits |
| 0x01 | WRITE_MEM | 4+2N bytes | raw DDR3 word write (N words) |
| 0x02 | READ_MEM | 6 bytes → 2N bytes out | raw DDR3 word read (N words) |
| 0x30 | REG_WRITE | 5 bytes | write a control register |
| 0x31 | REG_READ | 1 byte → 4 bytes out | read a status/ID register (0x00 DEVICE_ID, 0x01 CONTROL, 0x02 STATUS incl. DDR3-ready + Director-error, 0x03 N_SLOTS) |
| 0x40 | FLASH_XFER | N bytes → N bytes out (+2 margin) | raw passthrough to the config flash, see §5.3 |
Full byte-level field layouts are documented in `spi_host_bridge_v3.v`'s own
header comment — treat that file as the authoritative protocol spec, this
table is a summary/index.
## 7. Known-open items (honestly disclosed, not hidden)
- Scaling past N=2 compute cores (silicon budget allows up to ~30 per the
DSP48E1 count) is not yet built or timing-verified.
- The reset pin and other very-low-pin-count signals have no fixed PCB
location yet — assign once the rest of the board layout (reset circuit,
status LEDs, etc.) is decided.
- The §3 timing margin (+0.013ns) is real but thin — do not add logic
without a fresh real P&R to confirm it still closes.
- ESP32-side JTAG bit-banging firmware (§5.1) does not exist yet — it's
software work on the host side, not part of this FPGA RTL.
+98
View File
@@ -4933,3 +4933,101 @@ checkpoint. Remaining open work (scaling past N=2, a real activation-
fetch engine, PCB-specific pin constraints once the board layout is
underway, ESP32-side JTAG bootstrap firmware) is all disclosed and
outside this experiment's own scope.
EXP-0079 -- MILESTONE: real activation-fetch engine built, closing
the last major disclosed functional gap; full N=2 system re-verified
end-to-end with REAL DDR3 for BOTH weights and activations
(2026-09-20, same autonomous continuation, user's own explicit
request: "completiamo quello che manca per avere un codice ready to
use nell'hardware fisico")
CONTEXT: packed_slot.v's own header had disclosed, since EXP-0062,
that activation data was read through a combinational stand-in port
(act_tile_addr_a/b -> act_tile_data_a/b), with a real fetch engine
explicitly deferred. This was the single largest remaining gap between
"a verified compute architecture" and "a system that can actually run
on real data in real DDR3".
DESIGN: new hardware/v3/rtl/act_tile_fetch.v -- unlike the weight path
(prefetched once into an on-chip buffer, reused across many read-outs
per job), activation data has NO reuse (read exactly once per
position), so this engine reads DIRECTLY from DDR3 per tile, no
on-chip buffering. Reuses the SAME per-slot ctrl_req/addr/etc port
layer_prefetch_ctrl.v already owns (mutually exclusive in time by FSM
construction -- weight prefetch always fully completes before the
tile loop that needs activation data starts), muxed inside packed_slot.v
on a new act_mem_active signal. Two sequential burst reads per tile
request (lane A then lane B), with an explicit ctrl_busy wait between
them (mig_native_adapter.v's own S_DONE tail can keep busy asserted
one cycle past ready -- checked explicitly, not assumed safe).
MEMORY LAYOUT (a new, real, disclosed requirement): each tile occupies
its own full BURST_LEN=8-word burst slot (P_IN=8 bytes in the low 64
bits, upper 64 bits padding) -- deliberately 2x wasteful of DDR3
capacity, in exchange for needing ZERO runtime-indexed part-select in
the fetch logic (weight_tile_gather.v, EXP-0061, already flagged that
pattern as a real Fmax risk, and this project's P&R margin is
currently thin, EXP-0078 WNS +0.013ns -- not the moment to introduce a
new critical path). Documented in the new hardware/v3/constraints
physical doc for whoever prepares host-side data layout.
packed_slot.v's own S_TILEWAIT state was restructured into a real
two-source JOIN: latches (tile_seen/act_seen) independently track
whether the (fast, on-chip) weight tile and the (real-DDR3-latency)
activation tile have each arrived, proceeding to S_OPERAND only once
BOTH have been seen, correctly handling either arrival order (not just
the expected common case of weight-first).
VERIFICATION (three levels, matching this project's own "one variable
at a time" discipline):
1. hardware/v3/sim/tb_act_tile_fetch.v -- act_tile_fetch.v alone
against the SDR SDRAM placeholder: 6/6 PASS on the first real run
(no bugs found -- the nonblocking-assignment stimulus idiom,
already standard practice since EXP-0073/0075/0077, avoided the
testbench-race class that has bitten every PREVIOUS new module's
first draft in this project).
2. hardware/v3/sim/tb_packed_slot.v -- rewritten to preload REAL
activation data into the SDR placeholder (same technique already
used for weights) instead of a combinational lookup stand-in;
the OLD decimal-encoded x_base convention (li*100000+pos*1000)
was replaced by the new real word-address convention. 9/9 PASS,
0 errors, on the first real run after fixing one Verilog syntax
issue (can't part-select a function call's return value inline
in this dialect -- assign to a temp variable first).
3. hardware/v3/sim/tb_n2_system_ddr3.v -- the full real N=2 system
(Director + 2 packed_slot + arbiter + MIG + real ddr3_model.sv),
same update pattern, re-run via real xsim. **8/8 tests, 0 errors,
8/8 positions completed, bit-exact against the golden model --
the first time this project's compute path has been verified
end-to-end against REAL DDR3 for BOTH weights and activations,
not just weights.**
INTEGRATION: n2_system_ddr3_top.v (the real synthesis target) updated
to remove the old activation-stub top-level wiring entirely (the
free-running-counter stand-in from EXP-0074, itself a fix for an
earlier mistake of exposing act_addr/data as literal chip pins) --
activation fetch is now fully internal to each packed_slot instance,
using ports that already existed for other reasons. Net effect: FEWER
top-level signals than before, not more.
RETIRED (superseded, not fixed-in-place): hardware/v3/rtl/n2_system_top.v
and hardware/v3/sim/tb_np_director_n2_system.v (the pre-DDR3, SDR-
placeholder-era N=2 top/test, EXP-0066/0067) -- fully superseded by
n2_system_ddr3_top.v/tb_n2_system_ddr3.v, would have needed the exact
same class of update for zero forward benefit. Removed via `git rm`,
fully recoverable from git history if ever needed.
DECISION: this closes the last major disclosed FUNCTIONAL gap in the
V3 compute pipeline -- real DSP-packed cores, real weight-reuse
scheduling, real N-way arbitration, real DDR3 for BOTH weights and
activations, real host SPI protocol (jobs/registers/raw memory/config-
flash), all verified together end to end. What remains open (scaling
past N=2, PCB-specific pin finalization, ESP32 firmware) is genuinely
separate, disclosed, non-blocking work -- not a hidden correctness gap.
next_action: real in-context P&R re-verification (the activation
engine adds real logic on a path that matters -- EXP-0078's own margin
was already thin, +0.013ns, before this addition) -- must re-confirm
timing still closes before calling this "ready to use in physical
hardware". Also: finalize and commit docs/PHYSICAL_REALIZATION.md
(drafted this session, real pin/part/protocol/layout data).
+154
View File
@@ -0,0 +1,154 @@
`timescale 1ns/1ps
// ============================================================
// V3 -- act_tile_fetch.v: REAL activation-tile fetch engine, closing
// the gap packed_slot.v's own header has disclosed since EXP-0062
// ("a real activation fetch engine ... is a separate, later
// deliverable, NOT built here"). This is that deliverable.
//
// WHY A SEPARATE, SIMPLE ENGINE (not a prefetch/buffer pair like the
// weight path): weights are reused across M reuse-positions per
// Director-dispatched pair, so prefetching them once into an on-chip
// buffer (layer_prefetch_ctrl.v/layer_weight_buffer.v) amortizes real
// DDR3 latency across many reads. Activation data has NO such reuse
// -- each position's activation tile is read exactly once per job --
// so buffering it on-chip would only add complexity for zero benefit.
// This engine reads DIRECTLY from DDR3 per tile instead.
//
// MEMORY LAYOUT CONVENTION (real, disclosed, and REQUIRED of whoever
// prepares activation data in DDR3 -- documented in the physical
// realization doc too): each activation tile (P_IN=8 INT8 values)
// occupies its OWN full BURST_LEN=8-word (128-bit) burst slot, in the
// LOW 64 bits, upper 64 bits unused padding. Tile index t's word
// address is therefore `base + t*BURST_LEN`, always burst-aligned by
// construction. This is DELIBERATELY wasteful of DDR3 capacity (2x)
// in exchange for AVOIDING a runtime-indexed part-select to pick
// which half of a shared burst holds the tile -- weight_tile_gather.v
// already established (EXP-0061) that pattern is a real Fmax risk,
// and this project's own P&R margin is currently thin (EXP-0078,
// WNS +0.013ns) -- not the moment to introduce a new critical path.
// A future denser packing (2 tiles/burst, real part-select) is a
// disclosed, deliberate follow-up, not done here.
//
// PROTOCOL: one request (`req` pulse + base_a/base_b/tcnt) triggers
// TWO SEQUENTIAL burst reads (lane A then lane B) over the SAME
// shared ctrl port packed_slot.v already owns -- reusing the EXACT
// port layer_prefetch_ctrl.v uses during S_PREFETCH, since that
// phase has already finished (weight data is on-chip by the time
// this engine runs) and the port is genuinely free. Follows the same
// combinational-first-grant discipline as every other one-shot-pulse
// requester in this project (EXP-0066): `mem_active` must be visible
// to the arbiter the SAME cycle it asserts, `ctrl_req` is only issued
// after `mem_grant` is observed, never blind.
// ============================================================
module act_tile_fetch #(
parameter DATA_WIDTH = 8,
parameter P_IN = 8,
parameter BURST_LEN = 8,
parameter ADDR_WIDTH = 25 // word address, matches the shared ctrl port's own convention
)(
input wire clk,
input wire rst,
input wire req, // one-shot pulse
input wire [ADDR_WIDTH-1:0] base_a,
input wire [ADDR_WIDTH-1:0] base_b,
input wire [15:0] tcnt,
output reg valid, // one-cycle pulse, data_a/data_b valid
output reg signed [DATA_WIDTH*P_IN-1:0] data_a,
output reg signed [DATA_WIDTH*P_IN-1:0] data_b,
output wire mem_active,
input wire mem_grant,
output reg ctrl_req,
output reg ctrl_wr,
output reg [ADDR_WIDTH-1:0] ctrl_addr,
output wire [16*BURST_LEN-1:0] ctrl_wdata,
output wire [2*BURST_LEN-1:0] ctrl_wmask,
input wire [16*BURST_LEN-1:0] ctrl_rdata,
input wire ctrl_ready,
input wire ctrl_busy
);
assign ctrl_wdata = {(16*BURST_LEN){1'b0}};
assign ctrl_wmask = {(2*BURST_LEN){1'b0}}; // read-only engine, mask unused
localparam S_IDLE = 3'd0,
S_MEMWAIT = 3'd1,
S_REQ_A = 3'd2,
S_GAP = 3'd3, // wait for ctrl_busy to clear before firing lane B's request
S_REQ_B = 3'd4;
reg [2:0] state;
reg [ADDR_WIDTH-1:0] base_a_lat, base_b_lat;
reg [15:0] tcnt_lat;
assign mem_active = (state != S_IDLE);
wire [ADDR_WIDTH-1:0] tile_offset = {{(ADDR_WIDTH-16){1'b0}}, tcnt_lat} * BURST_LEN[ADDR_WIDTH-1:0];
always @(posedge clk) begin
if (rst) begin
state <= S_IDLE;
ctrl_req <= 1'b0; ctrl_wr <= 1'b0; ctrl_addr <= {ADDR_WIDTH{1'b0}};
valid <= 1'b0; data_a <= {(DATA_WIDTH*P_IN){1'b0}}; data_b <= {(DATA_WIDTH*P_IN){1'b0}};
base_a_lat <= {ADDR_WIDTH{1'b0}}; base_b_lat <= {ADDR_WIDTH{1'b0}}; tcnt_lat <= 16'd0;
end else begin
ctrl_req <= 1'b0;
valid <= 1'b0;
case (state)
S_IDLE: begin
if (req) begin
base_a_lat <= base_a;
base_b_lat <= base_b;
tcnt_lat <= tcnt;
state <= S_MEMWAIT;
end
end
S_MEMWAIT: begin
if (mem_grant) begin
ctrl_addr <= base_a_lat + tile_offset;
ctrl_wr <= 1'b0;
ctrl_req <= 1'b1;
state <= S_REQ_A;
end
end
S_REQ_A: begin
if (ctrl_ready) begin
data_a <= ctrl_rdata[0 +: DATA_WIDTH*P_IN];
ctrl_addr <= base_b_lat + tile_offset;
ctrl_wr <= 1'b0;
state <= S_GAP;
end
end
S_GAP: begin
// the shared controller may still be finishing its
// own internal completion sequence for lane A's
// request for one more cycle after ctrl_ready
// pulsed (mig_native_adapter.v's own S_DONE state
// keeps `busy` asserted through it) -- wait for
// !ctrl_busy before firing lane B's request,
// instead of assuming back-to-back is safe.
if (!ctrl_busy) begin
ctrl_req <= 1'b1;
state <= S_REQ_B;
end
end
S_REQ_B: begin
if (ctrl_ready) begin
data_b <= ctrl_rdata[0 +: DATA_WIDTH*P_IN];
valid <= 1'b1;
state <= S_IDLE;
end
end
default: state <= S_IDLE;
endcase
end
end
endmodule
+8 -28
View File
@@ -260,30 +260,14 @@ module n2_system_ddr3_top #(
wire [15:0] s0_nid_a, s0_nid_b, s1_nid_a, s1_nid_b;
wire [JOB_ADDR_WIDTH-1:0] s0_raddr_a, s0_raddr_b, s1_raddr_a, s1_raddr_b;
// ---- activation-fetch STUB (disclosed gap, see packed_slot.v's
// own header: no real activation-fetch engine exists yet). Kept
// fully INTERNAL rather than exposed as top-level chip pins --
// exposing act_addr/act_data literally as pins was a real mistake
// caught by this same P&R run: s0/s1's act_addr_a/b (JOB_ADDR_
// WIDTH=26 bits x4) + act_data_a/b (DATA_WIDTH*P_IN=64 bits x4)
// alone demand ~360 I/O, but XC7A100T-CSG324 has only 324 pins
// total (DDR3 alone already uses ~53) -- place_design failed with
// "IO Clock Placer failed" for exactly this reason. A free-
// running counter-addressed pattern stands in for real activation
// data until a real fetch engine (DDR3-backed, like the weight
// path) is built; this keeps real timing/placement meaningful for
// everything else in this P&R run without claiming activation
// fetch is solved.
wire [JOB_ADDR_WIDTH-1:0] s0_act_addr_a, s0_act_addr_b, s1_act_addr_a, s1_act_addr_b;
reg [DATA_WIDTH*P_IN-1:0] act_stub_reg;
always @(posedge ui_clk)
if (ui_clk_sync_rst) act_stub_reg <= {(DATA_WIDTH*P_IN){1'b0}};
else act_stub_reg <= act_stub_reg + 1'b1;
wire signed [DATA_WIDTH*P_IN-1:0] s0_act_data_a = act_stub_reg;
wire signed [DATA_WIDTH*P_IN-1:0] s0_act_data_b = act_stub_reg;
wire signed [DATA_WIDTH*P_IN-1:0] s1_act_data_a = act_stub_reg;
wire signed [DATA_WIDTH*P_IN-1:0] s1_act_data_b = act_stub_reg;
// ---- activation fetch: REAL now (EXP-0079) -- each packed_slot
// instance owns its own act_tile_fetch.v internally, sharing that
// SAME slot's existing ctrl_req/addr/etc port (already wired to
// the arbiter below) with its own weight-prefetch engine. No
// top-level activation ports exist any more -- the old stand-in
// (act_tile_addr_a/b -> act_tile_data_a/b, and before that, a
// free-running counter stub that nearly blew the package's whole
// I/O budget, see git history) is gone; this is fully internal.
packed_slot #(
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH),
.BURST_LEN(BURST_LEN), .ADDR_WIDTH(JOB_ADDR_WIDTH), .LAYER_BYTES(LAYER_BYTES)
@@ -302,8 +286,6 @@ module n2_system_ddr3_top #(
.result_node_id_a(s0_nid_a), .result_node_id_b(s0_nid_b),
.result_addr_a_out(s0_raddr_a), .result_addr_b_out(s0_raddr_b),
.mem_active(req_active[0]), .mem_grant(req_grant[0]),
.act_tile_addr_a(s0_act_addr_a), .act_tile_addr_b(s0_act_addr_b),
.act_tile_data_a(s0_act_data_a), .act_tile_data_b(s0_act_data_b),
.ctrl_req(req_req[0]), .ctrl_wr(req_wr[0]),
.ctrl_addr(req_addr[0*MEM_ADDR_WIDTH +: MEM_ADDR_WIDTH]),
.ctrl_wdata(req_wdata[0*16*BURST_LEN +: 16*BURST_LEN]),
@@ -330,8 +312,6 @@ module n2_system_ddr3_top #(
.result_node_id_a(s1_nid_a), .result_node_id_b(s1_nid_b),
.result_addr_a_out(s1_raddr_a), .result_addr_b_out(s1_raddr_b),
.mem_active(req_active[1]), .mem_grant(req_grant[1]),
.act_tile_addr_a(s1_act_addr_a), .act_tile_addr_b(s1_act_addr_b),
.act_tile_data_a(s1_act_data_a), .act_tile_data_b(s1_act_data_b),
.ctrl_req(req_req[1]), .ctrl_wr(req_wr[1]),
.ctrl_addr(req_addr[1*MEM_ADDR_WIDTH +: MEM_ADDR_WIDTH]),
.ctrl_wdata(req_wdata[1*16*BURST_LEN +: 16*BURST_LEN]),
-192
View File
@@ -1,192 +0,0 @@
`timescale 1ns/1ps
// ============================================================
// V3 -- synthesis top for the EXP-0066 verified N=2 multi-core
// system: neural_director_packed.v + 2 real packed_slot.v instances
// + sdram_slot_arbiter2.v + real sdram_controller.v, flat structural
// wiring, for a real P&R resource/timing check (same out-of-context
// methodology as EXP-0059/0063).
//
// Activation stand-in ports (see packed_slot.v's own header) are
// exposed per-slot at the top level, matching this module's own
// still-declared scope limit (no real activation fetch engine yet).
// ============================================================
module n2_system_top #(
parameter DATA_WIDTH = 8,
parameter P_IN = 8,
parameter ACC_WIDTH = 32,
parameter BURST_LEN = 8,
parameter ROW_BITS = 13,
parameter COL_BITS = 10,
parameter BANK_BITS = 2,
parameter SDRAM_ADDR_WIDTH = BANK_BITS + ROW_BITS + COL_BITS,
parameter ADDR_WIDTH = 26,
parameter LAYER_BYTES = 128,
parameter N_SLOTS = 2,
parameter QUEUE_DEPTH = 8
)(
input wire clk,
input wire rst,
// ---- Director job submission ----
input wire job_in_valid,
output wire job_in_ready,
input wire [ADDR_WIDTH-1:0] job_in_x_base,
input wire [ADDR_WIDTH-1:0] job_in_w_base,
input wire [15:0] job_in_n_tiles,
input wire [ADDR_WIDTH-1:0] job_in_result_addr,
input wire [15:0] job_in_node_id,
output wire job_out_done,
output wire [$clog2(N_SLOTS)-1:0] job_out_slot,
// ---- activation stand-ins, slot 0 ----
output wire [ADDR_WIDTH-1:0] s0_act_addr_a,
output wire [ADDR_WIDTH-1:0] s0_act_addr_b,
input wire signed [DATA_WIDTH*P_IN-1:0] s0_act_data_a,
input wire signed [DATA_WIDTH*P_IN-1:0] s0_act_data_b,
output wire signed [DATA_WIDTH-1:0] s0_result_data_a,
output wire signed [DATA_WIDTH-1:0] s0_result_data_b,
// ---- activation stand-ins, slot 1 ----
output wire [ADDR_WIDTH-1:0] s1_act_addr_a,
output wire [ADDR_WIDTH-1:0] s1_act_addr_b,
input wire signed [DATA_WIDTH*P_IN-1:0] s1_act_data_a,
input wire signed [DATA_WIDTH*P_IN-1:0] s1_act_data_b,
output wire signed [DATA_WIDTH-1:0] s1_result_data_a,
output wire signed [DATA_WIDTH-1:0] s1_result_data_b,
// ---- real SDRAM pins ----
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 [BANK_BITS-1:0] sdram_ba,
output wire [ROW_BITS-1:0] sdram_a,
inout wire [15:0] sdram_dq,
output wire [1:0] sdram_dqm
);
localparam BUFADDRW = $clog2(LAYER_BYTES);
wire [N_SLOTS-1:0] slot_job_start;
wire [ADDR_WIDTH*N_SLOTS-1:0] slot_x_base_a, slot_x_base_b, slot_w_base;
wire [ADDR_WIDTH*N_SLOTS-1:0] slot_result_addr_a, slot_result_addr_b;
wire [16*N_SLOTS-1:0] slot_n_tiles, slot_node_id_a, slot_node_id_b;
wire [N_SLOTS-1:0] slot_job_done;
wire [3:0] dir_state;
wire dir_error;
wire queue_empty;
neural_director_packed #(
.ADDR_WIDTH(ADDR_WIDTH), .N_SLOTS(N_SLOTS), .QUEUE_DEPTH(QUEUE_DEPTH)
) u_dir (
.clk(clk), .rst(rst),
.job_in_valid(job_in_valid), .job_in_ready(job_in_ready),
.job_in_x_base(job_in_x_base), .job_in_w_base(job_in_w_base),
.job_in_n_tiles(job_in_n_tiles), .job_in_result_addr(job_in_result_addr),
.job_in_node_id(job_in_node_id),
.slot_job_start(slot_job_start),
.slot_x_base_a(slot_x_base_a), .slot_x_base_b(slot_x_base_b),
.slot_w_base(slot_w_base), .slot_n_tiles(slot_n_tiles),
.slot_result_addr_a(slot_result_addr_a), .slot_result_addr_b(slot_result_addr_b),
.slot_node_id_a(slot_node_id_a), .slot_node_id_b(slot_node_id_b),
.slot_job_done(slot_job_done),
.job_out_done(job_out_done), .job_out_slot(job_out_slot),
.dir_state(dir_state), .dir_error(dir_error), .queue_empty(queue_empty)
);
wire [1:0] mem_active, mem_grant;
wire [1:0] s_ctrl_req, s_ctrl_wr;
wire [SDRAM_ADDR_WIDTH-1:0] s0_ctrl_addr, s1_ctrl_addr;
wire [16*BURST_LEN-1:0] s0_ctrl_wdata, s1_ctrl_wdata;
wire [2*BURST_LEN-1:0] s0_ctrl_wmask, s1_ctrl_wmask;
wire [16*BURST_LEN-1:0] s0_ctrl_rdata, s1_ctrl_rdata;
wire [1:0] s_ctrl_ready, s_ctrl_busy;
wire ctrl_req, ctrl_wr;
wire [SDRAM_ADDR_WIDTH-1:0] ctrl_addr;
wire [16*BURST_LEN-1:0] ctrl_wdata, ctrl_rdata;
wire [2*BURST_LEN-1:0] ctrl_wmask;
wire ctrl_ready, ctrl_busy;
sdram_slot_arbiter2 #(.ADDR_WIDTH(SDRAM_ADDR_WIDTH), .BURST_LEN(BURST_LEN)) u_arb (
.clk(clk), .rst(rst),
.slot0_active(mem_active[0]), .slot0_grant(mem_grant[0]),
.slot0_req(s_ctrl_req[0]), .slot0_wr(s_ctrl_wr[0]),
.slot0_addr(s0_ctrl_addr), .slot0_wdata(s0_ctrl_wdata), .slot0_wmask(s0_ctrl_wmask),
.slot0_rdata(s0_ctrl_rdata), .slot0_ready(s_ctrl_ready[0]), .slot0_busy(s_ctrl_busy[0]),
.slot1_active(mem_active[1]), .slot1_grant(mem_grant[1]),
.slot1_req(s_ctrl_req[1]), .slot1_wr(s_ctrl_wr[1]),
.slot1_addr(s1_ctrl_addr), .slot1_wdata(s1_ctrl_wdata), .slot1_wmask(s1_ctrl_wmask),
.slot1_rdata(s1_ctrl_rdata), .slot1_ready(s_ctrl_ready[1]), .slot1_busy(s_ctrl_busy[1]),
.ctrl_req(ctrl_req), .ctrl_wr(ctrl_wr), .ctrl_addr(ctrl_addr),
.ctrl_wdata(ctrl_wdata), .ctrl_wmask(ctrl_wmask),
.ctrl_rdata(ctrl_rdata), .ctrl_ready(ctrl_ready), .ctrl_busy(ctrl_busy)
);
sdram_controller #(
.CLK_FREQ_MHZ(64), .BURST_LEN(BURST_LEN),
.ROW_BITS(ROW_BITS), .COL_BITS(COL_BITS), .BANK_BITS(BANK_BITS)
) u_ctrl (
.clk(clk), .rst(rst),
.req(ctrl_req), .wr(ctrl_wr), .addr(ctrl_addr),
.wdata(ctrl_wdata), .wmask(ctrl_wmask),
.rdata(ctrl_rdata), .ready(ctrl_ready), .busy(ctrl_busy),
.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)
);
wire [15:0] s0_nid_a, s0_nid_b, s1_nid_a, s1_nid_b;
wire [ADDR_WIDTH-1:0] s0_raddr_a, s0_raddr_b, s1_raddr_a, s1_raddr_b;
packed_slot #(
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH),
.BURST_LEN(BURST_LEN), .ADDR_WIDTH(ADDR_WIDTH), .LAYER_BYTES(LAYER_BYTES)
) u_slot0 (
.clk(clk), .rst(rst),
.job_start(slot_job_start[0]),
.x_base_a(slot_x_base_a[0*ADDR_WIDTH +: ADDR_WIDTH]),
.x_base_b(slot_x_base_b[0*ADDR_WIDTH +: ADDR_WIDTH]),
.w_base(slot_w_base[0*ADDR_WIDTH +: ADDR_WIDTH]),
.n_tiles(slot_n_tiles[0*16 +: 16]),
.result_addr_a(slot_result_addr_a[0*ADDR_WIDTH +: ADDR_WIDTH]),
.result_addr_b(slot_result_addr_b[0*ADDR_WIDTH +: ADDR_WIDTH]),
.node_id_a(slot_node_id_a[0*16 +: 16]), .node_id_b(slot_node_id_b[0*16 +: 16]),
.job_done(slot_job_done[0]),
.result_data_a(s0_result_data_a), .result_data_b(s0_result_data_b),
.result_node_id_a(s0_nid_a), .result_node_id_b(s0_nid_b),
.result_addr_a_out(s0_raddr_a), .result_addr_b_out(s0_raddr_b),
.mem_active(mem_active[0]), .mem_grant(mem_grant[0]),
.act_tile_addr_a(s0_act_addr_a), .act_tile_addr_b(s0_act_addr_b),
.act_tile_data_a(s0_act_data_a), .act_tile_data_b(s0_act_data_b),
.ctrl_req(s_ctrl_req[0]), .ctrl_wr(s_ctrl_wr[0]), .ctrl_addr(s0_ctrl_addr),
.ctrl_wdata(s0_ctrl_wdata), .ctrl_wmask(s0_ctrl_wmask),
.ctrl_rdata(s0_ctrl_rdata), .ctrl_ready(s_ctrl_ready[0]), .ctrl_busy(s_ctrl_busy[0])
);
packed_slot #(
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH),
.BURST_LEN(BURST_LEN), .ADDR_WIDTH(ADDR_WIDTH), .LAYER_BYTES(LAYER_BYTES)
) u_slot1 (
.clk(clk), .rst(rst),
.job_start(slot_job_start[1]),
.x_base_a(slot_x_base_a[1*ADDR_WIDTH +: ADDR_WIDTH]),
.x_base_b(slot_x_base_b[1*ADDR_WIDTH +: ADDR_WIDTH]),
.w_base(slot_w_base[1*ADDR_WIDTH +: ADDR_WIDTH]),
.n_tiles(slot_n_tiles[1*16 +: 16]),
.result_addr_a(slot_result_addr_a[1*ADDR_WIDTH +: ADDR_WIDTH]),
.result_addr_b(slot_result_addr_b[1*ADDR_WIDTH +: ADDR_WIDTH]),
.node_id_a(slot_node_id_a[1*16 +: 16]), .node_id_b(slot_node_id_b[1*16 +: 16]),
.job_done(slot_job_done[1]),
.result_data_a(s1_result_data_a), .result_data_b(s1_result_data_b),
.result_node_id_a(s1_nid_a), .result_node_id_b(s1_nid_b),
.result_addr_a_out(s1_raddr_a), .result_addr_b_out(s1_raddr_b),
.mem_active(mem_active[1]), .mem_grant(mem_grant[1]),
.act_tile_addr_a(s1_act_addr_a), .act_tile_addr_b(s1_act_addr_b),
.act_tile_data_a(s1_act_data_a), .act_tile_data_b(s1_act_data_b),
.ctrl_req(s_ctrl_req[1]), .ctrl_wr(s_ctrl_wr[1]), .ctrl_addr(s1_ctrl_addr),
.ctrl_wdata(s1_ctrl_wdata), .ctrl_wmask(s1_ctrl_wmask),
.ctrl_rdata(s1_ctrl_rdata), .ctrl_ready(s_ctrl_ready[1]), .ctrl_busy(s_ctrl_busy[1])
);
endmodule
+93 -28
View File
@@ -14,17 +14,30 @@
// packed.v already expects (job_start/x_base_a/b/w_base/n_tiles/
// node_id_a/b -> job_done/result_data_a/b/result_node_id_a/b).
//
// SCOPE LIMITATION (disclosed, matches this project's own established
// precedent -- EXP-0058/0062's own header comments: "activation data
// ... representing the activation/sliding-window path, which is a
// separate, already-existing memory path not the subject of this
// test"): activations are read through a WIDE, per-tile, combinational
// stand-in port (act_tile_addr_a/b -> act_tile_data_a/b), mirroring
// this project's own earlier ideal_memory_model.v-style staging
// (establish the architectural contract before committing to a
// specific real fetch engine). A real activation fetch engine
// (analogous to weight_tile_gather.v, but for the sliding-window/
// activation path) is a separate, later deliverable, NOT built here.
// ACTIVATION FETCH (EXP-0079, real, closes the gap this header used to
// disclose as deferred): act_tile_fetch.v reads each tile's activation
// data DIRECTLY from the shared DDR3 bus, one tile at a time -- no
// on-chip buffering/prefetch (unlike weights, activation data is read
// exactly once per job, so buffering it would add complexity for zero
// reuse benefit). It shares THIS slot's own single ctrl_req/addr/etc
// port with layer_prefetch_ctrl.v (u_pf): the two are mutually
// exclusive in time by FSM construction (weight prefetch always fully
// completes, including its own consume_done, before the tile loop
// that needs activation data ever starts), muxed below on act_mem_
// active. The outer arbiter's grant (mem_active/mem_grant, this
// module's own top-level ports) is now also needed during activation
// fetch, not just weight prefetch -- held PER TILE (one 2-burst fetch,
// lane A then lane B), released between tiles, matching this
// project's own established "lock the grant for one whole logical
// fetch, not longer" discipline (avoids starving the other slot for
// the whole tile loop's duration).
//
// MEMORY LAYOUT this requires of activation data in DDR3: each tile
// occupies its own full BURST_LEN=8-word burst slot (see act_tile_
// fetch.v's own header for why -- avoiding a runtime-indexed part-
// select, a known Fmax risk this project's already-thin P&R margin,
// EXP-0078, can't afford right now). Documented for whoever prepares
// host-side data layout in the physical realization doc.
//
// Also disclosed: no result-writeback engine exists yet either --
// result_addr_a/b are passed through unused, for a future writeback
@@ -72,18 +85,11 @@ module packed_slot #(
output reg [ADDR_WIDTH-1:0] result_addr_b_out,
// high exactly while this slot needs exclusive access to the
// shared SDRAM controller (its own weight-fetch phase) -- a
// shared-controller arbiter uses this to lock a grant for the
// whole multi-burst fetch, not just one transaction.
// shared SDRAM controller (its own weight-fetch OR activation-
// fetch phase) -- a shared-controller arbiter uses this to lock a
// grant for the whole multi-burst fetch, not just one transaction.
output wire mem_active,
// ---- activation stand-in port (see header -- real fetch engine
// deferred) ----
output reg [ADDR_WIDTH-1:0] act_tile_addr_a,
output reg [ADDR_WIDTH-1:0] act_tile_addr_b,
input wire signed [DATA_WIDTH*P_IN-1:0] act_tile_data_a,
input wire signed [DATA_WIDTH*P_IN-1:0] act_tile_data_b,
// grant from a shared-controller arbiter (see mem_active's own
// comment): must be asserted before this slot may pulse its own
// layer_prefetch_ctrl.v start, since that module's ctrl_req is a
@@ -118,7 +124,6 @@ module packed_slot #(
S_DONE = 4'd9;
reg [3:0] state;
assign mem_active = (state == S_MEMWAIT) || (state == S_PREFETCH);
reg [ADDR_WIDTH-1:0] w_base_lat, x_base_a_lat, x_base_b_lat;
reg [15:0] n_tiles_lat;
reg [ADDR_WIDTH-1:0] result_addr_a_lat, result_addr_b_lat;
@@ -132,17 +137,58 @@ module packed_slot #(
wire [BUFADDRW-1:0] pf_fill_addr;
wire [DATA_WIDTH-1:0] pf_fill_data;
wire pf_ctrl_req, pf_ctrl_wr;
wire [ADDR_WIDTH-2:0] pf_ctrl_addr;
wire [16*BURST_LEN-1:0] pf_ctrl_wdata;
wire [2*BURST_LEN-1:0] pf_ctrl_wmask;
layer_prefetch_ctrl #(
.DATA_WIDTH(DATA_WIDTH), .LAYER_BYTES(LAYER_BYTES), .BURST_LEN(BURST_LEN), .ADDR_WIDTH(ADDR_WIDTH-1)
) u_pf (
.clk(clk), .rst(rst),
.start(pf_start), .layer_base(w_base_lat[ADDR_WIDTH-2:0]), .busy(pf_busy), .done(pf_done),
.fill_we(pf_fill_we), .fill_addr(pf_fill_addr), .fill_data(pf_fill_data),
.ctrl_req(ctrl_req), .ctrl_wr(ctrl_wr), .ctrl_addr(ctrl_addr),
.ctrl_wdata(ctrl_wdata), .ctrl_wmask(ctrl_wmask),
.ctrl_req(pf_ctrl_req), .ctrl_wr(pf_ctrl_wr), .ctrl_addr(pf_ctrl_addr),
.ctrl_wdata(pf_ctrl_wdata), .ctrl_wmask(pf_ctrl_wmask),
.ctrl_rdata(ctrl_rdata), .ctrl_ready(ctrl_ready), .ctrl_busy(ctrl_busy)
);
// ---- act_tile_fetch.v (EXP-0079): real activation fetch, shares
// this slot's own ctrl port with u_pf above (mutually exclusive in
// time -- see header) ----
reg act_req;
wire act_valid;
wire signed [DATA_WIDTH*P_IN-1:0] act_data_a_w, act_data_b_w;
wire act_mem_active;
wire act_ctrl_req, act_ctrl_wr;
wire [ADDR_WIDTH-2:0] act_ctrl_addr;
wire [16*BURST_LEN-1:0] act_ctrl_wdata;
wire [2*BURST_LEN-1:0] act_ctrl_wmask;
act_tile_fetch #(
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .BURST_LEN(BURST_LEN), .ADDR_WIDTH(ADDR_WIDTH-1)
) u_act (
.clk(clk), .rst(rst),
.req(act_req), .base_a(x_base_a_lat[ADDR_WIDTH-2:0]), .base_b(x_base_b_lat[ADDR_WIDTH-2:0]),
.tcnt(tcnt), .valid(act_valid), .data_a(act_data_a_w), .data_b(act_data_b_w),
.mem_active(act_mem_active), .mem_grant(mem_grant),
.ctrl_req(act_ctrl_req), .ctrl_wr(act_ctrl_wr), .ctrl_addr(act_ctrl_addr),
.ctrl_wdata(act_ctrl_wdata), .ctrl_wmask(act_ctrl_wmask),
.ctrl_rdata(ctrl_rdata), .ctrl_ready(ctrl_ready), .ctrl_busy(ctrl_busy)
);
// mutually exclusive by FSM construction (weight prefetch always
// fully completes, incl. consume_done, before the tile loop that
// triggers act_req ever starts) -- safe to select on act_mem_active alone.
assign ctrl_req = act_mem_active ? act_ctrl_req : pf_ctrl_req;
assign ctrl_wr = act_mem_active ? act_ctrl_wr : pf_ctrl_wr;
assign ctrl_addr = act_mem_active ? act_ctrl_addr : pf_ctrl_addr;
assign ctrl_wdata = act_mem_active ? act_ctrl_wdata : pf_ctrl_wdata;
assign ctrl_wmask = act_mem_active ? act_ctrl_wmask : pf_ctrl_wmask;
assign mem_active = (state == S_MEMWAIT) || (state == S_PREFETCH) || act_mem_active;
// ---- layer_weight_buffer.v ----
wire [BUFADDRW-1:0] lwb_rd_addr;
wire [DATA_WIDTH-1:0] lwb_rd_data;
@@ -158,6 +204,7 @@ module packed_slot #(
// ---- weight_tile_gather.v ----
reg tile_req;
reg [BUFADDRW-1:0] tile_base;
reg tile_seen, act_seen; // S_TILEWAIT join latches (weight vs activation, see header)
wire tile_valid;
wire [DATA_WIDTH*P_IN-1:0] tile_data;
@@ -214,6 +261,9 @@ module packed_slot #(
pf_start <= 1'b0;
consume_done <= 1'b0;
tile_req <= 1'b0;
act_req <= 1'b0;
tile_seen <= 1'b0;
act_seen <= 1'b0;
job_valid_np <= 1'b0;
operand_valid<= 1'b0;
tile_last <= 1'b0;
@@ -226,6 +276,7 @@ module packed_slot #(
pf_start <= 1'b0;
consume_done <= 1'b0;
tile_req <= 1'b0;
act_req <= 1'b0;
case (state)
S_IDLE: begin
@@ -278,16 +329,30 @@ module packed_slot #(
S_TILEREQ: begin
tile_req <= 1'b1;
tile_base <= tcnt[BUFADDRW-1:0]*P_IN[BUFADDRW-1:0];
act_tile_addr_a <= x_base_a_lat + {{(ADDR_WIDTH-16){1'b0}}, tcnt};
act_tile_addr_b <= x_base_b_lat + {{(ADDR_WIDTH-16){1'b0}}, tcnt};
act_req <= 1'b1;
tile_seen <= 1'b0;
act_seen <= 1'b0;
state <= S_TILEWAIT;
end
// Real join: weight_tile_gather.v's tile_valid (fast,
// on-chip) and act_tile_fetch.v's act_valid (real
// DDR3 latency, 2 bursts) do NOT arrive on the same
// cycle in general -- latch whichever comes first,
// proceed only once BOTH have been seen. Handles
// either arrival order correctly, not just the
// expected-common one (weight first).
S_TILEWAIT: begin
if (tile_valid) begin
weight_data_r <= tile_data;
input_data_a_r <= act_tile_data_a;
input_data_b_r <= act_tile_data_b;
tile_seen <= 1'b1;
end
if (act_valid) begin
input_data_a_r <= act_data_a_w;
input_data_b_r <= act_data_b_w;
act_seen <= 1'b1;
end
if ((tile_valid || tile_seen) && (act_valid || act_seen)) begin
tile_last <= (tcnt == n_tiles_lat - 16'd1);
operand_valid <= 1'b1;
state <= S_OPERAND;
+189
View File
@@ -0,0 +1,189 @@
`timescale 1ns/1ps
// ============================================================
// Isolated correctness test for act_tile_fetch.v -- real SDR SDRAM
// placeholder backend (same precedent as tb_host_mem_bridge.v/
// tb_sdram_arbiter_n.v: verify new glue logic against the fast
// backend first). Checks: (1) both lanes read back bit-exact from
// their own burst-aligned tile slot; (2) different tile indices
// correctly compute different burst addresses (tile_offset =
// tcnt*BURST_LEN); (3) back-to-back requests (multiple tiles in a
// row) all stay correct, exercising the S_GAP busy-wait logic.
// ============================================================
module tb;
localparam BURST_LEN = 8;
localparam ROW_BITS = 13;
localparam COL_BITS = 10;
localparam BANK_BITS = 2;
localparam ADDR_WIDTH = BANK_BITS + ROW_BITS + COL_BITS; // 25
localparam CLK_FREQ_MHZ = 64;
localparam CLK_PERIOD_NS = 1000.0/CLK_FREQ_MHZ;
localparam DATA_WIDTH = 8;
localparam P_IN = 8;
reg clk = 0;
always #(CLK_PERIOD_NS/2.0) clk = ~clk;
reg rst;
wire ctrl_req, ctrl_wr;
wire [ADDR_WIDTH-1:0] ctrl_addr;
wire [16*BURST_LEN-1:0] ctrl_wdata, ctrl_rdata;
wire [2*BURST_LEN-1:0] ctrl_wmask;
wire ctrl_ready, ctrl_busy;
wire cke, cs_n, ras_n, cas_n, we_n;
wire [BANK_BITS-1:0] ba;
wire [ROW_BITS-1:0] a;
wire [15:0] dq;
wire [1:0] dqm;
sdram_controller #(
.CLK_FREQ_MHZ(CLK_FREQ_MHZ), .BURST_LEN(BURST_LEN),
.ROW_BITS(ROW_BITS), .COL_BITS(COL_BITS), .BANK_BITS(BANK_BITS)
) u_ctrl (
.clk(clk), .rst(rst),
.req(ctrl_req), .wr(ctrl_wr), .addr(ctrl_addr), .wdata(ctrl_wdata), .wmask(ctrl_wmask),
.rdata(ctrl_rdata), .ready(ctrl_ready), .busy(ctrl_busy),
.sdram_cke(cke), .sdram_cs_n(cs_n), .sdram_ras_n(ras_n), .sdram_cas_n(cas_n), .sdram_we_n(we_n),
.sdram_ba(ba), .sdram_a(a), .sdram_dq(dq), .sdram_dqm(dqm)
);
sdram_model #(
.CLK_FREQ_MHZ(CLK_FREQ_MHZ), .ROW_BITS(ROW_BITS), .COL_BITS(COL_BITS), .BANK_BITS(BANK_BITS)
) u_mem (
.clk(clk), .cke(cke), .cs_n(cs_n), .ras_n(ras_n), .cas_n(cas_n), .we_n(we_n),
.ba(ba), .a(a), .dq(dq), .dqm(dqm)
);
// single requester -> tie grant = active, same precedent as
// tb_host_mem_bridge.v (a 1-requester arbiter would produce this).
wire req_active_dut;
wire mem_grant = req_active_dut;
reg req;
reg [ADDR_WIDTH-1:0] base_a, base_b;
reg [15:0] tcnt;
wire valid;
wire signed [DATA_WIDTH*P_IN-1:0] data_a, data_b;
wire dut_ctrl_req, dut_ctrl_wr;
wire [ADDR_WIDTH-1:0] dut_ctrl_addr;
wire [16*BURST_LEN-1:0] dut_ctrl_wdata;
wire [2*BURST_LEN-1:0] dut_ctrl_wmask;
act_tile_fetch #(
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .BURST_LEN(BURST_LEN), .ADDR_WIDTH(ADDR_WIDTH)
) u_dut (
.clk(clk), .rst(rst),
.req(req), .base_a(base_a), .base_b(base_b), .tcnt(tcnt),
.valid(valid), .data_a(data_a), .data_b(data_b),
.mem_active(req_active_dut), .mem_grant(mem_grant),
.ctrl_req(dut_ctrl_req), .ctrl_wr(dut_ctrl_wr), .ctrl_addr(dut_ctrl_addr),
.ctrl_wdata(dut_ctrl_wdata), .ctrl_wmask(dut_ctrl_wmask),
.ctrl_rdata(ctrl_rdata), .ctrl_ready(ctrl_ready), .ctrl_busy(ctrl_busy)
);
// ---- preload path: direct access to the SDRAM controller,
// bypassing act_tile_fetch.v entirely, same "pre_active" mux
// pattern as every other testbench in this project ----
reg pre_active;
reg pre_req, pre_wr;
reg [ADDR_WIDTH-1:0] pre_addr;
reg [16*BURST_LEN-1:0] pre_wdata;
// reroute: real DUT ctrl_* wires go through a mux so the testbench
// can preload memory directly before act_tile_fetch.v ever runs.
// (Re-declare the connection: DUT was wired directly above for
// simplicity of the DUT instantiation; use force-free approach by
// instead having the DUT's own ctrl_req/wr/addr/wdata feed the mux
// inputs below and the mux feed the real controller.)
assign ctrl_req = pre_active ? pre_req : dut_ctrl_req;
assign ctrl_wr = pre_active ? pre_wr : dut_ctrl_wr;
assign ctrl_addr = pre_active ? pre_addr : dut_ctrl_addr;
assign ctrl_wdata = pre_active ? pre_wdata : dut_ctrl_wdata;
assign ctrl_wmask = pre_active ? {(2*BURST_LEN){1'b0}} : dut_ctrl_wmask;
task automatic sdram_write_burst(input [ADDR_WIDTH-1:0] word_addr, input [16*BURST_LEN-1:0] data);
begin
@(posedge clk); while (ctrl_busy) @(posedge clk);
pre_req = 1'b1; pre_wr = 1'b1; pre_addr = word_addr; pre_wdata = data;
@(posedge clk); pre_req = 1'b0;
while (!ctrl_ready) @(posedge clk);
end
endtask
function automatic signed [7:0] act_byte(input integer base, input integer t, input integer k);
act_byte = $signed(8'((base*13 + t*31 + k*7 + 5) & 8'hFF));
endfunction
integer errors, tests;
task automatic 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
task automatic do_fetch(input [ADDR_WIDTH-1:0] ba, input [ADDR_WIDTH-1:0] bb, input [15:0] tc);
begin
@(posedge clk);
base_a <= ba; base_b <= bb; tcnt <= tc;
req <= 1'b1;
@(posedge clk);
req <= 1'b0;
while (!valid) @(posedge clk);
@(posedge clk);
end
endtask
reg signed [DATA_WIDTH*P_IN-1:0] exp_a, exp_b;
integer k, wi;
reg [16*BURST_LEN-1:0] burst;
initial begin
errors = 0; tests = 0;
rst = 1; pre_active = 1'b1; pre_req = 0; pre_wr = 0; pre_addr = 0; pre_wdata = 0;
req = 0; base_a = 0; base_b = 0; tcnt = 0;
repeat(5) @(posedge clk);
rst = 0;
@(posedge clk); while (ctrl_busy) @(posedge clk);
$display("=== preload 4 burst-aligned tile slots (2 lanes x 2 tiles) ===");
// lane A base = 0, lane B base = 100 (arbitrary, word-address units)
for (wi = 0; wi < 2; wi = wi + 1) begin // wi = tile index
for (k = 0; k < BURST_LEN; k = k + 1)
burst[k*16 +: 16] = (k < P_IN/2) ? {act_byte(0, wi, 2*k+1), act_byte(0, wi, 2*k)} : 16'h0000;
sdram_write_burst(0 + wi*BURST_LEN, burst);
for (k = 0; k < BURST_LEN; k = k + 1)
burst[k*16 +: 16] = (k < P_IN/2) ? {act_byte(100, wi, 2*k+1), act_byte(100, wi, 2*k)} : 16'h0000;
sdram_write_burst(100 + wi*BURST_LEN, burst);
end
@(posedge clk);
pre_active = 1'b0;
$display("=== TEST 1: fetch tile 0, both lanes ===");
do_fetch(25'd0, 25'd100, 16'd0);
for (k = 0; k < P_IN; k = k + 1) exp_a[k*DATA_WIDTH +: DATA_WIDTH] = act_byte(0, 0, k);
for (k = 0; k < P_IN; k = k + 1) exp_b[k*DATA_WIDTH +: DATA_WIDTH] = act_byte(100, 0, k);
check(data_a === exp_a, "T1: lane A tile 0 bit-exact");
check(data_b === exp_b, "T1: lane B tile 0 bit-exact");
$display("=== TEST 2: fetch tile 1, both lanes (different burst address) ===");
do_fetch(25'd0, 25'd100, 16'd1);
for (k = 0; k < P_IN; k = k + 1) exp_a[k*DATA_WIDTH +: DATA_WIDTH] = act_byte(0, 1, k);
for (k = 0; k < P_IN; k = k + 1) exp_b[k*DATA_WIDTH +: DATA_WIDTH] = act_byte(100, 1, k);
check(data_a === exp_a, "T2: lane A tile 1 bit-exact");
check(data_b === exp_b, "T2: lane B tile 1 bit-exact");
$display("=== TEST 3: back-to-back fetches (tile 0 then tile 1 immediately) ===");
do_fetch(25'd0, 25'd100, 16'd0);
for (k = 0; k < P_IN; k = k + 1) exp_a[k*DATA_WIDTH +: DATA_WIDTH] = act_byte(0, 0, k);
check(data_a === exp_a, "T3a: back-to-back fetch 1, lane A correct");
do_fetch(25'd0, 25'd100, 16'd1);
for (k = 0; k < P_IN; k = k + 1) exp_a[k*DATA_WIDTH +: DATA_WIDTH] = act_byte(0, 1, k);
check(data_a === exp_a, "T3b: back-to-back fetch 2, lane A correct");
$display("=== %0d/%0d tests, %0d errors ===", tests-errors, tests, errors);
if (errors == 0) $display("ALL TESTS PASSED (tb_act_tile_fetch)");
$finish;
end
endmodule
+35 -21
View File
@@ -15,8 +15,12 @@
// memory backend is swapped, isolating that as the one variable
// under test.
//
// Activation stand-in (see packed_slot.v's own header) is unchanged
// too -- still a disclosed, separate gap, not addressed here.
// EXP-0079 UPDATE: activations are now fetched via a REAL act_tile_
// fetch.v inside each packed_slot.v instance (real DDR3 reads, same
// physical bus each slot already uses for weights) -- no more stand-
// in. This test now preloads real activation data into the SAME real
// DDR3 model too (preload_ddr3_activations), on top of the weight
// preload that was already here.
//
// Uses mig_7series_0_mig_sim (SIM_BYPASS_INIT_CAL="FAST" default,
// EXP-0068's own real vendor-shipped fast-calibration simulation
@@ -236,19 +240,34 @@ module tb;
end
endtask
function automatic signed [DATA_WIDTH*P_IN-1:0] act_lookup(input [ADDR_WIDTH-1:0] addr);
integer li_d, pos_d, tidx_d, k;
reg signed [DATA_WIDTH*P_IN-1:0] r;
begin
li_d = addr / 100000;
pos_d = (addr / 1000) % 100;
tidx_d = addr % 1000;
for (k = 0; k < P_IN; k = k + 1)
r[k*DATA_WIDTH +: DATA_WIDTH] = input_byte(li_d, pos_d, tidx_d*P_IN + k);
act_lookup = r;
end
// ---- real activation preload (EXP-0079: packed_slot.v now wraps
// a real act_tile_fetch.v, no more stand-in) -- same convention as
// tb_packed_slot.v/tb_act_tile_fetch.v: one full BURST_LEN=8-word
// burst per tile, P_IN=8 bytes in the low 64 bits. ----
localparam [MIG_ADDR_WIDTH-1:0] ACT_MEM_BASE = 25'h10000;
function automatic [ADDR_WIDTH-1:0] act_x_base(input integer li, input integer pos);
act_x_base = {{(ADDR_WIDTH-MIG_ADDR_WIDTH){1'b0}}, ACT_MEM_BASE} + (li*M + pos) * (N_TILES*BURST_LEN);
endfunction
task automatic preload_ddr3_activations;
integer li, pos, t, k;
reg [16*BURST_LEN-1:0] burst_data;
reg [ADDR_WIDTH-1:0] base;
begin
for (li = 0; li < L; li = li + 1) begin
for (pos = 0; pos < M; pos = pos + 1) begin
base = act_x_base(li, pos);
for (t = 0; t < N_TILES; t = t + 1) begin
burst_data = {(16*BURST_LEN){1'b0}};
for (k = 0; k < P_IN/2; k = k + 1)
burst_data[k*16 +: 16] = {input_byte(li, pos, t*P_IN + 2*k+1), input_byte(li, pos, t*P_IN + 2*k)};
sdram_write_burst(base[MIG_ADDR_WIDTH-1:0] + t*BURST_LEN, burst_data);
end
end
end
end
endtask
// ---- neural_director_packed.v ----
reg job_in_valid;
wire job_in_ready;
@@ -308,11 +327,6 @@ module tb;
wire signed [DATA_WIDTH-1:0] res_a, res_b;
wire [15:0] res_nid_a, res_nid_b;
wire [ADDR_WIDTH-1:0] res_addr_a_out, res_addr_b_out;
wire [ADDR_WIDTH-1:0] act_addr_a, act_addr_b;
wire signed [DATA_WIDTH*P_IN-1:0] act_data_a, act_data_b;
assign act_data_a = act_lookup(act_addr_a);
assign act_data_b = act_lookup(act_addr_b);
packed_slot #(
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH),
@@ -332,8 +346,6 @@ module tb;
.result_node_id_a(res_nid_a), .result_node_id_b(res_nid_b),
.result_addr_a_out(res_addr_a_out), .result_addr_b_out(res_addr_b_out),
.mem_active(mem_active[gi]), .mem_grant(mem_grant[gi]),
.act_tile_addr_a(act_addr_a), .act_tile_addr_b(act_addr_b),
.act_tile_data_a(act_data_a), .act_tile_data_b(act_data_b),
.ctrl_req(s_ctrl_req[gi]), .ctrl_wr(s_ctrl_wr[gi]),
.ctrl_addr(s_ctrl_addr_flat[gi*MIG_ADDR_WIDTH +: MIG_ADDR_WIDTH]),
.ctrl_wdata(s_ctrl_wdata_flat[gi*16*BURST_LEN +: 16*BURST_LEN]),
@@ -429,6 +441,8 @@ module tb;
$display("=== preload SDRAM with %0d resident-filter weight sets ===", L);
preload_sdram_layers;
$display("=== preload SDRAM with real activation data (EXP-0079) ===");
preload_ddr3_activations;
@(posedge ui_clk);
pre_active = 1'b0;
repeat (5) @(posedge ui_clk);
@@ -436,7 +450,7 @@ module tb;
$display("=== N=2 system on REAL DDR3: submitting %0d layers x %0d positions ===", L, M);
for (li_i = 0; li_i < L; li_i = li_i + 1) begin
for (pp_i = 0; pp_i < M; pp_i = pp_i + 1) begin
submit_job(li_i*100000 + pp_i*1000, li_i*WORDS_PER_LAYER, N_TILES[15:0],
submit_job(act_x_base(li_i, pp_i), li_i*WORDS_PER_LAYER, N_TILES[15:0],
26'h9000 + li_i*10 + pp_i, (li_i*M + pp_i));
expect_node[n_expected] = (li_i*M + pp_i);
expect_val[n_expected] = golden_result(li_i, pp_i);
-373
View File
@@ -1,373 +0,0 @@
`timescale 1ns/1ps
// ============================================================
// First genuine multi-core (N=2) system correctness test: real
// neural_director_packed.v (EXP-0064) dispatching to TWO real
// packed_slot.v instances (EXP-0065), sharing ONE real SDRAM
// controller through sdram_slot_arbiter2.v. All real RTL except the
// activation stand-in (same disclosed scope as EXP-0065/packed_slot.v
// itself).
//
// Jobs are submitted ONE AT A TIME through the Director's own
// job_in_* producer interface (mimicking a host/dependency manager),
// letting the Director do its own pairing (matching w_base) and
// first-free-slot dispatch -- unlike EXP-0062/0065's own tests, which
// drove pairs/slots directly. This is the first test where the
// Director's OWN scheduling decisions (verified in isolation,
// EXP-0064) determine which physical slot executes which pair.
// ============================================================
module tb;
localparam BURST_LEN = 8;
localparam ROW_BITS = 13;
localparam COL_BITS = 10;
localparam BANK_BITS = 2;
localparam SDRAM_ADDR_WIDTH = BANK_BITS + ROW_BITS + COL_BITS; // 25
localparam CLK_FREQ_MHZ = 64;
localparam CLK_PERIOD_NS = 1000.0/CLK_FREQ_MHZ;
localparam DATA_WIDTH = 8;
localparam P_IN = 8;
localparam ACC_WIDTH = 32;
localparam ADDR_WIDTH = 26;
localparam N_INPUTS = 128;
localparam N_TILES = N_INPUTS/P_IN;
localparam LAYER_BYTES = N_INPUTS;
localparam WORDS_PER_LAYER = LAYER_BYTES/2;
localparam N_SLOTS = 2;
localparam QUEUE_DEPTH = 8;
localparam L = 3; // layers
localparam M = 4; // reuse positions per layer, paired 2 at a time
reg clk = 0;
always #(CLK_PERIOD_NS/2.0) clk = ~clk;
reg rst;
integer cyc;
always @(posedge clk) if (!rst) cyc <= cyc + 1;
// ---- real SDRAM controller + model, shared via the arbiter ----
wire ctrl_req, ctrl_wr;
wire [SDRAM_ADDR_WIDTH-1:0] ctrl_addr;
wire [16*BURST_LEN-1:0] ctrl_wdata;
wire [2*BURST_LEN-1:0] ctrl_wmask;
wire [16*BURST_LEN-1:0] ctrl_rdata;
wire ctrl_ready, ctrl_busy;
wire cke, cs_n, ras_n, cas_n, we_n;
wire [BANK_BITS-1:0] ba;
wire [ROW_BITS-1:0] a;
wire [15:0] dq;
wire [1:0] dqm;
reg wpre_req, wpre_wr;
reg [SDRAM_ADDR_WIDTH-1:0] wpre_addr;
reg [16*BURST_LEN-1:0] wpre_wdata;
reg pre_active;
wire arb_ctrl_req, arb_ctrl_wr;
wire [SDRAM_ADDR_WIDTH-1:0] arb_ctrl_addr;
wire [16*BURST_LEN-1:0] arb_ctrl_wdata;
wire [2*BURST_LEN-1:0] arb_ctrl_wmask;
assign ctrl_req = pre_active ? wpre_req : arb_ctrl_req;
assign ctrl_wr = pre_active ? wpre_wr : arb_ctrl_wr;
assign ctrl_addr = pre_active ? wpre_addr : arb_ctrl_addr;
assign ctrl_wdata = pre_active ? wpre_wdata : arb_ctrl_wdata;
assign ctrl_wmask = pre_active ? {(2*BURST_LEN){1'b0}} : arb_ctrl_wmask;
sdram_controller #(
.CLK_FREQ_MHZ(CLK_FREQ_MHZ), .BURST_LEN(BURST_LEN),
.ROW_BITS(ROW_BITS), .COL_BITS(COL_BITS), .BANK_BITS(BANK_BITS)
) u_ctrl (
.clk(clk), .rst(rst),
.req(ctrl_req), .wr(ctrl_wr), .addr(ctrl_addr), .wdata(ctrl_wdata), .wmask(ctrl_wmask),
.rdata(ctrl_rdata), .ready(ctrl_ready), .busy(ctrl_busy),
.sdram_cke(cke), .sdram_cs_n(cs_n), .sdram_ras_n(ras_n), .sdram_cas_n(cas_n), .sdram_we_n(we_n),
.sdram_ba(ba), .sdram_a(a), .sdram_dq(dq), .sdram_dqm(dqm)
);
sdram_model #(
.CLK_FREQ_MHZ(CLK_FREQ_MHZ), .ROW_BITS(ROW_BITS), .COL_BITS(COL_BITS), .BANK_BITS(BANK_BITS)
) u_mem (
.clk(clk), .cke(cke), .cs_n(cs_n), .ras_n(ras_n), .cas_n(cas_n), .we_n(we_n),
.ba(ba), .a(a), .dq(dq), .dqm(dqm)
);
function automatic signed [7:0] weight_byte(input integer li, input integer t);
weight_byte = $signed(8'((li*17 + t*29 + 13) & 8'hFF));
endfunction
function automatic signed [7:0] input_byte(input integer li, input integer pos, input integer t);
input_byte = $signed(8'((li*11 + pos*41 + t*7 + 3) & 8'hFF));
endfunction
task automatic sdram_write_burst(input [SDRAM_ADDR_WIDTH-1:0] word_addr, input [16*BURST_LEN-1:0] data);
begin
@(posedge clk); while (ctrl_busy) @(posedge clk);
wpre_req = 1'b1; wpre_wr = 1'b1; wpre_addr = word_addr; wpre_wdata = data;
@(posedge clk); wpre_req = 1'b0;
while (!ctrl_ready) @(posedge clk);
end
endtask
task automatic preload_sdram_layers;
integer li, bi, wb, tt;
reg [16*BURST_LEN-1:0] burst_data;
begin
for (li = 0; li < L; li = li + 1) begin
for (bi = 0; bi < (LAYER_BYTES/(2*BURST_LEN)); bi = bi + 1) begin
for (wb = 0; wb < BURST_LEN; wb = wb + 1) begin
tt = bi*(2*BURST_LEN) + wb*2;
burst_data[wb*16 +: 16] = {weight_byte(li, tt+1), weight_byte(li, tt)};
end
sdram_write_burst((li*WORDS_PER_LAYER + bi*BURST_LEN), burst_data);
end
end
end
endtask
function automatic signed [DATA_WIDTH*P_IN-1:0] act_lookup(input [ADDR_WIDTH-1:0] addr);
integer li_d, pos_d, tidx_d, k;
reg signed [DATA_WIDTH*P_IN-1:0] r;
begin
li_d = addr / 100000;
pos_d = (addr / 1000) % 100;
tidx_d = addr % 1000;
for (k = 0; k < P_IN; k = k + 1)
r[k*DATA_WIDTH +: DATA_WIDTH] = input_byte(li_d, pos_d, tidx_d*P_IN + k);
act_lookup = r;
end
endfunction
// ---- neural_director_packed.v ----
reg job_in_valid;
wire job_in_ready;
reg [ADDR_WIDTH-1:0] job_in_x_base, job_in_w_base, job_in_result_addr;
reg [15:0] job_in_n_tiles, job_in_node_id;
wire [N_SLOTS-1:0] slot_job_start;
wire [ADDR_WIDTH*N_SLOTS-1:0] slot_x_base_a, slot_x_base_b, slot_w_base;
wire [ADDR_WIDTH*N_SLOTS-1:0] slot_result_addr_a, slot_result_addr_b;
wire [16*N_SLOTS-1:0] slot_n_tiles, slot_node_id_a, slot_node_id_b;
wire [N_SLOTS-1:0] slot_job_done;
wire job_out_done;
wire [$clog2(N_SLOTS)-1:0] job_out_slot;
wire [3:0] dir_state;
wire dir_error;
neural_director_packed #(
.ADDR_WIDTH(ADDR_WIDTH), .N_SLOTS(N_SLOTS), .QUEUE_DEPTH(QUEUE_DEPTH)
) u_dir (
.clk(clk), .rst(rst),
.job_in_valid(job_in_valid), .job_in_ready(job_in_ready),
.job_in_x_base(job_in_x_base), .job_in_w_base(job_in_w_base),
.job_in_n_tiles(job_in_n_tiles), .job_in_result_addr(job_in_result_addr),
.job_in_node_id(job_in_node_id),
.slot_job_start(slot_job_start),
.slot_x_base_a(slot_x_base_a), .slot_x_base_b(slot_x_base_b),
.slot_w_base(slot_w_base), .slot_n_tiles(slot_n_tiles),
.slot_result_addr_a(slot_result_addr_a), .slot_result_addr_b(slot_result_addr_b),
.slot_node_id_a(slot_node_id_a), .slot_node_id_b(slot_node_id_b),
.slot_job_done(slot_job_done),
.job_out_done(job_out_done), .job_out_slot(job_out_slot),
.dir_state(dir_state), .dir_error(dir_error)
);
// ---- 2 real packed_slot.v instances + arbiter ----
wire [1:0] mem_active;
wire [1:0] mem_grant;
wire [1:0] s_ctrl_req, s_ctrl_wr;
wire [SDRAM_ADDR_WIDTH-1:0] s_ctrl_addr [0:1];
wire [16*BURST_LEN-1:0] s_ctrl_wdata [0:1];
wire [2*BURST_LEN-1:0] s_ctrl_wmask [0:1];
wire [16*BURST_LEN-1:0] s_ctrl_rdata [0:1];
wire [1:0] s_ctrl_ready, s_ctrl_busy;
sdram_slot_arbiter2 #(.ADDR_WIDTH(SDRAM_ADDR_WIDTH), .BURST_LEN(BURST_LEN)) u_arb (
.clk(clk), .rst(rst),
.slot0_active(mem_active[0]), .slot0_grant(mem_grant[0]), .slot0_req(s_ctrl_req[0]), .slot0_wr(s_ctrl_wr[0]),
.slot0_addr(s_ctrl_addr[0]), .slot0_wdata(s_ctrl_wdata[0]), .slot0_wmask(s_ctrl_wmask[0]),
.slot0_rdata(s_ctrl_rdata[0]), .slot0_ready(s_ctrl_ready[0]), .slot0_busy(s_ctrl_busy[0]),
.slot1_active(mem_active[1]), .slot1_grant(mem_grant[1]), .slot1_req(s_ctrl_req[1]), .slot1_wr(s_ctrl_wr[1]),
.slot1_addr(s_ctrl_addr[1]), .slot1_wdata(s_ctrl_wdata[1]), .slot1_wmask(s_ctrl_wmask[1]),
.slot1_rdata(s_ctrl_rdata[1]), .slot1_ready(s_ctrl_ready[1]), .slot1_busy(s_ctrl_busy[1]),
.ctrl_req(arb_ctrl_req), .ctrl_wr(arb_ctrl_wr), .ctrl_addr(arb_ctrl_addr),
.ctrl_wdata(arb_ctrl_wdata), .ctrl_wmask(arb_ctrl_wmask),
.ctrl_rdata(ctrl_rdata), .ctrl_ready(ctrl_ready), .ctrl_busy(ctrl_busy)
);
genvar gi;
generate
for (gi = 0; gi < N_SLOTS; gi = gi + 1) begin : GEN_SLOT
wire signed [DATA_WIDTH-1:0] res_a, res_b;
wire [15:0] res_nid_a, res_nid_b;
wire [ADDR_WIDTH-1:0] res_addr_a_out, res_addr_b_out;
wire [ADDR_WIDTH-1:0] act_addr_a, act_addr_b;
wire signed [DATA_WIDTH*P_IN-1:0] act_data_a, act_data_b;
assign act_data_a = act_lookup(act_addr_a);
assign act_data_b = act_lookup(act_addr_b);
packed_slot #(
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH),
.BURST_LEN(BURST_LEN), .ADDR_WIDTH(ADDR_WIDTH), .LAYER_BYTES(LAYER_BYTES)
) u_slot (
.clk(clk), .rst(rst),
.job_start(slot_job_start[gi]),
.x_base_a(slot_x_base_a[gi*ADDR_WIDTH +: ADDR_WIDTH]),
.x_base_b(slot_x_base_b[gi*ADDR_WIDTH +: ADDR_WIDTH]),
.w_base(slot_w_base[gi*ADDR_WIDTH +: ADDR_WIDTH]),
.n_tiles(slot_n_tiles[gi*16 +: 16]),
.result_addr_a(slot_result_addr_a[gi*ADDR_WIDTH +: ADDR_WIDTH]),
.result_addr_b(slot_result_addr_b[gi*ADDR_WIDTH +: ADDR_WIDTH]),
.node_id_a(slot_node_id_a[gi*16 +: 16]), .node_id_b(slot_node_id_b[gi*16 +: 16]),
.job_done(slot_job_done[gi]),
.result_data_a(res_a), .result_data_b(res_b),
.result_node_id_a(res_nid_a), .result_node_id_b(res_nid_b),
.result_addr_a_out(res_addr_a_out), .result_addr_b_out(res_addr_b_out),
.mem_active(mem_active[gi]), .mem_grant(mem_grant[gi]),
.act_tile_addr_a(act_addr_a), .act_tile_addr_b(act_addr_b),
.act_tile_data_a(act_data_a), .act_tile_data_b(act_data_b),
.ctrl_req(s_ctrl_req[gi]), .ctrl_wr(s_ctrl_wr[gi]), .ctrl_addr(s_ctrl_addr[gi]),
.ctrl_wdata(s_ctrl_wdata[gi]), .ctrl_wmask(s_ctrl_wmask[gi]),
.ctrl_rdata(s_ctrl_rdata[gi]), .ctrl_ready(s_ctrl_ready[gi]), .ctrl_busy(s_ctrl_busy[gi])
);
end
endgenerate
integer errors, tests;
task automatic submit_job(
input [ADDR_WIDTH-1:0] xb, input [ADDR_WIDTH-1:0] wb,
input [15:0] nt, input [ADDR_WIDTH-1:0] resaddr, input [15:0] nid
);
begin
@(posedge clk);
job_in_x_base = xb; job_in_w_base = wb; job_in_n_tiles = nt;
job_in_result_addr = resaddr; job_in_node_id = nid;
job_in_valid = 1'b1;
while (!job_in_ready) @(posedge clk);
@(posedge clk);
job_in_valid = 1'b0;
end
endtask
// ---- scoreboard: golden result per node_id, checked whenever
// EITHER slot's own job_done pulses (watching both slots directly,
// not just the Director's own lowest-index-wins job_out_done,
// per DEC-0007's own documented simplification) ----
reg [15:0] expect_node [0:63];
reg signed [7:0] expect_val [0:63];
integer n_expected;
function automatic signed [7:0] golden_result(input integer li, input integer pos);
integer t, acc;
reg signed [7:0] r;
begin
acc = 0;
for (t = 0; t < N_INPUTS; t = t + 1)
acc = acc + (input_byte(li, pos, t) * weight_byte(li, t));
if (acc <= 0) r = 0; else if (acc > 127) r = 8'sd127; else r = acc[7:0];
golden_result = r;
end
endfunction
integer completions;
integer si;
// Runs from time 0, independent of the main submission flow below
// -- a slot's job_done is a ONE-CYCLE pulse, and with QUEUE_DEPTH
// smaller than the total job count, early pairs can complete WHILE
// later jobs are still being submitted; a watcher that only starts
// AFTER all submissions finish would miss those pulses entirely
// (found empirically: only 2/12 results ever got checked, root-
// caused via hierarchical dir_state/q_count/slot state tracing
// showing the system genuinely idle by the time the old watcher
// loop started -- the real completions had already come and gone,
// unobserved).
always @(posedge clk) begin
if (!rst) begin
for (si = 0; si < N_SLOTS; si = si + 1) begin
if (slot_job_done[si]) begin
completions = completions + 2; // covers both A and B
case (si)
0: begin
check_completion(0, GEN_SLOT[0].u_slot.result_node_id_a, GEN_SLOT[0].u_slot.result_data_a);
check_completion(0, GEN_SLOT[0].u_slot.result_node_id_b, GEN_SLOT[0].u_slot.result_data_b);
end
1: begin
check_completion(1, GEN_SLOT[1].u_slot.result_node_id_a, GEN_SLOT[1].u_slot.result_data_a);
check_completion(1, GEN_SLOT[1].u_slot.result_node_id_b, GEN_SLOT[1].u_slot.result_data_b);
end
endcase
end
end
end
end
task automatic check_completion(input integer slot, input [15:0] nid, input signed [7:0] val);
integer idx, found;
begin
found = 0;
for (idx = 0; idx < n_expected; idx = idx + 1) begin
if (expect_node[idx] === nid && !found) begin
found = 1;
tests = tests + 1;
if (expect_val[idx] !== val) begin
$display("FAIL slot=%0d node_id=%0d: got=%0d expected=%0d", slot, nid, $signed(val), $signed(expect_val[idx]));
errors = errors + 1;
end else begin
$display("PASS slot=%0d node_id=%0d: result=%0d", slot, nid, $signed(val));
end
end
end
if (!found) begin
$display("FAIL slot=%0d node_id=%0d: completed but was NOT an expected pending job", slot, nid);
errors = errors + 1;
tests = tests + 1;
end
end
endtask
integer li_i, pp_i, wd;
initial begin
errors = 0; tests = 0; cyc = 0; n_expected = 0; completions = 0;
rst = 1; pre_active = 1'b1;
wpre_req = 0; wpre_wr = 0; wpre_addr = 0; wpre_wdata = 0;
job_in_valid = 0; job_in_x_base = 0; job_in_w_base = 0;
job_in_n_tiles = 0; job_in_result_addr = 0; job_in_node_id = 0;
repeat(5) @(posedge clk);
rst = 0;
@(posedge clk); while (ctrl_busy) @(posedge clk);
$display("=== preload SDRAM with %0d resident-filter weight sets ===", L);
preload_sdram_layers;
@(posedge clk);
pre_active = 1'b0;
$display("=== N=2 system: submitting %0d layers x %0d positions through neural_director_packed.v ===", L, M);
for (li_i = 0; li_i < L; li_i = li_i + 1) begin
for (pp_i = 0; pp_i < M; pp_i = pp_i + 1) begin
submit_job(li_i*100000 + pp_i*1000, li_i*WORDS_PER_LAYER, N_TILES[15:0],
26'h9000 + li_i*10 + pp_i, (li_i*M + pp_i));
expect_node[n_expected] = (li_i*M + pp_i);
expect_val[n_expected] = golden_result(li_i, pp_i);
n_expected = n_expected + 1;
end
end
wd = 0;
while (completions < n_expected && wd < 5000) begin
@(posedge clk);
wd = wd + 1;
end
if (completions < n_expected) begin
$display("FAIL: only %0d/%0d position-results completed within watchdog", completions, n_expected);
errors = errors + 1;
end
$display("=== %0d/%0d tests, %0d errors, %0d/%0d positions completed ===", tests-errors, tests, errors, completions, n_expected);
if (errors == 0 && completions == n_expected) $display("ALL TESTS PASSED (tb_np_director_n2_system)");
$finish;
end
endmodule
+39 -36
View File
@@ -9,11 +9,12 @@
// promotion from testbench-sequence to real RTL (EXP-0062 -> this)
// preserves bit-exact correctness.
//
// Activation stand-in (see packed_slot.v's own header): a simple
// combinational behavioral memory here, addressed by act_tile_addr_a/b
// (tile-index-based, matching packed_slot.v's own addressing:
// x_base + tile_count), standing in for the real (not yet built)
// activation fetch engine.
// EXP-0079 UPDATE: packed_slot.v now wraps a REAL act_tile_fetch.v
// (real DDR3 reads, no stand-in port left) -- this test now preloads
// activation data into the SAME real SDR SDRAM placeholder backend
// already used for weights (preload_sdram_activations, matching
// act_tile_fetch.v's own real memory layout: one full BURST_LEN=8-word
// burst per tile), instead of a combinational behavioral lookup.
// ============================================================
module tb;
localparam BURST_LEN = 8;
@@ -119,15 +120,36 @@ module tb;
end
endtask
// ---- activation stand-in: act_tile_addr = x_base + tile_index
// (packed_slot.v's own addressing) -- x_base itself is chosen as
// li*1000 + pos*100 below so a simple decode recovers (li,pos,t) ----
reg signed [DATA_WIDTH*P_IN-1:0] act_data_a, act_data_b;
wire [ADDR_WIDTH-1:0] act_addr_a, act_addr_b;
// ---- real activation preload (EXP-0079: act_tile_fetch.v replaces
// the old combinational stand-in) -- one full BURST_LEN=8-word
// burst PER TILE (act_tile_fetch.v's own real memory layout
// convention, see that module's header), P_IN=8 bytes in the low
// 64 bits, upper 64 bits padding. x_base(li,pos) = ACT_MEM_BASE +
// (li*M+pos)*(N_TILES*BURST_LEN), well clear of the weight region
// (word addresses 0..L*WORDS_PER_LAYER-1). ----
localparam [ADDR_WIDTH-1:0] ACT_MEM_BASE = 26'h10000;
function automatic [ADDR_WIDTH-1:0] act_x_base(input integer li, input integer pos);
act_x_base = ACT_MEM_BASE + (li*M + pos) * (N_TILES*BURST_LEN);
endfunction
// act_tile_addr = x_base + tile_index (packed_slot.v's own
// addressing); x_base itself encodes (li,pos) as li*100000+pos*1000
// so tile_index occupies the low 3 decimal digits directly.
task automatic preload_sdram_activations;
integer li, pos, t, k;
reg [16*BURST_LEN-1:0] burst_data;
reg [ADDR_WIDTH-1:0] base;
begin
for (li = 0; li < L; li = li + 1) begin
for (pos = 0; pos < M; pos = pos + 1) begin
base = act_x_base(li, pos);
for (t = 0; t < N_TILES; t = t + 1) begin
burst_data = {(16*BURST_LEN){1'b0}};
for (k = 0; k < P_IN/2; k = k + 1)
burst_data[k*16 +: 16] = {input_byte(li, pos, t*P_IN + 2*k+1), input_byte(li, pos, t*P_IN + 2*k)};
sdram_write_burst(base[SDRAM_ADDR_WIDTH-1:0] + t*BURST_LEN, burst_data);
end
end
end
end
endtask
// ---- packed_slot.v (DUT) ----
reg job_start;
@@ -151,33 +173,12 @@ module tb;
.result_data_a(result_data_a), .result_data_b(result_data_b),
.result_node_id_a(result_node_id_a), .result_node_id_b(result_node_id_b),
.result_addr_a_out(result_addr_a_out), .result_addr_b_out(result_addr_b_out),
.act_tile_addr_a(act_addr_a), .act_tile_addr_b(act_addr_b),
.act_tile_data_a(act_data_a), .act_tile_data_b(act_data_b),
.mem_grant(1'b1), // no arbiter in this single-slot test
.ctrl_req(slot_ctrl_req), .ctrl_wr(slot_ctrl_wr), .ctrl_addr(slot_ctrl_addr),
.ctrl_wdata(slot_ctrl_wdata), .ctrl_wmask(slot_ctrl_wmask),
.ctrl_rdata(ctrl_rdata), .ctrl_ready(ctrl_ready), .ctrl_busy(ctrl_busy)
);
// real activation decode: x_base encodes (li,pos) as li*100000+pos*1000;
// act_tile_addr = x_base + tile_index (0..N_TILES-1), so
// tile_index = act_addr % 1000, pos = (act_addr/1000) % 100, li = act_addr/100000
function automatic signed [DATA_WIDTH*P_IN-1:0] act_lookup(input [ADDR_WIDTH-1:0] addr);
integer li_d, pos_d, tidx_d, k;
reg signed [DATA_WIDTH*P_IN-1:0] r;
begin
li_d = addr / 100000;
pos_d = (addr / 1000) % 100;
tidx_d = addr % 1000;
for (k = 0; k < P_IN; k = k + 1)
r[k*DATA_WIDTH +: DATA_WIDTH] = input_byte(li_d, pos_d, tidx_d*P_IN + k);
act_lookup = r;
end
endfunction
always @(*) act_data_a = act_lookup(act_addr_a);
always @(*) act_data_b = act_lookup(act_addr_b);
integer errors, tests;
integer li_i, pp_i;
integer acc_a, acc_b, s_a, s_b, k, tt;
@@ -189,8 +190,8 @@ module tb;
tests = tests + 1;
@(posedge clk);
job_start = 1'b1;
x_base_a = li*100000 + pos_a*1000;
x_base_b = li*100000 + pos_b*1000;
x_base_a = act_x_base(li, pos_a);
x_base_b = act_x_base(li, pos_b);
w_base = li*WORDS_PER_LAYER; // WORD address, matching layer_prefetch_ctrl.v's
// own convention (EXP-0057/58/62) and this
// testbench's own preload_sdram_layers addressing
@@ -245,6 +246,8 @@ module tb;
$display("=== preload SDRAM with %0d resident-filter weight sets ===", L);
preload_sdram_layers;
$display("=== preload SDRAM with real activation data (EXP-0079) ===");
preload_sdram_activations;
@(posedge clk);
pre_active = 1'b0;