gc_bba_test: add header->data idle gap; device replies 22 bit-times late

First on-console run: the Identity suite failed with the device ID reading
00 00 00 10 instead of 04 02 02 00.  Bit analysis shows that is an EXACT
match for the correct stream delayed by 22 bits -- the device answers
correctly, just too late for a 32-clock window to catch more than its first
set bit.  Not corruption, and not a broken link: response latency.

At EXI index 5 (~27 MHz) 22 bit-times is ~815 ns, which is about what the CDC
chain costs (rx AsyncFIFO capture->exi, register-file decode, tx AsyncFIFO
exi->capture, SPI engine) at a 24 MHz exi clock.  The design expects the GC to
hold the clock idle between the header write and the data read so the register
file can prefetch the response; libogc's inter-call overhead alone is not
enough at full speed.

Adds bba_gap_us (default 16 us, busy-waited with CS asserted since we hold the
EXI lock) and a Gap sweep test that reports the minimum clean gap at the
current speed -- which measures the latency rather than guessing at it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 11:37:00 +02:00
parent b363e76ec4
commit abc61f3e66
3 changed files with 52 additions and 4 deletions
+17 -3
View File
@@ -1,8 +1,20 @@
#include "bba.h" #include "bba.h"
#include <string.h> #include <string.h>
#include <unistd.h> #include <unistd.h>
#include <ogc/lwp_watchdog.h>
u32 bba_speed = EXI_SPEED32MHZ; /* nominal name; ~27 MHz in reality */ u32 bba_speed = EXI_SPEED32MHZ; /* nominal name; ~27 MHz in reality */
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) ;
}
/* Build the 2-byte EXI transaction header. /* Build the 2-byte EXI transaction header.
* byte0: [7]=write [6:0]=addr[12:6] * byte0: [7]=write [6:0]=addr[12:6]
@@ -55,9 +67,10 @@ int bba_read(u16 addr, void *buf, u32 len)
if (len < 1 || len > 4) return -10; if (len < 1 || len > 4) return -10;
if ((rc = begin()) < 0) return rc; if ((rc = begin()) < 0) return rc;
if ((rc = put_hdr(addr, len, 0)) < 0) { end(); return rc; } if ((rc = put_hdr(addr, len, 0)) < 0) { end(); return rc; }
/* The real GC driver pauses the clock here while the device decodes the /* Hold the clock idle while the device decodes the header and prefetches
* header and prefetches the response into its TX FIFO. Separate EXI_Imm * the response into its TX FIFO. libogc's inter-call overhead alone is
* calls give us that gap for free. */ * NOT enough at full EXI speed -- see bba_gap_us. */
gap();
memset(buf, 0, len); memset(buf, 0, len);
if (EXI_ImmEx(BBA_EXI_CHAN, buf, len, EXI_READ) <= 0) { end(); return -5; } if (EXI_ImmEx(BBA_EXI_CHAN, buf, len, EXI_READ) <= 0) { end(); return -5; }
end(); end();
@@ -98,6 +111,7 @@ int bba_stream(u16 addr, void *buf, u32 len)
* >= 0x0100 and streams until CS rises, so ask for 4 and clock as many * >= 0x0100 and streams until CS rises, so ask for 4 and clock as many
* bytes as we actually want. */ * bytes as we actually want. */
if ((rc = put_hdr(addr, 4, 0)) < 0) { end(); return rc; } if ((rc = put_hdr(addr, 4, 0)) < 0) { end(); return rc; }
gap();
memset(buf, 0, len); memset(buf, 0, len);
if (EXI_ImmEx(BBA_EXI_CHAN, buf, len, EXI_READ) <= 0) { end(); return -5; } if (EXI_ImmEx(BBA_EXI_CHAN, buf, len, EXI_READ) <= 0) { end(); return -5; }
end(); end();
+10
View File
@@ -47,6 +47,16 @@
* oversamples it 2x at 54 MHz. Settable so the suite can sweep it. */ * oversamples it 2x at 54 MHz. Settable so the suite can sweep it. */
extern u32 bba_speed; extern u32 bba_speed;
/* Microseconds to hold the clock idle between the header write and the data
* phase, with CS still asserted. The register file decodes the header and
* PROACTIVELY pushes the response into its TX FIFO during this gap; until the
* first byte reaches the FIFO head the device drives MISO low. Measured on
* hardware 2026-10-03: at EXI index 5 the reply arrived 22 bit-times (~815 ns)
* late with no explicit gap, so a 4-byte read returned 00 00 00 10 -- the
* correct stream, just shifted out of the window. 0 = whatever gap libogc's
* inter-call overhead happens to give. */
extern u32 bba_gap_us;
/* All return 0 on success, negative on an EXI-layer failure. */ /* All return 0 on success, negative on an EXI-layer failure. */
/* Immediate read of `len` (1..4) bytes from `addr`. /* Immediate read of `len` (1..4) bytes from `addr`.
+25 -1
View File
@@ -156,6 +156,29 @@ static int t_speed_sweep(char *m, size_t n)
return best >= EXI_SPEED32MHZ ? 0 : 1; return best >= EXI_SPEED32MHZ ? 0 : 1;
} }
static int t_gap_sweep(char *m, size_t n)
{
static const u32 gaps[] = {0, 1, 2, 4, 8, 16, 32};
u32 saved = bba_gap_us;
int i, g, best = -1;
for (g = 0; g < (int)(sizeof(gaps)/sizeof(gaps[0])); g++) {
int bad = 0;
bba_gap_us = gaps[g];
for (i = 0; i < 30; i++)
if (bba_r8(REG_NWAYS) != NWAYS_LINKUP) bad++;
if (!bad) { best = (int)gaps[g]; break; }
}
bba_gap_us = saved;
if (best < 0) {
snprintf(m, n, "no gap up to 32us gave clean reads at speed idx %u",
(unsigned)bba_speed);
return 1;
}
snprintf(m, n, "min clean header->data gap = %d us at speed idx %u",
best, (unsigned)bba_speed);
return 0;
}
/* ───────────────────────── suite 5: ethernet ───────────────────────── */ /* ───────────────────────── suite 5: ethernet ───────────────────────── */
static u8 g_mac[6] = {0x00,0x09,0xBF,0x00,0x00,0x01}; static u8 g_mac[6] = {0x00,0x09,0xBF,0x00,0x00,0x01};
@@ -293,6 +316,7 @@ static test_t s_reset[] = {
static test_t s_exi[] = { static test_t s_exi[] = {
{"200x read", "no bit errors over 200 register reads", t_repeat, T_PENDING, {0}}, {"200x read", "no bit errors over 200 register reads", t_repeat, T_PENDING, {0}},
{"Speed sweep", "which EXI clock indices are reliable (0..5)", t_speed_sweep, T_PENDING, {0}}, {"Speed sweep", "which EXI clock indices are reliable (0..5)", t_speed_sweep, T_PENDING, {0}},
{"Gap sweep", "min header->data idle gap the device needs", t_gap_sweep, T_PENDING, {0}},
}; };
static test_t s_eth[] = { static test_t s_eth[] = {
{"MAC readback", "PAR0-5 hold a Nintendo-OUI MAC", t_mac_read, T_PENDING, {0}}, {"MAC readback", "PAR0-5 hold a Nintendo-OUI MAC", t_mac_read, T_PENDING, {0}},
@@ -308,7 +332,7 @@ suite_t g_suites[] = {
{"Identity", "device enumerates and reports itself", s_identity, 3}, {"Identity", "device enumerates and reports itself", s_identity, 3},
{"Registers", "register file is readable and writable", s_regs, 4}, {"Registers", "register file is readable and writable", s_regs, 4},
{"Reset/control", "NCRA reset and control bits behave", s_reset, 2}, {"Reset/control", "NCRA reset and control bits behave", s_reset, 2},
{"EXI integrity", "the EXI link is error-free, and how fast", s_exi, 2}, {"EXI integrity", "the EXI link is error-free, and how fast", s_exi, 3},
{"Ethernet", "frames get to and from the W5100", s_eth, 3}, {"Ethernet", "frames get to and from the W5100", s_eth, 3},
{"SPRAM/DMA", "bulk reads stream correctly from the ring", s_spram, 2}, {"SPRAM/DMA", "bulk reads stream correctly from the ring", s_spram, 2},
}; };