8691b09676
300908 bytes in 6.4 s (0.38 Mbit/s) with txto=0, down from 40.1 s and 16.9 s. The whole cost was round-trip, not bandwidth: stop-and-wait is one block per round trip. The counter-intuitive result is recorded deliberately: raising the host timeout 0.1 -> 0.3 barely changed the retry count (110 -> 95) but tripled the cost of each, making the transfer 2.4x SLOWER. Short is correct here because the GC re-ACKs a duplicate immediately, so a premature retry costs one timeout and nothing else. Anyone tuning this later will reach for a longer timeout first; the note says not to. Also corrects my own earlier claim. I concluded from the 0.3 s result that the misses were LOST ACKs rather than late ones. txto=0 shows every ACK reached the W5100 with TI set, so the drain and EXI side are clean -- but TI is not proof of transmission, and a 6.4 s total is hard to reconcile with ~95 genuine losses. The honest state is that it is not established either way, and the retry COUNT at 0.05 s is the measurement that would settle it. Recorded as open rather than fixed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1137 lines
63 KiB
Markdown
1137 lines
63 KiB
Markdown
# CLAUDE.md
|
||
|
||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||
|
||
## Project: GC BBA FPGA Replacement
|
||
|
||
Replace the GameCube Broadband Adapter (DOL-015 / MX98730EC) with an iCEbreaker
|
||
FPGA (Lattice iCE40UP5K) written in Amaranth HDL. The FPGA emulates the BBA
|
||
register interface over the GameCube EXI bus and bridges to a WIZnet ethernet
|
||
chip for real 100BASE-TX ethernet — default **W5100** (indirect parallel bus,
|
||
reaches the EXI throughput ceiling) or **W5500** (SPI Pmod, simpler wiring but
|
||
~12 Mbit/s). GC software (Swiss homebrew) sees an identical BBA. See "W5100 vs
|
||
W5500 ethernet back-end".
|
||
|
||
---
|
||
|
||
## Development Environment
|
||
|
||
**Preferred:** Use the devcontainer (`.devcontainer/`) which includes Python 3.12,
|
||
`nextpnr-ice40`, and `fpga-icestorm` pre-installed.
|
||
|
||
**Windows host + WSL2 devcontainer — USB flashing setup:**
|
||
1. Install `usbipd-win` (https://github.com/dorssel/usbipd-win/releases)
|
||
2. Run `.devcontainer/attach-icebreaker.ps1` as Administrator before opening the devcontainer
|
||
3. The devcontainer runs `--privileged` AND bind-mounts `/dev/bus/usb` so libusb
|
||
(`iceprog`, `pyftdi`) can see the FT2232H; the `vscode` user flashes via
|
||
passwordless `sudo` (the USB nodes are root-owned). If you change
|
||
`.devcontainer/`, **rebuild the container** for it to take effect.
|
||
4. Flashing the re-bba-rb board (bitstream + FT2232H EEPROM) is documented as the
|
||
`flash-re-bba-rb` skill — see `.claude/skills/flash-re-bba-rb/SKILL.md`.
|
||
|
||
**Local venv (outside devcontainer):**
|
||
```bash
|
||
python -m venv .venv
|
||
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
||
pip install -r requirements.txt
|
||
```
|
||
|
||
Yosys is bundled in `amaranth-yosys`; `nextpnr-ice40` and `iceprog` must be
|
||
installed separately (via apt on Linux, or via the devcontainer).
|
||
|
||
---
|
||
|
||
## Commands
|
||
|
||
**Build and flash the iCEbreaker (must run from workspace root):**
|
||
```bash
|
||
python rebbarb/rebbarb.py
|
||
```
|
||
Runs synthesis (yosys), place-and-route (nextpnr-ice40), and flashes via `iceprog`.
|
||
Set `ICEPROG=/path/to/iceprog` env var to override the binary location.
|
||
Note: `rebbarb/rebbarb.py` builds a 36 MHz LED blink demo. The BBA
|
||
implementation (`exi_bba/`) clocks the SPI bit engine from the GC's EXI clock
|
||
pin itself and everything else from `sync` @ 24 MHz (internal HFOSC); there is
|
||
no PLL.
|
||
Synthesize/flash the real design with `python -m exi_bba.synth [--flash]`.
|
||
|
||
**Run a simulation:**
|
||
```bash
|
||
# New-API testbench style (preferred for new code):
|
||
python rebbarb/toggle_button.py # writes ToggleButton.vcd
|
||
python rebbarb/pulse_button.py # writes PulseButton.vcd
|
||
|
||
# Old-API process style (reference only, do not replicate in new code):
|
||
python examples/amaranth_cdc.py # CDC primitives demo
|
||
python examples/async_fifo.py # AsyncFIFO behaviour
|
||
python examples/icebreaker_fifo.py # iCEbreaker-specific FIFO (Verilog dump)
|
||
```
|
||
Open VCD output with `gtkwave`. Simulations are the primary testing mechanism —
|
||
there is no separate test runner.
|
||
|
||
**Clock sources:** there is no PLL and no external crystal any more. `sync`
|
||
comes from the internal `SB_HFOSC` ÷2 = 24 MHz; the EXI bit engine is clocked
|
||
by the GC's own EXI clock pin. Pin 35 (the old 12 MHz input) and the PLL hard
|
||
macro are both free.
|
||
|
||
---
|
||
|
||
## Current Implementation State
|
||
|
||
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`).
|
||
|
||
✅ **FULL TEST SUITE PASSES ON A REAL CONSOLE (2026-10-09)** — every suite in
|
||
`gc_bba_test`: Identity (device ID `04 02 02 00`, HIPR, NWAYS), Registers,
|
||
Reset/control, EXI integrity (speed sweep `ok=[1,1,1,1,1,1]`, per-speed dump
|
||
clean at all six indices, raw reg dump `0x17` in slot 0), Ethernet, and
|
||
SPRAM/DMA. The EXI front end runs at the real 27 MHz hardware rate.
|
||
|
||
(`Raw ID dump` reports SKIP once an earlier suite has issued the NCRA reset
|
||
that clears `id_phase` — reflash or reset the FPGA to restore it.)
|
||
|
||
**Timing closes on EVERY seed (2026-10-03).** `python -m exi_bba.synth
|
||
--seeds 8`:
|
||
|
||
| domain | target | result |
|
||
|---|---|---|
|
||
| `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 **3**: `clk` 30.68 MHz, EXI 49.14 MHz. Flash `build/seed<N>/top.bin`.
|
||
|
||
⚠️ **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
|
||
domain (the bit engine is now clocked by the EXI pin — see "Key Architecture
|
||
Decisions") took the pass rate from **2 of 8 to 8 of 8** and made the old
|
||
capture-domain tuning levers moot: the Gray-code CDC loop that was the binding
|
||
path, the pin co-location experiment, and the proposed ping-pong TX CDC are all
|
||
obsolete. Do not reintroduce them.
|
||
|
||
⚠️ **The binding constraint has FLIPPED.** `clk` (sync, 1.10–1.34× margin) is
|
||
now tighter than the EXI domain (1.69–2.39×). Rank seeds by **worst relative
|
||
margin**, not by the EXI number — `synth.py` does this. Added logic now
|
||
threatens `sync`, not the EXI front-end.
|
||
|
||
⚠️ **Still sweep.** `clk` at 26.41 MHz on seed 5 is only 10% over target, so a
|
||
seed can still land uncomfortably close even though all 8 pass today.
|
||
|
||
### `exi_bba/` module status
|
||
|
||
| Module | File | Tests pass |
|
||
|---|---|---|
|
||
| `BBATop` | `exi_bba/bba_top.py` | ✅ EXI integration + full W5100→SPRAM→GC RX loop, at `EXI_HALF=1/2/8`; synth PASS |
|
||
| `ExiCapture` | `exi_bba/exi_capture.py` | ✅ rx/tx byte-stream + over-push/flush |
|
||
| `SPIMode3Slave` | `exi_bba/spi_mode3_slave.py` | ✅ 7 checks (rx/tx edges, tx_load, MISO idle) — **mutation-tested**: flipping either clock edge fails it |
|
||
| `BBARegisterFile` | `exi_bba/bba_register_file.py` | ✅ 7 tests (real 4-byte header, proactive push + DMA stream) |
|
||
| `SPRAMArbiter` | `exi_bba/spram_arbiter.py` | ✅ 3 tests |
|
||
| `RXFrameAssembler` | `exi_bba/rx_frame_assembler.py` | ✅ 3 tests |
|
||
| `TXFrameDrain` | `exi_bba/tx_frame_drain.py` | ✅ 2 tests |
|
||
| `W5100ParallelMaster` | `exi_bba/w5100_parallel_master.py` | ✅ MACRAW init/TX/RX (T1–T5) + socket-N UDP send/receive (U1–U4) vs bus model, incl. ring wrap — **default eth back-end** |
|
||
| `W5500SPIMaster` | `exi_bba/w5500_spi_master.py` | ✅ init/TX/RX vs SPI-slave model (alt back-end) |
|
||
| `StatusPanel` | `exi_bba/status_panel.py` | ✅ 6 tests (heartbeat, stretched activity LEDs, debounced buttons, freeze) |
|
||
| `UARTConsole` | `exi_bba/uart_console.py` | ✅ 7 tests (event log + 'r' reinit) — event-logger console (`--console` build) |
|
||
| `UARTShell` | `exi_bba/uart_shell.py` | ✅ 8 tests (help/unicast/broadcast/reply-print/timeout/badip/backspace) — interactive UDP bring-up shell (**default build**) |
|
||
| `EEPROMModel` | `exi_bba/eeprom_model.py` | ✅ 4 tests |
|
||
|
||
**Bring-up status panel (optional):** `BBATop(status_panel=True)` adds a
|
||
`StatusPanel` driving onboard iCEbreaker LEDs + button (dedicated pins, so it
|
||
coexists with EXI + W5100). `synth.py` enables it: **LEDG=heartbeat**,
|
||
**LEDR=EXI activity** (the GC is talking), **RGB red=rx / green=tx / ready**
|
||
(via `SB_RGBA_DRV` on pins 39/40/41), **BTN_N=manual re-init**. All 5 panel
|
||
LEDs are now mapped on the iCEbreaker. For this build's timing see the seed
|
||
table above — it does NOT close on every seed.
|
||
|
||
On the **re-bba-rb board** the third RGB element (`led[4]`, "ready", pin 41) is
|
||
**D13 = YELLOW** (LCSC C2287), not blue — `synth.py`'s comment still calls it
|
||
blue, which describes the iCEbreaker dev board, not this PCB. D11=RED (rx) and
|
||
D12=GREEN (tx) are correct on both.
|
||
|
||
**Ethernet back-end is selectable:** `BBATop(eth="w5100")` (default — indirect
|
||
parallel bus, reaches the ~27 Mbit/s EXI ceiling) or `BBATop(eth="w5500")` (SPI,
|
||
~12 Mbit/s). Both masters expose the identical tx/rx/init/par streaming
|
||
interface; only the physical pins differ. See "W5100 vs W5500" below.
|
||
|
||
### Run all module testbenches (from workspace root)
|
||
```bash
|
||
python -m exi_bba.spi_mode3_slave
|
||
python -m exi_bba.exi_capture
|
||
python -m exi_bba.bba_register_file
|
||
python -m exi_bba.spram_arbiter
|
||
python -m exi_bba.rx_frame_assembler
|
||
python -m exi_bba.tx_frame_drain
|
||
python -m exi_bba.w5100_parallel_master # T1-5 MACRAW + U1-4 socket-N UDP tx/rx
|
||
python -m exi_bba.w5500_spi_master
|
||
python -m exi_bba.status_panel # 6 tests: heartbeat/activity/buttons
|
||
python -m exi_bba.uart_console # 7 tests: event log + 'r' reinit
|
||
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
|
||
|
||
```
|
||
|
||
### UART shell + UDP bring-up test (default flash build)
|
||
|
||
`BBATop(uart_shell=True)` adds an **interactive UART command shell** that shares
|
||
the FT2232H channel-B UART pins with (and replaces) the event-log console, and
|
||
drives a **UDP send/receive test on a second W5100 socket** (default socket 3)
|
||
alongside the BBA's socket-0 MACRAW path. `synth.py` builds it **by default**
|
||
(pass `--console` for the old event-logger instead). EXI/BBA keeps priority: the
|
||
UDP test is the lowest-priority branch in the W5100 master's bus arbiter, so a
|
||
GC frame always preempts a queued send/poll.
|
||
|
||
Shell (115200 8N1, `rebbarb> ` prompt):
|
||
- `help` — list commands
|
||
- `udp unicast <ip> [msg]` — send a UDP datagram to `<ip>:dst_port` (the W5100
|
||
ARPs the target), then wait (bounded) for a reply
|
||
- `udp broadcast [msg]` — same to 255.255.255.255 (limited broadcast; no ARP)
|
||
- after a send, the shell prints `sent`, waits up to `reply_timeout_cycles`
|
||
(~1 s) for a UDP reply on the socket, and prints `rx <payload>` or `timeout`
|
||
(so it never hangs). Example: `udp broadcast Hello World` → a responder
|
||
unicasts back → `rx <reply>` on the console.
|
||
|
||
Config is build-time via `synth.py` env vars (the board's own IP identity —
|
||
match your LAN): `UDP_SOCKET` (1–3, default 3), `UDP_SRC_IP`, `UDP_SUBNET`,
|
||
`UDP_GATEWAY`, `UDP_DST_IP`, `UDP_SRC_PORT`, `UDP_DST_PORT`. Receive on the PC
|
||
with `nc -ul <port>` or `sudo tcpdump -n udp port <port>`.
|
||
|
||
**MACRAW + UDP coexistence is datasheet-confirmed** (W5100S §4.6: *"MACRAW Mode
|
||
SOCKET 0 does not receive any Data Packet for other SOCKET"*; feature list:
|
||
*"Support 4 independent SOCKETs simultaneously"*). All 4 sockets get a 2 KB
|
||
RX + 2 KB TX buffer at reset (`RMSR/TMSR=0x55`), so socket-3 frames are stored
|
||
in the chip independently — no extra FPGA buffer needed. Register addresses are
|
||
still datasheet-from-memory — **confirm at hardware bring-up**.
|
||
|
||
### ✅ Self-update over the BBA works on hardware (2026-10-09)
|
||
|
||
Press **Z** in `gc_bba_test` and run `sudo ./scripts/send-dol.py <iface>
|
||
<file.dol>` on a Linux host on the same segment. The build is received over the
|
||
adapter, written to the SD card, **read back and verified**, rotated in (the
|
||
previous build is kept as `<name>.prev.dol`), and then the app boots
|
||
**`/boot.dol`** — a known-good Swiss. Nothing received is ever launched
|
||
directly, so a broken test build costs nothing. Proven end to end on console:
|
||
300076 bytes in 294 blocks, verify OK, Swiss boots, and new builds are now
|
||
delivered this way instead of by SD card.
|
||
|
||
**Measured 2026-10-09: 300908 bytes in 6.4 s (0.38 Mbit/s), `txto=0`.**
|
||
|
||
Getting there was all round-trip cost, not bandwidth — stop-and-wait means one
|
||
block per round trip. Three things mattered, in order of effect:
|
||
|
||
| change | result |
|
||
|---|---|
|
||
| host `--timeout` 0.3 → **0.05 s** | 40.1 s → 6.4 s |
|
||
| `send_ack` TI poll 200 → **8** | 200 EXI transactions ≈ 12 ms, spent only when TI was slow — i.e. exactly when the host was closest to timing out |
|
||
| block 256 → **1024** | 1166 blocks → 294 |
|
||
|
||
⚠️ **The timeout result is counter-intuitive and worth remembering:** raising it
|
||
0.1 → 0.3 barely changed the retry count (110 → 95) but tripled the cost of
|
||
each, so it made the transfer 2.4× SLOWER. Short is right here, because the GC
|
||
re-ACKs a duplicate block immediately — a premature retry costs one timeout
|
||
and nothing else.
|
||
|
||
⚠️ `txto=0` means every ACK was handed to the W5100 with TI set, so the drain
|
||
and the EXI side are clean. **TI is not proof of transmission**, so a residual
|
||
retry rate points past the hand-off (the chip, or the wire). Whether the
|
||
remaining misses are lost or merely late is NOT established — the retry count
|
||
is roughly timeout-independent, which suggests loss, but the 6.4 s result is
|
||
hard to reconcile with ~95 genuine losses. Get the retry count alongside
|
||
`txto` before theorising.
|
||
|
||
Details and the recovery ladder are in `gc_bba_test/README.md`. Key points:
|
||
- **Raw ethertype `0x88B5`, not IP** — no ARP, no IP stack, no libogc network
|
||
driver competing for the EXI channel. It reuses only `bba_tx_bytes()` and the
|
||
SPRAM ring, so each update also soak-tests the BBA data path.
|
||
- **It cannot be driven from the devcontainer.** Raw ethernet is link-local, so
|
||
the sender must run on a host with the physical NIC (the container has a
|
||
veth and the frames go nowhere).
|
||
- **Stop-and-wait is load-bearing** — see the RBFI note; one frame in flight
|
||
means the ring is never contended.
|
||
- `bba_tx_bytes()` streams in **8-byte** chunks, one EXI transaction each.
|
||
Writing byte-by-byte cost a full EXI_Lock/Select/Deselect/Unlock per byte —
|
||
~60 transactions per ACK — and that latency, not any transmit failure, is
|
||
what made ~⅓ of blocks retry (`txto=0` on hardware: TI always set).
|
||
|
||
⚠️ **The chunk size is bounded by `tx_bytes`, which is 16 deep.** Within one
|
||
transaction the bytes arrive back-to-back at EXI speed, far faster than
|
||
`TXFrameDrain` starts consuming, and `BBARegisterFile` silently drops bytes
|
||
when `w_rdy` is low because SPI cannot back-pressure. A 64-byte chunk
|
||
overran the FIFO, the frame went out truncated and the host saw no valid ACK
|
||
at all. Byte-by-byte only worked because the per-byte EXI overhead happened
|
||
to pace the drain. 8 is half the FIFO depth, so it cannot overflow even if
|
||
the drain consumes nothing for a whole transaction. **Do not raise it
|
||
without deepening `tx_bytes` or giving the TX path real back-pressure.**
|
||
|
||
### ⚠️ OPEN: ring descriptor sometimes lands at page offset +1
|
||
|
||
On console the SPRAM ring descriptor is at page+0 for ~23% of frames and
|
||
page+1 for ~77% (`desc@+0=100 +1=343` in one run), **varying frame to frame**.
|
||
`netboot`'s reader probes both offsets and takes whichever yields a plausible
|
||
length, so it is worked around, not fixed.
|
||
|
||
What has been ruled out:
|
||
- **Not ring overflow** — `lapped=0` with RBFI active.
|
||
- **Not the two dropped-write handshake bugs** in `RXFrameAssembler` (fixed,
|
||
mutation-tested; T2 would catch a recurrence).
|
||
- **Not write-port contention** — `eth_wr_ready` is asserted combinationally
|
||
whenever `eth_wr_valid` is high (ETH has absolute priority in
|
||
`SPRAMArbiter`), so the write port is never stalled. T5 models a stall
|
||
anyway and the layout is unchanged.
|
||
- **Not the late-SPRAM-response leak** — guarding on `outstanding != 0` and
|
||
clearing `sp_pending` in `HEADER0` fixed a real latent bug but left the
|
||
ratio unchanged.
|
||
|
||
⚠️ **THE MISSING TEST:** a `bba_top`-level test that performs an EXI DMA read
|
||
from the SPRAM ring **while** the ethernet side writes frames into it. ETH
|
||
preempts EXI reads, so only under that overlap can a read response be delayed
|
||
past the end of its transaction. No simulation does this, which is why the
|
||
offset is still unexplained. Write that before guessing again.
|
||
|
||
### ⚠️ Simulation rate — always re-run at the hardware rate
|
||
|
||
`bba_top`'s testbench clocks EXI at `27 MHz / EXI_HALF`. The default
|
||
`EXI_HALF=8` gives **3.4 MHz** — eight times slower than a real GC — which is
|
||
fast to simulate but hides every latency bug. **Run `EXI_HALF=1 python -m
|
||
exi_bba.bba_top` before trusting any timing-related change.** The current
|
||
design passes at `EXI_HALF=1`, `2` and `8`.
|
||
|
||
Historically **every testbench drove MOSI many ticks before toggling the
|
||
clock**, so MOSI was stable at BOTH edges and the sampling-edge convention was
|
||
structurally undetectable — that is how a Mode 2/Mode 3 mix-up reached silicon
|
||
with a fully green suite. Both `bba_top` and `spi_mode3_slave` now model a
|
||
strict Mode 3 master (change MOSI on the falling edge, sample MISO in the
|
||
clock-low phase), and `spi_mode3_slave`'s testbench is **mutation-tested**:
|
||
flipping either edge in the RTL makes it fail.
|
||
|
||
### ✅ FIXED 2026-10-03 — edge convention, and MISO latency
|
||
|
||
Two bugs, both now resolved by the pin-clocked rewrite:
|
||
|
||
**1. Wrong sampling edge (was Mode 2).** These notes once said "Mode 3
|
||
(CPOL=1, CPHA=1)" and then described sampling MOSI on the **falling** edge.
|
||
Idle-high + sample-on-falling is **Mode 2**. Real Mode 3 samples on the second
|
||
edge — the **rising** one — and that is what the GameCube does: it DRIVES MOSI
|
||
on the falling edge, so sampling there latched the previous bit. Symptom:
|
||
every byte one bit late in both directions at every clock speed; the `0x80`
|
||
register-space header byte read as `0x00`, which decodes as a command-space
|
||
device-ID query, so NWAYS reads returned the device ID. Raw 8-slot dump after
|
||
an FPGA reset: `00 00 02 01 01 00 00 00` = `04 02 02 00 >> 1`.
|
||
|
||
**2. MISO ~2 bits late at 27 MHz.** With MOSI fixed, register reads were
|
||
correct up to 4 MHz and failed from 8 MHz (hardware sweep: `ok=[1,1,1,0,0,0]`);
|
||
at index 5 a NWAYS read returned `05 C0` = `0x17 >> 2`. The MISO path was
|
||
delayed ~3 capture ticks ≈ 55 ns — 2 ticks of input `FFSynchronizer` on EXI CLK
|
||
plus 1 to register the shift — against a 37 ns bit time.
|
||
|
||
**Lowering the EXI clock was never an option:** Swiss hardcodes
|
||
`EXI_SPEED_32MHZ` in `exi_select()` (`cube/patches/bba/bba.c`).
|
||
|
||
**The fix was structural, not compensatory:** clock the bit engine from the EXI
|
||
clock pin itself (pin 44 sits at die (6,0) = global buffer input net 6, so it
|
||
drives a real clock network with no board change). This removes oversampling
|
||
and the synchroniser entirely, making MISO clock-to-Q. The two alternatives
|
||
were rejected: output-bit lookahead would have *tuned* a constant that may not
|
||
hold across parts and temperature, and 3× oversampling needs 81 MHz, which this
|
||
part cannot close. `SPIMode3Slave` has no `mode2` parameter any more — there is
|
||
only one correct convention and it is enforced by mutation tests.
|
||
|
||
### ✅ MISO timing SOLVED 2026-10-09 — launch a full cycle early
|
||
|
||
**Confirmed on a console:** speed sweep `ok=[1,1,1,1,1,1]`, per-speed dump
|
||
`0:1700 … 5:1700`, raw reg dump `0x17` in slot 0. Register reads are correct
|
||
at **every** EXI speed index including 5 (27 MHz, the real hardware rate).
|
||
|
||
The engine drives MISO on the **RISING** edge carrying the bit the host will
|
||
sample at the NEXT rising edge, so the output path gets a **FULL period
|
||
(37 ns)** rather than the half period (18.5 ns) a falling-edge launch gives.
|
||
`tx_out` is clocked on the rising edge and loads at `rx_bit == 7`; there is no
|
||
`exiclk_n` domain any more — one clock edge, one phase reference.
|
||
|
||
**Why a half period was not enough, and why no RTL change could fix it.**
|
||
~9 ns was removed from the fabric output path in three steps — the EXI_J4 pad
|
||
merge, then the `Mux(cs_n, 1, …)` LUT, then the entire fabric route by moving
|
||
the output into an `SB_IO` register — and the hardware result stayed **byte
|
||
identical** every time (`0B 80`). Nine nanoseconds out of 18.5 cannot change
|
||
nothing if propagation were the whole story. What remains is irreducible:
|
||
input pad + **global clock-network insertion** + IOB clock-to-Q + output pad,
|
||
plus the host's own setup. So the fix was to double the budget, not shrink the
|
||
delay.
|
||
|
||
`PHASE_METER` is what settled it: `0:8080 1:8080 2:8080 3:8080 4:8080 5:C040`
|
||
— the bit engine's own byte phase, perfect to 16 MHz and one bit late at 27.
|
||
The 8-slot dump read `C0 40 40 40 C0 40 40 40`: a uniform one-bit lag with
|
||
extra highs at bits 0 and 32, where libogc splits the read into two 4-byte
|
||
transfers and MISO tri-states to its pull-up between them.
|
||
|
||
⚠️ **Testbench consequence.** A master must sample MISO in the **CLK-LOW
|
||
phase** (the value latched ON the rising edge). Sampling after raising the
|
||
clock sees the NEXT bit in a zero-delay simulation, so the suite would report
|
||
everything one bit early while hardware was correct. Both `bba_top` and
|
||
`spi_mode3_slave` now sample in the low phase.
|
||
|
||
⚠️ **This relies on the delay landing in a window:** MISO must change late
|
||
enough to clear the host's hold time on the previous bit, and within a full
|
||
period for its setup. The measured insertion delay (~12–20 ns against 37 ns)
|
||
sits comfortably inside. If a LOW speed index ever regresses while 27 MHz
|
||
works, that is the hold edge of the window — the launch is too early — not a
|
||
setup problem.
|
||
|
||
Side effect: EXI-domain fmax rose 41.6–49.7 → **62.1–65.4 MHz**, because the
|
||
setup path into the IOB register became posedge→posedge against a full period
|
||
instead of posedge→negedge against a half. Same doubling, seen internally.
|
||
|
||
### ✅ Device ID answered by the BIT ENGINE (fixed 2026-10-09)
|
||
|
||
`bba_devid` returned `00 04 02 02` on a console — the ID correct but **one
|
||
byte late** — and it failed in its REAL shape (16 µs `gap()`, exactly four
|
||
bytes clocked), so it was not a probe artefact. Register-space reads were
|
||
fine throughout; only command space was affected.
|
||
|
||
**Cause was latency, not phase.** A command-space header is two bytes, so the
|
||
reply must be in the tx FIFO by the **last rising edge of the header
|
||
transfer** — one byte time, 296 ns at 27 MHz. The round trip is rx `AsyncFIFO`
|
||
(~125 ns) + a sync cycle + tx `AsyncFIFO` (~110 ns) ≈ 280–320 ns: marginal by
|
||
construction. `HEADER0` already pushed in the same cycle it consumed byte 0,
|
||
so the FSM side was already as early as possible.
|
||
|
||
⚠️ **The inter-transfer gap cannot help, and this is the non-obvious part.**
|
||
The load happens on that last rising edge and the clock is PAUSED afterwards,
|
||
so no edge exists during the gap for anything to update. That is why
|
||
`bba_devid`'s 16 µs gap made no difference, and why "give it more idle time"
|
||
is not a fix. **Pre-pushing at CS-fall does not work either:** the engine's
|
||
unconditional per-byte pops would eat it, and the header length is not known
|
||
until byte 0 has been decoded — which is the latency in question.
|
||
|
||
**Fix: take the round trip out of the path.** The device ID is a hardwired
|
||
constant and belongs to the physical interface, not the register file, and
|
||
`byte 0 == 0x00` identifies the query exactly (bit 7 clear = command space,
|
||
bit 6 clear = read, reg 0). `SPIMode3Slave` recognises it and answers
|
||
`04 02 02 00` from the EXI clock domain with **no CDC at all**. The only thing
|
||
it needs from `sync` is `id_phase`, a single bit that changes once, crossed
|
||
with an `FFSynchronizer`.
|
||
|
||
The register file's ID path was **removed**, not kept as a fallback: left in,
|
||
it pushes bytes a 2-byte header has too few pops to drain — exactly how an
|
||
earlier version made the *next* enumeration come back a slot late.
|
||
|
||
Tests: `spi_mode3_slave` **T7** (reply is `04 02 02 00` with the tx FIFO
|
||
deliberately feeding `0x00`, so only the fast path can answer) and **T8** (it
|
||
is GATED — with `id_arm` low, `0x5A` comes through from the FIFO; without this
|
||
the fast path would hijack address 0 forever and the GC could never read NCRA
|
||
back after its own reset). `bba_register_file` **T1** asserts the FSM pushes
|
||
NOTHING for an ID query.
|
||
|
||
⚠️ **Simulation cannot reproduce this bug.** `bba_top` passed the device ID at
|
||
`EXI_HALF=1` *before* the fix, because the sync domain's latency relative to
|
||
the EXI clock in simulation does not match silicon closely enough to miss a
|
||
296 ns window. T7/T8 prove the fast path works; they do not prove it is
|
||
necessary. Do not "simplify" the ID back into the register file because the
|
||
suite stays green — it will, and hardware will not.
|
||
|
||
### ⚠️ TX bit phase MUST come from the rising-edge counter (fixed 2026-10-03)
|
||
|
||
**Never give the TX shift register its own free-running falling-edge counter.**
|
||
`SPIMode3Slave` takes its byte phase from `rx_bit`, the RISING-edge counter —
|
||
the same edge the host counts bits on. A separate `tx_bit` counting falling
|
||
edges only stays in step while the host emits exactly as many falls as rises,
|
||
and it does not: a transfer that begins on a RISING edge (CS asserted while
|
||
CLK is already low, or the clock gating between two back-to-back host
|
||
transfers) delivers a rise with no matching fall. `tx_bit` then slips one bit
|
||
and never recovers. RX never notices, because the rising count always equals
|
||
the host's bit count — so the header decodes perfectly and only MISO is wrong.
|
||
|
||
**Symptom on console:** every MISO byte shifted right by 2 bits, *identically
|
||
at every EXI speed index*. A NWAYS read returned `05 C0` (= `0x17` with two
|
||
leading zero bits) and the device ID `01 00 80 80`. The speed sweep read
|
||
`ok=[0,0,0,0,0,0]`.
|
||
|
||
**How it was found, after two wrong diagnoses.** It is not propagation delay:
|
||
index 0 is 1 MHz, a 1000 ns bit time, and no output path fails there —
|
||
speed-independence is the tell. It is not the output edge either: the old
|
||
rising-edge build and the new falling-edge build returned *byte-identical*
|
||
`05 C0`, and an edge flip would have moved it. What settled it was a
|
||
measurement, not more theory — see PHASE_METER below.
|
||
|
||
**`PHASE_METER=1 python -m exi_bba.synth`** builds a DIAGNOSTIC,
|
||
NON-FUNCTIONAL bitstream in which the bit engine transmits a constant `0x80`
|
||
for every byte instead of the FIFO head. The register file and both byte FIFOs
|
||
drop out of the MISO path, so whatever the host reads back is purely the
|
||
engine's byte phase: `0x80` aligned, `0x40` one bit late, `0x20` two bits late.
|
||
On hardware it read `0x20` in all 8 slots, which localised the fault to the bit
|
||
engine and ruled out everything upstream in one run. Re-flash a normal build
|
||
afterwards.
|
||
|
||
**Regression test:** `spi_mode3_slave` T5 asserts CS while CLK is LOW, so the
|
||
transaction begins on a rising edge and carries one fewer falling edge. The
|
||
old counter fails it by exactly one bit (`A5 3C` → `52 9E`). Every earlier
|
||
testbench asserted CS with CLK high, so falls and rises always matched and the
|
||
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 — REMOVED 2026-10-03
|
||
|
||
`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.
|
||
|
||
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.
|
||
|
||
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
|
||
device and exercised the register file end-to-end:
|
||
|
||
| test | result |
|
||
|---|---|
|
||
| device ID @0x0000, 4 B | `04 02 02 00` — **21/21** |
|
||
| NWAYS @0x31 | `0x17` — 21/21 |
|
||
| HIPR @0x3A | `0x01` — 21/21 |
|
||
| PAR0–3 write `DE AD BE EF` + readback | **match**; restore to `00 09 BF 00` verified |
|
||
| IMR @0x08 write `0x86` + readback | `0x86` |
|
||
| NCRA @0x00 write `0x01` (reset) | self-cleared to `0x00` on the first poll |
|
||
|
||
**The full Swiss init sequence runs on hardware (2026-09-30)**, driven from the
|
||
Pi over J4 with the board on 12 V — device ID `04 02 02 00`, NCRA reset
|
||
self-clearing, PAR0–5, MAR0–7, ANALOG, NWAYC, IMR `0x86` readback, NCRA SR,
|
||
and NWAYS `0x17`. See `hardware/re-bba-rb/exi_fullpath_rpi.py`.
|
||
|
||
✅ **FULL ROUND TRIP ON HARDWARE 2026-09-30.** The board sent 5 ARP requests
|
||
over EXI and received all 5 replies back: `op=2`, unicast to our own MAC
|
||
`00:09:bf:00:00:01`, sender 10.0.0.50 / target 10.0.0.99, read back out of the
|
||
SPRAM ring by EXI DMA with RWP advancing exactly one page per exchange. The PC
|
||
could only reply, and only address it to our MAC, if our request reached it —
|
||
so this closes the loop independently of any packet capture.
|
||
|
||
Getting there took four W5100 fixes, none of which simulation could catch
|
||
because the bus model reproduces neither the chip's reset timing nor its
|
||
interrupt latching. Each masked the next:
|
||
1. `tx_bytes` overflow — the drain waited for a length that only arrived at the
|
||
ST trigger, so frames truncated at exactly 16 bytes (see TWD fix below).
|
||
2. Post-reset settle was 1 ms; the W5100S needs 60.3 ms. Marginal, so it
|
||
*sometimes* worked — which is what made everything look flaky.
|
||
3. `w5100_rst_n` was declared and NEVER DRIVEN. The only reset was the MR
|
||
software reset, which a wedged chip cannot answer because it arrives over
|
||
the stuck bus — so a wedge survived NCRA reset, reflashing and even FPGA
|
||
reconfiguration, and needed a 12 V power cycle.
|
||
4. `RX_RSR_CHK` returned to IDLE on RSR==0 without clearing `Sn_IR`. SEND_OK
|
||
(bit 4) fires after every transmit and only RECV (bit 2) was ever cleared,
|
||
so INT_N latched low; since IDLE tests `~w5100_int_n` BEFORE `tx_valid`, the
|
||
FSM span and TX starved. Symptom: first frame transmits, all later ones
|
||
never set TI while RX keeps working.
|
||
|
||
⚠️ Note TI only means `TXFrameDrain` handed the frame to the W5100 — it does
|
||
NOT mean the chip put it on the wire. Do not treat TI as proof of transmission;
|
||
the round trip above is the real test.
|
||
|
||
✅ **BOTH DIRECTIONS PROVEN ON HARDWARE 2026-09-30 — the BBA data path works.**
|
||
|
||
*GC → wire* (after the TWD fix below): a full **60-byte ARP request**, captured
|
||
on the PC byte-for-byte correct — broadcast dst, `00:09:bf:00:00:01` source,
|
||
ethertype 0806, sender 10.0.0.99, target 10.0.0.50, padded to 60. TI sets.
|
||
|
||
*wire → GC*: a real **342-byte DHCP broadcast** from the PC was received by the
|
||
W5100 in MACRAW mode, assembled into the SPRAM ring by `RXFrameAssembler`, and
|
||
DMA-read back over EXI from 0x100. RI set. The ring descriptor read
|
||
`00 00 01 5A` = 346 = 342 + 4, and the frame's own IP total-length field (328)
|
||
+ 14 bytes of ethernet header = 342 — so the assembler's length is
|
||
independently confirmed correct, not just plausible.
|
||
|
||
Together: EXI ↔ BBARegisterFile ↔ {tx_bytes→TXFrameDrain, SPRAM ring} ↔
|
||
W5100ParallelMaster ↔ PHY, in both directions, on silicon.
|
||
|
||
✅ **GC→wire TX REACHES THE WIRE — the full path is proven.** A frame written
|
||
over EXI to TXDATA and triggered via the NCRA ST bits was captured on a PC:
|
||
`ff ff ff ff ff ff | 00 09 bf 00 00 01 | 08 06 | 00 01` — broadcast dst, our
|
||
PAR0–5 source MAC and the ARP ethertype, all byte-perfect. So
|
||
EXI → BBARegisterFile → tx_bytes → TXFrameDrain → W5100 → PHY works.
|
||
|
||
✅ **FIXED 2026-09-30: TX used to truncate at exactly 16 bytes** = the
|
||
`tx_bytes` FIFO depth. Past byte 16 the captured frame restarted from its own
|
||
beginning and mixed in stale bytes from earlier attempts. Cause:
|
||
`TXFrameDrain` sat in IDLE until it had a LENGTH, and the length only reached
|
||
`tx_ctrl` when the NCRA ST bits were written — i.e. *after* the GC had streamed
|
||
the whole frame. So the entire frame had to fit a 16-deep FIFO, and
|
||
`BBARegisterFile` silently drops bytes when `tx_bytes.w_rdy` is low (SPI cannot
|
||
back-pressure).
|
||
|
||
Fix: the length is pushed into `tx_ctrl` when **TWD_HI (0x35)** is written,
|
||
which the GC sets *before* streaming, so the drain consumes concurrently with
|
||
the EXI writes and the 16-deep FIFO becomes a rate-matching buffer instead of
|
||
whole-frame storage (`twd_armed` / `tx_queued` in `bba_register_file.py`; the
|
||
NCRA ST path is now only a fallback for when TWD_HI did not queue). Verified on
|
||
hardware — a full 60-byte ARP request captured byte-for-byte correct.
|
||
|
||
Simulation never caught the original bug: `TXFrameDrain`'s own tests drive the
|
||
FIFO signals directly, and the `BBATop` test only exercises the
|
||
W5100→SPRAM→GC **RX** loop. That gap is still open — there is no end-to-end
|
||
GC→wire TX test in simulation.
|
||
|
||
When probing TX, **NCRA-reset between every attempt**: a failed attempt leaves
|
||
the drain waiting on a stale length, and later writes feed that pending
|
||
request, which makes an unrelated frame length appear to succeed.
|
||
|
||
⚠️ **The W5100 can wedge and does NOT recover on FPGA reconfigure.** Symptom:
|
||
the UART shell's own socket-3 `udp unicast` echoes the command and never prints
|
||
`sent`, and any EXI TX then hangs waiting on the bus master. `iceprog -t` does
|
||
not clear it (RST_N floats to its pull-up while the FPGA is unconfigured). A
|
||
12 V power cycle is required. Check ETH_3V3 and GC_ON (U12 gates the W5100 rail
|
||
off GC_ON) before suspecting gateware.
|
||
|
||
**`id_phase` is a ONE-SHOT.** The device ID is returned for reads of address 0
|
||
only until the first NCRA write with the reset bit, which clears `id_phase`
|
||
permanently (`bba_register_file.py:330`); after that address 0 reads back NCRA.
|
||
This is correct — it matches the GC's init order — but it means a re-run of any
|
||
probe that already issued an NCRA reset will read `00 00 00 00` for the device
|
||
ID and look broken. Reconfigure the FPGA (`iceprog -t` is enough) to restore it.
|
||
|
||
Two bring-up gotchas, both cost real debugging time:
|
||
- **The master must sample MISO during the CLK-LOW phase.** The FPGA drives MISO
|
||
on the RISING edge (Mode 3), so sampling just after that edge races the
|
||
slave's own update and lands one bit position early — `04 02 02 00` comes back
|
||
as `08 ...`. Sample after taking CLK low, before raising it (this is exactly
|
||
what the simulation's `spi_byte` does).
|
||
- **Clock EXACTLY the requested byte count on a register read.** Over-clocking
|
||
(asking for 4 bytes then clocking 8–12) leaves stale prefetched bytes queued
|
||
and desynchronises following transactions — the symptom is a *deterministic*
|
||
every-Nth-read correctness cycle that looks like a signal-integrity fault but
|
||
is speed-independent. The GC driver never over-clocks a register read, so this
|
||
is a probe artefact, NOT a gateware bug; reads ≥ 0x100 deliberately stream
|
||
until CS (the DMA path). `hardware/re-bba-rb/exi_regcheck_rpi.py` clocks
|
||
correctly and is the reference host-side probe.
|
||
|
||
⚠️ **The two measurements below are HISTORICAL** — they predate the
|
||
2026-10-03 pin-clocked rewrite and refer to the `capture_clk` domain, which no
|
||
longer exists. Current numbers are in "Current Implementation State" above.
|
||
They are kept only because they document how added `sync`-domain cells moved
|
||
the pass rate, which still applies.
|
||
|
||
**Measured 2026-09-30 AFTER the TWD TX fix**, `EXI_J4=1` + socket-3 UDP,
|
||
`--seeds 8`: `clk` 28.1–30.2 MHz (PASS on all 8); `capture_clk` **2 of 8 PASS**
|
||
(seeds 4, 7), best **seed 7 = 60.03 MHz** (+6.0 MHz / 11% margin). The fix's
|
||
handful of extra exi-domain cells (`twd_armed`/`tx_queued`) dropped the pass
|
||
rate from 4/8 to 2/8 even though none of that logic is in the capture domain —
|
||
added cells raise congestion and capture is routing-bound. Best seed is
|
||
comfortably usable; just sweep and flash the winner. Earlier figures, before
|
||
the fix:
|
||
|
||
**Measured 2026-09-30 (pre-fix)**, `EXI_J4=1` + socket-3 UDP, `--seeds 8`:
|
||
`clk` 29.2–31.4 MHz (PASS on all 8); `capture_clk` **4 of 8 PASS** (seeds 2, 5,
|
||
6, 8), best **seed 8 = 63.09 MHz** (+9.1 MHz / 17% margin). Same pass rate as
|
||
the default build and a better best seed, so the merge costs no measurable
|
||
capture timing — but it is still seed-dependent, so **always sweep**.
|
||
|
||
### Pending work
|
||
- **Synthesis/timing**: ✅ — see the seed table above. Removing the PLL-fed
|
||
capture domain eliminated the constraint that had limited the design to a
|
||
minority of passing seeds.
|
||
- **W5500 init/TX/RX**: ✅ done — `W5500SPIMaster` has a real Mode-0 byte engine,
|
||
a generic register-transaction engine (header + wbuf/stream payload), the full
|
||
init sequence (MR reset, SHAR, S0_MR MACRAW, S0_CR OPEN, S0_IMR), MACRAW TX
|
||
(read TX_WR → stream frame to TX buffer → advance TX_WR → SEND) and MACRAW RX
|
||
(RSR → RD → 2-byte length → stream frame out → advance RD → RECV). All verified
|
||
on the wire by a responding W5500 SPI-slave model in the testbench.
|
||
- **PAR0–5 → W5500 SHAR**: ✅ done — `reg.par` wired to `w5500.par` in `BBATop`
|
||
(PAR0 packed in the low byte so it is the first SHAR octet).
|
||
- **NCRA SR bit**: ✅ done — `BBARegisterFile.ncra_sr` (= NCRA[3]) gates
|
||
`asm.rx_enabled` in `BBATop` (was hard-wired to 1).
|
||
- **W5500 SPI throughput**: SCK = sync÷2 = 12 MHz (~12 Mbit/s) — exceeds
|
||
real-world GC BBA TCP throughput (~6–10 Mbit/s) but is below the 27 Mbit/s raw
|
||
EXI ceiling. Pushing past 12 Mbit/s was investigated and found NOT achievable
|
||
on this UP5K (the W5500-operating logic is distributed ~40 MHz, not just the
|
||
bit-bang) — see the "Full-rate W5500 SPI" item below.
|
||
`W5500SPIMaster(clk_div=N)` divides SCK further if signal integrity needs it.
|
||
- **EXI DMA bulk reads**: ✅ done — SPRAM-region reads (addr ≥ 0x100) now STREAM
|
||
until CS deasserts instead of stopping at the header's 2-bit length, so they
|
||
serve both ≤4-byte immediate reads (Swiss) AND arbitrary-length DMA reads
|
||
(other GC software, and a future Swiss path for loading ROMs from a network
|
||
file store). Implementation:
|
||
- `SPIMode3Slave.cs_active` (synchronised CS level) → `ExiCapture` crosses it
|
||
to the exi domain (FFSynchronizer) → `BBARegisterFile.cs_active`.
|
||
- `BBARegisterFile` SPRAM_STREAM state: auto-increments the SPRAM address,
|
||
prefetches up to SP_LIMIT=4 reads in flight, pushes responses to tx_fifo;
|
||
SPRAM_END drains the in-flight pipeline + rx dummies on CS-rise.
|
||
- `ExiCapture` flushes tx_fifo on CS-fall to clear prefetch over-push so a
|
||
truncated DMA read can't leak stale bytes into the next transaction.
|
||
Tested: register-file streaming read (SPRAM model, 12 bytes), ExiCapture
|
||
over-push/flush, AND the full BBATop loop — a W5500 model delivers a frame →
|
||
W5500 master RX → RXFrameAssembler writes the SPRAM ring → GC reads RWP then
|
||
DMA-reads the descriptor+frame back (verified byte-for-byte).
|
||
Note: a DMA read header must keep length-1 within the 2-bit field; the GC
|
||
driver sets it ≤3 and clocks the real length via CS (the design streams
|
||
until CS regardless). (EXI DMA *writes* are not implemented; the GC's
|
||
DMA-write engine has a 1-bit-shift bug and Swiss avoids them — see
|
||
design-doc §"EXI DMA bug".)
|
||
- **S0_IR interrupt clear after RX**: ✅ done — `W5500SPIMaster` RX_CLR_IR state
|
||
writes Sn_IR[2]=1 after RECV so `INT_N` deasserts (else the FSM would re-enter
|
||
RX_CHECK forever on real hardware).
|
||
- **Full-rate W5500 SPI (27 Mbit/s) — INVESTIGATED, NOT achievable on UP5K**:
|
||
the W5500 SCK is sync÷2 = 12 MHz. Raising it needs the SPI engine on a ≥54 MHz
|
||
clock, but a standalone synth of `W5500SPIMaster` in the capture domain closes
|
||
only **40 MHz** — and the slack histogram shows the failure is *distributed*
|
||
(~140 endpoints fail 54, incl. the `wbuf`/header mux feeding the shift
|
||
register), NOT a single cuttable path. So the bottleneck is the **logic that
|
||
operates the SPI device** (transaction FSM, byte sourcing), not the bit-bang.
|
||
Consequences:
|
||
- The "split the bit engine to capture + per-byte CDC handshake" idea nets
|
||
only ~14 Mbit/s — the CDC round-trip ≈ the SPI byte time — not worth it.
|
||
- A capture-domain "streaming executor" would still contain that distributed
|
||
~40 MHz logic, so it wouldn't close 54 either.
|
||
- Hardware `SB_SPI` wouldn't help (it only offloads the bit-bang, which was
|
||
never the bottleneck) and is unsimulatable.
|
||
- There is no usable clock between 24 (HFOSC) and 54 (the one PLL, needed at
|
||
54 for the EXI front-end); PLL÷2 = 27 → SCK 13.5 MHz, a ~12% gain, not
|
||
worth the fabric divider.
|
||
Net: 12 Mbit/s is the practical W5500 ceiling on this part. It exceeds
|
||
real-world GC BBA TCP throughput and is fine for chunked ROM streaming.
|
||
Reaching 27 Mbit/s would need a faster FPGA or a much shallower W5500-operating
|
||
redesign (uncertain) — **OR a parallel-bus ethernet chip (see W5100 below)**,
|
||
which is the implemented solution for the ROM-streaming throughput target.
|
||
|
||
## W5100 vs W5500 ethernet back-end
|
||
|
||
The throughput insight: SPI serialises 8 bits/byte, so the W5500 byte rate is
|
||
(operating-logic clock)/16 — and that logic caps ~40 MHz on this UP5K → ~12
|
||
Mbit/s. A **parallel** bus moves a whole byte per access, so the *same* ~24 MHz
|
||
`sync` logic clears the 27 Mbit/s EXI ceiling (the real hard limit — the GC EXI
|
||
bus tops out there). So `W5100ParallelMaster` is the throughput path and is now
|
||
the `BBATop` default.
|
||
|
||
- **Interface:** W5100 **indirect parallel bus** (IDM). Only A[1:0] are wired
|
||
(board ties A[14:2]=0 so a power-up direct access at A=00 still hits MR):
|
||
`00`=MR, `01`=IDM_AR0(hi), `10`=IDM_AR1(lo), `11`=IDM_DR. A register/buffer
|
||
access = write IDM_AR (the 16-bit address) then read/write IDM_DR. With MR.AI
|
||
set, IDM_DR auto-increments → a multi-byte block is one address-set + a burst.
|
||
- **Bus engine:** drives A + D with `/CS` and `/RD`|`/WR` asserted for
|
||
`strobe_cycles` (default 3 ≈ 125 ns at 24 MHz, ≥ the W5100's ~80 ns access).
|
||
DATA[7:0] is bidirectional → an SB_IO tristate (`bus_data_o`/`oe`/`i`).
|
||
- **Pins (15):** A[1:0]=2, D[7:0]=8, /CS,/RD,/WR=3, /INT=1, /RST=1. With EXI (5)
|
||
+ clk (1) = **21 of ~34 usable SG48 I/O** — comfortable. See `synth.py`.
|
||
- **MR.AI requires init first:** unlike the W5500 (each SPI transaction is
|
||
self-framed), the W5100's multi-byte accesses depend on MR.AI, so the init
|
||
sequence (triggered by the GC's NCRA reset) MUST run before any TX/RX. The
|
||
BBATop test issues NCRA-reset before its RX loop for this reason; on hardware
|
||
the GC driver already does. (`BBATop(reset_cycles=N)` shrinks the MR settle
|
||
wait for sim.)
|
||
- **Ring wraparound is in fabric:** the W5100 does NOT auto-wrap the IDM address
|
||
at the socket-buffer boundary (the W5500 did), so the streamer re-sets IDM_AR
|
||
to the buffer base when the running address reaches the 2 KB boundary. Handled
|
||
in the SW/SR/RB paths (`xfer_wrap`/`xfer_wbase`/`xfer_wend`/`cur_addr`); both
|
||
TX and RX wrap cases are tested.
|
||
- **Register map differs from the W5500:** common regs at 0x0000 (MR, SHAR 0x09,
|
||
IMR 0x16, RMSR/TMSR 0x1A/0x1B), socket 0 at 0x0400 (S0_MR/CR/IR, TX_WR 0x424,
|
||
RX_RSR 0x426, RX_RD 0x428), TX buffer 0x4000, RX buffer 0x6000. MACRAW mode.
|
||
- **Status:** init/TX/RX (with wrap) verified vs a bus model; BBATop full
|
||
W5100→SPRAM→GC RX loop passes byte-for-byte; synth PASS. **Hardware bring-up
|
||
2026-09-20: a real UDP datagram was transmitted and captured on a PC** — the
|
||
ethernet back-end is proven end-to-end. Register addresses confirmed on silicon,
|
||
with three W5100S-specific gotchas the from-memory map got wrong:
|
||
- **NETLCKR (0x0071) must be unlocked with 0x3A** before GWR/SUBR/SHAR/SIPR are
|
||
writable (default 0x00 = locked). The original W5100 has no such register, so
|
||
without this every network-config write is silently dropped and the chip
|
||
transmits with a zero source MAC/IP.
|
||
- **The parallel/indirect (IDM) bus decodes only a 15-bit offset**, so the TX/RX
|
||
buffers are at the original **0x4000 / 0x6000** — NOT the 0x8000/0xC000 the
|
||
W5100S datasheet pseudo-code quotes. Using 0x8000 makes payload writes alias
|
||
onto 0x0000+ and overwrite SHAR/SIPR. Proven with an address-aliasing probe
|
||
(0xC000 and 0x4000 read back the same cell). Do not "fix" this again.
|
||
- **The W5100S needs 60.3 ms after RSTn** before it answers; polling sooner reads
|
||
all-zeros and looks like a dead chip.
|
||
- UDP **unicast** works (the chip ARPs the peer first, per Sn_MR FARP). A send to
|
||
255.255.255.255 sets SEND_OK but was not observed on the wire — prefer unicast
|
||
or a subnet-directed broadcast for bring-up tests.
|
||
|
||
### `rebbarb/` — LED blink demo (unchanged)
|
||
- `rebbarb.py` — blinks LEDs via a PLL (36 MHz), demonstrates `IceBreakerPlatform`
|
||
- `debouncer.py` — `Debouncer(cycles)` — synchronous debounce, configurable hold
|
||
- `toggle_button.py` — `ToggleButton` — edge-to-toggle state machine (wraps Debouncer)
|
||
- `pulse_button.py` — `PulseButton` — single-cycle pulse on rising edge (wraps Debouncer)
|
||
|
||
These components are reusable building blocks. The `Debouncer` and button wrappers
|
||
will be needed for any physical input in `exi_bba/`.
|
||
|
||
**Import note:** `rebbarb/` files use bare imports (`from debouncer import Debouncer`).
|
||
Run them as `python rebbarb/<file>.py` from the workspace root so Python adds
|
||
`rebbarb/` to `sys.path` automatically.
|
||
|
||
**Simulation at module level:** `toggle_button.py` and `pulse_button.py` run
|
||
their simulations unconditionally (no `__main__` guard) — importing either file
|
||
triggers a VCD write. New modules should guard simulation code with
|
||
`if __name__ == "__main__":`.
|
||
|
||
`examples/amaranth_cdc.py` contains handwritten `SyncFF` and `TogglePulseSync`
|
||
reference implementations — use `amaranth.lib.cdc` primitives (`FFSynchronizer`,
|
||
`PulseSynchronizer`) in production code instead.
|
||
|
||
`hardware/sp1_test_plug/` — KiCad project for a physical SP1 edge-connector test
|
||
plug (schematic, PCB, custom GameCube symbol library). Used to verify pad geometry
|
||
before ordering the interposer PCB; not part of the FPGA build.
|
||
|
||
`hardware/re-bba-rb/` — the actual adapter PCB (FT2232H + iCE40UP5K + W5100S,
|
||
powered from SP1 12 V via buck). Three companion documents, all mandatory
|
||
reading before touching this board:
|
||
- **`hardware/re-bba-rb/TODO.md`** — open action items (what still needs doing:
|
||
PCB placement of the 2026-07-18 fix parts, board PCF, bench checks, JLC
|
||
ordering options).
|
||
- **`hardware/re-bba-rb/REVIEW.md`** — the review **coverage record**: every
|
||
circuit that has been verified against its datasheet/reference design, with
|
||
verdicts — including things that LOOK wrong but are settled-correct (FT2232H
|
||
VPHY/VPLL on 3.3 V; D4/D5 floating VBUS + split-net flow-through; TPS562201
|
||
EN at 12 V; RSET 12.4 k; no TPS22810 CT cap; SP1 DO = GC→adapter = MOSI).
|
||
**Do not re-review or "fix" anything listed there without new evidence, and
|
||
keep REVIEW.md updated in the same commit whenever a reviewed circuit
|
||
changes or a new review is performed.**
|
||
- **`hardware/re-bba-rb/CPL_ROTATIONS.md`** — per-part assembly rotation
|
||
corrections applied on top of `kicad-cli`'s raw CPL export (KiCad's raw
|
||
rotation frequently doesn't match what the assembler expects, and a
|
||
generic community correction table was tried and found wrong for this
|
||
board — do not reapply it). **Regenerating `re-bba-rb-cpl.csv` "the normal
|
||
way" silently wipes these corrections** — always reapply the corrections
|
||
in this file after any CPL regen, and read its confidence notes before
|
||
trusting any specific part's rotation.
|
||
The authoritative board pin map (EXI, ETH bus, UART crossover, LED/DBG pins)
|
||
is in TODO.md and REVIEW.md; use it when writing the board PCF.
|
||
|
||
---
|
||
|
||
## Amaranth Simulator API
|
||
|
||
Two API generations are present in this repo:
|
||
|
||
| API | Where used | Status |
|
||
|---|---|---|
|
||
| `sim.add_testbench(async_fn)` + `await ctx.tick()` + `Period(MHz=n)` | `rebbarb/*.py` | **Use this for new code** |
|
||
| `sim.add_sync_process(gen_fn)` + `sim.run_until(t)` | `examples/` | Old — reference only |
|
||
|
||
New modules should use the testbench API (`add_testbench`, `sim.write_vcd(ctx)`
|
||
context manager). The old process API still works but is not idiomatic in current
|
||
Amaranth.
|
||
|
||
**Critical testbench timing rule:** `ctx.get(signal)` reads signal values AFTER
|
||
the clock edge (post-update registered values). Combinatorial signals that depend
|
||
on registered signals that were updated by the SAME tick will already reflect the
|
||
new registered values. For example: if `tx_sof = tx_bytes_r_rdy & is_first` and
|
||
`is_first` is cleared synchronously on the first byte, then reading `tx_sof` after
|
||
the first byte's tick always returns 0 — read BEFORE the tick instead.
|
||
|
||
**`ctx.set()` takes effect immediately** (combinatorial, not registered). Use it
|
||
AFTER `await ctx.tick()` to prepare inputs for the NEXT tick.
|
||
|
||
The full design specification lives in `docs/gc_bba_fpga_design.md`.
|
||
|
||
---
|
||
|
||
## Key Architecture Decisions
|
||
|
||
- **No network stack in the FPGA.** The GC CPU runs TCP/IP. The FPGA is a dumb
|
||
MAC bridge.
|
||
- **The EXI bit engine is clocked BY the GC's EXI clock pin** (2026-10-03
|
||
rewrite). There is no PLL and no oversampling.
|
||
- `exiclk` / `exiclk_n` — the EXI clock pin itself (~27 MHz), posedge and
|
||
negedge. Created inside `ExiCapture`; async-reset by CS. Hosts ONLY the
|
||
SPI bit engine. MOSI is sampled on the posedge, MISO driven on the negedge.
|
||
- `exififo_w` / `exififo_r` — the same pin, posedge, reset-less. The byte
|
||
FIFO sides. (Reset-less because resetting one side of an `AsyncFIFO`
|
||
desynchronises its gray pointers; and posedge because `AsyncFIFO`
|
||
instantiates a posedge-only reset CDC internally, so a negedge domain
|
||
cannot host a FIFO side.)
|
||
- `sync` — 24 MHz, internal `SB_HFOSC` ÷2. **Everything else**: register
|
||
file, SPRAM arbiter, RX/TX engines, ethernet master. The former separate
|
||
`exi` domain was merged into `sync` (they always shared the HFOSC net).
|
||
- **Why:** oversampling put BIT-level timing across a CDC boundary. The
|
||
engine's view of the clock lagged the pin by 2 `FFSynchronizer` stages plus
|
||
a register — ~55 ns against a 37 ns bit time — so MISO came out ~2 bits
|
||
late and the GC read `0x17` as `05 C0`. No amount of oversampling fixes it:
|
||
54 MHz gives only 2 ticks per bit and the part cannot close the 81 MHz that
|
||
3× would need. Clocking from the pin makes MISO clock-to-Q and leaves the
|
||
byte-level FIFOs as the only crossing, with a whole byte-time (~296 ns) of
|
||
slack instead of a fraction of a bit.
|
||
- **Timing consequence:** `capture_clk` is gone as a constraint. It was the
|
||
binding path for the whole design and the reason builds closed on only a
|
||
minority of seeds.
|
||
- **EXI clock reality:** the GC EXI clock tops out at ~27 MHz. libogc's
|
||
`EXI_SPEED32MHZ` is a nominal name — the real rate is 27 MHz. The old
|
||
"96 MHz = 3× 32 MHz EXI" target was doubly wrong and unreachable on UP5K
|
||
(which caps ~44 MHz for non-trivial logic).
|
||
- **TX/MISO across the split:** the register file PROACTIVELY pushes read
|
||
responses into the tx byte FIFO during the EXI clock-idle gap (the GC pauses
|
||
the clock between an EXI_Imm header-write and the data-read). The bit engine
|
||
drives MISO live from the FIFO head; see `ExiCapture` / `SPIMode3Slave`.
|
||
- **All CDC via `amaranth.lib.cdc`.** Never pass raw multi-bit signals across
|
||
domains. Use `FFSynchronizer` for slow single bits, `PulseSynchronizer` for
|
||
events, `AsyncFIFO` for data streams, `ResetSynchronizer` for resets.
|
||
- **Register file lives entirely in `exi` domain.** The `sync` domain only
|
||
communicates through AsyncFIFOs and PulseSynchronizers — never direct register
|
||
reads/writes.
|
||
|
||
---
|
||
|
||
## Critical Protocol Notes
|
||
|
||
### EXI / SPI Mode 3
|
||
- CLK idles **HIGH** (CPOL=1, CPHA=1).
|
||
- The master **changes MOSI on the falling edge**; both ends **sample on the
|
||
rising edge**. The device therefore **samples MOSI on the RISING edge and
|
||
drives MISO on the FALLING edge** (MISO must settle before the master
|
||
samples it on the rising edge).
|
||
- ⚠️ This file previously stated the opposite ("MOSI sampled on falling, MISO
|
||
driven on rising"), which is Mode **2**, and the implementation followed the
|
||
doc. Driving MISO on the rising edge is one bit late by construction — that
|
||
was the `0x17` → `05 C0` hardware bug. `spi_mode3_slave.py`'s testbench now
|
||
mutation-tests both edges so neither can silently regress.
|
||
- CS is active **low**, delineates each transaction, and async-resets the
|
||
bit engine.
|
||
|
||
### EXI Transaction Header
|
||
|
||
**Implemented 2026-10-03** (`bba_register_file.py`), from the two drivers that
|
||
actually run — libogc (`libdb/uIP/bba.c`) and Swiss (`cube/patches/bba/bba.c`).
|
||
There are TWO address spaces, selected by bit 7 of the first byte, and **no
|
||
length field**:
|
||
|
||
```
|
||
COMMAND space (6-bit reg) one 4-byte READWRITE transfer
|
||
write: [0x40|reg, 00, 00, data]
|
||
read : [reg, 00, 00, xx ] <- response is the 4th byte
|
||
REGISTER space (16-bit reg) 4-byte header, then a SEPARATE data phase
|
||
read : [0x80, reg>>8, reg&0xFF, 0x00] then N bytes read (Swiss uses DMA)
|
||
write: [0xC0, reg>>8, reg&0xFF, 0x00] then N bytes written
|
||
```
|
||
NCRA, IR, NWAYS, RWP, RRP, PAR… all live in the REGISTER space
|
||
(`bba_in8`/`bba_out8`/`bba_ins`/`bba_outs`). Only the device-ID query uses a
|
||
short header — the single case where the two formats coincide.
|
||
|
||
**What the old (guessed) format cost.** The previous decoder used
|
||
`[7]=write, [6:0]=addr[12:6]` / `[7:2]=addr[5:0], [1:0]=len-1`. Under it a
|
||
`NWAYS` read (`80 00 31 00`) decoded as *write 0x31 to NCRA* — and bit 0 of
|
||
0x31 is RESET, so every register read Swiss performed would have reset the
|
||
device. Enumeration still appeared to work because `00 00` decodes to address
|
||
0 either way, which is exactly why the error survived so long. Do not
|
||
reintroduce a length field.
|
||
|
||
**The trailing `0x00` byte is the device's prefetch window.** The address is
|
||
complete one full byte before the data phase, giving ~8 bit-times (≈296 ns at
|
||
27 MHz) to fetch the response. The engine pops the TX FIFO once per byte
|
||
boundary, so the response the register file pushes during this turnaround byte
|
||
lands in data slot 0 — see the TX comment in `spi_mode3_slave.py`.
|
||
|
||
⚠️ **Command space has only ONE drain slot.** Pops during the header drain
|
||
whatever the previous transaction left in the FIFO, but a 2-byte header gives
|
||
just one such slot, so a command-space response must push **exactly** its
|
||
bytes and stop. `DEVID` streaming zeros until CS left two stale bytes behind,
|
||
one too many to drain, and the *next* enumeration came back a slot late
|
||
(`04 02 02 00` → `00 04 02 02`). Register-space reads may over-push safely:
|
||
their 4-byte header gives three drain slots.
|
||
|
||
**Data-phase length is whatever the GC clocks** (it streams until CS), so the
|
||
register path streams like the SPRAM path. `bba_ins` reads with **EXI DMA**;
|
||
DMA *reads* are fine (the GC's DMA-*write* engine is the buggy one).
|
||
|
||
### Device ID Query
|
||
On power-on the GC writes `0x0000` (2 bytes) then reads 4 bytes.
|
||
Must return: `0x04 0x02 0x02 0x00`. **Hardware-confirmed 2026-10-03** on a
|
||
real console.
|
||
|
||
---
|
||
|
||
## Memory Map (abridged)
|
||
|
||
| Range | Region |
|
||
|---|---|
|
||
| 0x0000–0x0033 | MAC control registers (register file, exi domain) |
|
||
| 0x0048 | TXDATA — bulk TX data port (→ `tx_bytes` AsyncFIFO) |
|
||
| 0x0100–0x0FFF | RX ring buffer in SPRAM (15 × 256-byte pages, pages 1–15) |
|
||
| 0x0100–0x1FFF | any read ≥ 0x0100 streams from SPRAM (DMA path); the ring proper is pages 1–15 above |
|
||
|
||
---
|
||
|
||
## Key Registers
|
||
|
||
| Addr | Name | Notes |
|
||
|---|---|---|
|
||
| 0x00 | NCRA | [0]=RESET self-clears; pulses `ncra_rst` to sync domain |
|
||
| 0x08 | IMR | Interrupt mask |
|
||
| 0x09 | IR | Write-1-to-clear. [1]=RI, [2]=TI. INT_N asserts when IR & IMR ≠ 0 |
|
||
| 0x16–17 | RWP | RX write pointer — updated by sync domain via `rx_wptr` FIFO |
|
||
| 0x18–19 | RRP | RX read pointer — GC writes after consuming frames |
|
||
| 0x20–25 | PAR0–5 | MAC address; also forwarded to W5500 as SHAR |
|
||
| 0x31 | NWAYS | Hardcode **0x17** (100M full-duplex link up, autoneg complete) |
|
||
| 0x3A | HIPR | Hardcode **0x01** (BBA present) |
|
||
| 0x48 | TXDATA | GC streams TX frame bytes here |
|
||
|
||
---
|
||
|
||
## Module Breakdown
|
||
|
||
| Module | Domain | File |
|
||
|---|---|---|
|
||
| `BBATop` | all | `exi_bba/bba_top.py` |
|
||
| `ExiCapture` | creates exiclk/exiclk_n/exififo_* (+sync side) | `exi_bba/exi_capture.py` |
|
||
| `SPIMode3Slave` | exiclk / exiclk_n | `exi_bba/spi_mode3_slave.py` |
|
||
| `BBARegisterFile` | sync | `exi_bba/bba_register_file.py` |
|
||
| `SPRAMArbiter` | sync | `exi_bba/spram_arbiter.py` |
|
||
| `RXFrameAssembler` | sync | `exi_bba/rx_frame_assembler.py` |
|
||
| `TXFrameDrain` | sync | `exi_bba/tx_frame_drain.py` |
|
||
| `W5100ParallelMaster` | sync | `exi_bba/w5100_parallel_master.py` (default eth) |
|
||
| `W5500SPIMaster` | sync | `exi_bba/w5500_spi_master.py` (alt eth) |
|
||
| `EEPROMModel` | sync | `exi_bba/eeprom_model.py` |
|
||
|
||
`ExiCapture` **creates the four pin-driven clock domains** (so the byte FIFOs
|
||
can name them) and wraps `SPIMode3Slave` plus the rx/tx byte AsyncFIFOs.
|
||
`SPIMode3Slave` just uses `m.d.exiclk` / `m.d.exiclk_n`; it has no `domain`
|
||
parameter any more. `BBARegisterFile` consumes the rx byte stream and
|
||
proactively pushes read responses into the tx byte FIFO — it never sees the
|
||
per-bit SPI cadence.
|
||
|
||
**Everything except the bit engine is in one `sync` domain.** The old separate
|
||
`exi` domain was merged into `sync`, which deleted six FIFOs (SPRAM req/rsp,
|
||
RWP, RRP, and the TX byte/length pairs became `SyncFIFO`s) and three
|
||
`PulseSynchronizer`s (now plain wires). The SPRAM response FIFO was removed
|
||
outright — the register file handles responses directly, which is what finally
|
||
closed the DMA read offset at 27 MHz.
|
||
|
||
---
|
||
|
||
## CDC Signal Inventory
|
||
|
||
Only **two** crossings remain, both byte-level, both with a full byte-time
|
||
(~296 ns at 27 MHz) of slack:
|
||
|
||
| Signal | Direction | Primitive |
|
||
|---|---|---|
|
||
| RX byte stream | `exififo_w` → `sync` | `AsyncFIFO` 8-bit, depth=4 |
|
||
| TX byte stream | `sync` → `exififo_r` | `AsyncFIFO` 8-bit, depth=2 |
|
||
| cs_active (transaction in progress) | EXI pin → `sync` | `FFSynchronizer` (stages=2) |
|
||
|
||
Everything that used to cross `exi`↔`sync` is now a same-domain `SyncFIFO` or
|
||
a plain wire. Note what is deliberately NOT here: the EXI CLK/MOSI/CS pins are
|
||
no longer synchronised into a fast domain — CLK *is* the clock and CS *is* the
|
||
async reset. Synchronising them was the bug, not the safety measure.
|
||
|
||
⚠️ **The FIFO domains must stay reset-less.** `exififo_w`/`exififo_r` are not
|
||
reset by CS: resetting one side of an `AsyncFIFO` desynchronises its gray
|
||
pointers against the other side. They are also both **posedge**, because
|
||
`AsyncFIFO` instantiates a posedge-only reset CDC internally and cannot host a
|
||
negedge side; the engine pops in `exififo_r` and hands the byte to its negedge
|
||
output stage through a plain register (a half-period path, 18.5 ns at 27 MHz).
|
||
|
||
---
|
||
|
||
## W5500 Configuration (on NCRA reset)
|
||
|
||
The W5500 selects the register **block** via the BSB field of the control byte,
|
||
NOT via the address — so register addresses below are **block offsets**, not flat
|
||
0x4000-style addresses (see `_W5500_*` and `_CTRL_*` in `w5500_spi_master.py`).
|
||
```
|
||
1. Write MR = 0x80 (common block, offset 0x0000) software reset
|
||
2. Wait ~1 ms
|
||
3. Write SHAR = MAC (common block, offset 0x0009, 6 bytes from PAR0–5)
|
||
4. Write S0_MR = 0x04 (socket-0 reg block, offset 0x0000) MACRAW
|
||
5. Write S0_CR = 0x01 (socket-0 reg block, offset 0x0001) OPEN
|
||
6. Write S0_IMR = 0x05 (socket-0 reg block, offset 0x002C) RECV | SEND_OK
|
||
```
|
||
|
||
W5500 SPI is **Mode 0** (CPOL=0 CPHA=0); SCK = **12 MHz** (the 24 MHz `sync`
|
||
domain ÷ 2 via a toggle clock-enable). Connect W5500 `INT_N` to an FPGA input
|
||
for low-latency RX detection. (The W5500 is the alternate back-end; the W5100
|
||
parallel master is the default — see "W5100 vs W5500".)
|
||
|
||
---
|
||
|
||
## Physical Interface (SP1 Edge Connector)
|
||
|
||
- PCB must be **1.2 mm thick, ENIG finish**.
|
||
- Staggered (not mirrored) top/bottom contact rows — same geometry as PCI/ISA.
|
||
- Derive exact pad geometry from **SP1ETH KiCad project** (silverstee1/SP1ETH),
|
||
cross-referenced with ETH2SP1 (LaserBear). Do not rely on YAGCD alone.
|
||
- Add **100 µF bulk cap** on the interposer near FPGA power pins (3.3 V budget
|
||
is tight: iCEbreaker ~80 mA + W5500 ~150 mA ≈ 230 mA).
|
||
- **Pin 5 is 12 V — do not connect to FPGA I/O.** Test point or leave open.
|
||
- `EXTIN` (pin 1): tie to 3.3 V via 10 kΩ — required for GC device enumeration.
|
||
- All signal levels are 3.3 V. No level shifting needed.
|
||
|
||
---
|
||
|
||
## SPRAM Notes
|
||
|
||
- iCE40UP5K has 128 KB SPRAM (SB_SPRAM256KA, 16-bit wide).
|
||
- **1-cycle synchronous read latency** — result of read at cycle N is valid at N+1.
|
||
- Byte writes via `MASKWREN`: lower byte = `0b0011`, upper byte = `0b1100`.
|
||
- Address to SPRAM = byte_address >> 1.
|
||
- ETH writes take priority over EXI reads in the arbiter (safe by ring-buffer
|
||
invariant: GC only reads pages the ETH engine has already finished).
|
||
|
||
---
|
||
|
||
## GC Initialisation Sequence (Swiss/BBA driver)
|
||
|
||
```
|
||
1. Write 0x0000 × 2, read 4 B → must get 0x04020200 (device ID)
|
||
2. Write NCRA = 0x01 (reset, self-clears; resets W5500 + SPRAM ptrs)
|
||
3. Poll NCRA bit 0 until 0 (wait reset complete)
|
||
4. Write PAR0–5 (MAC address)
|
||
5. Write MAR0–7 = 0xFF (promiscuous multicast)
|
||
6. Write ANALOG = 0xD6 (enable PHY — no FPGA effect, just store)
|
||
7. Write NWAYC (autoneg config — store only)
|
||
8. Write IMR = 0x86 (enable RBFI | TI | RI interrupts)
|
||
9. Write GCA (AUTOPUB bit)
|
||
10. Write NCRA SR bit = 0x08 (start receive)
|
||
11. Poll NWAYS until link up → return hardcoded 0x17 immediately
|
||
```
|
||
|
||
---
|
||
|
||
## Implementation Notes & Gotchas
|
||
|
||
- **`NWAYS` must return `0x17` always.** GC polls it to confirm 100 Mbps link
|
||
before enabling RX. Do not attempt to reflect real W5500 link status.
|
||
- **`EEPROMModel` can be stubbed initially.** Many GC BBA drivers write their own
|
||
MAC to PAR0–5 rather than using the EEPROM. Pre-populate PAR0–5 reset state
|
||
with a valid Nintendo OUI MAC (`00:09:BF:xx:xx:xx`).
|
||
- **`tx_load` timing in `SPIMode3Slave`:** pulses at CS assertion (first byte)
|
||
and after each complete received byte. Upstream must register next TX byte
|
||
within one `exi` clock.
|
||
- **No PLL.** The bit engine is clocked by the EXI pin; `sync` is HFOSC÷2.
|
||
Do not reintroduce an oversampling capture domain — see the clocking note
|
||
under "Key Architecture Decisions" for why it cannot work on this part.
|
||
- **TX buffer selection (NCRA ST bits):** Ignore buffer select (ST1 vs ST0).
|
||
Treat any non-zero ST as a TX trigger.
|
||
- **If nextpnr fails capture-domain timing at 54 MHz:** the isolated bit engine
|
||
closes ~91 MHz, so 54 has margin; if a seed fails, sweep seeds
|
||
(`synth.py --seeds N`) or instruct users to configure Swiss to a lower EXI
|
||
clock index.
|