92f7d716d9
RXFrameAssembler ignored RRP entirely, so it lapped the reader: a traffic
burst overwrote pages the GC had not read, and the "descriptor" the driver
then parsed was frame data from whatever had replaced it. That is where the
hardware's 00 00 00 01 came from -- not a dropped write.
This is a product bug, not just a netboot one. Until now ANY burst could
corrupt a frame Swiss was midway through reading, with nothing to indicate it
had happened.
The assembler now takes the GC's read pointer and computes free space as
(rrp - rwp - 1) mod 15. On a frame start with fewer than 6 free pages -- the
worst case for 1518 bytes plus a 4-byte descriptor -- it enters DROP_FRAME,
discards to EOF, and pulses rx_overflow, which sets IR[7] (RBFI). That is the
bit the GC's IMR = 0x86 already enables, so a driver that loses a frame is
told so instead of reading corruption.
Deliberate details:
- rx_ready stays HIGH while dropping. Stalling the ethernet master instead
would back-pressure into the W5100's own buffer and turn a dropped frame
into a wedged receiver.
- Six pages are reserved up front because a frame's length is not known
until EOF. Starting with less risks running into unread data partway
through, which is worse than refusing cleanly.
- have_room is REGISTERED, not combinational. Feeding a mod-15 subtract and
comparator into the FSM branch cost real depth in `sync`, the domain with
the least margin: clk fell 27.8-32.2 -> 27.0-28.0 MHz. Registering it
restored 28.4-31.4. One cycle of staleness is harmless -- rwp moves only
at end-of-frame and rrp only when the GC writes it.
T4 proves the property that matters, and the negative control REPRODUCES THE
HARDWARE FAULT IN SIMULATION: with the check removed the unread page reads
back as [245, 248, 251, 254, ...] -- frame data where a descriptor belongs.
First time one of today's RX faults has been reproducible on this side rather
than inferred from the console. T4 also checks the receiver un-latches once
RRP advances, so a full ring cannot kill RX permanently.
Fixed a test artifact in the same pass: rx_overflow fires on the very tick
that consumes the EOF byte, inside send_frame, so polling for a 1-cycle pulse
afterwards missed it and a correctly-dropped frame looked like a stall. Both
pulses are now latched in the tick helper.
netboot also clears IR (including RBFI) at init so a stale bit is not
inherited from an earlier run.
--seeds 8: clk 28.4-31.4 MHz, EXI 55.2-62.7 MHz, 8/8 PASS. Best seed 4
(clk 31.39, EXI 61.85), flashed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
685 lines
23 KiB
C
685 lines
23 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];
|
|
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[4];
|
|
u32 rpage, wpage, dlen, flen, addr;
|
|
|
|
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, 4) < 0) return -1;
|
|
memcpy(s_last_desc, hdr, 4);
|
|
s_last_page = rpage;
|
|
dlen = ((u32)hdr[2] << 8) | hdr[3];
|
|
if (dlen <= 4 || dlen > 1518 + 4) {
|
|
/* 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;
|
|
}
|
|
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;
|
|
s_rrp = next;
|
|
}
|
|
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, 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;
|
|
|
|
if (status) {
|
|
status("waiting for host (send-dol.py)...");
|
|
status("NOTE: quiet the link first, or other traffic laps the ring:");
|
|
status(" sudo ./scripts/gc-testlink.sh up <iface>");
|
|
}
|
|
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, %u lapped)",
|
|
got ? "timeout mid-transfer" : "timeout: no match",
|
|
(unsigned)seen, (unsigned)bad, (unsigned)lapped);
|
|
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 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);
|
|
}
|