Author SHA1 Message Date
micheleandClaude Sonnet 5 785e9461a0 docs: ESP32 host firmware specification (docs/FIRMWARE_SPEC.md)
Real, from-source spec for the ESP32-S3 host firmware -- every field
layout, address formula, and timing number is read directly from the
committed RTL (spi_host_bridge_v3.v, neural_director_grouped.v,
result_writeback.v, host_mem_bridge.v, flash_spi_master.v), not
guessed. Covers: physical interfaces, boot/bootstrap (JTAG factory,
Master-SPI autonomous, FLASH_XFER field update), the full management
SPI opcode reference and register map, job submission (including the
critical, real N=16 octet-dispatch requirement -- 8 jobs must share
w_base/n_tiles or the Director's queue stalls permanently with no
in-band recovery), result readback addressing, raw memory access,
config-flash relay with its real 2-dummy-byte timing requirement,
error handling, and honestly disclosed open items (no firmware exists
yet -- this is the specification).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUG92aM9m68TRc4rG55BcC
2026-09-22 08:39:29 +02:00
micheleandClaude Sonnet 5 e3fe007469 docs: reframe primary docs -- N=16 is now the real fabrication target
Physical board fabrication moved to N=16 (with the EXP-0097 MAC-
pipeline fix); v3-artix7 fast-forward merged with n16-timing-closure.
No physical board change needed -- DDR3/SPI/flash/config pins are
identical across all N_GROUPS values. N=2/N=8 kept documented as real,
valid fallback signoffs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUG92aM9m68TRc4rG55BcC
2026-09-22 08:36:37 +02:00
3 changed files with 431 additions and 29 deletions
+8 -10
View File
@@ -1,16 +1,14 @@
# FPGA-Neural V3 — Architecture Analysis: Timing, Bottlenecks, and Recommended Interventions
Scope: the current, real, P&R-verified V3 design (`hardware/v3/`, branch
`v3-artix7` for the physically-fabricated board; branch
`n16-timing-closure` for the real, consolidated follow-up below),
updated through EXP-0097/0098 (a real MAC-pipeline fix that closes
timing for the WHOLE real family — N=2/N=8/N=16 — not just N=8). The
board **currently in physical fabrication** uses N=8's own original,
unmodified EXP-0095/0096 signoff (`n8_system_ddr3_top.v`, 32-bit DDR3,
155.039MHz, WNS=0.000ns exact, 8 real parallel PEs). Separately, on an
isolated real branch, EXP-0097/0098 found that one shared-core fix
closes N=16 too (WNS=+0.269ns) and improves N=2 (+0.389ns) and N=8
(+0.108ns) as well — see §5.6 for the full real story. Every
`v3-artix7`, now fast-forward merged with the former
`n16-timing-closure` branch). **UPDATE (2026-09-22): the real physical
fabrication target moved to N=16** (`n16_system_ddr3_top.v`, with the
real EXP-0097 MAC-pipeline fix, WNS=+0.269ns, 16 real parallel PEs) —
no physical board change needed, DDR3/SPI/flash/config pins are
identical regardless of `N_GROUPS`. The same MAC-pipeline fix also
improves N=2 (+0.389ns) and N=8 (+0.108ns), both kept as real, valid
fallback signoffs — see §5.6 for the full real story. Every
number in this document is either
directly measured (real simulation trace, real P&R report) or a calculation
built from directly-measured building blocks — the two are labeled
+386
View File
@@ -0,0 +1,386 @@
# FPGA-Neural — ESP32 Host Firmware Specification
Real, from-source specification for the ESP32-S3 host firmware, derived
directly from the actual RTL protocol implementation (`spi_host_bridge_v3.v`,
`neural_director_grouped.v`, `result_writeback.v`, `host_mem_bridge.v`,
`flash_spi_master.v`) and the real, closed timing signoff in
`docs/PHYSICAL_REALIZATION.md`. Nothing below is guessed — every field
layout, address formula, and timing number is read directly from the
committed RTL's own header comments or real, measured project data.
**Status**: this is a specification only. No ESP32-side firmware exists
yet in this repository — this document exists to make that firmware
buildable without re-deriving the protocol from RTL by hand. Real,
disclosed scope: management SPI protocol, job submission, result
readback, raw memory access, config-flash relay, boot/bootstrap. Out
of scope: FreeRTOS task structure, Wi-Fi/network layer, higher-level
model-graph scheduling above "submit one job" — those are real
firmware design decisions, not yet made anywhere in this project.
**Real fabrication target (2026-09-22)**: N=16 (`n16_system_ddr3_top.v`,
16 real parallel processing elements, 4 groups × 4 PEs). This document
is written for N=16 throughout; N=2/N=8 fallback configurations use
the byte-for-byte identical protocol (§3), differing only in the real
octet/pairing requirement of §4.2.
---
## 1. Physical interfaces
Two electrically separate SPI buses, plus JTAG. Full real pin
assignments: `docs/PINOUT.md` / `docs/PHYSICAL_REALIZATION.md` §2.
| Bus | FPGA role | Purpose | Pins (bank 15/16, LVCMOS33) |
|---|---|---|---|
| Management SPI | **slave** | Job submission, status, raw memory access, config-flash relay | sclk=A15, mosi=B16, miso=B17, cs_n=A16 |
| Config-flash SPI | **master** (relayed) | Bitstream/config flash access, reached only via `FLASH_XFER` (§6) | not directly host-visible — internal to the FPGA |
| JTAG | target | Factory/recovery programming | TCK=E10, TDI=E11, TMS=E12, TDO=E13 |
Additional real, dedicated signals on the management SPI bus's own
bank:
| Signal | Pin | Direction (ESP32 side) | Purpose |
|---|---|---|---|
| `sys_rst` | G13 (tentative real pin, §7 of PHYSICAL_REALIZATION.md) | output | Board-level reset, separate from the SPI-driven soft-reset (§3) |
| `data_ready_n` | D14 | input, active-low | Sticky IRQ — see §5.1 |
**Real, important note**: the ESP32 has **no direct electrical
connection** to the config flash. The only path is `FLASH_XFER` (§6),
relayed through the FPGA over the management SPI bus.
---
## 2. Boot / bootstrap sequence
Three real, distinct paths (`docs/PHYSICAL_REALIZATION.md` §5):
1. **Factory-first / recovery (JTAG, ESP32-driven, one-time or rare)**
— a factory-fresh board has a **blank** config flash. The FPGA
cannot relay `FLASH_XFER` commands until it is already running
logic that does that — a real chicken-and-egg constraint. The only
way to bootstrap a blank board is for the ESP32 to bit-bang JTAG
(TAP state machine, IR/DR shifting) and either (a) load a bitstream
directly into the FPGA's configuration memory (volatile — lost on
power-cycle), or (b) run Vivado's own real "indirect SPI flash
programming" sequence over JTAG to write the flash for the first
time. **Real, disclosed gap: this JTAG bit-banging firmware does
not exist yet anywhere in this project** — it is real, necessary,
unbuilt software work, out of scope for this document beyond
flagging it.
2. **Normal boot (Master SPI, autonomous, every power-on)** — the FPGA
self-configures from the flash via its own dedicated configuration
hardware (mode pins `M[2:0]=001`, §2.4 of PHYSICAL_REALIZATION.md).
**No ESP32 involvement at all.** Firmware's own real startup
sequence should simply wait for `init_calib_complete` (readable via
`REG_READ` on STATUS, §3.3, bit3) before issuing any job or memory
access — DDR3 traffic before real PHY calibration completes is
unsafe.
3. **Field firmware update (`FLASH_XFER`, §6)** — once the FPGA is
already running (path 2 completed), the ESP32 can rewrite the
config flash by relaying raw SPI-NOR bytes through the FPGA. This
is the **only** path from ESP32 to flash during normal operation.
### 2.1 Real, recommended firmware startup sequence
```
1. Assert sys_rst (G13), hold >= a few clk periods, release.
(Real note: sys_rst's own real, permanent board location is still
tentative per PHYSICAL_REALIZATION.md §7 -- confirm against the
real board schematic before relying on this pin number.)
2. Wait for the FPGA's own real Master-SPI autonomous boot to complete
-- there is no host-visible "boot done" signal separate from
init_calib_complete; DONE (pin P10) is a real, physical
configuration-status pin if the ESP32 has a GPIO wired to it
(board-dependent, not guaranteed).
3. REG_READ(0x00 DEVICE_ID) -- confirm 32'h4E50_5601 ("NPV" + protocol
version 1) before trusting anything else. A mismatch means either
the flash holds the wrong bitstream, or config-boot has not
actually completed -- do not proceed.
4. Poll REG_READ(0x02 STATUS) bit3 (init_calib_complete) until set.
Real, measured DDR3 calibration timing has not been characterized
on real hardware yet (only in simulation, where SIM_BYPASS_INIT_CAL
shortcuts it) -- budget real time for this, do not assume it is
instantaneous.
5. Only once bit3 is set: safe to issue WRITE_MEM (weight/activation
preload, SS3) and WRITE_JOB (SS5).
```
---
## 3. Management SPI protocol — real opcode reference
One opcode byte (MSB-first), sent as the first byte of a CS-low SPI
transaction; multi-byte fields are MSB-first. Authoritative source:
`spi_host_bridge_v3.v`'s own header comment — this table is a summary.
| Opcode | Name | Direction | Payload (MOSI) | Response (MISO) |
|---|---|---|---|---|
| `0x00` | NOP | — | 0 bytes | inert (0x00) |
| `0x0F` | RESET | host→FPGA | 0 bytes | pulses `soft_rst_pulse` for 1 real clk cycle after CS rises |
| `0x10` | WRITE_JOB | host→FPGA | 16 bytes (§4.1) | — |
| `0x20` | STATUS | host→FPGA | 0 bytes | 1 byte (§3.1) |
| `0x01` | WRITE_MEM | host→FPGA | 4 + 2·N bytes (§6.1) | — |
| `0x02` | READ_MEM | host→FPGA | 6 bytes | 2·N bytes (§6.1) |
| `0x30` | REG_WRITE | host→FPGA | 5 bytes (§3.2) | — |
| `0x31` | REG_READ | host→FPGA | 1 byte | 4 bytes (§3.3) |
| `0x40` | FLASH_XFER | bidir | N bytes | N+2 bytes (§6.2) |
**Real, load-bearing rule**: any opcode byte not in this table is
treated as NOP — the bus is never wedged by an unrecognized command.
Do not rely on this for real protocol negotiation; it exists purely as
a safety fallback.
### 3.1 `0x20` STATUS — 1 response byte
| Bit | Meaning |
|---|---|
| 0 | `job_busy` — a `WRITE_JOB` is currently waiting on `job_in_ready` |
| 1 | `mem_busy` — a `WRITE_MEM`/`READ_MEM` is waiting on `mem_ready` |
| 2 | `last_job_accepted` (sticky, cleared by the next `WRITE_JOB`) |
| 7:3 | reserved, always 0 |
### 3.2 `0x30` REG_WRITE / `0x31` REG_READ
`REG_WRITE`: `byte0=reg_addr[7:0]`, `byte1:4=value[31:0]` MSB-first —
applied the instant the last byte lands (purely internal, no backend
handshake).
`REG_READ`: `byte0=reg_addr[7:0]`, then 4 response bytes MSB-first. An
unmapped address reads back `0xFFFFFFFF` (deliberately distinct from a
real `0`, so firmware can tell "unmapped register" from "real zero").
Writing a read-only or unmapped register is inert (accepted, no
effect).
### 3.3 Real register map (v1)
| Addr | Name | R/W | Real meaning |
|---|---|---|---|
| `0x00` | DEVICE_ID | RO | `32'h4E50_5601` ("NPV" + protocol version 1) |
| `0x01` | CONTROL | RW | bit0: write 1 to pulse `soft_rst_pulse` (same physical effect as `0x0F`); always reads back 0; bits[31:1] reserved |
| `0x02` | STATUS | RO | bit0 `job_busy`, bit1 `mem_busy`, bit2 `last_job_accepted`, bit3 `init_calib_complete`, bit4 `dir_error`; bits[31:5] reserved |
| `0x03` | N_SLOTS | RO | real, total parallel-PE count this bitstream was built with (**16** for the current N=16 fabrication target) — read this instead of hardcoding the core count in firmware |
---
## 4. Job submission
### 4.1 `0x10` WRITE_JOB — real 16-byte payload
| Bytes | Field | Real convention |
|---|---|---|
| 0:1 | `node_id[15:0]` | host-assigned identifier, returned unchanged with the result (§5) |
| 2:5 | `x_base[25:0]` | byte-address convention (26-bit `JOB_ADDR_WIDTH`); byte2's MSBs are `{6'b0, x_base[25:24]}` |
| 6:9 | `w_base[25:0]` | same convention — **must match, together with `n_tiles`, across the real octet requirement of §4.2** |
| 10:11 | `n_tiles[15:0]` | number of `P_IN=8`-wide tiles to stream (real activation length = `n_tiles × 8` bytes) |
| 12:15 | `result_addr[25:0]` | where `result_writeback.v` will write the real result — see §5 for the real readback formula |
`job_in_valid` is asserted and **held** until `job_in_ready` also
reads 1 in the same real cycle — never a blind, unacknowledged pulse.
Firmware does not need to do anything special for this; it is entirely
internal to the bridge.
### 4.2 REAL, CRITICAL N=16 requirement: octet dispatch
**This is the single most important real constraint in this
document.** `neural_director_grouped.v` (the real N=16 job scheduler)
only dispatches jobs in **groups of 8** — it inspects the **8 oldest**
entries in its own internal queue and requires **all eight** to share
the exact same `w_base` **and** `n_tiles`. If they do, they are
dispatched together (one octet = one real systolic group's own job,
weight fetched once, broadcast to 4 PEs × 2 lanes). If they do not,
**the queue stalls — permanently, with no in-band recovery.**
Real, verified behavior (`tb_neural_director_grouped.v`): a mismatched
octet does not corrupt anything and does not silently mis-pair jobs —
it simply stops draining the queue. The **only** real recovery is a
hardware reset (`sys_rst`, or `CONTROL` register bit0 / `RESET`
opcode) — there is no software-visible "skip this job" or "flush the
queue" command.
**Real, mandatory firmware discipline**: submit exactly 8 `WRITE_JOB`
transactions sharing the same `w_base` and `n_tiles`, back-to-back,
with no other job submitted in between, for every real layer-position
batch. If a real workload's position count is not a multiple of 8,
**pad the batch** (e.g. repeat the last real position, or submit a
real dummy position with a `result_addr` the firmware will simply
ignore) rather than submit a short, mismatched batch — the RTL has no
concept of a "partial octet."
**Real, deliberate, unchanged property**: the `WRITE_JOB` payload
itself is byte-for-byte identical whether the octet requirement
applies (N=16, N=8's own pair-of-4 requirement) or not (N=2's own
simple pairing) — only the batch **size** firmware must submit before
a batch reliably drains differs (8 for N=16, 2 for N=2). Always read
`N_SLOTS` (§3.3) at startup and derive the real required batch size as
`N_SLOTS / 2` rather than hardcoding 8.
---
## 5. Result readback
Results are **not** returned over SPI directly — they are written by
`result_writeback.v` into DDR3 at the job's own `result_addr`, and
must be read back via the existing `READ_MEM` (`0x02`) opcode. No new
opcode exists or is needed for this.
**Real, exact addressing formula** (from `result_writeback.v`'s own
header — `result_addr` here is the 26-bit value submitted in the
job's own `WRITE_JOB` payload, §4.1):
```
mem_addr (value) = result_addr[24:0] * 2
mem_addr (node_id) = result_addr[24:0] * 2 + 1
```
Two `READ_MEM` transactions per real result lane (one job produces
**two** results, lane A and lane B — see `packed_pe.v`'s own real A/B
lane structure):
- `mem_addr` above reads back a 16-bit word: low byte = the real INT8
result value (`result_data`, zero-extended into the low 8 bits of
the word); the other bytes are `8'h00`.
- `mem_addr+1` reads back the real 16-bit `node_id` the job was
submitted with — use this to match a result back to the job that
produced it, since results may complete out of submission order
across different groups.
**Real, disclosed firmware design question, not yet decided at the RTL
level**: how the host knows a specific result is ready (vs. polling
`READ_MEM` speculatively) is answered by §5.1 below (`data_ready_n`),
but `data_ready_n` is a single, whole-device sticky flag, not
per-job — firmware must track its own real outstanding-job count/set
and read back results as they become plausible, not per-job-precise
from the hardware alone.
### 5.1 `data_ready_n` — real, sticky, active-low interrupt
Real, board-level pin (D14). Lets firmware be interrupt-driven instead
of polling `STATUS` in a loop.
- **Asserted (driven low)** when a real job/octet completes
(`job_out_done`) **or** while `dir_error` is active.
- **Sticky**: the job-completion contribution stays low even after the
underlying completion pulse ends, until the host acknowledges by
completing a real `STATUS` (`0x20`) **or** `REG_READ` of STATUS
(`0x31`, reg `0x02`) transaction. A `REG_READ` of any *other*
register does **not** acknowledge it.
- The `dir_error` contribution is **live**, not latched — it clears
the instant `dir_error` itself does (typically only after a real
reset, since the Director has no in-band error-clear per §4.2).
**Real, recommended firmware pattern**: wire `data_ready_n` to an
ESP32 GPIO interrupt; on falling edge, read `STATUS` (acknowledges the
sticky latch and tells firmware whether it was a real completion or a
`dir_error` condition), then read back whichever real results are now
plausible.
---
## 6. Raw memory access (weight/activation preload, debug)
### 6.1 `0x01` WRITE_MEM / `0x02` READ_MEM
**Real, different address convention from `WRITE_JOB`**: these opcodes
use `host_mem_bridge.v`'s own **16-bit-WORD** address space
(`MEM_ADDR_WIDTH=25` bits), not the 26-bit byte-base convention
`x_base`/`w_base`/`result_addr` use. Do not mix the two conventions.
- `WRITE_MEM`: `byte0:3 = addr[24:0]` (word address; `byte0`'s MSBs
are `{7'b0, addr[24]}`), then `len_words` (2 bytes, right after
addr) × 2 payload bytes, MSB-first per word. Each word is written
via one real `mem_req`/`mem_ready` handshake before the next word's
bytes are accepted — a real, sequential, per-word transaction, not a
burst.
- `READ_MEM`: same 6-byte header (4 addr + 2 `len_words`), 0 further
MOSI payload; `2·len_words` response bytes clocked out on MISO
starting at payload byte 7, MSB-first per word.
**Real, disclosed limitation** (not a bug, a deliberate, documented
design choice, `host_mem_bridge.v`'s own header): this path halves the
host's own reachable byte range for a given `ADDR_WIDTH`, since the
underlying shared bus is 32-bit-word-native (EXP-0084's real DDR3
widening) while this host-facing contract stays a fixed 16-bit word —
`mem_addr`'s own LSB additionally selects which 16-bit half of the
real 32-bit ctrl-bus word to target. Not currently a practical
constraint at this project's real usage scale.
**Real use**: preloading weight/activation data into DDR3 before
submitting jobs that reference it. See `docs/PHYSICAL_REALIZATION.md`
§4 for the real, exact DDR3 memory layout convention (weight-per-layer
addressing, 4-tiles-per-burst activation packing) firmware must follow
when computing `x_base`/`w_base` values for `WRITE_JOB`.
### 6.2 `0x40` FLASH_XFER — config-flash relay
Raw, byte-for-byte SPI passthrough to the FPGA's own config flash. The
FPGA relays every MOSI byte it receives onto the physical flash's own
MOSI line, and relays whatever the flash returns back on MISO. **The
FPGA knows nothing about SPI-NOR command semantics** — firmware is
responsible for sending a real, complete flash command sequence,
exactly as if directly wired to the flash.
**Real SPI-NOR opcodes** (Winbond W25Q32JV, verified against the
actual datasheet):
| Opcode | Command |
|---|---|
| `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 |
**Real, critical timing requirement** (measured via simulation,
`spi_host_bridge_v3.v`'s own header, EXP-0077): a relayed byte's real
flash response only becomes stable starting the transfer's own **byte
N+2** — one extra host-clocked byte is not enough (`flash_spi_
master.v`'s own internal transfer, ~640ns at 155.039MHz/DIV=4, starts
only once byte N is fully received, overlapping byte N+1's own
window). Firmware **must** clock **two** trailing dummy bytes at the
end of a `FLASH_XFER` transaction to safely receive the final real
response — one dummy byte is a real, previously-reproduced bug, not a
hypothetical concern.
**Real update procedure**: after writing a new bitstream to the flash
via `FLASH_XFER`, reconfigure either by pulsing `PROGRAM_B` externally
(if the ESP32 has a GPIO wired to it — board-dependent), or (real,
disclosed future work, not built) via an `ICAPE2`-based warm
self-reconfiguration triggered over the same SPI bus.
---
## 7. Error handling
`dir_error` (readable via `STATUS`, contributes to `data_ready_n`) is
the only real, hardware-visible error signal. It is a **latch**, not a
transient flag — per `neural_director_grouped.v`'s own real, disclosed
design (§4.2), there is **no in-band recovery**: once set (e.g. by a
real octet-mismatch stall), only a hardware reset clears it.
**Real, recommended firmware policy**: treat `dir_error` as fatal to
the current batch of in-flight jobs — issue a real reset (`RESET`
opcode or `CONTROL` register bit0), re-establish `DEVICE_ID`/`STATUS`
sanity (§2.1 steps 34), and re-submit any real jobs that were
in-flight at the time of the error, since the Director's own internal
queue state is not preserved across a reset.
---
## 8. Real, disclosed open items
- No ESP32-side firmware exists yet in this repository — this
document is the specification, not an implementation.
- JTAG bit-banging firmware (§2, path 1) is real, necessary, unbuilt
software.
- Real DDR3 calibration timing (§2.1 step 4) has not been measured on
real hardware — only in simulation with `SIM_BYPASS_INIT_CAL`.
- Whether/how the ESP32 firmware tracks outstanding jobs per-node_id
(§5) — a real firmware design decision, not yet made.
- `ICAPE2`-based warm self-reconfiguration (§6.2) is disclosed future
work, not built.
- `sys_rst`'s own real, permanent board pin location is still
tentative (`docs/PHYSICAL_REALIZATION.md` §7) — confirm against the
real board schematic before finalizing firmware GPIO assignments.
+37 -19
View File
@@ -165,26 +165,44 @@ 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 — N=8 in physical fabrication; N=2/N=8/N=16 all real, closed on branch `n16-timing-closure` (EXP-0095…0098)
## 3. Real timing signoff — N=16 IS NOW THE REAL FABRICATION TARGET (EXP-0095…0098)
**Real, current, two-track status (2026-09-22)**: the **physical board
currently being fabricated** uses **N=8** (`hardware/v3/rtl/
n8_system_ddr3_top.v`, unmodified, EXP-0095/0096 signoff below) — this
was the real, definitive decision made on 2026-09-21 after a real,
measured N=4/8/16 timing curve found N=8 the largest configuration
that closed cleanly at the time. Separately, on an isolated real
development branch (`n16-timing-closure`, does NOT affect the board
already in fabrication), a real fix (EXP-0097 — an extra real pipeline
stage in `neural_processor_packed.v`'s own MAC datapath, see
`docs/ARCHITECTURE_ANALYSIS.md` §5.6) has since **closed real timing
for N=16 too, and improved N=2's and N=8's own margins** — the whole
real product family (N=2/N=8/N=16) now closes with positive margin
under that fix. Whether/when to adopt this fix for a future board
revision (potentially N=16) is a real, open hardware decision, not an
RTL blocker — see `docs/ARCHITECTURE_ANALYSIS.md` §5.6 for the full
story.
**UPDATE (2026-09-22): the user moved physical board fabrication to
N=16.** `hardware/v3/rtl/n16_system_ddr3_top.v` (with the real EXP-0097
MAC-pipeline fix) is now the real, current target — `v3-artix7` (the
branch representing what is actually being built) has been fast-
forward merged with the former `n16-timing-closure` branch, so both
now point at the same real, consolidated state. **No physical board
change is needed for this** — DDR3/SPI/flash/config pins are all
package-level and identical regardless of `N_GROUPS` (confirmed real,
not assumed, since N=2/N=8/N=16 all route through the exact same real
XDC). Only the bitstream target changes.
### 3.0 N=8, in physical fabrication (`n8_system_ddr3_top.v`, EXP-0095/0096, unmodified core)
N=8's own real signoff (§3.0 below) remains accurate and valid as a
real, simpler fallback if ever needed, but is no longer what is
physically being fabricated. N=2 (EXP-0088) remains the simplest real
fallback below that. See `docs/ARCHITECTURE_ANALYSIS.md` §5.6 for the
full real story of how N=16 was closed (EXP-0097/0098).
### 3.0 N=16, the current real fabrication target (`n16_system_ddr3_top.v`, EXP-0097/0098, with the MAC-pipeline fix)
| Metric | Value |
|---|---|
| DDR3 PHY clock (sys_clk_p/n) | **310.078 MHz** (3.225ns period) |
| Compute domain clock (ui_clk/clk_pll_i) | **155.039 MHz** |
| WNS (setup slack) | **+0.269 ns** — real, closed, 0 failing setup endpoints |
| WHS (hold slack) | +0.026 ns |
| Failing endpoints | 0 of 106087 (setup), 0 of 106084 (hold) |
| LUTs used | 19903 / 63400 (31.39%) |
| Registers used | 35409 / 126800 (27.93%) |
| DSP48E1 used | 128 / 240 (53.33%) — 8 per PE × 16 real parallel PEs |
| Real parallelism | **16 real parallel processing elements** |
| Real functional verification | 32/32 PASS, real DDR3 model (`tb_n16_system_ddr3.v`) |
See `docs/ARCHITECTURE_ANALYSIS.md` §5.6.1 for the full real root-cause
and fix story (the MAC-datapath pipeline split that closed this).
### 3.0-fallback N=8, real, valid fallback signoff (`n8_system_ddr3_top.v`, EXP-0095/0096, unmodified core)
Top-level RTL: `hardware/v3/rtl/n8_system_ddr3_top.v` — 2 groups × 4
PEs, shared-weight-broadcast systolic architecture
@@ -217,7 +235,7 @@ reach this result, EXP-0094/0095) after ANY further RTL change before
trusting timing again — do not assume it still holds. This zero-margin
result is exactly why EXP-0097's own pipeline fix (below) was pursued.
### 3.0.1 The real, consolidated family — EXP-0097/0098, branch `n16-timing-closure` (not yet on the fabricated board)
### 3.0.1 The real, consolidated family — EXP-0097/0098 (N=16 is the fabrication target; N=2/N=8 kept real and valid as fallbacks)
A single real fix (`neural_processor_packed.v`, an extra real pipeline
stage in the packed-MAC datapath — full real root-cause story in