Files
rebbarb/gc_bba_test/source/netboot.c
T
Roflin ee889eb40f netboot: make the transfer round-trip efficient
Transfer now works on hardware -- the block counter climbs -- but slowly, with
some ACKs missing.  It is ROUND-TRIP bound, not bandwidth bound: stop-and-wait
means one block per round trip, so three things dominated and all three were
conservative bring-up choices rather than considered ones.

  - Block size 256 -> 1024.  A 298 KB image is 1166 blocks at 256 but only 292
    at 1024.  256 was chosen to stay near the only multi-page frame ever proven
    on hardware (a 342-byte DHCP broadcast); large frames are now safe because
    RBFI stops a full ring being overwritten and stop-and-wait keeps exactly
    one frame in flight, so five pages of a fourteen-page budget is never
    contended.  --block 256 remains as a fallback.

  - Host ACK timeout 0.5 s -> 0.1 s.  The round trip is a few milliseconds on
    a LAN, so half a second meant every lost ACK cost half a second.

  - The GC's TI poll slept 500 us up to 20 times, costing up to 10 ms per ACK
    even when the transmit succeeded immediately -- on every block.  It now
    polls without sleeping; a bba_r8 is itself tens of microseconds, so a bare
    loop samples often enough and returns as soon as TI sets.

Absence of TI is explicitly NOT fatal.  TI only means TXFrameDrain handed the
frame to the W5100, so the frame may well have gone out; and if it did not,
the host's retransmit covers it.  Blocking longer would slow the common case
to protect against the rare one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 19:04:52 +02:00

755 lines
26 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];
/* Last descriptor ring_next() looked at, kept for the error path: when a
* length is rejected, the four raw bytes are the whole diagnosis. */
static u8 s_last_desc[4];
/* How often the descriptor was found at each page offset. The assembler is
* supposed to put it at +0 always; hardware shows it sometimes landing at +1,
* varying frame to frame, so count both rather than assume either. */
static u32 s_shift_hits[2];
/* ACKs where TI never set -- the frame did not reach the W5100. */
static u32 s_tx_timeouts;
static u8 s_last_dump[32];
static u32 s_last_page;
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. */
/* Pages the writer is ahead of us, over the 15-page ring. */
static u32 ring_lag(u32 rpage, u32 wpage)
{
return (wpage >= rpage) ? (wpage - rpage) : (wpage + 15u - rpage);
}
/* Our own view of RRP. Re-reading it from the device every poll cost an EXI
* transaction for a value we had just written ourselves -- pure overhead in
* the one loop whose speed decides whether the writer laps us. */
static u32 s_rrp;
static int ring_next(u8 *buf, u32 cap)
{
u8 wp[2], hdr[8];
u32 rpage, wpage, dlen, flen, addr;
u32 shift;
if (bba_read(REG_RWP_LO, wp, 2) < 0) return -1;
rpage = s_rrp;
wpage = ((u32)wp[1] << 8) | wp[0];
if (rpage == wpage) return 0; /* empty */
if (rpage < 1 || rpage > 15) return -2; /* pointer corrupt */
/* THE RING HAS NO OVERFLOW PROTECTION. Nothing in the gateware consumes
* RRP, so RXFrameAssembler keeps writing and laps us: the real BBA has
* RBFI for this and we do not. Reading from a page the writer has
* already reused yields a "descriptor" that is really frame data -- which
* is where 00 00 00 01 came from, not a dropped write.
*
* Our poll loop costs ~5 EXI transactions per frame (~200 us) while a
* 100 Mbit link delivers a 342-byte frame every ~30 us, and the ring is
* only 15 pages, so under continuous traffic we lose that race. Bail out
* once the writer is more than two thirds of the ring ahead: the data we
* would read is not trustworthy, and resyncing is strictly better than
* parsing someone else's bytes. */
if (ring_lag(rpage, wpage) > 10) return -4;
addr = rpage << 8;
if (bba_stream((u16)addr, hdr, 8) < 0) return -1;
memcpy(s_last_desc, hdr, 4);
s_last_page = rpage;
/* LOCATE the descriptor rather than assume it is at +0.
*
* It should always be at +0: four bytes [00 00 len_hi len_lo] followed
* immediately by the frame. On hardware it sometimes sits at +1 instead,
* and which one varies frame to frame -- 18 of 23 frames were rejected
* for an implausible length while 5 parsed fine, in the same run. That
* is a race in the assembler, not a constant offset, and the gateware bug
* behind it is not yet found.
*
* Taking the length from whichever offset yields a plausible one lets the
* transfer proceed either way AND measures the split, which is the
* measurement needed to find the real cause. A length is a strong check:
* it must exceed the 4 descriptor bytes and fit a legal frame. */
for (shift = 0; shift <= 1; shift++) {
dlen = ((u32)hdr[shift + 2] << 8) | hdr[shift + 3];
if (dlen > 4 && dlen <= 1518 + 4) break;
}
if (shift > 1) {
dlen = ((u32)hdr[2] << 8) | hdr[3]; /* report the nominal one */
/* Only now is a hex dump worth an EXI transaction. Doing it for
* EVERY frame (as an earlier version did) added a round trip to the
* poll loop that is already losing the race against the writer --
* i.e. the diagnostic made the fault it was diagnosing worse. */
bba_stream((u16)addr, s_last_dump, sizeof(s_last_dump));
return -3;
}
s_shift_hits[shift]++;
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 = shift + dlen;
if (want > sizeof(tmp)) want = sizeof(tmp);
if (bba_stream((u16)addr, tmp, want) < 0) return -1;
memcpy(buf, tmp + shift + 4, flen);
}
/* Advance RRP by the pages this frame occupied, wrapping 1..15. */
{
u32 pages = (shift + dlen + 255) >> 8;
u32 next = ((rpage - 1 + pages) % 15) + 1;
if (bba_w8(REG_RRP_LO, (u8)next) < 0) return -1;
s_rrp = next;
}
return (int)flen;
}
static int send_ack(u16 seq)
{
u8 f[NB_MIN];
int i, ir = 0;
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);
/* THE FULL SEQUENCE, matching t_tx() which is proven on hardware.
* bba_tx_bytes() only streams bytes into the TXDATA FIFO -- it does NOT
* set the length or fire the transmit. An earlier version of this
* function called it alone, so every ACK was written into the FIFO and
* never put on the wire: the host retried block 0 twenty times, saw no
* ACK and gave up, while the GC reported frames arriving normally
* ("22 seen, 0 bad" against the sender's 20 retries).
*
* TWD must be written BEFORE streaming: that is what queues the length
* so TXFrameDrain can consume concurrently with the EXI writes. Without
* it the drain waits for a length that only arrives at the ST trigger and
* the frame has to fit the 16-deep FIFO whole -- the truncate-at-16-bytes
* bug. The NCRA ST write afterwards is the fallback trigger. */
bba_w8(REG_IR, 0xFF); /* clear TI/RI */
bba_w8(REG_TWD_LO, (u8)(sizeof(f) & 0xFF));
bba_w8(REG_TWD_HI, (u8)((sizeof(f) >> 8) & 0xFF));
if (bba_tx_bytes(f, sizeof(f)) < 0) return -1;
bba_w8(REG_NCRA, NCRA_SR | NCRA_ST0);
/* Poll for TI WITHOUT sleeping. usleep(500) x 20 meant every ACK cost
* up to 10 ms even when the transmit succeeded immediately, and the
* transfer is round-trip bound -- that is 10 ms on every one of ~292
* blocks. A bba_r8 is itself tens of microseconds, so a bare loop polls
* often enough and finishes as soon as TI sets.
*
* Absence of TI is NOT treated as fatal: the frame may still have gone
* out (TI only means TXFrameDrain handed it to the W5100), and if it did
* not, the host's retransmit covers it. Blocking longer here would only
* slow the common case to protect against the rare one. */
for (i = 0; i < 200; i++) {
ir = bba_r8(REG_IR);
if (ir >= 0 && (ir & IR_TI))
return 0;
}
s_tx_timeouts++;
return -1;
}
/* 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, bad = 0, seen = 0, lapped = 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;
/* Clear any stale interrupt flags, RBFI (bit 7) included: the gateware
* now reports a ring-full drop there, and we would rather see a fresh
* one than inherit a set bit from an earlier run. */
bba_w8(REG_IR, 0xFF);
/* 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;
s_rrp = wp[0];
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;
/* Keep this to ONE line. The console holds about 25 lines and the
* failure diagnostics are what matter, so guidance text here just
* pushes them off the top of the screen. */
if (status) status("waiting for host (send-dol.py)...");
t0 = gettime();
for (;;) {
int n = ring_next(frame, sizeof(frame));
if (n == -4) {
/* Lapped. Skip to the writer and carry on; with stop-and-wait
* the host simply retransmits the block we just threw away. */
lapped++;
if (lapped <= 3 && status)
status("ring lapped (too much other traffic) - resyncing");
{
u8 wp[2];
if (bba_read(REG_RWP_LO, wp, 2) < 0) return -1;
if (wp[0] >= 1 && wp[0] <= 15) {
bba_w8(REG_RRP_LO, wp[0]);
s_rrp = wp[0];
}
}
continue;
}
if (n == -3) {
/* A descriptor we cannot make sense of must NOT kill the
* transfer. The ring collects EVERYTHING on the wire --
* mDNS, IPv6 RAs, ARP from the host -- so pressing Z on a
* live network hits stray frames long before the sender
* starts. Log the first few, resync, and carry on. */
bad++;
if (bad <= 3 && status) {
int i;
snprintf(line, sizeof(line),
"skip bad descriptor at page %u: %02X %02X %02X %02X",
(unsigned)s_last_page, s_last_desc[0],
s_last_desc[1], s_last_desc[2], s_last_desc[3]);
status(line);
for (i = 0; i < 32; i += 16) {
snprintf(line, sizeof(line),
" +%02X %02X %02X %02X %02X %02X %02X %02X %02X "
"%02X %02X %02X %02X %02X %02X %02X %02X", i,
s_last_dump[i+0], s_last_dump[i+1], s_last_dump[i+2],
s_last_dump[i+3], s_last_dump[i+4], s_last_dump[i+5],
s_last_dump[i+6], s_last_dump[i+7], s_last_dump[i+8],
s_last_dump[i+9], s_last_dump[i+10], s_last_dump[i+11],
s_last_dump[i+12], s_last_dump[i+13], s_last_dump[i+14],
s_last_dump[i+15]);
status(line);
}
}
if (bad > 200) {
if (status) status("too many bad descriptors - giving up");
return -2;
}
/* Resync to the gateware's own pointer and keep waiting. */
{
u8 wp[2];
if (bba_read(REG_RWP_LO, wp, 2) < 0) return -1;
if (wp[0] >= 1 && wp[0] <= 15) {
bba_w8(REG_RRP_LO, wp[0]);
s_rrp = wp[0];
}
}
continue;
}
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);
snprintf(line, sizeof(line),
" page %u descriptor = %02X %02X %02X %02X",
(unsigned)s_last_page, s_last_desc[0],
s_last_desc[1], s_last_desc[2], s_last_desc[3]);
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) {
snprintf(line, sizeof(line),
"%s (%u seen, %u bad, desc+0=%u +1=%u, txto=%u)",
got ? "timeout mid-transfer" : "timeout: no match",
(unsigned)seen, (unsigned)bad,
(unsigned)s_shift_hits[0],
(unsigned)s_shift_hits[1],
(unsigned)s_tx_timeouts);
status(line);
}
return -3;
}
continue;
}
seen++;
if ((u32)n < NB_HDR) continue;
if (rd16(frame + 12) != NB_ETHERTYPE) {
/* Dump the first non-matching frame. If the sender is running
* and nothing matches, the bytes show whether the body is
* offset -- the ethertype would appear at 11 or 13 instead of
* 12. */
if (seen == 1 && status) {
snprintf(line, sizeof(line),
"frame 1 (%d B) etype=%04X at 12; "
"%02X %02X %02X %02X %02X %02X %02X %02X",
n, rd16(frame + 12), frame[0], frame[1], frame[2],
frame[3], frame[4], frame[5], frame[6], frame[7]);
status(line);
}
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, %u blocks "
"(desc@+0=%u +1=%u, %u bad)",
(unsigned)got, (unsigned)nblocks,
(unsigned)s_shift_hits[0],
(unsigned)s_shift_hits[1],
(unsigned)bad);
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);
}