diff --git a/gc_bba_test/README.md b/gc_bba_test/README.md index 70c4534..f5105ce 100644 --- a/gc_bba_test/README.md +++ b/gc_bba_test/README.md @@ -122,6 +122,33 @@ suite already proves on hardware — `bba_tx_bytes()` for TX and the SPRAM ring at RRP for RX — so each update doubles as a few hundred round trips of soak testing through that path. +### ⚠️ Quiet the link first — the ring has no overflow protection + +```bash +sudo ./scripts/gc-testlink.sh up # static IP, no DHCP, NM off +``` + +Nothing in the gateware consumes RRP, so `RXFrameAssembler` keeps writing and +**laps the reader** — the real BBA has RBFI for this and this design does not. +The poll loop costs ~5 EXI transactions per frame (~200 µs) while a 100 Mbit +link delivers a 342-byte frame every ~30 µs, and the ring is only 15 pages +(~3–11 frames). Under continuous traffic pages are overwritten before they are +read, and the "descriptor" you then parse is really frame data from the frame +that replaced it. + +That is where a descriptor of `00 00 00 01` comes from. It is **not** a dropped +write — it is someone else's bytes. + +The biggest offender is usually the host's own DHCP client broadcasting on that +interface (a 342-byte broadcast). `gc-testlink.sh up` sets a static address and +takes the device out of NetworkManager, which stops it. Disabling IPv6 and mDNS +on that interface helps further. + +`netboot_receive()` detects being lapped (writer more than two thirds of the +ring ahead), resyncs and continues rather than parsing untrustworthy bytes, and +reports the count on timeout — but detection is not prevention. A busy link +will still cost retransmissions. + ### Stop-and-wait is load-bearing The gateware does not consume RRP, so `RXFrameAssembler` can overwrite ring diff --git a/gc_bba_test/source/netboot.c b/gc_bba_test/source/netboot.c index bc7a346..a4060a6 100644 --- a/gc_bba_test/source/netboot.c +++ b/gc_bba_test/source/netboot.c @@ -39,6 +39,12 @@ static void wr16(u8 *p, u16 v) { p[0] = (u8)(v >> 8); p[1] = (u8)v; } * 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); +} + static int ring_next(u8 *buf, u32 cap) { u8 rp[2], wp[2], hdr[4]; @@ -51,6 +57,20 @@ static int ring_next(u8 *buf, u32 cap) 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); @@ -111,7 +131,7 @@ 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; + u32 got = 0, nblocks = 0, bad = 0, seen = 0, lapped = 0; u16 expect = 0; char line[80]; u64 t0; @@ -179,11 +199,29 @@ int netboot_receive(void *dst, u32 maxlen, void (*status)(const char *msg)) * 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)..."); + 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 "); + } 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]); + } + continue; + } if (n == -3) { /* A descriptor we cannot make sense of must NOT kill the * transfer. The ring collects EVERYTHING on the wire -- @@ -255,9 +293,9 @@ int netboot_receive(void *dst, u32 maxlen, void (*status)(const char *msg)) if (idle > (got ? 5000u : 30000u)) { if (status) { snprintf(line, sizeof(line), - "%s (%u frames seen, %u bad descriptors)", + "%s (%u seen, %u bad desc, %u lapped)", got ? "timeout mid-transfer" : "timeout: no match", - (unsigned)seen, (unsigned)bad); + (unsigned)seen, (unsigned)bad, (unsigned)lapped); status(line); } return -3;