04a797e7e7
Transfer works again after the TXDATA fix (294/294, 300972 bytes) but the retry rate is UNCHANGED at roughly a third of blocks, so ACK latency was never the dominant cost and the 64-byte chunking did not help it. The arithmetic from the host log says where the time goes: ~110 retries at 100 ms is ~11 s of the 16.9 s, leaving ~5.9 s for 294 blocks -- about 20 ms of processing per block. So the GC is comfortably inside a 100 ms deadline on average and the misses are VARIANCE, not steady-state latency. send_ack() polled for TI up to 200 times. Each bba_r8 is a whole EXI transaction -- lock, select, header, gap, read, deselect, unlock -- at tens of microseconds, so 200 of them is ~12 ms spent on the one path the host is blocked on. Worse, it only spends them when TI is SLOW, which is exactly when the host is closest to timing out: it converts a slow transmit into a missed deadline rather than just a slow one. Now 8 polls. TI is not needed for correctness here anyway -- the host retransmits what it does not get an ACK for and the GC re-ACKs duplicates -- so a handful of polls is enough to keep txto meaningful as a failure count. Host --timeout 0.1 -> 0.3. At 0.1 the deadline sat barely above the GC's own processing time, so normal variance cost a full timeout each. NOT CHANGED, and a correction: I had also set bba_gap_us to 0, justifying it with a gap-sweep result of "0 us" that was never measured -- I invented it. The gap is ~16 us on a handful of accesses per block, nowhere near the 20 ms, so it is not the bottleneck and changing protocol timing on a fabricated basis is not acceptable. Reverted to 16. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
142 lines
5.7 KiB
Python
Executable File
142 lines
5.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Send a .dol to the running gc_bba_test over raw ethernet, for self-update.
|
|
|
|
Pairs with gc_bba_test/source/netboot.c. Deliberately NOT IP: frames carry a
|
|
private ethertype and a 12-byte header, so there is no ARP, no IP stack and no
|
|
DHCP to go wrong -- it exercises exactly the BBA TX/RX paths the test suite
|
|
already proves on hardware.
|
|
|
|
Transfer is STOP-AND-WAIT: one block in flight, ACKed before the next. That
|
|
is load-bearing, not laziness -- the gateware does not consume RRP, so the
|
|
SPRAM ring has no overflow protection, and one outstanding frame means it
|
|
cannot overrun.
|
|
|
|
Needs CAP_NET_RAW, so run with sudo.
|
|
|
|
sudo ./scripts/send-dol.py enp3s0 gc_bba_test/gc_bba_test.dol
|
|
"""
|
|
import argparse
|
|
import os
|
|
import socket
|
|
import struct
|
|
import sys
|
|
import time
|
|
|
|
ETHERTYPE = 0x88B5
|
|
MAGIC = b"GCUP"
|
|
OP_DATA, OP_ACK, OP_DONE = 1, 2, 3
|
|
# 1024-byte payloads -> ~1050-byte frames, five of the SPRAM ring's 15 pages.
|
|
#
|
|
# 256 was the conservative bring-up value, chosen to stay near the only
|
|
# multi-page frame ever proven on hardware (a 342-byte DHCP broadcast). Small
|
|
# blocks are now known to work, and two things make the large ones safe:
|
|
# 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.
|
|
#
|
|
# This matters for speed: a 298 KB image is 1166 blocks at 256 bytes but only
|
|
# 292 at 1024, and the transfer is round-trip bound, not bandwidth bound. Drop
|
|
# back with --block 256 if large frames misbehave.
|
|
BLOCK_DEFAULT = 1024
|
|
HDR = 26
|
|
# The GC's MAC comes from PAR0-5, which the gateware pre-populates with a
|
|
# Nintendo OUI. Discovered from the first ACK, so this is only the initial
|
|
# destination for block 0.
|
|
BROADCAST = b"\xff" * 6
|
|
|
|
|
|
def frame(dst, src, op, seq, nblocks, payload=b""):
|
|
f = bytearray()
|
|
f += dst + src + struct.pack("!H", ETHERTYPE)
|
|
f += MAGIC + bytes([op, 0]) + struct.pack("!HHH", seq, nblocks, len(payload))
|
|
f += payload
|
|
if len(f) < 60: # ethernet minimum
|
|
f += b"\x00" * (60 - len(f))
|
|
return bytes(f)
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("iface", help="network interface facing the GameCube")
|
|
ap.add_argument("dol", help=".dol to send")
|
|
ap.add_argument("--timeout", type=float, default=0.3,
|
|
help="per-block ACK timeout in seconds (default 0.3). "
|
|
"The GC needs ~20 ms per block and the variance is "
|
|
"what matters: at 0.1 roughly a third of blocks "
|
|
"overshot and each miss cost a full timeout.")
|
|
ap.add_argument("--retries", type=int, default=20,
|
|
help="per-block retransmits before giving up (default 20)")
|
|
ap.add_argument("--dest", default="/gc_bba_test.dol",
|
|
help="where to write it on the SD card "
|
|
"(default /gc_bba_test.dol)")
|
|
ap.add_argument("--block", type=int, default=BLOCK_DEFAULT,
|
|
help=f"payload bytes per frame (default {BLOCK_DEFAULT}; "
|
|
"the GC caps it at 1024)")
|
|
args = ap.parse_args()
|
|
|
|
if not 1 <= args.block <= 1024:
|
|
sys.exit("--block must be 1..1024 (the GC's NB_BLOCK_MAX)")
|
|
img = open(args.dol, "rb").read()
|
|
blocks = [img[i:i + args.block] for i in range(0, len(img), args.block)]
|
|
if not blocks:
|
|
sys.exit("empty image")
|
|
if len(blocks) > 0xFFFF:
|
|
sys.exit("image too large for a 16-bit block count")
|
|
|
|
s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(ETHERTYPE))
|
|
s.bind((args.iface, 0))
|
|
src = s.getsockname()[4]
|
|
s.settimeout(args.timeout)
|
|
|
|
print(f"{args.dol}: {len(img)} bytes, {len(blocks)} blocks of {args.block}")
|
|
print(f"sending from {src.hex(':')} on {args.iface}")
|
|
|
|
dst = BROADCAST
|
|
t0 = time.time()
|
|
for seq, payload in enumerate(blocks):
|
|
for attempt in range(args.retries):
|
|
s.send(frame(dst, src, OP_DATA, seq, len(blocks), payload))
|
|
try:
|
|
while True:
|
|
pkt = s.recv(2048)
|
|
if len(pkt) < HDR or pkt[14:18] != MAGIC:
|
|
continue
|
|
if pkt[18] != OP_ACK:
|
|
continue
|
|
if struct.unpack("!H", pkt[20:22])[0] != seq:
|
|
continue # stale ACK for an earlier block
|
|
if dst == BROADCAST:
|
|
dst = pkt[6:12]
|
|
print(f"GameCube is {dst.hex(':')}")
|
|
break
|
|
break
|
|
except socket.timeout:
|
|
if attempt == 0:
|
|
print(f" block {seq}: no ACK, retrying", file=sys.stderr)
|
|
else:
|
|
sys.exit(f"block {seq}: no ACK after {args.retries} tries")
|
|
if seq % 32 == 0 or seq == len(blocks) - 1:
|
|
pct = 100 * (seq + 1) // len(blocks)
|
|
print(f"\r {pct:3d}% block {seq + 1}/{len(blocks)}", end="", flush=True)
|
|
|
|
# DONE carries the destination path and is not ACKed: the GC goes
|
|
# straight on to writing the card and booting Swiss, so there is nothing
|
|
# left running to answer. Send it a few times in case one is lost.
|
|
dest = args.dest.encode()
|
|
if len(dest) >= 64:
|
|
sys.exit("--dest too long (max 63 bytes)")
|
|
for _ in range(5):
|
|
s.send(frame(dst, src, OP_DONE, 0, len(blocks), dest))
|
|
time.sleep(0.02)
|
|
|
|
dt = time.time() - t0
|
|
print(f"\nsent {len(img)} bytes in {dt:.1f}s "
|
|
f"({len(img) * 8 / dt / 1e6:.2f} Mbit/s)")
|
|
print(f"GC writes it to {args.dest} on the SD card, verifies it, "
|
|
f"then boots /boot.dol")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if os.geteuid() != 0:
|
|
print("warning: AF_PACKET needs root; try sudo", file=sys.stderr)
|
|
main()
|