Rework the EXI decoder to the real GC<->BBA protocol

Replaces the invented 2-byte header (addr split across both bytes, 2-bit
length field) with the protocol libogc and Swiss actually speak:

  command  (bit7=0, 6-bit reg)   [reg, 00, 00, data]  one 4-byte transfer
  register (bit7=1, 16-bit reg)  [0x80|w<<6, ah, al, 00] + data until CS

This was not an optimisation: the old decoder read Swiss's NWAYS read
(80 00 31 00) as 'write 0x31 to NCRA', and bit 0 of 0x31 is RESET, so every
register read would have reset the device.

Changes:
  * 16-bit address; no length field -- register reads and writes now stream
    until CS, like the SPRAM path already did
  * RHDR_LO starts the SPRAM fetch as soon as the address completes, using
    the trailing header byte as the prefetch window the protocol provides
  * command space serves EXI regs 0x02/0x03 (interrupt mask/status), the only
    two Swiss uses
  * EXI_GetID gets its own path: it offers just ONE header byte of warning, so
    the first ID byte is pushed the instant byte 0 identifies the query

Two timing subtleties, both found by simulation and commented in place:
  * MISO is driven LIVE from the tx FIFO head, so a byte pushed while the pad
    byte is on the wire appears in the pad slot.  A filler byte is parked
    there (MISO is don't-care through the header) so the first real response
    lands in data slot 0.
  * the prefetch must be one-shot; re-firing while waiting for the pad byte
    consumed the register stream early and shifted reads by a byte.

All testbenches updated to the real format and passing: bba_register_file
(7), bba_top (8 incl. the full W5100->SPRAM->GC loop and TX), exi_j4,
exi_capture, spi_mode3_slave, tx_frame_drain, spram_arbiter,
rx_frame_assembler, w5100_parallel_master.  gc_bba_test rebuilt against the
new protocol.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 11:58:52 +02:00
parent f2762f613d
commit 2458884160
6 changed files with 319 additions and 131 deletions
+32 -35
View File
@@ -16,18 +16,13 @@ static void gap(void)
while (ticks_to_microsecs(diff_ticks(t0, gettime())) < bba_gap_us) ;
}
/* Build the 2-byte EXI transaction header.
* byte0: [7]=write [6:0]=addr[12:6]
* byte1: [7:2]=addr[5:0] [1:0]=len-1
* Returned packed big-endian in a u16 so EXI_Imm(...,2,...) clocks byte0 first
* (the GameCube is big-endian, so the high byte goes out first). */
static u16 hdr(u16 addr, u32 len, int write)
{
u8 b0 = (write ? 0x80 : 0x00) | ((addr >> 6) & 0x7F);
u8 b1 = (u8)(((addr & 0x3F) << 2) | ((len - 1) & 0x03));
return ((u16)b0 << 8) | b1;
}
/* 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. */
static int begin(void)
{
int tries;
@@ -53,10 +48,10 @@ static void end(void)
EXI_Unlock(BBA_EXI_CHAN);
}
static int put_hdr(u16 addr, u32 len, int write)
static int put_hdr(u16 addr, int write)
{
u16 h = hdr(addr, len, write);
if (EXI_Imm(BBA_EXI_CHAN, &h, 2, EXI_WRITE, NULL) <= 0) return -3;
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;
}
@@ -64,12 +59,9 @@ static int put_hdr(u16 addr, u32 len, int write)
int bba_read(u16 addr, void *buf, u32 len)
{
int rc;
if (len < 1 || len > 4) return -10;
if (len < 1) return -10;
if ((rc = begin()) < 0) return rc;
if ((rc = put_hdr(addr, len, 0)) < 0) { end(); return rc; }
/* Hold the clock idle while the device decodes the header and prefetches
* the response into its TX FIFO. libogc's inter-call overhead alone is
* NOT enough at full EXI speed -- see bba_gap_us. */
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; }
@@ -80,16 +72,30 @@ int bba_read(u16 addr, void *buf, u32 len)
int bba_write(u16 addr, const void *buf, u32 len)
{
int rc;
u8 tmp[4];
if (len < 1 || len > 4) return -10;
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, len, 1)) < 0) { end(); 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;
@@ -104,18 +110,9 @@ int bba_w8(u16 addr, u8 val)
int bba_stream(u16 addr, void *buf, u32 len)
{
int rc;
if (len < 1) return -10;
if ((rc = begin()) < 0) return rc;
/* Length field is capped at 2 bits; the device ignores it for addresses
* >= 0x0100 and streams until CS rises, so ask for 4 and clock as many
* bytes as we actually want. */
if ((rc = put_hdr(addr, 4, 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;
/* 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)
+9 -5
View File
@@ -59,15 +59,19 @@ extern u32 bba_gap_us;
/* All return 0 on success, negative on an EXI-layer failure. */
/* Immediate read of `len` (1..4) bytes from `addr`.
* Clocks EXACTLY `len` bytes: over-clocking a register read leaves stale
* prefetched bytes queued in the gateware and desynchronises the NEXT
* transaction (hardware-confirmed 2026-09-30). Never pad this. */
/* REGISTER-space read: 4-byte header [0x80, ah, al, 0x00] then `len` bytes.
* The real protocol has NO length field -- the data phase runs until CS, so
* `len` is unbounded. The trailing 0x00 header byte is the device's prefetch
* window. See docs/exi_protocol_real.md. */
int bba_read(u16 addr, void *buf, u32 len);
/* Immediate write of `len` (1..4) bytes to `addr`. */
/* REGISTER-space write: 4-byte header [0xC0, ah, al, 0x00] then `len` bytes. */
int bba_write(u16 addr, const void *buf, u32 len);
/* EXI_GetID: write 0x0000 (two bytes) then read 4. Returns the device ID,
* which is a ONE-SHOT -- the first NCRA reset clears it until power cycle. */
int bba_devid(u8 id[4]);
/* 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);
+1 -1
View File
@@ -19,7 +19,7 @@ static int t_devid(char *m, size_t n)
snprintf(m, n, "skipped: one-shot, needs a console power cycle");
return 2;
}
if ((rc = bba_read(0x0000, b, 4)) < 0) { snprintf(m, n, "EXI error %d", rc); return 1; }
if ((rc = bba_devid(b)) < 0) { snprintf(m, n, "EXI error %d", rc); return 1; }
snprintf(m, n, "%02X %02X %02X %02X (want 04 02 02 00)", b[0], b[1], b[2], b[3]);
return (b[0]==0x04 && b[1]==0x02 && b[2]==0x02 && b[3]==0x00) ? 0 : 1;
}