Files
rebbarb/gc_bba_test/source/netboot.c
T
Roflin 7cd0071e7c netboot: reset and enable the adapter before waiting for frames
Pressing Z straight after launch is the normal case, and in that case NOTHING
has issued the NCRA reset that triggers the W5100's own init.  MACRAW would
not be running, no frames would be stored, and the transfer would sit in
"timeout: no frames" for a reason with nothing to do with netboot -- the most
confusing possible first-run failure.

Swiss may have initialised the adapter, or an earlier test may have, or
neither.  An NCRA reset here makes all three cases identical: poll until the
RESET bit self-clears, wait out the W5100's 60.3 ms settle (twice over, hence
400 ms), then set the SR bit so RXFrameAssembler actually writes the ring.

Resetting also clears the SPRAM ring pointers, which is wanted rather than
incidental: a clean ring with RRP == RWP == page 1 means the first frame we
see is genuinely the host's first block, not a leftover from a test.

PAR0-5 is now read AFTER the reset, so the source address in our ACKs is the
one the adapter is actually filtering on.

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

515 lines
16 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;
/* 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) {
if (status) { snprintf(line, sizeof(line), "ring error %d", n);
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);
}