#include "netboot.h" #include "bba.h" #include #include #include #include #include #include #include #include /* ── 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 ".tmp" and ".prev.dol" from ".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); }