Drive MISO from an SB_IO output register

Removing ~2 ns from the fabric MISO path (EXI_J4 merge + the CS mux) did NOT
move the hardware result: the GC still returned a byte-identical 0B 80 at
27 MHz while reading correctly at 16 MHz and below.  Identical rather than
intermittent means the path is well outside the window, not marginally inside
it, so trimming fabric delay further was not going to get there.

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.
Driving from fabric spent 8.4-11.5 ns of that on routing alone.

An SB_IO output register sits in the IO tile: clock-to-pad is the IO flop's
own delay with no fabric routing.  nextpnr confirms the path left the fabric
entirely -- miso_io.D_OUT_0 packs at die (9,0), the pad itself, SB_IO count is
unchanged at 24, and no negedge -> <async> path is reported any more.  What
replaces it is a setup path INTO the IOB register (10.07 ns against a
half-period), which is internal and has nothing stacked on top of it.

PIN_TYPE was decoded from yosys's own SB_IO model rather than from memory,
because wrong bits there produce a silently dead or mistimed output:
  [5:4]=10     tri-state, combinational OUTPUT_ENABLE
  [3]=0,[2]=1  dout = dout_q_0, single-rate REGISTERED output
  [1:0]=01     simple input (unused)
  NEG_TRIGGER=1 moves the IO registers to the FALLING edge.

To keep simulation and silicon agreeing about MISO, the slave exposes
miso_next -- the value the shift register WILL hold after the falling edge --
and spi_mode3_slave T6 proves it predicts spi_miso bit for bit.  Without that
the IOB register would be an untested sim/hardware divergence, which is the
exact class of bug that has cost this design the most time.

BEHAVIOUR CHANGE: MISO is now TRI-STATED when CS is high, held high by the
pad's PULLUP, instead of actively driven high.  EXI MISO is a SHARED bus --
SP1 is on channel 0 alongside memory-card slot A -- so driving it while
deselected was wrong and could fight a card.

--seeds 8: clk 28.5-30.7 MHz, EXI 41.6-49.7 MHz, 8/8 PASS.  The EXI numbers
fall (62 -> 49) because the posedge->negedge setup path into the IOB register
is now charged to that domain at a half-period constraint; 41.6 MHz worst case
is still 1.5x the 27 MHz target.  Best seed 3 (clk 30.68, EXI 49.14), flashed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 19:26:08 +02:00
parent 5f7f20db43
commit 2f081a3e69
5 changed files with 113 additions and 22 deletions
+26 -18
View File
@@ -83,28 +83,36 @@ 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).** `python -m exi_bba.synth
--seeds 8`, 68% LC:
--seeds 8`:
| domain / path | target | result |
| domain | target | result |
|---|---|---|
| `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** |
| `clk` (sync, HFOSC) | 24 MHz | 28.5–30.7 MHz — **PASS on all 8 seeds** |
| EXI pin domain | 27 MHz | 41.6–49.7 MHz — **PASS on all 8 seeds** |
Best seed **8**: `clk` 30.35 MHz, EXI 49.56 MHz, MISO 8.38 ns.
Best seed **3**: `clk` 30.68 MHz, EXI 49.14 MHz. Flash `build/seed<N>/top.bin`.
⚠️ **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).
⚠️ **fmax is NOT the only constraint — MISO clock-to-pad matters more.** MISO
is source-synchronous: it leaves on the FALLING edge and the GameCube samples
it on the next RISING edge, so the path must fit a **half period (18.5 ns at
27 MHz)** minus global-buffer insertion, pad, connector and the GC's own
setup. A build measuring ~10.5 ns of that *internally* read every byte one bit
late at 27 MHz while reading correctly at 16 MHz — with every seed reporting
≥1.29× fmax margin. `synth.py` therefore parses this delay and ranks seeds by
it when it exists.
**MISO is now driven from an `SB_IO` output register** (`synth.py`), so
clock-to-pad is the IO tile's own delay and never enters the fabric — the
sweep prints `IOB (none)` because no `negedge → <async>` path exists any more.
Getting there also meant removing the EXI_J4 pad merge and the
`Mux(cs_n, 1, …)` LUT from the data path (see those notes). The EXI fmax
numbers dropped (62 → 49 MHz) because the posedge→negedge setup path into the
IOB register is now counted against that domain at a half-period constraint;
41.6 MHz worst case is still 1.5× over the 27 MHz target.
⚠️ **MISO is TRI-STATED when CS is high** (held high by the pad's PULLUP),
not actively driven. EXI MISO is a SHARED bus — SP1 is on channel 0 with
memory-card slot A — so driving it while deselected was wrong.
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