Correct the framing: the two command forms are padding, not protocols

My earlier note read as "Swiss's menu and a patched game speak different
protocols to the same chip", which is both suspicious-sounding and wrong.

Both send the IDENTICAL 2-byte header [0x40|reg, 0x00] and differ only in how
many bytes follow, which the device never cared about -- the data phase runs
until CS, exactly as in register space.  Swiss's 4-byte form is an EXI
efficiency choice: one exi_imm_read_write(..., 4) is a single EXI immediate
operation, where libogc2 spends two separate EXI_ImmEx calls.  For code
injected into a running game, halving the EXI operations per register access is
worth a padding byte.

Menu vs game is not a protocol split either, just which library is linked:
cube/swiss/source/bba.c contains no BBA register access at all -- its header
exposes only network config, detection and display over <network.h> -- so the
menu is entirely libogc2, and the patch driver is Swiss's own leaner code.
Verified: the only menu-side device access is EXI_GetID for detection and
MX98730EC_GetExiSpeed, which just returns EXI_SPEED32MHZ (confirming Swiss
hardcodes speed index 5 for the real BBA), while bba_requires_init() returns
id != EXI_MX98730EC_ID, i.e. the real BBA needs no Swiss-side init.

Also sharpens the multi-byte caveat.  No single rule satisfies both Swiss's
padded single-byte write and libogc2's bba_cmd_outs(0x04, &devid, 2), so the
real device's behaviour there cannot be deduced from the drivers -- and it is
unobservable in them anyway: devid is the hardcoded constant 0xD107, is never
read back, and bba_cmd_out8(0x05, acstart) on the next line overwrites half of
it under auto-increment.  Settling it would need a real BBA on a logic
analyser, so the note now says not to try it from source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 11:18:09 +02:00
parent 0d100eb84f
commit 49d7e8fcff
+29 -2
View File
@@ -1086,6 +1086,24 @@ Sources, so this can be re-checked rather than re-guessed:
- stock libogc — the same, read out of `libbba.a` (`gcif.o`, `__bba_init`): - stock libogc — the same, read out of `libbba.a` (`gcif.o`, `__bba_init`):
`EXI_Imm(len=2)` of `42 00`, then `EXI_ImmEx(len=1)` of the data. `EXI_Imm(len=2)` of `42 00`, then `EXI_ImmEx(len=1)` of the data.
**They are NOT two protocols — only two amounts of padding.** This is worth
stating plainly, because "the menu and a game speak different protocols to the
same chip" is rightly suspicious and is not what happens. Both send the
*identical* 2-byte header `[0x40|reg, 0x00]`. They differ only in how many
bytes follow, and the device is length-agnostic — the data phase runs until CS,
exactly as in register space. Swiss's 4-byte version is an **EXI efficiency
choice**, not a different protocol: one `exi_imm_read_write(…, 4)` is a single
EXI immediate operation (two register pokes), where libogc2 spends two separate
`EXI_ImmEx` calls. For code injected into a running game, halving the EXI
operations per register access is worth a padding byte. The real BBA tolerates
both because it never cared about the length in the first place.
Menu vs game is therefore not a protocol split, it is just which library is
linked: `cube/swiss/source/bba.c` contains **no BBA register access at all**
(its header exposes only network config, detection and display, over
`<network.h>`), so the menu is 100% libogc2; the patch driver is Swiss's own
code and spends fewer EXI operations.
**How one decoder serves both:** they share bytes 0 and 1, and differ only in **How one decoder serves both:** they share bytes 0 and 1, and differ only in
how many bytes follow. So the device takes the **LAST data byte before CS how many bytes follow. So the device takes the **LAST data byte before CS
rise** as the write value (Swiss sends `[00, val]`, libogc sends `[val]` — rise** as the write value (Swiss sends `[00, val]`, libogc sends `[val]` —
@@ -1132,8 +1150,17 @@ data phase has two meanings that cannot be told apart on the wire:
libogc2 really does multi-byte command writes — `bba_cmd_outs(0x04, libogc2 really does multi-byte command writes — `bba_cmd_outs(0x04,
&priv->devid, 2)` runs unconditionally in its init. "Last byte wins" is wrong &priv->devid, 2)` runs unconditionally in its init. "Last byte wins" is wrong
for those, and auto-increment is wrong for Swiss. It does not matter today for those, and auto-increment is wrong for Swiss, so **no single rule satisfies
because: both** and the real device's behaviour here cannot be deduced from the drivers.
⚠️ **It is also unobservable in them**, which is why it has never mattered:
`priv->devid` is the hardcoded constant `0xD107`, is never read back from the
device, and `bba_cmd_out8(0x05, priv->acstart)` on the very next line writes
`0x05` separately anyway — so under auto-increment half of that write is
immediately overwritten. Neither driver ever reads `0x04`/`0x05`. Do not try to
settle this from source; it would need a real BBA on a logic analyser.
It does not matter for us because:
- the only command registers implemented are **0x02** (IMR) and **0x03** (IR), - the only command registers implemented are **0x02** (IMR) and **0x03** (IR),
and BOTH drivers access those a single byte at a time; and and BOTH drivers access those a single byte at a time; and
- libogc2's multi-byte command accesses target `0x04`/`0x05` and - libogc2's multi-byte command accesses target `0x04`/`0x05` and