TX: count the TXDATA bytes, because libogc never writes TWD

Third contract checked against libogc rather than our notes.  __bba_link_tx is
four transactions -- read NWAYS, ONE register-space write of the whole frame to
0x0048, read NCRA, write NCRA = 0x04 -- with no TWD_LO/TWD_HI anywhere.  So the
device has to count the bytes it was handed.

The NCRA ST fallback queued tx_frame_len, which only TWD sets, so any driver
that skips TWD would have transmitted a stale or zero length.  BBARegisterFile
now keeps txdata_cnt, incremented per byte accepted at 0x0048 and cleared
whenever a frame is queued from either path, and the ST path queues that.

T11 drives TX in libogc's shape with no TWD write and checks both the queued
length and the drained bytes.  Mutation-tested: switching the ST path back to
tx_frame_len queues length 0, which is exactly the bug.  gc_bba_test gains the
hardware counterpart, "TX without TWD", which deliberately omits the TWD writes
that t_tx makes.

The TWD path stays and is still preferred when present: writing the length
before streaming is what lets TXFrameDrain consume concurrently and keeps
tx_bytes a rate-matching buffer rather than whole-frame storage.  Swiss writes
TWD, libogc does not, so both are supported.

Also recorded: libogc streams the payload with EXI_Dma (32-byte-aligned bulk
after PPCDCacheStoreAsync, then EXI_ImmEx for the remainder).  CLAUDE.md said
EXI DMA writes are avoided for the GC's 1-bit-shift bug; that is true of Swiss,
not libogc.  Nothing in the gateware changes -- a DMA write is just bytes
clocked in -- but the path will be exercised, and if that bug is real it
appears as corrupted TX frames that are not fixable here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 10:49:11 +02:00
parent 0aa1fa884a
commit 71ec6a2b37
3 changed files with 121 additions and 2 deletions
+33 -1
View File
@@ -439,6 +439,37 @@ static int t_mac_is_readable(char *m, size_t n)
return (mac[0] == 0x00 && mac[1] == 0x09 && mac[2] == 0xBF) ? 0 : 1;
}
static int t_tx_no_twd(char *m, size_t n)
{
u8 f[60];
u32 len = build_arp(f);
int i, ir = 0;
/* libogc's __bba_link_tx writes NO TWD at all: read NWAYS, one
* register-space write of the whole frame to 0x0048, read NCRA, write
* NCRA = 0x04. So the device must count the bytes it was handed. The
* NCRA ST fallback used to queue the TWD value, which a driver that
* skips TWD never sets -- the frame went out with a stale or zero
* length. Deliberately does NOT write TWD_LO/TWD_HI. */
bba_w8(REG_IR, 0xFF);
if (bba_tx_bytes(f, len) < 0) {
snprintf(m, n, "TXDATA stream failed"); return 1;
}
bba_w8(REG_NCRA, NCRA_SR | NCRA_ST0);
for (i = 0; i < 60; i++) {
ir = bba_r8(REG_IR);
if (ir >= 0 && (ir & IR_TI)) {
snprintf(m, n, "%u B sent with NO TWD, TI after %d poll(s)",
(unsigned)len, i);
return 0;
}
msleep(10);
}
snprintf(m, n, "TI never set (IR=0x%02X) without TWD", ir);
return 1;
}
static int t_init_sequence(char *m, size_t n)
{
/* The register writes libogc issues, in order, with the values it uses.
@@ -625,6 +656,7 @@ static test_t s_init[] = {
{"cmd-space write","2-byte header + data phase, as libogc sends it", t_cmd_write, 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}},
};
suite_t g_suites[] = {
@@ -634,7 +666,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, 3},
{"libogc init", "the real driver's init sequence and shapes", s_init, 4},
};
const int g_nsuites = sizeof(g_suites) / sizeof(g_suites[0]);