Status: the GameCube → GBA Link Cable controller mode (gc2usb autoboot of an embedded GBA payload, GBA reads back as a HID gamepad) is production. The USB-vendor bridge to a forked Dolphin emulator is experimental and disabled by default in the firmware.
The Nintendo DOL-011 GameCube-to-Game-Boy-Advance Link Cable connects an unmodified GBA to a GameCube's SI bus on Port 2. The GameCube can:
- Upload a multiboot ROM to the GBA's RAM (the BIOS "multiboot" handshake, ~16 KB max, Kawasedo cipher).
- Talk to the running multiboot payload via joybus (4-byte WRITE / 4-byte READ over the SIO link).
Joypad OS implements both halves on the bridge side (RP2040 + joybus PIO), and uses them in two different ways depending on what's plugged in to the bridge's USB-host port.
This is the default behavior of gc2usb_feather_usbhost and the other gc2usb targets:
- On boot, the firmware initializes the joybus PIO state machine for the link-cable wire.
- Once it sees a GBA on the link, it uploads an embedded multiboot ROM (
src/native/host/gc/gba_payload.c, the "joypad eyes" payload) using the Kawasedo cipher. - The payload runs on the GBA and constantly publishes the GBA's button state back over joybus.
- The firmware reads that button state every poll and routes it through the standard input event pipeline. The GBA appears as a HID gamepad on the host computer alongside any other connected controller.
Hardware:
- Adafruit Feather RP2040 USB Host (or Pico variants — see Makefile target list).
- DOL-011 link cable (or compatible bare wire) tapped to
GC_PIN_DATAon the bridge. Default pin isGP4on Feather andGP29on the Pico variants. - The GBA must be powered (battery or AC) and on the BIOS multiboot wait screen.
Caveats:
- The first joybus exchange after a GBA cold-start fails ~50% of the time (transient PIO/wire glitch), so the firmware retries
RESET/STATUS(idempotent) on timeout. Steady-state reliability after the warm-up is effectively 100%. - Both halves of the link cable share one wire (open-drain joybus), so the firmware also has to settle the bus 150 µs between back-to-back commands to avoid GBA-side overrun.
The idea: instead of having Dolphin talk to a TCP-based GBA proxy (the upstream
GBA (TCP)mode), have it talk directly to the bridge over USB so a real GBA on a real link cable can be the GBA in a Dolphin GameCube session. Targets games like Madden 2003 (Madden Cards) and FFCC where the in-game GBA half is normally simulated via libmgba.
Firmware side (src/usb/usbd/modes/gba_link_mode.c, gated on CONFIG_JOYBUS_BRIDGE):
USB_OUTPUT_MODE_GBA_LINK(mode 14) — composite USB device, CDC + vendor-class. VID 0x057E / PID 0x0338 ("Nintendo / GameCube GBA Link Cable").- Vendor bulk OUT receives raw joybus command bytes from the host (1 byte for
RESET/STATUS/READ, 5 bytes forWRITE). - Bridge forwards to the GBA over real joybus, reads the GBA's reply, returns it on vendor bulk IN.
- Per-cmd timeout/retry budget tuned per command type (STATUS 30 ms × 5, others 5 ms × 2) — see telemetry below.
- Diagnostic CDC commands:
JOYTEST(single RESET probe),JOYPIN?(sample the data pin),GBALINK?/GBALINK!/GBALINK0(per-cmd telemetry + reset).
Dolphin fork side (~/git/dolphin/, branch joypad-gba-usb):
CSIDevice_GBA_USB— libusb-bulk transport inSource/Core/Core/HW/SI/SI_DeviceGBAUSB.{cpp,h}.- Dropdown entry "GBA (USB)" in
GamecubeControllersWidget.cpp, gated onHAS_GBA_USBdefine (set when libusb is found).
- End-to-end
RESET → STATUS → READ session_key → WRITE our_key → stream 4170 body WRITEs → WRITE fcrc → READ crc_replymultiboot of the embedded payload viatools/gba-bridge/usbgba-multiboot.py— reliable, ~3.8 s wall-clock (faster than real GameCube hardware's ~10 s). - Madden 2003 multiboot via the fork — multiboots and reaches Madden Cards, GBA runs the Madden Cards mini-game ROM.
- FFCC — connects and plays.
Madden's menu / connect time via the bridge is many minutes vs ~1 minute with libmgba, and the slowness is present from the moment "GBA (USB)" is selected (before any multiboot is even attempted). FFCC has similar in-game lag.
Root cause: libusb_bulk_transfer in Dolphin's CPU thread blocks for the entire USB+joybus round-trip on every SI tick (~750 µs/tick). With Madden doing dozens of SI ops per frame, that's milliseconds of frozen CPU per frame — game-wide slowness whenever the device is selected.
The original CSIDevice_GBA (TCP) doesn't have this issue because SFML socket Receive() is non-blocking. CSIDevice_GBAEmu (libmgba) doesn't have it because the GBA core runs in its own thread with shared-memory queue I/O. To match either pattern, CSIDevice_GBA_USB needs to move the libusb calls off Dolphin's CPU thread. Two attempts during the May 17 session both broke detection:
- Worker thread + std::queue (commit
c3ba522, reverted in61bc6f2) — only 9 STATUS in 8 minutes; suspected state-machine race I couldn't bisect without live observability. - libusb async API + event-pump thread (commit
3a28891, reverted in7e6e8eb) — only 129 STATUS in many minutes; same failure mode. Likely libusb-event-loop wiring issue.
Both compiled cleanly and didn't crash; the failure mode was "Madden sees the device as broken." Dolphin already does async libusb correctly in IOS::USB — the fix is probably to crib that pattern wholesale rather than reinvent.
- Switching Dolphin to DSP-HLE instead of DSP-LLE — made things much worse. HLE's instant cipher math caused Dolphin to send SI commands faster than the real GBA could keep up; READ/WRITE timeouts appeared, Madden retried multiboot 100+ times. LLE's per-cycle pacing is exactly what the real GBA needs.
- Bypassing Dolphin's
WaitTransferTimeSI-pacing for our device — broke Madden in subtle ways. Madden's protocol expects commands at simulated SI-bus rate; arriving faster confused it. - Intercept-replay (recognize Madden's multiboot pattern, capture encrypted bytes on-device, run native upload, fake replies to Dolphin) — was implemented in
gc2eth_featherthen disabled (s_intercept_enabled = false,src/apps/gc2eth_feather/app.c:372). Madden polls STATUS between WRITEs and watches the JSTAT.RECV bit toggle to confirm bytes are landing on the real GBA. Faking replies → Madden aborts within a few WRITEs. - Aggressive STATUS retries (50 × 30 ms) — made connect worse: when joybus genuinely glitched, the long retry chain blocked the USB pipe for >150 ms per failure, longer than Madden's per-handshake-step budget; Madden gave up and issued 100+ RESETs.
- Shorter joybus timeouts (1 ms WRITE) — caused cipher desync (late GBA reply we falsely treated as timeout). Multiboot would complete but GBA booted to a black screen.
The settled sync config (commit 7c2eabf in the fork) — 30 ms STATUS × 5 retries, 5 ms WRITE/READ × 2, 2 ms libusb_bulk_transfer poll on Receive — is the configuration that has actually been observed to let Madden complete multiboot in 1–3 attempts and reach the Cards screen. Just slowly, because every SI tick still blocks the CPU thread for the round trip.
RESET n=3 avg=145 µs max=147 µs retries=0 to=0 ← perfect
STATUS n=680 avg=3 ms max=91 ms retries=474 to=82 ← intrinsic GBA-BIOS lag during multiboot bursts
READ n=4681 avg=207 µs max=235 µs retries=0 to=0 ← perfect (includes steady-state polling)
WRITE n=71269 avg=210 µs max=218 µs retries=0 to=0 ← perfect
WRITE bad_jstat = 0
The 750 µs/SI-tick block is not in any of those numbers — they only measure the joybus xfer wall-clock. The blocking lives in Dolphin's libusb call, not in the bridge.
The firmware code is preserved behind CONFIG_JOYBUS_BRIDGE. To build a firmware with the GBA Link mode (mode 14) selectable:
cd src && rm -rf build
cmake -G "Unix Makefiles" -DFAMILY=rp2040 \
-DPICO_BOARD=adafruit_feather_rp2040_usb_host \
-DJOYPAD_ENABLE_GBA_LINK_BRIDGE=ON \
-B build
cd build && make joypad_gc2usb_feather_usbhost -j8The device will then enumerate as GameCube GBA Link Cable (VID 0x057E / PID 0x0338) when in mode 14, and the MODE.LIST CDC command will include mode 14.
To use it from Dolphin you need the fork (or to port the CSIDevice_GBA_USB patch to upstream Dolphin yourself). Branch is joypad-gba-usb on github.com/RobertDaleSmith/dolphin (or wherever the user's fork lives).
If you're picking this up to make Dolphin actually fast:
- The bottleneck is not USB, not joybus, not the bridge firmware — it's Dolphin's CPU thread blocking on
libusb_bulk_transferper SI tick. The bridge already runs faster than real hardware. - Pattern to copy is libmgba's
CSIDevice_GBAEmu—SendJoybusCommandqueues async,GetJoybusResponsepolls a queue. No blocking on the SI thread. - libusb async API (
libusb_submit_transfer+libusb_handle_events) is the right tool. Two attempts in May 17 session failed mysteriously without live debug logs; next attempt should turn on libusb's own debug logging (LIBUSB_OPTION_LOG_LEVEL = LIBUSB_LOG_LEVEL_DEBUG) and Dolphin'sSERIALINTERFACElog channel before testing. - Dolphin already does async libusb correctly in
Source/Core/Core/IOS/USB/— read that pattern before writing a new one. - The fork's
joypad-gba-usbbranch has the failed attempts in the reverted commits, useful as a starting point for what NOT to do.