Support BOTH command-space forms: Swiss's 4-byte and libogc's 2-byte

Reading Swiss's own driver (cube/patches/bba/bba.c) shows the previous commit
fixed libogc and BROKE Swiss.  The two disagree, and both are real:

  Swiss   bba_cmd_out8: exi_imm_read_write(0x40<<24 | reg<<24 | val, 4)
          -> ONE 4-byte transfer [0x40|reg, 00, 00, val], and bba_cmd_in8
             returns the LOW byte, so a read response must be in BYTE 3.
  libogc2 bba_cmd_outsnosel: req = (reg<<8)|0x4000;
          EXI_ImmEx(&req, 2, WRITE); EXI_ImmEx(val, len, WRITE)
          -> THREE bytes, value in byte 2, and a read takes byte 2.
  stock libogc: identical to libogc2 (read out of libbba.a).

They share bytes 0 and 1 and differ only in how many bytes follow, so one
decoder serves both: take the LAST data byte before CS rise as the write value
(Swiss sends [00, val], libogc sends [val] -- last is val either way), and push
a read response TWICE so bytes 2 and 3 both carry it.  Swiss clocks 4 bytes and
pops both; libogc clocks 3 and leaves one, which the next transaction's header
pops drain -- a 2-byte header gives exactly one drain slot, so the leftover is
bounded at one and cannot accumulate.

Critically this means NO auto-increment of cmd_reg across the data phase: that
applied Swiss's padding 00 to the register and val to reg+1, which is precisely
how the libogc-only version broke Swiss.

T12 covers the Swiss form and asserts bytes 2 AND 3; T10 keeps the libogc form.
Both mutation-tested: re-adding the auto-increment fails T12 with 0x00 in both
slots, and pushing the response once fails T12's byte 3.  gc_bba_test gains
"cmd-space Swiss" alongside "cmd-space write", with bba_cmd_write4/read4.

Also corrects two things in CLAUDE.md now checkable against three drivers:
- NO driver writes TWD -- not Swiss, not libogc, not libogc2.  The "GC sets
  TWD before streaming" note was self-confirming: t_tx writes TWD because we
  wrote t_tx.  The counted TXDATA length is the primary path, not a fallback.
- Swiss's exi_dma_write rounds the length UP to 32 bytes (OSRoundUp32B), so
  TXDATA gets up to 31 trailing bytes past the frame and a counted length sends
  that much extra.  Real hardware must do the same since Swiss works on it;
  trailing padding past the IP total length is ignored.  A 1514-byte frame
  rounds to 1536 though, over the 1518 maximum.

Both drivers trigger TX with NCRA ST1, which the existing any-ST test accepts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 11:01:33 +02:00
parent 71ec6a2b37
commit b3685237b2
5 changed files with 223 additions and 52 deletions
+29
View File
@@ -139,6 +139,35 @@ int bba_cmd_read(u8 creg)
return (int)v;
}
/* SWISS's command-space form: ONE 4-byte transfer, value in byte 3, and a
* read takes byte 3 (the low byte of the 32-bit result). Swiss's
* bba_cmd_out8/in8 use exi_imm_read_write(..., 4).
*
* This DIFFERS from bba_cmd_write/bba_cmd_read above, which use libogc's
* 2-byte-header form. Both are real drivers and the device must serve both,
* so both shapes are exercised. */
int bba_cmd_write4(u8 creg, u8 val)
{
int rc;
u32 w = ((u32)(0x40 | (creg & 0x3F)) << 24) | val;
if ((rc = begin_raw()) < 0) return rc;
if (EXI_Imm(BBA_EXI_CHAN, &w, 4, EXI_WRITE, NULL) <= 0) { end_raw(); return -3; }
if (EXI_Sync(BBA_EXI_CHAN) <= 0) { end_raw(); return -4; }
end_raw();
return 0;
}
int bba_cmd_read4(u8 creg)
{
int rc;
u32 w = ((u32)(creg & 0x3F) << 24);
if ((rc = begin_raw()) < 0) return rc;
if (EXI_Imm(BBA_EXI_CHAN, &w, 4, EXI_READWRITE, NULL) <= 0) { end_raw(); return -3; }
if (EXI_Sync(BBA_EXI_CHAN) <= 0) { end_raw(); return -4; }
end_raw();
return (int)(w & 0xFF); /* Swiss reads the LOW byte = byte 3 */
}
int bba_r8(u16 addr)
{
u8 v;
+6
View File
@@ -82,6 +82,12 @@ void end_raw(void);
int bba_cmd_write(u8 creg, u8 val);
int bba_cmd_read(u8 creg);
/* Swiss's command-space form: ONE 4-byte transfer, value/response in byte 3.
* Differs from bba_cmd_write/read above (libogc's 2-byte-header form); the
* device must serve both. */
int bba_cmd_write4(u8 creg, u8 val);
int bba_cmd_read4(u8 creg);
/* Convenience single-byte helpers. bba_r8 returns the byte, or <0 on error. */
int bba_r8(u16 addr);
int bba_w8(u16 addr, u8 val);
+26 -1
View File
@@ -421,6 +421,30 @@ static int t_cmd_write(char *m, size_t n)
return rb == 0x00 ? 0 : 1;
}
static int t_cmd_write_swiss(char *m, size_t n)
{
int rc, rb;
/* Swiss sends ONE 4-byte transfer with the value as byte 3 and reads the
* response from byte 3; libogc (and libogc2) send a 2-byte header plus a
* data phase, value in byte 2. Both are real drivers. The device takes
* the LAST data byte as the value and pushes a read response twice, so
* bytes 2 and 3 both carry it -- this test is the Swiss half. */
if ((rc = bba_cmd_write4(0x02, 0x5A)) < 0) {
snprintf(m, n, "Swiss cmd write failed: EXI error %d", rc); return 1;
}
if ((rb = bba_cmd_read4(0x02)) < 0) {
snprintf(m, n, "Swiss cmd read failed: EXI error %d", rb); return 1;
}
if (rb != 0x5A) {
snprintf(m, n, "Swiss 4-byte form: wrote 0x5A, byte 3 = 0x%02X", rb);
bba_cmd_write4(0x02, 0x00);
return 1;
}
bba_cmd_write4(0x02, 0x00);
snprintf(m, n, "4-byte form ok (byte 3 = 0x%02X)", rb);
return 0;
}
static int t_mac_is_readable(char *m, size_t n)
{
u8 mac[6];
@@ -654,6 +678,7 @@ static test_t s_spram[] = {
static test_t s_init[] = {
{"cmd-space write","2-byte header + data phase, as libogc sends it", t_cmd_write, T_PENDING, {0}},
{"cmd-space Swiss","4-byte transfer, value in byte 3, as Swiss sends", t_cmd_write_swiss, T_PENDING, {0}},
{"MAC readable", "libogc READS the MAC from PAR0-5; it must be sane", t_mac_is_readable, T_PENDING, {0}},
{"init replay", "libogc's 30 init writes, in order, do no harm", t_init_sequence, T_PENDING, {0}},
{"TX without TWD","libogc never writes TWD; length must be counted", t_tx_no_twd, T_PENDING, {0}},
@@ -666,7 +691,7 @@ suite_t g_suites[] = {
{"EXI integrity", "the EXI link is error-free, and how fast", s_exi, 6},
{"Ethernet", "frames get to and from the W5100", s_eth, 3},
{"SPRAM/DMA", "bulk reads stream correctly from the ring", s_spram, 2},
{"libogc init", "the real driver's init sequence and shapes", s_init, 4},
{"driver shapes", "Swiss + libogc init sequences and transfer shapes", s_init, 5},
};
const int g_nsuites = sizeof(g_suites) / sizeof(g_suites[0]);