Files
rebbarb/gc_bba_test/source/netboot.c
T
Roflin 8493c0ce9d netboot: sync RRP to RWP at init instead of trusting what we inherited
Pressing Z returned "net-update failed (-2)" -- ring_next() rejecting an RRP
outside the ring (pages 1-15) before a single frame was read.

The pointer was never ours to assume.  Swiss initialises the BBA before
launching us and writes RRP itself, and an NCRA reset does NOT restore the
register file's copy of either pointer: their init values only apply at FPGA
configuration.  So RRP can legitimately be anything on entry, including 0,
and the previous code treated that as a fatal error instead of a state to
establish.

netboot_receive() now reads RWP and writes RRP = RWP after the reset, which
declares the ring empty and consistent whatever it was, so the first frame
read is genuinely the host's first block.  RWP itself is still checked,
because that one IS the gateware's own pointer and an out-of-range value there
would be a real fault rather than inherited state.

The ring-error message now reports RRP and RWP and spells out which of the
three failures it was (EXI access, RRP out of range, implausible descriptor).
Each test costs an SD card copy by hand, so a bare error code is not worth the
round trip.

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

553 lines
18 KiB
C

#include "netboot.h"
#include "bba.h"
#include <stdio.h>
#include <string.h>
#include <malloc.h>
#include <unistd.h>
#include <ogc/lwp_watchdog.h>
#include <ogc/cache.h>
#include <fat.h>
#include <sdcard/gcsd.h>
/* ── frame layout ────────────────────────────────────────────────────────
* 0 dst MAC (6)
* 6 src MAC (6)
* 12 ethertype (2) = NB_ETHERTYPE
* 14 magic (4) = "GCUP"
* 18 opcode (1)
* 19 reserved (1)
* 20 seq (2, big endian)
* 22 nblocks (2, big endian)
* 24 len (2, big endian)
* 26 data (len)
*/
#define NB_HDR 26u
#define NB_MIN 60u /* ethernet minimum, padded */
static u8 s_our_mac[6];
static u8 s_peer_mac[6];
static u16 rd16(const u8 *p) { return ((u16)p[0] << 8) | p[1]; }
static void wr16(u8 *p, u16 v) { p[0] = (u8)(v >> 8); p[1] = (u8)v; }
/* ── RX: pull one frame out of the SPRAM ring ────────────────────────────
* The ring is pages 1-15 with a 4-byte descriptor at each page start whose
* length field INCLUDES the descriptor. RRP is ours to advance; the
* gateware does not act on it. */
static int ring_next(u8 *buf, u32 cap)
{
u8 rp[2], wp[2], hdr[4];
u32 rpage, wpage, dlen, flen, addr;
if (bba_read(REG_RRP_LO, rp, 2) < 0) return -1;
if (bba_read(REG_RWP_LO, wp, 2) < 0) return -1;
rpage = ((u32)rp[1] << 8) | rp[0];
wpage = ((u32)wp[1] << 8) | wp[0];
if (rpage == wpage) return 0; /* empty */
if (rpage < 1 || rpage > 15) return -2; /* pointer corrupt */
addr = rpage << 8;
if (bba_stream((u16)addr, hdr, 4) < 0) return -1;
dlen = ((u32)hdr[2] << 8) | hdr[3];
if (dlen <= 4 || dlen > 1518 + 4) return -3; /* descriptor implausible */
flen = dlen - 4;
if (flen > cap) flen = cap;
/* Descriptor + frame are contiguous from the page start. Read both and
* drop the descriptor rather than streaming from a mid-page address, so
* one DMA read covers it. */
{
static u8 tmp[1536 + 4];
u32 want = dlen;
if (want > sizeof(tmp)) want = sizeof(tmp);
if (bba_stream((u16)addr, tmp, want) < 0) return -1;
memcpy(buf, tmp + 4, flen);
}
/* Advance RRP by the pages this frame occupied, wrapping 1..15. */
{
u32 pages = (dlen + 255) >> 8;
u32 next = ((rpage - 1 + pages) % 15) + 1;
if (bba_w8(REG_RRP_LO, (u8)next) < 0) return -1;
}
return (int)flen;
}
static int send_ack(u16 seq)
{
u8 f[NB_MIN];
memset(f, 0, sizeof(f));
memcpy(f + 0, s_peer_mac, 6);
memcpy(f + 6, s_our_mac, 6);
wr16(f + 12, NB_ETHERTYPE);
f[14] = 'G'; f[15] = 'C'; f[16] = 'U'; f[17] = 'P';
f[18] = NB_OP_ACK;
wr16(f + 20, seq);
wr16(f + 22, 0);
wr16(f + 24, 0);
return bba_tx_bytes(f, sizeof(f));
}
/* Destination path carried in the DONE frame, so the host decides where the
* build lands rather than it being baked in here. */
static char s_dest[64] = "/gc_bba_test.dol";
const char *netboot_dest_path(void) { return s_dest; }
int netboot_receive(void *dst, u32 maxlen, void (*status)(const char *msg))
{
static u8 frame[1536];
u8 *out = (u8 *)dst;
u32 got = 0, nblocks = 0;
u16 expect = 0;
char line[80];
u64 t0;
int have_peer = 0;
/* Bring the device up deterministically rather than hoping someone else
* already did. Pressing Z straight after launch is the normal case, and
* then NOTHING has issued the NCRA reset that triggers the W5100's own
* init -- MACRAW is not running, no frames are stored, and this would sit
* in "timeout: no frames" for a reason that has nothing to do with the
* transfer. Swiss may have initialised it, or an earlier test may have,
* or neither; a reset here makes all three cases identical.
*
* The reset also clears the SPRAM ring pointers, which is what we want:
* a clean ring with RRP == RWP == page 1, so the first frame we see is
* genuinely the host's first block. */
if (status) status("resetting the adapter...");
if (bba_w8(REG_NCRA, NCRA_RESET) < 0) return -1;
{
int i, v = -1;
for (i = 0; i < 50; i++) {
v = bba_r8(REG_NCRA);
if (v >= 0 && !(v & NCRA_RESET)) break;
usleep(5000);
}
if (i == 50) {
if (status) status("NCRA reset never self-cleared");
return -1;
}
/* The W5100 re-inits behind that: RSTn pulse plus a 60.3 ms settle,
* done twice. Polling it sooner reads all-zeros and looks dead. */
usleep(400000);
}
/* Start receive. Without the SR bit the assembler never writes the
* ring, so frames arrive at the W5100 and go nowhere. */
if (bba_w8(REG_NCRA, NCRA_SR) < 0) return -1;
/* Establish a KNOWN ring state instead of trusting what we inherited.
* Swiss initialises the BBA before launching us and writes RRP itself,
* and an NCRA reset does not restore the register file's copy of either
* pointer -- their init values only apply at FPGA configuration. So
* RRP can legitimately be anything, including 0, which is outside the
* ring (pages 1-15) and used to abort the whole transfer.
*
* Setting RRP = RWP declares the ring empty and consistent, whatever it
* was, so the first frame we read is genuinely the host's first block. */
{
u8 wp[2];
if (bba_read(REG_RWP_LO, wp, 2) < 0) return -1;
if (wp[0] < 1 || wp[0] > 15) {
/* RWP is the gateware's own pointer, so this should not
* happen -- say so loudly rather than papering over it. */
if (status) { snprintf(line, sizeof(line),
"RWP=%u outside ring pages 1-15", wp[0]);
status(line); }
return -5;
}
if (bba_w8(REG_RRP_LO, wp[0]) < 0) return -1;
if (status) { snprintf(line, sizeof(line),
"ring synced at page %u", wp[0]); status(line); }
}
/* PAR0-5 survives the reset (it is the gateware's power-on MAC), and it
* is read AFTER the reset so the source address we put in ACKs is the
* one the adapter is actually filtering on. */
if (bba_read(REG_PAR0, s_our_mac, 6) < 0) return -1;
if (status) status("waiting for host (send-dol.py)...");
t0 = gettime();
for (;;) {
int n = ring_next(frame, sizeof(frame));
if (n < 0) {
/* Report the pointers, not just the code: -1 is an EXI access
* failure, -2 an RRP outside the ring, -3 a descriptor whose
* length is implausible. Which one it is, and what the
* pointers read, is the whole diagnosis. */
u8 rp[2] = {0xFF, 0xFF}, wp[2] = {0xFF, 0xFF};
bba_read(REG_RRP_LO, rp, 2);
bba_read(REG_RWP_LO, wp, 2);
if (status) {
snprintf(line, sizeof(line),
"ring error %d (RRP=%u RWP=%u) %s", n, rp[0], wp[0],
n == -1 ? "EXI access failed" :
n == -2 ? "RRP outside pages 1-15" :
"descriptor length implausible");
status(line);
}
return -2;
}
if (n == 0) {
/* 30 s with nothing at all, or 5 s mid-transfer, is a bail-out
* rather than a hang -- a wedged update must not require a
* console power cycle. */
u32 idle = (u32)ticks_to_millisecs(diff_ticks(t0, gettime()));
if (idle > (got ? 5000u : 30000u)) {
if (status) status(got ? "timeout mid-transfer" : "timeout: no frames");
return -3;
}
continue;
}
if ((u32)n < NB_HDR) continue;
if (rd16(frame + 12) != NB_ETHERTYPE) continue;
if (frame[14] != 'G' || frame[15] != 'C' ||
frame[16] != 'U' || frame[17] != 'P') continue;
if (!have_peer) { memcpy(s_peer_mac, frame + 6, 6); have_peer = 1; }
t0 = gettime();
if (frame[18] == NB_OP_DONE) {
/* DONE may carry the destination path as its payload. */
u16 plen = rd16(frame + 24);
if (plen && plen < sizeof(s_dest) && NB_HDR + plen <= (u32)n) {
memcpy(s_dest, frame + NB_HDR, plen);
s_dest[plen] = 0;
}
if (status) { snprintf(line, sizeof(line),
"received %u bytes in %u blocks",
(unsigned)got, (unsigned)nblocks);
status(line); }
return (int)got;
}
if (frame[18] != NB_OP_DATA) continue;
{
u16 seq = rd16(frame + 20);
u16 len = rd16(frame + 24);
nblocks = rd16(frame + 22);
if (len > NB_BLOCK_MAX || NB_HDR + len > (u32)n) continue;
if (seq == expect) {
if (got + len > maxlen) {
if (status) status("image larger than buffer");
return -4;
}
memcpy(out + got, frame + NB_HDR, len);
got += len;
expect++;
if ((expect & 0x1F) == 0 && status) {
snprintf(line, sizeof(line), "block %u/%u (%u KB)",
(unsigned)expect, (unsigned)nblocks,
(unsigned)(got >> 10));
status(line);
}
}
/* ACK even a duplicate: it means our previous ACK was lost, and
* re-ACKing is what lets the host move on. */
send_ack(seq);
}
}
}
/* ── DOL loader ──────────────────────────────────────────────────────────
* 7 text + 11 data sections, each with a file offset, load address and size,
* then BSS and the entry point. All big endian. */
typedef struct {
u32 text_off[7];
u32 data_off[11];
u32 text_addr[7];
u32 data_addr[11];
u32 text_len[7];
u32 data_len[11];
u32 bss_addr;
u32 bss_len;
u32 entry;
u32 pad[7];
} dolhdr_t;
/* Reserved region at the top of MEM1:
*
* [ staged image .............. ][ stub ][ descriptor ][ new stack ]
* ^ s_stage ^ s_stub
*
* Nothing here may overlap any DOL section, and netboot_launch() checks that
* before it jumps rather than trusting it. */
#define NB_STAGE_MAX (6u << 20) /* generous: Swiss is ~2-3 MB */
#define NB_TAIL (64u << 10) /* stub + descriptor + stack */
extern void nb_stub(void *desc);
extern void nb_stub_end(void);
static u8 *s_stage;
static u8 *s_tail;
void *netboot_reserve(u32 *out_max)
{
u32 hi = (u32)SYS_GetArena1Hi();
u32 base = (hi - (NB_STAGE_MAX + NB_TAIL)) & ~31u;
if (base <= (u32)SYS_GetArena1Lo()) return NULL;
s_stage = (u8 *)base;
s_tail = (u8 *)(base + NB_STAGE_MAX);
/* Tell libogc this is not its memory any more. */
SYS_SetArena1Hi(s_stage);
if (out_max) *out_max = NB_STAGE_MAX;
return s_stage;
}
static int overlaps(u32 a, u32 alen, u32 b, u32 blen)
{
if (!alen || !blen) return 0;
return a < b + blen && b < a + alen;
}
int netboot_launch(const void *dol)
{
const dolhdr_t *h = (const dolhdr_t *)dol;
const u8 *base = (const u8 *)dol;
u32 reserved = (u32)s_stage;
u32 reserved_len = NB_STAGE_MAX + NB_TAIL;
u32 stub_len = (u32)((const u8 *)nb_stub_end - (const u8 *)nb_stub);
u32 *desc;
void (*stub)(void *);
int i, nsec = 0;
if (!s_stage) return -1;
/* Refuse a mis-parsed or truncated header rather than jumping into it:
* a bad jump hangs the console hard enough to need a power cycle. */
if ((h->entry >> 24) != 0x80 || h->text_len[0] == 0 ||
(h->text_addr[0] >> 24) != 0x80)
return -2;
/* Build the copy table in the reserved tail, where the image cannot
* reach it. While doing so, verify no section lands on the reserved
* region -- that would clobber the staged image, the stub we are about
* to run from, or this very table. */
desc = (u32 *)(s_tail + 0x1000);
for (i = 0; i < 7; i++) {
if (!h->text_len[i]) continue;
if (overlaps(h->text_addr[i], h->text_len[i], reserved, reserved_len))
return -3;
desc[5 + nsec * 3 + 0] = (u32)(base + h->text_off[i]);
desc[5 + nsec * 3 + 1] = h->text_addr[i];
desc[5 + nsec * 3 + 2] = h->text_len[i];
nsec++;
}
for (i = 0; i < 11; i++) {
if (!h->data_len[i]) continue;
if (overlaps(h->data_addr[i], h->data_len[i], reserved, reserved_len))
return -3;
desc[5 + nsec * 3 + 0] = (u32)(base + h->data_off[i]);
desc[5 + nsec * 3 + 1] = h->data_addr[i];
desc[5 + nsec * 3 + 2] = h->data_len[i];
nsec++;
}
if (h->bss_len &&
overlaps(h->bss_addr, h->bss_len, reserved, reserved_len))
return -3;
desc[0] = h->entry;
/* Hand the new image a stack inside the reserved tail, 64-byte aligned
* and clear of the descriptor. */
desc[1] = ((reserved + reserved_len) - 64) & ~63u;
desc[2] = (u32)nsec;
desc[3] = h->bss_addr;
desc[4] = h->bss_len;
/* Relocate the copier out of harm's way and make it executable there. */
stub = (void (*)(void *))s_tail;
memcpy(s_tail, (const void *)nb_stub, stub_len);
DCFlushRange(s_tail, NB_TAIL);
ICInvalidateRange(s_tail, NB_TAIL);
/* No interrupts from here on: we are about to overwrite the memory this
* program is running from, and the scheduler must not preempt us into
* code that no longer exists. */
IRQ_Disable();
stub(desc);
return 0; /* not reached */
}
/* ── SD card: save the new build, then boot a known-good image ───────────
* The update deliberately does NOT launch what it just received. It writes
* the image to the card and then boots /boot.dol (Swiss), so a broken test
* build is recoverable: Swiss comes up regardless and can load the previous
* one. The only image we ever jump into is one that was already known good. */
static int s_sd_mounted;
static int sd_mount(void (*status)(const char *msg))
{
/* Try every interface a GameCube might have. Auto-detecting beats
* asking: SD Gecko can be in either memory-card slot, and SD2SP2 is on
* its own EXI channel. */
static const struct { const char *name; const DISC_INTERFACE *io; } ifs[] = {
{ "SD2SP2", &__io_gcsd2 },
{ "SD Gecko slot B", &__io_gcsdb },
{ "SD Gecko slot A", &__io_gcsda },
};
unsigned i;
char line[64];
if (s_sd_mounted) return 0;
for (i = 0; i < sizeof(ifs) / sizeof(ifs[0]); i++) {
if (fatMountSimple("sd", ifs[i].io)) {
if (status) {
snprintf(line, sizeof(line), "SD mounted (%s)", ifs[i].name);
status(line);
}
s_sd_mounted = 1;
return 0;
}
}
if (status) status("no SD card found (SD2SP2 or SD Gecko)");
return -1;
}
/* Build "<base>.tmp" and "<base>.prev.dol" from "<base>.dol".
*
* The temp file deliberately does NOT end in .dol: Swiss lists .dol files, and
* a half-written image that Swiss offers to boot is exactly what we are trying
* to prevent. The backup DOES end in .dol, because booting it from Swiss is
* the whole point of keeping it. */
static void derive_names(const char *path, char *tmp, size_t ntmp,
char *prev, size_t nprev)
{
char base[80];
size_t n = strlen(path);
if (n >= sizeof(base)) n = sizeof(base) - 1;
memcpy(base, path, n);
base[n] = 0;
if (n > 4 && strcasecmp(base + n - 4, ".dol") == 0)
base[n - 4] = 0;
snprintf(tmp, ntmp, "sd:%s.tmp", base);
snprintf(prev, nprev, "sd:%s.prev.dol", base);
}
int netboot_sd_write(const char *path, const void *buf, u32 len,
void (*status)(const char *msg))
{
char final[96], tmp[112], prev[112], line[160];
FILE *f;
u32 done;
int rc = 0;
if (sd_mount(status) < 0) return -1;
snprintf(final, sizeof(final), "sd:%s", path);
derive_names(path, tmp, sizeof(tmp), prev, sizeof(prev));
/* Write to a TEMP file and verify it BEFORE touching the existing
* build. Ordering matters: if the transfer, the write or the verify
* fails, the copy you are running from is still on the card untouched,
* so you can always try the update again. Only once the new image is
* known good do we rotate. */
if (status) { snprintf(line, sizeof(line), "writing %s", tmp); status(line); }
f = fopen(tmp, "wb");
if (!f) { if (status) status("open for write failed"); return -2; }
done = (u32)fwrite(buf, 1, len, f);
fclose(f);
if (done != len) {
remove(tmp);
if (status) status("short write - existing build left alone");
return -3;
}
/* Read it back. A half-written .dol that still parses is the failure
* this design exists to avoid, and the card is the one component here
* with no other integrity check. */
if (status) status("verifying...");
f = fopen(tmp, "rb");
if (!f) { remove(tmp); if (status) status("reopen for verify failed"); return -4; }
{
static u8 chunk[4096];
const u8 *src = (const u8 *)buf;
u32 off = 0;
while (off < len) {
u32 want = len - off;
if (want > sizeof(chunk)) want = sizeof(chunk);
if (fread(chunk, 1, want, f) != want) { rc = -5; break; }
if (memcmp(chunk, src + off, want) != 0) { rc = -6; break; }
off += want;
}
}
fclose(f);
if (rc < 0) {
remove(tmp);
if (status) status("VERIFY FAILED - existing build left alone");
return rc;
}
/* Rotate: keep exactly one generation back. remove()/rename() of a
* file that is not there is fine and ignored -- that is just the first
* update on a fresh card. */
remove(prev);
rename(final, prev);
if (rename(tmp, final) != 0) {
/* Should not happen, but if it does the old build is sitting in
* .prev.dol and is still bootable from Swiss -- say so rather
* than leaving the user to guess. */
if (status) {
snprintf(line, sizeof(line),
"rename failed; previous build is at %s", prev);
status(line);
}
return -7;
}
if (status) {
snprintf(line, sizeof(line), "wrote %u bytes; previous build kept as %s",
(unsigned)len, prev);
status(line);
}
return 0;
}
int netboot_boot_file(const char *path, void *dst, u32 maxlen,
void (*status)(const char *msg))
{
char full[96], line[128];
FILE *f;
long sz;
u32 got;
if (sd_mount(status) < 0) return -1;
snprintf(full, sizeof(full), "sd:%s", path);
f = fopen(full, "rb");
if (!f) {
if (status) { snprintf(line, sizeof(line), "%s not found", full);
status(line); }
return -2;
}
fseek(f, 0, SEEK_END);
sz = ftell(f);
fseek(f, 0, SEEK_SET);
if (sz <= 0 || (u32)sz > maxlen) {
fclose(f);
if (status) status("boot image size implausible");
return -3;
}
got = (u32)fread(dst, 1, (size_t)sz, f);
fclose(f);
if (got != (u32)sz) {
if (status) status("short read of boot image");
return -4;
}
if (status) { snprintf(line, sizeof(line), "booting %s (%u bytes)",
full, (unsigned)got); status(line); }
/* Unmount before we disable interrupts and jump: leaving the card
* mounted hands the next image a filesystem that libfat still thinks it
* owns, on an EXI channel it is about to reinitialise. */
fatUnmount("sd");
s_sd_mounted = 0;
return netboot_launch(dst);
}