Cut the MISO output path: drop EXI_J4 and the CS mux; rank seeds by it
Remaining after the bit-phase fix was a SPEED-DEPENDENT failure -- the GC read
correctly at 1/2/4/8/16 MHz and failed only at index 5 (27 MHz), returning
0B 80 = 0x17 late by exactly ONE bit. Opposite signature to the phase slip it
was hiding under, and this one really is propagation delay.
MISO is source-synchronous: driven on the falling edge, sampled by the GC on
the next rising edge, so the whole path must fit a HALF period -- 18.5 ns at
27 MHz -- minus global-buffer insertion, pad, connector and the GC's setup.
The failing build measured ~10.5 ns of that internally. Critical path:
clk-to-q 1.39 tx_shift (3,12)
routing 3.60 tx_shift[7] (3,12) -> (5,5) to the CS mux
logic 1.21 Mux(cs_n, 1, ...) LUT
routing 4.79 miso (5,5) -> (17,0) to the J4 MISO pad
10.99 ns (2.60 logic, 8.39 routing)
Two removals, both of things that were pure cost here:
1. EXI_J4 is GONE entirely (exi_j4.py, the resource, the AND merge, the docs
section, scripts/pi-exi-wirecheck.py) -- recoverable from git history. The
AND put a LUT in the CLOCK path, denying the EXI clock pad its dedicated
global-buffer route, and MISO fanned out to the J4 pad at die (17,0) which
was the far end of the critical output path: 4.79 ns of routing to a header
with nothing plugged into it. 10.2-11.5 ns -> 9.3-9.6 ns.
2. The Mux(cs_n, 1, ...) LUT is gone from the MISO data path. It only parked
MISO high when deselected, which the flop's own reset value does for free:
tx_shift now has init=0xFF and CS is that domain's async reset, so MISO is
tx_shift[7] with nothing in between. Removes 1.21 ns of logic plus the
3.60 ns route feeding it and lets the placer sit the flop by the pad.
synth.py now PARSES AND RANKS BY the MISO clock-to-pad delay. Ranking by fmax
was how a board that failed at 27 MHz passed every seed at >=1.29x margin --
fmax was never the binding constraint for this design.
--seeds 8: clk 28.7-32.5 MHz, EXI 49.6-62.1 MHz, MISO 8.38-10.22 ns, 8/8 PASS.
Best seed 8 (clk 30.35, EXI 49.56, MISO 8.38 ns), flashed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -82,16 +82,29 @@ The `exi_bba/` module tree is **fully implemented** with simulation testbenches.
|
||||
All modules elaborate without errors and pass their unit tests, including
|
||||
`bba_top` at the real 27 MHz EXI rate (`EXI_HALF=1`).
|
||||
|
||||
**Timing closes on EVERY seed (2026-10-03).** Measured with `EXI_J4=1` +
|
||||
socket-3 UDP, `python -m exi_bba.synth --seeds 8`, 68% LC:
|
||||
**Timing closes on EVERY seed (2026-10-03).** `python -m exi_bba.synth
|
||||
--seeds 8`, 68% LC:
|
||||
|
||||
| domain | target | result |
|
||||
| domain / path | target | result |
|
||||
|---|---|---|
|
||||
| `clk` (sync, HFOSC) | 24 MHz | 26.4–32.2 MHz — **PASS on all 8 seeds** |
|
||||
| EXI pin domain | 27 MHz | 45.6–64.4 MHz — **PASS on all 8 seeds** |
|
||||
| `clk` (sync, HFOSC) | 24 MHz | 28.7–32.5 MHz — **PASS on all 8 seeds** |
|
||||
| EXI pin domain | 27 MHz | 49.6–62.1 MHz — **PASS on all 8 seeds** |
|
||||
| **MISO clock→pad** | see below | **8.38–10.22 ns** |
|
||||
|
||||
Best seed **3**: `clk` 32.18 MHz, EXI 55.86 MHz (worst relative margin 1.34×).
|
||||
Flash `build/seed<N>/top.bin`.
|
||||
Best seed **8**: `clk` 30.35 MHz, EXI 49.56 MHz, MISO 8.38 ns.
|
||||
|
||||
⚠️ **fmax is NOT the binding constraint — MISO clock-to-pad is.** MISO is a
|
||||
source-synchronous output: it leaves on the falling edge and the GameCube
|
||||
samples it on the next RISING edge, so the whole path must fit a **half
|
||||
period — 18.5 ns at 27 MHz** — minus global-buffer insertion, the pad, the
|
||||
connector and the GC's own setup time. A build measuring ~10.5 ns failed on
|
||||
console at 27 MHz while passing at 16 MHz, with every seed reporting ≥1.29×
|
||||
fmax margin. `synth.py` therefore reports this delay and **ranks seeds by it**,
|
||||
not by fmax. Two changes bought the headroom: removing the EXI_J4 pad merge
|
||||
(MISO no longer fans out to the far J4 pad, and the clock pad regains its
|
||||
dedicated global-buffer route) and removing the `Mux(cs_n, 1, …)` LUT from the
|
||||
MISO data path (`tx_shift` has `init=0xFF`, so CS — the domain's async reset —
|
||||
parks MISO high by itself).
|
||||
|
||||
This replaces the long-standing "closes on a minority of seeds" problem, which
|
||||
was entirely an artefact of the PLL-fed 54 MHz `capture` domain. Removing that
|
||||
@@ -161,7 +174,7 @@ python -m exi_bba.uart_shell # 8 tests: shell cmds + reply/timeout
|
||||
python -m exi_bba.eeprom_model
|
||||
python -m exi_bba.bba_top # end-to-end EXI integration test (W5100 RX loop)
|
||||
EXI_HALF=1 python -m exi_bba.bba_top # ...at the REAL 27 MHz rate — always do this
|
||||
python -m exi_bba.exi_j4 # EXI_J4 pad-merge truth table + enumeration
|
||||
|
||||
```
|
||||
|
||||
### UART shell + UDP bring-up test (default flash build)
|
||||
@@ -285,44 +298,24 @@ slip was *structurally invisible* — the same failure of imagination as the
|
||||
Mode 2/3 mix-up, which was invisible to a master that parked MOSI early. When
|
||||
adding SPI tests, vary the edge the transaction starts on.
|
||||
|
||||
### EXI on the J4 debug header (`EXI_J4=1`) — bench host without SP1
|
||||
### EXI on the J4 debug header — REMOVED 2026-10-03
|
||||
|
||||
`EXI_J4=1` lets a bench host (the Pi) drive EXI through the unpopulated J4
|
||||
header instead of the SP1 edge connector. It does NOT repin the EXI resource —
|
||||
it **merges** a second set of pads onto the existing ones, so SP1 stays live and
|
||||
the default build is unchanged:
|
||||
`EXI_J4=1` used to merge a second set of EXI pads (the unpopulated J4 debug
|
||||
header, FPGA pins 9/10/11/12/6) onto the SP1 pads with an AND, so a bench host
|
||||
(a Raspberry Pi) could drive EXI without the edge connector. **The whole path
|
||||
is gone** — `exi_bba/exi_j4.py`, the `exi_j4` resource, the AND merge and
|
||||
`scripts/pi-exi-wirecheck.py`. Recover it from git history if a bench host is
|
||||
ever needed again.
|
||||
|
||||
| J4 | DBG | FPGA pin | EXI |
|
||||
|---|---|---|---|
|
||||
| 1 | — | — | GND |
|
||||
| 2 | DBG0 | 9 | CLK (in) |
|
||||
| 3 | DBG1 | 10 | MOSI (in) |
|
||||
| 4 | DBG2 | 11 | MISO (out) |
|
||||
| 5 | DBG3 | 12 | CS_N (in) |
|
||||
| 6 | DBG4 | 6 | INT_N (out) |
|
||||
It was removed because it was actively costing MISO timing, and the GameCube
|
||||
itself is now the test host:
|
||||
- the AND put a **LUT in the clock path**, so the EXI clock pad could not use
|
||||
its dedicated global-buffer route; and
|
||||
- MISO fanned out to both pads, and the J4 pad at die (17,0) was the far end
|
||||
of the **critical output path** — 4.79 ns of routing to a header with
|
||||
nothing plugged into it.
|
||||
|
||||
The three inputs are **AND-merged** with `PULLUP=1` on both pad sets. AND is
|
||||
exact, not a hack: every EXI input is inactive-HIGH (CPOL=1 → CLK idles high,
|
||||
CS_N is active-low, MOSI only matters while CS_N is low), so the unplugged side
|
||||
idles at 1 through its pull-up and passes the other side through — no select
|
||||
signal needed. Outputs drive both pads. **Drive one side at a time**; a GC on
|
||||
SP1 plus a host on J4 will fight and the AND yields the mangled product.
|
||||
Build refuses to run alongside `UART_J4` (also pins 9/10) or an `ETH_D3_PIN`
|
||||
aimed at a J4 pad.
|
||||
|
||||
**Why merge rather than repin:** all five J4 pads are on the SAME bottom die
|
||||
edge as the EXI pads (x=13–18 vs x=5–9, both y=0 per icestorm's `pinloc_db`
|
||||
`5k-sg48`), so the EXI clock domain keeps its bottom-edge anchor. Repinning
|
||||
would have perturbed the one placement variable that the pin-co-location
|
||||
experiment showed was worth ±5 MHz. (That experiment mattered when the
|
||||
PLL-fed capture domain was the binding constraint; with ~2.4× margin on the
|
||||
EXI domain today it is no longer a concern either way.)
|
||||
|
||||
⚠️ **The merge changes which net is the clock.** On a plain build the clock is
|
||||
the pad itself; with `EXI_J4=1` it is the AND of the two pads, a distinct net.
|
||||
`synth.py` constrains whichever one applies — constraining the pad on a J4
|
||||
build misses, and nextpnr then silently falls back to a 12 MHz default and
|
||||
under-optimises the whole EXI domain.
|
||||
Dropping it cut MISO clock-to-pad from ~10.2–11.5 ns to ~9.3–9.6 ns.
|
||||
|
||||
**HARDWARE BRING-UP 2026-09-30: the EXI capture path is PROVEN on silicon.**
|
||||
A Raspberry Pi 4 bit-banging EXI into J4 (EXI_J4 build, seed 8) enumerated the
|
||||
|
||||
Reference in New Issue
Block a user