Files
rebbarb/gc_bba_test/source/bba.c
T
Roflin d67f111019 Don't advance the write address across TXDATA; deepen tx_bytes to 2048
Two coupled fixes, both exposed by batching TXDATA writes in the test app.

1. eff_addr advanced on EVERY data byte of a register-space write.  But
   TXDATA (0x48) is a FIFO PORT, not a register: a whole frame is written to
   that one address.  So an 8-byte write put byte 0 in TXDATA and bytes 1..7
   into 0x49+, which the switch silently ignores.  The frame was an eighth of
   its declared length, TXFrameDrain blocked waiting for the rest, and TI
   never set (hardware: txto=16, "no ACK after 20 tries").

   SWISS WOULD HAVE HIT THIS.  It writes an entire frame with bba_outs to
   BBA_WRTXFIFOD -- one transaction, one address -- so its TX would have
   delivered ONE BYTE per frame.  The path was never exercised because the
   test app wrote TXDATA one byte per EXI transaction, giving every byte a
   fresh header at 0x48.  Every other register must still advance, so that
   bba_outs can write PAR0-5 or MAR0-7 as a block; the exception is TXDATA
   only.

2. tx_bytes was 16 deep.  SPI cannot back-pressure -- there is no way to tell
   the GC to wait -- so BBARegisterFile silently drops bytes when w_rdy is
   low, which makes the depth the only thing between a drain stall and a
   corrupted frame, not a buffering preference.  16 bytes is ~4.7 us at the
   EXI byte rate, and TXFrameDrain shares the W5100 bus arbiter with RX and
   the socket-3 UDP test, so any longer stall truncated a frame mid-flight.
   With (1) fixed, Swiss streams up to 1514 bytes in a single transaction, so
   16 could never have held.

   Now 2048: a whole maximum-size frame plus margin, so a frame cannot be
   truncated even if the drain makes no progress for the entire transaction.
   Costs 3 of 27 free BRAMs (ICESTORM_RAM 3/30 -> 6/30, LC 68% -> 70%).

   Dennis spotted that the 8-byte workaround could not survive whole-frame
   writes, which is what prompted fixing the depth rather than shipping it.

bba_tx_bytes() consequently streams 64-byte chunks (bba_write()'s buffer
limit), so a 60-byte ACK is ONE transaction instead of sixty.

T8 covers both and is mutation-tested: restoring the increment gives
"1/8 bytes MISMATCH -- got [17], want [17, 34, 51, ...]".  It now writes 64
bytes so it exercises the deepened FIFO too.

--seeds 8: clk 27.6-29.4 MHz, EXI 51.2-64.7 MHz, 8/8 PASS.  Best seed 1
(clk 29.35, EXI 60.80), flashed.

STILL NO FLOW CONTROL on the TX path: a frame larger than 2048 would drop
silently rather than error.  Fine for ethernet's 1518 ceiling; revisit if the
multiworld socket work ever wants larger payloads.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 19:36:21 +02:00

159 lines
4.8 KiB
C

#include "bba.h"
#include <string.h>
#include <unistd.h>
#include <ogc/lwp_watchdog.h>
u32 bba_speed = EXI_SPEED32MHZ; /* nominal name; ~27 MHz in reality */
static int begin(void);
static void end(void);
u32 bba_gap_us = 16; /* see bba.h -- 0 reproduces the raw failure */
/* Busy-wait, not usleep(): we are holding the EXI lock with CS asserted, so
* yielding here is both unnecessary and imprecise at these timescales. */
static void gap(void)
{
u64 t0;
if (!bba_gap_us) return;
t0 = gettime();
while (ticks_to_microsecs(diff_ticks(t0, gettime())) < bba_gap_us) ;
}
/* Real GC<->BBA protocol (docs/exi_protocol_real.md), verified against libogc
* and Swiss. Register space is a 4-byte header followed by a data phase that
* runs until CS -- there is no length field.
* read [0x80, addr>>8, addr&0xFF, 0x00] + N bytes
* write [0xC0, addr>>8, addr&0xFF, 0x00] + N bytes
* The trailing 0x00 is the device's prefetch window: the address completes a
* whole byte before the data phase so the register file can fetch in time. */
int begin_raw(void) { return begin(); }
void end_raw(void) { end(); }
static int begin(void)
{
int tries;
/* EXI_Lock fails outright (rather than blocking) when another subsystem
* holds the channel -- Swiss or a libogc driver may have it briefly.
* Retry for ~100 ms before giving up so a transient collision does not
* surface as a test failure. */
for (tries = 0; tries < 100; tries++) {
if (EXI_Lock(BBA_EXI_CHAN, BBA_EXI_DEV, NULL) > 0) break;
usleep(1000);
}
if (tries == 100) return -1;
if (EXI_Select(BBA_EXI_CHAN, BBA_EXI_DEV, bba_speed) <= 0) {
EXI_Unlock(BBA_EXI_CHAN);
return -2;
}
return 0;
}
static void end(void)
{
EXI_Deselect(BBA_EXI_CHAN);
EXI_Unlock(BBA_EXI_CHAN);
}
static int put_hdr(u16 addr, int write)
{
u32 h = ((u32)(write ? 0xC0 : 0x80) << 24) | ((u32)addr << 8);
if (EXI_Imm(BBA_EXI_CHAN, &h, 4, EXI_WRITE, NULL) <= 0) return -3;
if (EXI_Sync(BBA_EXI_CHAN) <= 0) return -4;
return 0;
}
int bba_read(u16 addr, void *buf, u32 len)
{
int rc;
if (len < 1) return -10;
if ((rc = begin()) < 0) return rc;
if ((rc = put_hdr(addr, 0)) < 0) { end(); return rc; }
gap();
memset(buf, 0, len);
if (EXI_ImmEx(BBA_EXI_CHAN, buf, len, EXI_READ) <= 0) { end(); return -5; }
end();
return 0;
}
int bba_write(u16 addr, const void *buf, u32 len)
{
int rc;
u8 tmp[64];
if (len < 1 || len > sizeof(tmp)) return -10;
memcpy(tmp, buf, len);
if ((rc = begin()) < 0) return rc;
if ((rc = put_hdr(addr, 1)) < 0) { end(); return rc; }
if (EXI_ImmEx(BBA_EXI_CHAN, tmp, len, EXI_WRITE) <= 0) { end(); return -6; }
end();
return 0;
}
int bba_devid(u8 id[4])
{
int rc;
u16 h = 0x0000; /* command space, reg 0, read */
if ((rc = begin()) < 0) return rc;
if (EXI_Imm(BBA_EXI_CHAN, &h, 2, EXI_WRITE, NULL) <= 0) { end(); return -3; }
if (EXI_Sync(BBA_EXI_CHAN) <= 0) { end(); return -4; }
gap();
memset(id, 0, 4);
if (EXI_ImmEx(BBA_EXI_CHAN, id, 4, EXI_READ) <= 0) { end(); return -5; }
end();
return 0;
}
int bba_r8(u16 addr)
{
u8 v;
int rc = bba_read(addr, &v, 1);
return rc < 0 ? rc : (int)v;
}
int bba_w8(u16 addr, u8 val)
{
return bba_write(addr, &val, 1);
}
int bba_stream(u16 addr, void *buf, u32 len)
{
/* Identical to bba_read now: the real protocol always streams until CS,
* so there is no separate "DMA" header form. */
return bba_read(addr, buf, len);
}
int bba_tx_bytes(const u8 *buf, u32 len)
{
u32 off = 0;
/* Stream in 64-byte chunks, ONE EXI transaction each.
*
* Writing byte by byte with bba_w8() costs a full EXI_Lock / EXI_Select /
* header / EXI_Deselect / EXI_Unlock PER BYTE -- about sixty transactions
* for one 60-byte ACK. That latency is why ~1/3 of netboot blocks were
* retried: the ACK always reached the W5100 (txto=0, TI always set) but
* missed the host's timeout.
*
* ⚠️ BUT THE CHUNK SIZE IS BOUNDED BY tx_bytes, WHICH IS 16 DEEP.
* BBARegisterFile silently drops bytes when tx_bytes.w_rdy is low,
* because SPI cannot back-pressure. Within one transaction the bytes
* arrive back-to-back at EXI speed -- far faster than TXFrameDrain can
* start consuming -- so a 64-byte chunk overran the FIFO, the frame went
* out truncated, and the host saw no valid ACK at all ("no ACK after 20
* tries"). Byte-by-byte worked only because the per-byte EXI overhead
* happened to give the drain time.
*
* tx_bytes is now 2048 deep -- a whole maximum-size frame -- so the old
* 8-byte cap is gone and 64 (bba_write()'s buffer limit) sends a 60-byte
* ACK in a SINGLE transaction. The depth is what makes this safe: SPI
* cannot back-pressure, so the FIFO must absorb the entire burst. */
while (off < len) {
u32 n = len - off;
int rc;
if (n > 64) n = 64;
rc = bba_write(REG_TXDATA, buf + off, n);
if (rc < 0) return rc;
off += n;
}
return 0;
}