fd956435939e5d02e1f8ab9f50da5386c51254f6
161 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fd95643593 |
scripts: add a read-only FSP server so Swiss can stream a disc image
sudo ./scripts/fsp-server.py --root /workspace --password alltheroms
Swiss streams games over FSP/UDP rather than TCP, so hosting an image needs an
FSP server. Serves a directory read-only: VERSION, STAT, GET_DIR, GET_FILE,
BYE, with CC_ERR for anything else. Reports the achieved read rate every two
seconds, which is the measurement this was really built for -- the 850 KB/s
figure came from the menu's lwIP/TCP path and says nothing about streaming.
The protocol was taken from Swiss's own two clients rather than from memory,
because they disagree about nothing but exercise different commands:
cube/swiss/source/devices/fsp/fsplib.c (menu: browse, stat, read)
cube/patches/bba/tcpip.c (in-game streaming reads)
Details that are easy to get wrong, all verified against that source:
* checksum: sum = total_size + every byte with the checksum byte zeroed,
then sum += sum >> 8, low 8 bits.
* password: the data field is "path\npassword\0" -- the '\n' REPLACES the
path's terminating NUL (fsplib buildfilename), it is not appended after it.
* GET_DIR: entries are time(4) size(4) type(1) name(NUL) padded to 4 bytes;
the client reads blocks until one is shorter than the first, so the whole
listing then an EMPTY reply is what terminates it. dirpos % 4 != 0 is
rejected outright by fsplib, hence the alignment padding.
* STAT: the same 9-byte time/size/type prefix; type 0 means ENOENT.
* GET_FILE: sequence AND position must be echoed -- the in-game client
discards any reply where either differs.
* ⚠️ a GET_FILE reply must be a multiple of 32 bytes unless it is the tail,
because the in-game client does
data_size = MIN(requested, OSRoundDown32B(reply_len))
so a non-32-aligned reply is silently rounded DOWN and the stream stalls.
test-fsp-server.py runs a real server on 127.0.0.1 and drives it with a client
that packs requests exactly as Swiss does: checksums verify both ways, STAT
returns the true size, GET_DIR parses with fsplib's own walk and the second
page is empty, GET_FILE returns byte-exact data at three offsets with seq and
pos echoed, a 1024-byte request comes back 32-aligned, a wrong password gives
CC_ERR, and ../../etc/passwd is refused.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
2da721d7ed |
scripts: add a GameCube disc-image validator for BBA dumps
./scripts/check-gcm.py dvd.iso Checks the header (DVD magic, game/maker code, title, DOL/FST offsets, apploader), parses the FST, then READS EVERY FILE -- because a dump that lost data still has a perfect header, so structure alone proves nothing. Also reports how much of the image the filesystem accounts for, which is where a truncated or zero-filled tail shows up, and prints SHA-1 for comparison against a reference or a second dump. ⚠️ The zero-run heuristic was wrong in its first form and the fix is the interesting part. Flagging any zero run >= 32 KiB as a dropped transfer gave 7 false positives on a known-good Wind Waker dump, all in Audiores/Stream/*.afc: streamed audio legitimately begins with silence right after its 16-byte header. What a dropped transfer actually looks like is a run ALIGNED to the transfer granularity -- whole 2048-byte disc sectors, because nothing reads or writes a fraction of a sector. Structural silence starts and ends wherever the audio does. So alignment is the discriminator, not length. Sector-aligned runs are now a failure; unaligned ones are reported as structural with that reasoning spelled out. .gitignore: *.iso and *.gcm, so a 1.4 GB dump sitting in the workspace cannot be committed by accident. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4e7a2ef429 |
analyse-pcap: count UDP, because ROM streaming is UDP/FSP not TCP
The 850 KB/s dump measurement does not bound ROM streaming, and the tool could not have measured streaming at all: it only counted TCP payload, so a streaming capture would have reported ~zero throughput. Swiss's ROM streaming runs in the game via the patch driver's own stack (cube/patches/bba/tcpip.c), which contains no TCP whatsoever -- UDP + FSP, plus ICMP and ARP. Reads are sized to the path MTU rather than 1024: _fsp.data_length = MIN(length, env->pmtu - (fsp->data - eth->data)); so ~1446 B of payload per datagram at a 1500 MTU, with QUEUE_SIZE 2 outstanding and udp->checksum = 0x0000 (no payload checksum). None of the menu path's TCP window, lwIP overhead or 1024-byte writes apply. The analyser now counts UDP payload and picks the bulk direction by which way the payload actually flows -- streaming is PC -> GC, the opposite of a disc dump -- with --bulk to_gc to steer the gap analysis. Self-test H replays the streaming shape (1446 B datagrams, 2 per request) and checks the bulk direction is identified and the payload counted. Recorded in the TODO that "can it stream?" is still OPEN, but open on the GC's side: the dump established the adapter has headroom (zero retransmissions, ~17% W5100 bus utilisation, 26% of the EXI ceiling). The hard ceiling is 3375 KB/s and the GC's disc delivers ~2000-3100 KB/s, so full disc rate has no margin -- but games read in bursts, not continuously at peak. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d8ddea13ba |
Measure the throughput limit: window x RTT, not the adapter
Swiss dumping a disc over the adapter measures 850 KB/s (6.97 Mbit/s, 25.8% of the 27 Mbit/s EXI ceiling). The capture decomposes it completely: 18395 data frames / 9200 ACKs = 2.0 frames per ACK 18.84 MB / 9200 = 2048 B per round trip 22.78 s / 9200 = 2.48 ms per round trip 2048 B / 2.48 ms = 826 KB/s ~= the measured 850 So the flow is window x RTT bound and nothing else. Payload per frame is exactly 1024 B (wire 1078 minus 54 of headers) against a 2920 B window, so two segments fit in flight. Zero retransmissions means the 15-page ring never overflowed; 0.2% stall time means nothing waits on the disc; our W5100 bus engine sits at ~17% utilisation. The adapter is not the constraint. Levers, and the first two are not ours: a larger lwIP TCP_SND_BUF (scales almost linearly while window-bound), and full-MSS segments -- Swiss writes 1024 B at a time, so the same window and RTT at 1460 B would give ~1150 KB/s, a 1.43x gain for free. Third is a shorter round trip, which is GC-side interrupt dispatch plus lwIP, plus EXI lock contention: libogc2's bba_event_handler defers when EXI_Lock fails, so RX queues behind TX on the single channel. ⚠️ This REVERSES an earlier recommendation in the TODO. RX interrupt coalescing (NCRB.RXINTC, RXINTT) was listed as a throughput lever; for a window-bound flow it makes things worse, because delaying the ACK's interrupt lengthens the round trip and throughput is bytes-per-round-trip / RTT. The measurement showed the guess was backwards. Noted in item 3 as "implement for fidelity, not for speed". analyse-pcap.py now derives frames-per-ACK, bytes-per-round-trip and the implied RTT itself, identifies a window-bound flow, quantifies the full-MSS gain, and warns against coalescing -- so the next person gets the conclusion rather than the raw numbers. Self-test G replays this exact shape. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d6fb7c81b9 |
scripts: add a pcap analyser to find the BBA throughput limit
Swiss dumps a disc over the adapter at 740-780 KB/s, which is ~23% of the
27 Mbit/s EXI ceiling (3375 KB/s), and no published real-BBA figure exists to
compare against. Rather than reason about where the rest goes, measure it.
./scripts/analyse-pcap.py /tmp/bba.pcap --gc-ip 10.0.0.64 reports, per
direction: throughput as a fraction of the EXI ceiling, MEAN FRAME SIZE,
retransmissions, the GC's advertised TCP window, zero-window spans, and the
largest gaps between data frames. Each one discriminates a different
bottleneck:
mean frame far below 1514 -> per-frame overhead (interrupts, EXI
transactions, socket bookkeeping) dominates
retransmissions -> we are dropping frames, e.g. the 15-page RX
ring overflowing (RBFI)
small advertised window -> capped at window/RTT regardless of the wire
zero-window spans -> the GC cannot drain: CPU/stack bound
large gaps, little sending -> the limit is upstream of the wire entirely
(disc read, CPU, stack), not the adapter
Classic pcap only, stdlib only, no root. pcapng is detected and refused with
a conversion hint rather than misparsed.
scripts/test-analyse-pcap.py synthesises captures with known properties and
checks each conclusion is reached: 1000 full frames in 2 s reports ~22% of the
ceiling, 200-byte payloads raise the overhead warning, a repeated seq counts 9
retransmissions, one 300 ms hole in 100 frames is found and attributed, a
zero-window span is called CPU/stack bound, and a pcapng header is rejected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
0c5a7bf9db |
mini-dhcpd: stop reporting ARP as a corrupted frame
Swiss got a DHCP lease on hardware (10.0.0.64/24), but the script flagged
every ARP from the GC as "frame did NOT parse as DHCP ... this is what a
corrupted TX frame looks like". That was wrong and actively misleading: the
frames are valid lwIP duplicate-address probes (sender 0.0.0.0, target =
the offered address), sent to check the lease is free before accepting it.
The parser conflated "not DHCP" with "corrupted". A diagnostic that cries
wolf on normal traffic is worse than none, because 'malformed' then means
nothing. Now:
'info' from the client but not DHCP -- ARP decoded properly (probe vs
request vs reply, with addresses), other ethertypes named
'malformed' CLAIMED IPv4/UDP->67 and then failed to parse, i.e. what a
genuinely damaged TX frame would look like
So the corruption diagnostic still works -- that was the point of using a raw
socket -- but it only fires when it means something.
scripts/test-mini-dhcpd.py keeps the self-test, now including the exact
60-byte frame from the console (T6), a damaged DHCP packet that must still be
flagged (T7), and a raw ethertype (T8).
One note for anyone extending that test: the real frame goes in as ONE string
literal. Splitting the hex across two adjacent literals is how the first
version of T6 silently lost a byte and blamed the parser for decoding the
target as 0.10.0.0 -- the parser was correct.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
29ce281a7f |
scripts: add a minimal raw-socket DHCP server for BBA bring-up
sudo ./scripts/mini-dhcpd.py enp0s31f6 Serves one address, logs every exchange, and does not touch the rest of the system. Needed because the Swiss DHCP timeout proves nothing while there is no server on the segment to answer. Deliberately AF_PACKET rather than a UDP socket on port 67. A UDP socket only sees frames the kernel accepts as well-formed IP/UDP, and one of the open questions is whether the GC's frames arrive INTACT: libogc streams TX payload with EXI_Dma and the GC's DMA-write engine is documented to have a 1-bit-shift bug. A corrupted DISCOVER would never reach a UDP socket and the server would look "fine, just no client". On a raw socket every frame from the adapter is parsed by hand and anything that fails is reported as MALFORMED with the reason and a hexdump -- which is exactly the evidence that would confirm or kill the corruption hypothesis. Replies go out as raw ethernet UNICAST to the client's MAC, so a successful lease also demonstrates the new MACRAW MF filter accepts unicast to SHAR and not just broadcast. Self-tested without a network: a well-formed DISCOVER parses with the right chaddr/xid, a corrupted magic cookie is reported as malformed rather than ignored, frames from other MACs are skipped, and the generated OFFER is a valid 342-byte frame -- unicast to the client, correct IP checksum and total length, ports 67->68, BOOTP padded to the 300-byte minimum, yiaddr as configured. On-wire behaviour is NOT tested here; the devcontainer has only a veth. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2893e7f6fb |
Filter RX to own-MAC + broadcast: unfiltered MACRAW hung Swiss
Compat TODO item 1. S0_MR now sets the MACRAW MF bit, so the W5100 delivers
frames addressed to SHAR or broadcast only -- what the real BBA does, since
libogc2 configures NCRB = AB|CA|2_PACKETS_PER_INT (PR=0, PM=0, AB=1).
NCRB.PR (bit 0) clears MF if the GC asks for promiscuous, sampled when socket 0
is OPENed, so a later PR change applies on the next NCRA reset.
This was not a performance issue, it was a HANG. Swiss sat on "Initialising
Network" forever. libogc2's bba_interrupt() loops
while (status) { clear IR; start_rx; status = IR & IMR; }
which only exits when IR & IMR reaches zero. Unfiltered, every frame on the
segment set RI, re-asserting faster than the loop could clear it, so it never
exited. Unplugging the ethernet cable let Swiss run all the way through and
then fail DHCP for want of a server -- a one-minute experiment that identified
it after a lot of theorising about descriptors and interrupts.
Filtering against SHAR turned out to be safe for this path after all. The
earlier worry was that MF filters on SHAR, which we write only at init, so a
driver WRITING PAR0-5 would leave it stale and MF would drop everything.
libogc2 READS the MAC out of PAR0-5 instead, so SHAR -- written from reg.par in
that same init -- cannot go stale underneath it. Broadcast still passes MF and
a DHCP OFFER unicast to our MAC passes too.
Left open and recorded: refreshing SHAR on a PAR0-5 write plus a socket
re-open, for a driver that does write it (the official SDK driver is unread).
The W5500 back-end is unchanged, being the unused alternate path; it would need
the same Sn_MR treatment if revived.
T1 expects S0_MR = MACRAW|MF in the init sequence, T1b checks promisc high
re-inits with MF clear. Mutation-tested: never setting MF fails both.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
90a583dd46 |
Remove the device-ID one-shot: it made Swiss see no adapter at all
id_phase was set at reset and cleared permanently by the GC's first NCRA reset, after which a command-space read of register 0 returned 0x00000000. Swiss calls EXI_GetID from bba_exists() AND again from bba_requires_init(), long after init, and matches the result against EXI_MX98730EC_ID -- so with the one-shot spent no case matched and Swiss reported NO ADAPTER, not a broken one. Running gc_bba_test was enough to spend it, so the adapter would "disappear" after any test session. The justification in the comment was wrong. It claimed the fast path would otherwise hijack address 0 so the GC could never read NCRA back -- but NCRA is REGISTER space (0x80/0xC0 header) while the fast path requires byte 0 == 0x00, i.e. COMMAND space. They were never ambiguous, and no driver reads command register 0 for anything else (libogc2 reads 0x01/0x03/0x0B/0x0F, Swiss 0x03). Real hardware answers EXI_GetID whenever asked; that is how EXI enumerates devices. id_arm is now tied high, and the id_phase output, the FFSynchronizer that crossed it into the pin domain, and the BBATop wiring are all gone. The port stays only so the PHASE_METER build can disarm the fast path, which would otherwise send ID bytes instead of its constant 0x80. bba_top T1b reads the device ID AFTER an NCRA reset, which T1 (before any reset) structurally cannot catch. Mutation-tested: disarming the fast path returns 00 00 00 00 for both, exactly what Swiss saw. gc_bba_test: the ID tests no longer skip. A wrong ID is a FAILURE again, with no iface_alive() discrimination and no ordering requirement between suites -- the ID is readable at any point in a run. g_ncra_reset_done and iface_alive() are removed outright rather than left as a dead flag. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
991342c0b5 |
docs: geom_honoured is asserted but not wired anywhere yet
The ring-geometry note claimed a rejected BP/RHBP is "observable rather than silent". That is overstated: geom_honoured exists on RXFrameAssembler and the tests check it, but BBATop wires it to nothing -- no status register, no LED, no UART line -- so a rejected geometry is silent to both the GC and the bench. Recorded as a loose end; the guarantee today is "rejected rather than obeyed", not "reported". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
052e1a4252 |
gc_bba_test: shorten the driver-shapes suite description to fit the row
"Swiss + libogc init sequences and transfer shapes" is 49 characters and wrapped by exactly one. The menu row is 28 columns of prefix -- " " + name(%-14s) + " " + "pp/ff/kk" + " " -- and the console is 76 wide (console_init starts at x=20 on a 640-pixel framebuffer at 8 pixels per character), leaving 48. Now "Swiss + libogc init and transfer shapes", 39 characters, in line with the 41 of the longest original. Added a comment above g_suites with the 48-column budget and where it comes from, so the next suite does not rediscover it by wrapping on a console. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6b2e6724a2 |
Fix the 0/8 timing failure the ring-geometry change caused
The sweep after honouring BP/RHBP came back clk 16.5-17.6 MHz, 0/8 seeds
passing against the 24 MHz target -- from ~29 MHz before. The build was
unusable.
Cause: the sanitised bounds were combinational, so
bp/rhbp -> comparators -> Mux -> subtract -> pg_total -> add -> raw
-> compare -> free -> compare -> have_room
became ONE path where it had been constant-folded. Two fixes were needed, and
the first alone was not enough:
1. Register pg_first/pg_last/pg_total and rrp_s. They change only when the GC
writes BP/RHBP/RRP, so a cycle of staleness costs nothing, and `free`
already reaches the FSM through the registered have_room. This got it to
~20 MHz, still failing.
2. Narrow them from 8 bits to 4. Ring pages are 1-15. An 8-bit variable-vs-
variable comparison is a carry chain where variable-vs-CONSTANT folds into
a couple of LUTs -- precisely what making the bounds dynamic had thrown
away. geom_ok guarantees bp < rhbp <= 0x0F, which makes the narrow slice
safe.
Now 26.90 and 28.50 MHz on seeds 1-2, against 28.00/29.21 for the build
without the geometry change.
Attribution needed an A/B: the register-space decode (item 4) landed in the
same stretch and had never been swept on its own. Measured separately it
closes 28.0-29.2 MHz and costs nothing, so the whole regression was the
assembler. The critical-path report named rx_frame_assembler.py:215 -- the
rrp clamp -- which is what pointed at the comparator width rather than at the
arithmetic everyone would suspect first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
147ef8fcfc |
Honour the GC's ring geometry (BP / RHBP) instead of hardcoding it
Compat TODO item 2, the last silent-corruption risk. RXFrameAssembler took the RX ring as a compile-time constant (pages 1-15), so a driver configuring a SMALLER ring was ignored and we kept writing pages it would never read, with nothing reporting an error. The bounds now come from bp/rhbp, wired in BBATop from the GC's BP (0x0A/0B) and RHBP (0x1A/1B) registers, and both the page wrap and the free-space calculation use them. Garbage is rejected rather than obeyed: the driver's values are used only when bp >= 1 (page 0 is the TX buffer), rhbp <= 0x0F and rhbp > bp. Otherwise the hardcoded layout stands and geom_honoured goes low, so a mismatch is observable. Without that check BP=0 or an RHBP below BP gives a zero or negative ring size and the assembler writes outside it. ⚠️ Recorded as a known limitation: a ring must be LARGER than _MAX_FRAME_PAGES (6) or nothing is ever stored, because a frame's length is unknown until EOF so the worst case (1518+4 = 6 pages) is reserved up front. Found the honest way -- the test was written with RHBP=4 first and passed VACUOUSLY, storing nothing at all. Making that work needs a length-aware reservation, which is a bigger change than this one. T7 (8-page ring honoured, frames only in pages 1-8, and at least one actually stored) and T7b (all three garbage cases rejected, fallback still stores). Mutation-tested: wrapping at the hardcoded page 15 writes pages 9-11 and fails T7; removing the sanity check fails all four T7b assertions. Item 1 (RX filtering) remains the only open gap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4a455bbe88 |
Decode NCRB and the registers with non-zero hardware defaults
Compat TODO items 3 and 4. Every register libogc2 writes now stores writes and reads back, with the documented hardware reset defaults: NCRB (0x01), BP (0x0A/0B), TLBP (0x0C/0D), RXINTT (0x14/15), RHBP (0x1A/1B), NWAYC (0x30, default 0x84), GCA (0x32), MISC (0x3D, default 0x3C), MISC2 (0x50). BP and RHBP reset to OUR layout (page 1, page 15) so a driver reading them back without writing sees something consistent with where the ring actually is. Previously all of these read 0x00 and dropped writes, so a driver doing read-modify-write or sanity-checking a default saw zero -- and libogc2 does read NWAYC before writing it. Deliberately still reading 0x00: LTPS, LRPS, TWP, TRP, TXFIFOCNT. Those are STATUS registers, not configuration, so storing what was written to them would be a lie rather than a default. Drivers read LRPS/LTPS only on error bits and 0x00 reads as "no error", which is truthful for a device that does not detect them. NCRB, BP and RHBP are also exported as BBARegisterFile.ncrb/.bp/.rhbp for the RX-filtering work (item 1) and the ring-geometry check (item 2). Nothing consumes them yet -- the decode lands first so the behaviour that uses it is a separate, testable change. T15 checks the defaults, replays libogc2's twelve init writes in its order and reads every one back, and asserts the NCRB export is 0x52 with PR=0, PM=0, AB=1 -- i.e. that the real BBA delivers own-MAC + broadcast only, which is the fact item 1 has to act on. Mutation-tested: dropping NWAYC's 0x84 default fails it, as does dropping NCRB writes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e10849f73d |
docs: track the BBA compatibility gaps as TODOs
Records the differences between this implementation and what real GC software expects, with the evidence and the driver source for each, so they are worked through rather than rediscovered. Open items: no hardware RX filtering, hardcoded ring geometry, NCRB undecoded, and registers that read 0x00 where real hardware has non-zero defaults. Also records what is deliberately NOT to be implemented (the device-initiated challenge/response) and that two of our registers (TWD, HIPR) do not exist on real hardware. Notes explicitly that games use the official Nintendo SDK driver, which none of the three readable drivers is, so "no open driver does this" is weaker evidence than "the hardware has no such register". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
530d58df4f |
Record the measured timing cost of the command register file
A/B on identical seeds rather than comparing against a different sweep, since
seed-to-seed spread on this design is larger than the effect being measured:
seed 1 clk seed 2 clk SB_DFFE SB_LUT4
before 28.72 27.04 934 2818
after 29.07 26.39 1038 2959
So +104 FFs, +141 LUTs, and about -2.4% fmax on the worst seed while seed 1
improved -- most of the spread is placement noise over a modest real cost.
8/8 seeds still close; best seed 3 at 29.74 MHz is 1.24x over the 24 MHz
target.
Also records why the storage covers the full low nibble rather than only the
registers libogc2 touches: a sparse decode would silently drop writes to
anything unlisted, which is exactly the behaviour this replaced. Noted as the
obvious thing to narrow if clk margin ever needs reclaiming.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
7116bc1ecc |
Implement the command registers libogc2 actually touches
Previously every command register except 0x02/0x03 read 0x00 and dropped writes. Real software will hit them, so they are now implemented, enumerated from libogc2's gcif.c rather than guessed at random (Swiss's patch driver touches only 0x02 and 0x03): 0x01 read revision ID -> priv->revid 0x02 write interrupt mask (IRMASKALL / IRMASKNONE) 0x03 read / w1c interrupt status 0x04 write 2 bytes priv->devid = 0xD107 0x05 write priv->acstart = 0x4E 0x08 read 4 bytes challenge | device-initiated path, 0x09 write 4 bytes response | gated on status bit 0x10 0x0B read challenge/response status 0x0F read (slow) in __bba_reset(), result DISCARDED 0x01 returns 0x00 and is read-only. That is a real-hardware value, not a guess: the BBA never received a functional firmware revision that incremented it. libogc2's priv->revid = 0xf0 is its PRE-INIT PLACEHOLDER, overwritten by whatever this read returns, so it must not be mistaken for the device value. revid/devid/acstart feed only bba_calc_response(), whose path is device-initiated, so they are functionally inert for us. 0x04 and up are plain read/write storage, which IS a guess: both drivers treat them as write-only and never read them back, so read-what-you-wrote is the least surprising behaviour and lets our tests confirm the write landed. Ignoring them (the old behaviour) is strictly worse -- the write vanished. Registers above 0x0F still read 0x00 so a stray high register cannot alias onto a real one. Noted for later: bba_cmd_in8_slow(0x0F) sits in __bba_reset() between two 10 ms delays, holds CS for 200 us and discards the result. A read whose value is thrown away but whose timing is deliberate looks like a reset strobe; we treat it as an ordinary register, and the NCRA reset follows immediately in that sequence, so ignoring the side effect costs nothing today. T14 covers the whole table and is mutation-tested: making 0x01 plain storage fails it, and dropping the 0x04+ writes fails it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
49d7e8fcff |
Correct the framing: the two command forms are padding, not protocols
My earlier note read as "Swiss's menu and a patched game speak different protocols to the same chip", which is both suspicious-sounding and wrong. Both send the IDENTICAL 2-byte header [0x40|reg, 0x00] and differ only in how many bytes follow, which the device never cared about -- the data phase runs until CS, exactly as in register space. Swiss's 4-byte form is an EXI efficiency choice: one exi_imm_read_write(..., 4) is a single EXI immediate operation, where libogc2 spends two separate EXI_ImmEx calls. For code injected into a running game, halving the EXI operations per register access is worth a padding byte. Menu vs game is not a protocol split either, just which library is linked: cube/swiss/source/bba.c contains no BBA register access at all -- its header exposes only network config, detection and display over <network.h> -- so the menu is entirely libogc2, and the patch driver is Swiss's own leaner code. Verified: the only menu-side device access is EXI_GetID for detection and MX98730EC_GetExiSpeed, which just returns EXI_SPEED32MHZ (confirming Swiss hardcodes speed index 5 for the real BBA), while bba_requires_init() returns id != EXI_MX98730EC_ID, i.e. the real BBA needs no Swiss-side init. Also sharpens the multi-byte caveat. No single rule satisfies both Swiss's padded single-byte write and libogc2's bba_cmd_outs(0x04, &devid, 2), so the real device's behaviour there cannot be deduced from the drivers -- and it is unobservable in them anyway: devid is the hardcoded constant 0xD107, is never read back, and bba_cmd_out8(0x05, acstart) on the next line overwrites half of it under auto-increment. Settling it would need a real BBA on a logic analyser, so the note now says not to try it from source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0d100eb84f |
Implement the COMMAND-level interrupt: without it no driver sees a frame
The BBA has two interrupt levels and we implemented only one. Both drivers' handlers are identical in shape -- libogc2's bba_event_handler and Swiss's exi_coroutine: status = bba_cmd_in8(0x03); /* COMMAND level */ bba_cmd_out8(0x02, BBA_CMD_IRMASKALL); if (status & 0x80) bba_interrupt(); /* ONLY then the MAC level */ and bba_interrupt() is what reads register-space IR/IMR and calls bba_start_rx(). Bit 7 of command register 0x03 means "a MAC interrupt is pending". r_cmd_ir was write-1-clear with nothing ever setting it, so a command read of 0x03 returned 0x00, every branch of both handlers fell through, and NEITHER DRIVER WOULD EVER HAVE PROCESSED A RECEIVED FRAME. Our own tooling could not have caught it: gc_bba_test and netboot poll RWP/IR directly and never use the interrupt path, so the whole command-level mechanism was untested and unused. Green suite, dead interface. cmd_ir[7] is now a live level equal to (IR & IMR) != 0, and INT_N is (cmd_ir & cmd_imr) != 0. A level rather than a latch because both drivers clear the MAC IR inside bba_interrupt() and only then write-1-clear command 0x03: a latched bit would be cleared by that write even if a new frame had arrived in between, while a level stays asserted as long as the condition does. INT_N honouring the command-level mask is what stops the line re-asserting while a handler runs (IRMASKALL on entry, IRMASKNONE on exit). r_cmd_imr resets to 0xF8 since the real power-on value is unknown and both drivers set it explicitly first thing. T13 and T13b cover it, both mutation-tested: reading the latched bits only leaves 0x00 after an RI, and ignoring the command mask leaves INT_N asserted through IRMASKALL. Also records in CLAUDE.md which driver actually talks to us, since Swiss has several back-ends and it is easy to read the wrong one: cube/patches/bba/ has one file per chip (bba.c is ours; enc28j60.c, w5500.c, w6100.c, w6300.c are other adapters), Swiss's menu networking goes through libogc2's gcif.c, and cube/patches/bba/bba.c is the in-game patch driver. Detection is by EXI device ID, so returning 04 02 02 00 is what gets us handed to the BBA path at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
88d05988b5 |
Document that the command decoder is one mechanism, and its real limit
Records why supporting both command-space forms is a single decoder rather than two paths, and where that abstraction genuinely stops. The unification: Swiss's "4-byte transfer" is not a second protocol. From the device's side it is the same 2-byte header followed by a data phase that happens to be 2 bytes (00, val) instead of 1 (val). Command space was the last place still assuming a fixed transfer length, which register space already avoids. Both rules are length-agnostic -- last data byte wins, fill the first two data slots -- so there is no driver detection, no mode flag, and with comments stripped the words "Swiss" and "libogc" do not appear in the RTL. The limit, which is real but inert: a 2-byte data phase means "pad + value" (Swiss) or "two consecutive registers" (libogc2), and those cannot be told apart on the wire. libogc2 does do multi-byte command writes -- bba_cmd_outs(0x04, &priv->devid, 2) runs unconditionally in its init -- so last byte wins is wrong for those and auto-increment is wrong for Swiss. It does not matter today because the only command registers implemented are 0x02 (IMR) and 0x03 (IR), which both drivers access one byte at a time, while libogc2's multi-byte accesses target 0x04/0x05 and 0x08/0x09, none of which are decoded. If 0x04/0x05 are ever implemented the fix is per-register width knowledge, not a driver mode -- noted so nobody resolves it by sniffing transfer length. Also records that the challenge/response handshake must NOT be implemented: libogc2 reads a 4-byte challenge from command reg 0x08 and writes the response to 0x09, but only when the DEVICE raises status bit 0x10 in command reg 0x03. It is device-initiated, we never set that bit, so the path cannot be entered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b3685237b2 |
Support BOTH command-space forms: Swiss's 4-byte and libogc's 2-byte
Reading Swiss's own driver (cube/patches/bba/bba.c) shows the previous commit
fixed libogc and BROKE Swiss. The two disagree, and both are real:
Swiss bba_cmd_out8: exi_imm_read_write(0x40<<24 | reg<<24 | val, 4)
-> ONE 4-byte transfer [0x40|reg, 00, 00, val], and bba_cmd_in8
returns the LOW byte, so a read response must be in BYTE 3.
libogc2 bba_cmd_outsnosel: req = (reg<<8)|0x4000;
EXI_ImmEx(&req, 2, WRITE); EXI_ImmEx(val, len, WRITE)
-> THREE bytes, value in byte 2, and a read takes byte 2.
stock libogc: identical to libogc2 (read out of libbba.a).
They share bytes 0 and 1 and differ only in how many bytes follow, so one
decoder serves both: take the LAST data byte before CS rise as the write value
(Swiss sends [00, val], libogc sends [val] -- last is val either way), and push
a read response TWICE so bytes 2 and 3 both carry it. Swiss clocks 4 bytes and
pops both; libogc clocks 3 and leaves one, which the next transaction's header
pops drain -- a 2-byte header gives exactly one drain slot, so the leftover is
bounded at one and cannot accumulate.
Critically this means NO auto-increment of cmd_reg across the data phase: that
applied Swiss's padding 00 to the register and val to reg+1, which is precisely
how the libogc-only version broke Swiss.
T12 covers the Swiss form and asserts bytes 2 AND 3; T10 keeps the libogc form.
Both mutation-tested: re-adding the auto-increment fails T12 with 0x00 in both
slots, and pushing the response once fails T12's byte 3. gc_bba_test gains
"cmd-space Swiss" alongside "cmd-space write", with bba_cmd_write4/read4.
Also corrects two things in CLAUDE.md now checkable against three drivers:
- NO driver writes TWD -- not Swiss, not libogc, not libogc2. The "GC sets
TWD before streaming" note was self-confirming: t_tx writes TWD because we
wrote t_tx. The counted TXDATA length is the primary path, not a fallback.
- Swiss's exi_dma_write rounds the length UP to 32 bytes (OSRoundUp32B), so
TXDATA gets up to 31 trailing bytes past the frame and a counted length sends
that much extra. Real hardware must do the same since Swiss works on it;
trailing padding past the IP total length is ignored. A 1514-byte frame
rounds to 1536 though, over the 1518 maximum.
Both drivers trigger TX with NCRA ST1, which the existing any-ST test accepts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
71ec6a2b37 |
TX: count the TXDATA bytes, because libogc never writes TWD
Third contract checked against libogc rather than our notes. __bba_link_tx is four transactions -- read NWAYS, ONE register-space write of the whole frame to 0x0048, read NCRA, write NCRA = 0x04 -- with no TWD_LO/TWD_HI anywhere. So the device has to count the bytes it was handed. The NCRA ST fallback queued tx_frame_len, which only TWD sets, so any driver that skips TWD would have transmitted a stale or zero length. BBARegisterFile now keeps txdata_cnt, incremented per byte accepted at 0x0048 and cleared whenever a frame is queued from either path, and the ST path queues that. T11 drives TX in libogc's shape with no TWD write and checks both the queued length and the drained bytes. Mutation-tested: switching the ST path back to tx_frame_len queues length 0, which is exactly the bug. gc_bba_test gains the hardware counterpart, "TX without TWD", which deliberately omits the TWD writes that t_tx makes. The TWD path stays and is still preferred when present: writing the length before streaming is what lets TXFrameDrain consume concurrently and keeps tx_bytes a rate-matching buffer rather than whole-frame storage. Swiss writes TWD, libogc does not, so both are supported. Also recorded: libogc streams the payload with EXI_Dma (32-byte-aligned bulk after PPCDCacheStoreAsync, then EXI_ImmEx for the remainder). CLAUDE.md said EXI DMA writes are avoided for the GC's 1-bit-shift bug; that is true of Swiss, not libogc. Nothing in the gateware changes -- a DMA write is just bytes clocked in -- but the path will be exercised, and if that bug is real it appears as corrupted TX frames that are not fixable here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0aa1fa884a |
Fix command-space writes, and replay libogc's real init sequence
Second protocol mismatch found the same way as the descriptor: by disassembling libogc's own driver instead of trusting our notes. COMMAND SPACE IS A 2-BYTE HEADER + DATA PHASE, not a single 4-byte transfer. CLAUDE.md said [0x40|reg, 00, 00, data] and the decoder followed it, waiting for a fourth byte and consuming the DATA byte as header byte 2 -- so every command-space WRITE was silently dropped. libogc's __bba_init writes command register 2 (its interrupt mask) at both the start and the end of init, so none of that took effect. What it actually sends: EXI_Imm (len=2, write) -> 42 00 the whole header, 0x4000 | (reg << 8) EXI_Sync EXI_ImmEx(len=1, write) -> 00 the data Three bytes between CS fall and CS rise. The device-ID read survived the wrong decode only because SPIMode3Slave answers it on a fast path independent of this FSM -- which is precisely why no test caught it: the only command-space traffic under test bypassed the decoder being tested. CHDR_2/CHDR_3 are replaced by CMD_WRITE, which takes the data byte straight after header byte 1 and auto-increments the command register so a multi-byte command write behaves like the register-space path. cmd_wreg latches the target because the write applies a cycle later, by which time cmd_reg has moved on. bba_register_file T10 writes command register 2 and reads it back, and is mutation-tested: restoring the 4-byte decode fails it. Also replaces the GUESSED init sequence in CLAUDE.md with the real transaction list. The old one was wrong in three ways that matter: libogc READS the MAC out of PAR0-5 rather than writing one (so our reset value is what the console uses), IMR is 0xDF not 0x86, and it writes RWP (0x0016/17) and RHBP (0x001A/1B) which the register file does not accept. The latter is harmless today -- RHBP is 0x000F, matching our hardcoded 15-page ring -- but a driver configuring a smaller ring would be ignored while the assembler kept writing outside it. New gc_bba_test suite 7 "libogc init" replays the sequence on hardware: cmd-space write (2-byte header, read back), MAC readable (one 6-byte PAR read, Nintendo OUI), and init replay (all 30 writes in order, then NWAYS/HIPR/NCRA.SR must still read correctly -- the old guessed header format turned a NWAYS read into an NCRA reset, so a misdecoded write is a real risk). bba.c gains bba_cmd_write/bba_cmd_read in libogc's shape. Every module testbench and bba_top at EXI_HALF=1/2/8 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d83f7beef8 |
Fix the RX ring descriptor to the format libogc actually parses
The assembler wrote 00 00 len[11:8] len[7:0] with bytes 0 and 1 zero. The real format, read out of libogc's own BBA driver, is a PACKED 32-BIT LITTLE-ENDIAN word: next_packet_ptr in bits 0-11, packet_len in bits 12-23, status in bits 24-31. Under that driver our descriptor for a 342-byte frame decoded as a 12-byte frame with a next-pointer of 0 into the reserved page, so Swiss and anything else built on libogc would have initialised perfectly and then misparsed every received frame. Found by disassembling /opt/devkitpro/libogc/lib/cube/libbba.a (gcif.o, bba_interrupt) rather than by guessing: lwbrx r9,0,r9 # 4 bytes as a little-endian word rlwinm r4,r9,20,20,31 # packet_len = (word >> 12) & 0xFFF addi r4,r4,-4 # payload = packet_len - 4 cmpwi r4,1540 srwi r9,r9,24 # status = word >> 24 and. r9,r9,r18 # r18 = 0x..84 -> errors are 0x80 | 0x04 and the low 12 bits are written straight back to RRP (0x0018/0x0019), which is why next_packet_ptr has to be there at all -- it is the driver's read pointer. The length semantics and addressing were already correct (descriptor at page<<8, body at +4, length including the descriptor); only the packing was wrong and next_packet_ptr was never written. RXFrameAssembler now computes the next page BEFORE writing the descriptor (CALC_NEXT, formerly ADVANCE_RWP, moved ahead of the backfill) and writes bytes 0-3 as WRITE_D0..WRITE_D3. WRITE_D3 fixes a latent bug this exposed: WRITE_PLACEHOLDER issues four writes but the first lands twice, so desc_base+3 was NEVER written. That was harmless while the length write covered byte 3. As the status byte it would have been whatever the previous frame left in that cell, and a stale 0x80 or 0x04 makes the driver discard the frame. Readers updated to decode identically: netboot (which also now takes RRP from the descriptor's own next_packet_ptr, as libogc does, instead of recomputing the page span) and t_dma, including its page map. netboot's +0/+1 offset probe is removed -- the underrun that shifted descriptors is fixed and confirmed on console, and under a packed format a +1 read is not a shifted descriptor but garbage. The assembler's testbench now decodes with libogc_decode(), reproducing the instruction sequence above, so the tests check the DRIVER'S algorithm rather than our own packing. Every module testbench and bba_top at EXI_HALF=1/2/8 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5ee1b9d07b |
gc_bba_test: tell a spent id_phase from a real fault using the hardware
The ID tests guarded on g_ncra_reset_done, which is software state standing in for hardware state. The two come apart exactly when a build is delivered over the wire: netboot replaces the DOL and restarts the app with the flag clear, while the FPGA keeps running with id_phase already spent by the previous run. The app then queries address 0, gets NCRA back -- correct behaviour -- and reported a FAILURE for it. That is why the ID tests would sometimes skip and sometimes fail for the same expected condition. Both tests now ask the hardware instead. HIPR and NWAYS are hardwired constants in the register file and do not depend on id_phase, so if both read correctly the EXI path is demonstrably fine and a wrong answer at address 0 can only be a spent id_phase -> SKIP, with "reflash FPGA" in the message. If they are wrong too, something real is broken and the test still FAILS. The flag is kept only as a fast path for a reset issued during the same run, with a comment saying it can be stale. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2b0cbdd809 |
gc_bba_test: free the RX ring before waiting for a frame
t_rx reported "no frame in 2 s (RWP still 0x000F)" on a healthy adapter after the Ethernet suite had been run a few times. Page 15 is the giveaway: the app never writes RRP, so ambient broadcast traffic fills the 15-page ring until free = (1 + 15 - 15) mod 15 = 1, below _MAX_FRAME_PAGES (6). RBFI then drops every frame -- correctly, that is what it is for -- RWP stops advancing permanently, and a test that waits for RWP to move can never pass again for the rest of the session. t_rx now catches RRP up to RWP before waiting, declaring what is already in the ring consumed. That is what a real driver does once it has read the frames out and what netboot already does at start-up. It also leaves exactly the newly received frame unconsumed at RRP, which is where the SPRAM/DMA suite reads, so t_dma still has something to parse. No gateware change: the drop behaviour was correct and the bug was in the test app's failure to consume. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e20b34b5e9 |
gc_bba_test: report where ring frames actually are when t_dma fails
t_dma said "p1 len 0x0002 implausible (RWP=11)", which names the symptom but
not the cause: a length alone cannot distinguish an unwritten descriptor from a
shifted read from a frame written at a page other than the one RRP points at.
On failure it now walks all 15 ring pages, classifies each descriptor ('-'
all-zero/never written, 'o' plausible length, 'x' non-zero but implausible,
'?' read failed) and prints that map plus the 4 raw descriptor bytes at the RRP
page. One run then shows which of those it is.
Diagnostic only; no change to the pass/fail criterion or to any gateware.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
abab26232b |
Fix the +1 descriptor offset: drop the arbiter's response register
SPRAMArbiter latched the SPRAM word into rsp_buf and raised exi_rsp_valid a cycle later. BBARegisterFile already took the response the cycle it arrived, so the buffer bought nothing and cost a 24 MHz cycle (~42 ns) inside a deadline that was short by about that much. Both are now combinational in the cycle SPRAM's output is valid. The data sampling is unchanged -- the old code read spram_dout in that same cycle, it just registered it before handing it over -- so only the handoff moved. bba_top T9 goes from 1/6 to 6/6 byte-exact at EXI_HALF=1, and stays 6/6 at 2 and 8. The whole suite passes at the hardware rate. Nothing else changed: BBARegisterFile is untouched. Found by measuring instead of guessing, after four falsified attempts. A counter in ExiCapture recording tx_load & ~tx_fifo.r_rdy, with a shift-register history of which byte boundary underran, showed good and bad reads BOTH underrun at pop 3 (harmless, mid-header) while a bad read adds pop 4 -- the pop that loads data slot 0. So the late byte is the FIRST data byte, which rules out every "prefetch further ahead" idea, because byte 0's request cannot be issued before RHDR_LO where the low address byte arrives. That makes the deadline arithmetic with no contention in it: rx CDC ~125ns + arbiter 42 + SPRAM 42 + rsp_buf 42 + tx CDC ~110 = ~361ns against a 296ns byte time. Over budget idle, which is why fair arbitration changed nothing -- ETH contention was only what pushed a marginal path over on some frames. Two further changes were implemented, measured and then removed as unnecessary: issuing byte 1's request during the pad byte, and two requests in flight with a 3-deep response hold. With rsp_buf gone, T9 is 6/6 with both removed, with either alone, and with the register file reverted completely. They were real pipelining but not the constraint, and they cost logic on sync, the binding timing domain. CLAUDE.md records them as do-not-re-add, along with the trap that sank the earlier 2-outstanding attempt: incrementing `outstanding` in the FSM and decrementing it in the response handler are two m.d.sync assignments that Amaranth resolves by PRIORITY, not arithmetic, so the increment is silently discarded when both fire in one cycle. Arbiter TB updated (the response is a cycle earlier) and mutation-tested: re-registering the response fails T1/T2/T4, swapping the byte lanes fails T1/T2. New T4 pins exi_rsp_valid to exactly one cycle. Not yet confirmed on hardware; netboot's reader still probes both offsets. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
007c4d40f9 |
Record the 16-bit SPRAM response as falsified, and why
Returning both bytes of the SPRAM word per request was the one approach that looked like it addressed the real constraint rather than working around it. Implemented fully (16-bit exi_rsp_data + exi_rsp_odd in SPRAMArbiter, sp_buf/ sp_cnt pair-pushing in BBARegisterFile, address advance by 2, tests updated) and it made things WORSE: bba_top T9 went 6/6 / 3/6 / 1/6 at EXI_HALF=8/2/1 against the baseline's 6/6 / 6/6 / 1/6, breaking HALF=2, leaving the DE DE duplication at the hardware rate, and adding a new fault where the first read after an idle period returns all zeros. Reverted. The reason is worth keeping: the budget only doubles if the next request issues while the second byte is still being pushed, and it cannot, because that byte sits in a holding register the prefetch guard has to wait on. The sequence stays request -> response -> push -> push -> request, so round-trip latency per pair is unchanged and the first byte of each pair faces the same deadline as before. The constraint is unhidden round-trip latency, not bytes per round trip. Also corrects the recorded T9 ratio at EXI_HALF=1 from 2/6 to 1/6, which is what the committed test deterministically prints. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
bd4c69c5f2 |
Establish the +1 offset mechanism: tx FIFO underrun, not a stale byte
Reading the frame body at 0x104 (first byte a known 0xDE) instead of the
descriptor at 0x100 (first byte 0x00, indistinguishable from a stale byte)
shows what actually happens:
good: DE AD BE EF 01 02
bad: DE DE AD BE EF 01 <-- the FIRST BYTE IS DUPLICATED
So it is a tx FIFO UNDERRUN, not an inserted byte. When the FIFO runs dry for
one byte-time the bit engine reloads the stale FIFO head and sends the previous
byte again, shifting the rest of the read.
The register file must sustain one byte per 296 ns at 27 MHz. The round trip
is request -> spram_req SyncFIFO -> arbiter (ETH has absolute priority and
preempts it) -> SPRAM -> response -> push -> tx AsyncFIFO CDC (2-3 EXI cycles).
That is marginal even idle and over budget during a frame write, and only ONE
request may be in flight so the latency is never hidden.
THREE FIXES TRIED AND FALSIFIED, recorded in CLAUDE.md so they are not
repeated:
- Fair round-robin arbitration instead of strict ETH priority: still 2/6.
Bounds the wait to ~2 cycles but does not close the gap, and breaks
spram_arbiter T3. Reverted.
- Deepen tx_fifo 2 -> 8 for cushion: 0/6 with leading zeros. The engine
drains leftovers with ONE POP PER HEADER BYTE, so a deeper FIFO leaks
stale bytes into the data phase. tx_depth=2 is deliberate. Reverted.
- Allow 2 outstanding requests to pipeline: 0/6 at EVERY rate, including
HALF=8 which previously passed. With no response FIFO, responses must be
consumed the cycle they are valid; two in flight loses bytes. The
existing comment was right. Reverted.
Tree is back to the T9-reproduction baseline: 6/6 at HALF=8, 1-2/6 at HALF=1.
PROMISING DIRECTION, not attempted: SPRAM is 16 BITS WIDE and the arbiter
reads a word then returns one byte selected by address bit 0. Returning both
bytes per request would push two bytes per round trip, doubling the budget to
592 ns and making it comfortable. That is the only approach identified that
addresses the constraint rather than working around it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
2eba68a013 |
Reproduce the +1 descriptor offset in simulation (bba_top T9)
First time one of this session's RX faults has been reproducible on this side
rather than inferred from a console.
T9 performs EXI DMA reads from the SPRAM ring WHILE the ethernet side writes
frames into it. No earlier test did: T6 waits for the frame to complete before
reading, so reads and writes never overlapped -- and ETH has ABSOLUTE priority
in SPRAMArbiter, so only under that overlap can an EXI read be preempted.
EXI_HALF=8 6/6 byte-exact
EXI_HALF=2 6/6 byte-exact
EXI_HALF=1 2/6 byte-exact <-- the hardware rate
2-of-6 matches the console ratio closely (desc@+0=100 +1=343, 23% good), and
the corruption is exactly the hardware symptom -- ONE EXTRA BYTE inserted at
the front, shifting everything:
expected: 00 00 00 0C DE AD BE EF
got: 00 00 00 00 0C DE AD BE
That it is INSERTED rather than wrong is the useful part: an extra push into
the tx FIFO is a COUNT mismatch in the request/response path, not corrupted
SPRAM data. It only shows at 27 MHz because the read has to complete inside
the protocol's turnaround window while ETH preempts it.
This is the bug to chase first, ahead of the residual ACK retries: netboot
only works because its reader probes both offsets. Swiss does not probe -- it
would read a frame length of 1 -- so real networking on this adapter is broken
while our own tooling reports success.
bba_top now FAILS at EXI_HALF=1, deliberately. The failure is real and the
test is the reproduction; do not relax T9 to make the suite green. HALF=8 and
HALF=2 still pass for unrelated work.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
8691b09676 |
docs: record netboot throughput at 6.4 s and what txto=0 does and does not say
300908 bytes in 6.4 s (0.38 Mbit/s) with txto=0, down from 40.1 s and 16.9 s. The whole cost was round-trip, not bandwidth: stop-and-wait is one block per round trip. The counter-intuitive result is recorded deliberately: raising the host timeout 0.1 -> 0.3 barely changed the retry count (110 -> 95) but tripled the cost of each, making the transfer 2.4x SLOWER. Short is correct here because the GC re-ACKs a duplicate immediately, so a premature retry costs one timeout and nothing else. Anyone tuning this later will reach for a longer timeout first; the note says not to. Also corrects my own earlier claim. I concluded from the 0.3 s result that the misses were LOST ACKs rather than late ones. txto=0 shows every ACK reached the W5100 with TI set, so the drain and EXI side are clean -- but TI is not proof of transmission, and a 6.4 s total is hard to reconcile with ~95 genuine losses. The honest state is that it is not established either way, and the retry COUNT at 0.05 s is the measurement that would settle it. Recorded as open rather than fixed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ada9378628 |
Restore the TX-done clears to the TX handler; shorten the ACK timeout
TIMING MEASUREMENT, which is the useful part. Raising the host timeout
0.1 -> 0.3 barely moved the retry count (110 -> 95) but tripled the cost of
each one: 16.9 s -> 40.1 s. If those were LATE ACKs, three times the deadline
would have all but eliminated them. They are LOST ACKs, so a SHORT timeout is
correct, not a long one -- the GC re-ACKs a duplicate block, so retrying early
costs nothing. Host default is now 0.05.
REGRESSION, mine, from
|
||
|
|
04a797e7e7 |
netboot: stop the TI poll turning a slow ACK into a missed deadline
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> |
||
|
|
d67f111019 |
Don't advance the write address across TXDATA; deepen tx_bytes to 2048
Two coupled fixes, both exposed by batching TXDATA writes in the test app. 1. eff_addr advanced on EVERY data byte of a register-space write. But TXDATA (0x48) is a FIFO PORT, not a register: a whole frame is written to that one address. So an 8-byte write put byte 0 in TXDATA and bytes 1..7 into 0x49+, which the switch silently ignores. The frame was an eighth of its declared length, TXFrameDrain blocked waiting for the rest, and TI never set (hardware: txto=16, "no ACK after 20 tries"). SWISS WOULD HAVE HIT THIS. It writes an entire frame with bba_outs to BBA_WRTXFIFOD -- one transaction, one address -- so its TX would have delivered ONE BYTE per frame. The path was never exercised because the test app wrote TXDATA one byte per EXI transaction, giving every byte a fresh header at 0x48. Every other register must still advance, so that bba_outs can write PAR0-5 or MAR0-7 as a block; the exception is TXDATA only. 2. tx_bytes was 16 deep. SPI cannot back-pressure -- there is no way to tell the GC to wait -- so BBARegisterFile silently drops bytes when w_rdy is low, which makes the depth the only thing between a drain stall and a corrupted frame, not a buffering preference. 16 bytes is ~4.7 us at the EXI byte rate, and TXFrameDrain shares the W5100 bus arbiter with RX and the socket-3 UDP test, so any longer stall truncated a frame mid-flight. With (1) fixed, Swiss streams up to 1514 bytes in a single transaction, so 16 could never have held. Now 2048: a whole maximum-size frame plus margin, so a frame cannot be truncated even if the drain makes no progress for the entire transaction. Costs 3 of 27 free BRAMs (ICESTORM_RAM 3/30 -> 6/30, LC 68% -> 70%). Dennis spotted that the 8-byte workaround could not survive whole-frame writes, which is what prompted fixing the depth rather than shipping it. bba_tx_bytes() consequently streams 64-byte chunks (bba_write()'s buffer limit), so a 60-byte ACK is ONE transaction instead of sixty. T8 covers both and is mutation-tested: restoring the increment gives "1/8 bytes MISMATCH -- got [17], want [17, 34, 51, ...]". It now writes 64 bytes so it exercises the deepened FIFO too. --seeds 8: clk 27.6-29.4 MHz, EXI 51.2-64.7 MHz, 8/8 PASS. Best seed 1 (clk 29.35, EXI 60.80), flashed. STILL NO FLOW CONTROL on the TX path: a frame larger than 2048 would drop silently rather than error. Fine for ethernet's 1518 ceiling; revisit if the multiworld socket work ever wants larger payloads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1a1557ff54 |
Add R on the main menu to boot Swiss and leave the test app
Getting back to Swiss previously meant power-cycling the console. R boots /boot.dol from the SD card directly. It reuses netboot_boot_file(), so there is ONE DOL launcher and one set of safety checks (MEM1 entry point, section overlap against the reserved staging region, libfat unmounted before the jump) rather than a second copy to keep in step. Confirmed rather than one-touch, and main-menu only: R is an easy trigger to catch by accident, test results are not saved anywhere, and losing a full run to a stray press is worse than one extra button press. Inside a suite R would be easy to hit while navigating, so it is ignored there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1e370deb49 |
Bound the TXDATA chunk to half the tx_bytes depth
|
||
|
|
a2e8e5ceaf |
Batch TXDATA writes; record netboot working and the +1 offset as open
Self-update is proven end to end on console: 300076 bytes in 294 blocks, verify OK, /boot.dol boots, and new builds are now delivered this way instead of by SD card. bba_tx_bytes() wrote TXDATA byte by byte with bba_w8(), i.e. a full EXI_Lock / EXI_Select / header / EXI_Deselect / EXI_Unlock PER BYTE -- about sixty transactions to send one 60-byte ACK. Hardware showed txto=0, so every ACK did reach the W5100 with TI set; the ~1/3 of blocks the host retried were not transmit failures but ACKs that missed the host's timeout. The data phase of a register-space write runs until CS, so there was never a reason to pay the per-byte cost. It now streams in 64-byte chunks, one transaction each (64 is bba_write()'s buffer limit, not a protocol limit). t_tx() benefits equally. CLAUDE.md records the facility, including that it CANNOT be driven from the devcontainer -- raw ethernet is link-local and the container has a veth, so the sender must run on a host holding the physical NIC. Also records the +1 descriptor offset as OPEN, with what has been ruled out: not overflow (lapped=0 under RBFI), not the two dropped-write handshake bugs (fixed and mutation-tested), not write-port contention (ETH has absolute priority so the port is never stalled; T5 models a stall anyway and the layout holds), and not the late-response leak (guarded; ratio unchanged at desc@+0=100 +1=343). And names the test nobody has written: a bba_top-level EXI DMA read from the ring WHILE the ethernet side writes into it. ETH preempts EXI reads, so only that overlap can delay a response past its transaction. Write it before guessing again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
799692a5f4 |
netboot: stop with the summary before booting Swiss
The transfer completes on hardware -- 293/293 blocks, 299908 bytes in 15.7 s --
but roughly a third of blocks needed a retry, and the numbers that say WHY are
printed by the GC and then scrolled away when Swiss boots. So the one side
that knows whether an ACK failed to transmit or was lost on the wire could not
be read.
netboot now stops after writing the SD card and waits for a button press, and
the summary carries the counters that separate the cases:
desc@+0=N +1=M bad=N lapped=N txto=N
txto is the discriminator: it counts ACKs where TI never set, meaning the
frame never reached the W5100. High txto alongside the host's retries points
at the TX path; txto near zero points at frames sent but lost.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
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>
|
||
|
|
e9b1a75bfe |
Discard SPRAM responses with no request outstanding
SPRAMArbiter gives ETH writes ABSOLUTE priority -- eth_wr_ready is asserted
combinationally whenever eth_wr_valid is high, so the write port is never
stalled. Two consequences worth recording:
- The T5 stall model added to rx_frame_assembler does NOT reflect hardware.
Its value was eliminating a hypothesis, not modelling reality.
- EXI READS are what get preempted, so a read response can be delayed --
including past the end of the transaction that asked for it.
The register file did not guard against that. The response handler pushed
whatever arrived, and HEADER0 reset `outstanding` and `sp_req` but not
`sp_pending`. So a late response was pushed into tx_fifo anyway, a held byte
leaked across the transaction boundary, and `outstanding - 1` from zero
underflowed a 4-bit counter to 15 -- which then broke the prefetch bound for
the rest of that transaction.
Now guarded on (outstanding != 0), and sp_pending is cleared in HEADER0.
WHAT THIS DOES NOT CLAIM. I cannot demonstrate that this causes the +1
descriptor offset seen on hardware (17 of 22 frames, varying frame to frame).
A test written in bba_register_file passes with AND without the guard --
checked both ways -- because that testbench models TX as a capture list per
transaction, with no persistent tx FIFO and no header drain pops, which is
precisely the mechanism by which a leaked byte would shift the next read. A
test that cannot fail is worse than no test, so it is deleted rather than
banked, and the gap is documented where it was.
THE MISSING TEST, recorded in bba_register_file.py: a bba_top-level test that
performs an EXI DMA read from the SPRAM ring WHILE the ethernet side writes
frames into it. Only under that overlap can a response be delayed past its
transaction. No simulation does this today, which is why the offset is still
formally unexplained -- the fix here is a real latent bug, not a proven cause.
--seeds 8: clk 28.1-30.3 MHz, EXI 54.9-62.3 MHz, 8/8 PASS. Best seed 7
(clk 30.26, EXI 61.09), flashed. Gateware only -- .dol unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
f62afff6f3 |
netboot: actually TRANSMIT the ACK
"timeout mid-transfer (22 seen, 0 bad, 0 lapped, desc@+0=5 +1=17)". Reception now works -- 0 bad, 0 lapped, and the descriptor locator handles both offsets -- but 22 frames seen is almost exactly the sender's 20 retries of block 0. It never saw an ACK and gave up. send_ack() called bba_tx_bytes() and nothing else. That helper only streams bytes into the TXDATA FIFO: it does not write TWD (the frame length) and does not fire the NCRA ST trigger. So every ACK was written into the FIFO and never put on the wire. Entirely my bug -- t_tx(), which is proven on hardware, has done the full sequence all along and I did not follow it. send_ack() now matches t_tx(): clear IR, write TWD_LO/TWD_HI, stream the bytes, then write NCRA SR|ST0. TWD must come BEFORE the stream -- that is what queues the length so TXFrameDrain consumes concurrently with the EXI writes; without it the drain waits for a length that only arrives at the ST trigger and the whole frame must fit the 16-deep FIFO, which is the truncate-at-16-bytes bug. It then waits briefly for TI and counts failures as txto=N in the status line. TI is not proof the frame reached the wire, but its absence is a clear failure. Blocking here costs nothing: stop-and-wait means the host is idle until the ACK lands anyway. Also recorded: the descriptor offset is genuinely MIXED on hardware, +0 for 5 frames and +1 for 17 in one run. A race in the assembler, still unexplained, and not reproducible in simulation -- T2/T5 keep the descriptor at +0 with the write port free-running and stalled 1-in-3. The locator makes the transfer work regardless while the counts stay visible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4831c1353c |
netboot: locate the descriptor, report the offset split, stop scrolling
Hardware with RBFI active: "timeout mid-transfer (5 seen, 18 bad desc, 0
lapped)", plus a frame dump of "1E FF FF FF FF FF FF 00".
Three things that tells us:
- lapped = 0, so the ring is no longer being overwritten. RBFI works and
overflow is eliminated as the cause.
- got > 0 ("mid-transfer"), so some blocks DID arrive and match.
- 0x1E is 286 & 0xFF, the low byte of total_len, followed by the full
six-byte broadcast MAC and then 0x00. The frame body is intact but starts
one byte later than the reader expects.
18 of 23 frames rejected while 5 parsed in the SAME run means the offset
VARIES frame to frame -- a race in the assembler, not a constant. Simulation
does not reproduce it: T2/T5 put the descriptor at +0 and the body at +4 with
the write port both free-running and stalled 1-in-3, and the mutation controls
for both known handshake bugs fail as expected. So the cause is still unknown
and I am not going to guess at it a fifth time.
ring_next() now LOCATES the descriptor instead of assuming +0: it probes
offsets 0 and 1 and takes whichever yields a plausible length (> 4 descriptor
bytes, <= a legal frame), then reads the body and advances RRP from that same
offset. A length is a strong discriminator, so this is a measurement rather
than a guess, and it both lets the transfer proceed and COUNTS the split --
"desc@+0=N +1=M" on success and on timeout. That distribution is the
measurement needed to find the real gateware bug.
Also trimmed the on-screen guidance: the console holds ~25 lines and the
explanatory text was pushing the failure diagnostics off the top, which cost
two blind round trips. Status during a transfer is now one line, and the
menu blurb one line.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
2aa50a7cc3 |
Reset the assembler's own write pointer on NCRA reset too
Hardware: "timeout: no match (0 seen, 0 bad desc, 0 lapped)". Zero frames
reached the ring at all -- a regression from
|
||
|
|
f69b4d084a |
Model write-port stalls in the assembler testbench (T5)
eth_wr_ready was held constantly HIGH by every test this file ever had, which
is a serious blind spot: on hardware the SPRAM arbiter stalls that port
constantly, because the GC reads the ring over EXI while frames are written
into it. Any address bookkeeping that depends on ready timing is invisible
until ready actually goes low.
T5 repeats T2's 300-byte frame with ready asserted 1 cycle in 3 and requires
the descriptor and body offset to be IDENTICAL to the unstalled case.
Result: contention does NOT reproduce the one-byte body offset seen on
hardware. Under stalls the descriptor is still [0,0,1,48] and all 300 body
bytes land at base+4. So that hypothesis is eliminated, which was the point
of writing it.
Two harness faults found and fixed while building it, both of which produced
false results rather than no result:
- rx_irq and rx_overflow are 1-cycle pulses, and rx_overflow fires on the
very tick that consumes the EOF byte -- inside send_frame. Polling for it
afterwards missed it entirely, so a correctly DROPPED frame looked like a
stall. Both are now latched in the tick helper.
- send_frame read rx_ready BEFORE tick() applied the next eth_wr_ready.
rx_ready is combinational on it, so the loop was a cycle out of phase with
the accept the DUT actually made: it waited on a byte already taken, and
reported "rx_ready never asserted" for a frame that had stored correctly.
Ready is now applied, then read, then clocked.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
92f7d716d9 |
Add RBFI ring-overflow protection: drop frames instead of overwriting
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>
|
||
|
|
a16d6f0432 |
netboot: stop the diagnostics slowing the loop they diagnose
Hardware dump: "frame 1 (110 B) etype=6c86 at 12: 72 33 33 00 00 00 16 00".
33 33 00 00 00 16 is an IPv6 MLDv2 multicast MAC from the host, but it sits at
offset ONE, behind a leading 0x72 -- and 0x72 is 114, the low byte of the
W5100's MACRAW 2-byte length prefix. Hence 6c86 at offset 12: the last byte
of the source MAC and the first of 86DD, both one byte late.
An earlier dump in the SAME build read FF FF FF FF FF FF 00 2B, perfectly
aligned. Two frames, one build, different alignment -- that is a lapped read
of a partially overwritten page, not a systematic off-by-one. The alignment
fix is intact.
Two self-inflicted costs removed from the poll loop, whose speed is exactly
what decides whether the writer laps us:
- The 32-byte hex dump ran on EVERY frame, adding an EXI transaction per
iteration. The diagnostic was making the fault it was diagnosing worse.
It now runs only when a descriptor is actually rejected.
- RRP was re-read from the device every poll, costing another transaction
for a value we had just written ourselves. It is now tracked locally and
kept in step at the three places that move it.
That takes the per-frame cost from six EXI transactions to four. It does not
make the race winnable on a busy link -- the ring is 15 pages and has no
overflow protection -- but it stops me handing back a loop that is slower than
the one that produced the measurement.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
1327f82719 |
netboot: detect being lapped by the ring writer; quiet-link guidance
The alignment bug is FIXED -- hardware now reports "frame 1 (342 B) etype=0800 at 12: FF FF FF FF FF FF 00 2B", i.e. broadcast dst at 0, host MAC at 6, ethertype at 12. Perfectly aligned, correctly received, correctly skipped as not ours. So 00 00 00 01 was never a dropped write. It is frame data being read as a descriptor, because the ring HAS NO OVERFLOW PROTECTION: nothing in the gateware consumes RRP, so RXFrameAssembler keeps writing and laps the reader (the real BBA has RBFI for exactly this). The 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 15 pages -- roughly 3 to 11 frames. Under continuous traffic pages are reused before they are read. That is also why it "worked once out of three tries": it is a race against the host's background traffic. ring_next() now returns -4 when the writer is more than two thirds of the ring ahead, and the caller resyncs and continues instead of parsing bytes that belong to a different frame. The timeout reports seen / bad-descriptor / lapped counts so the three cases stay distinguishable. Detection is not prevention, so the app and the README both now say to quiet the link first with gc-testlink.sh, which sets a static address and takes the interface out of NetworkManager -- stopping the DHCP client whose 342-byte broadcasts are the main offender. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0477e07b84 |
netboot: survive stray frames, and dump the page instead of guessing
Pressing Z failed instantly, BEFORE the sender was started -- so the frames in the ring were stray traffic from the host (mDNS, IPv6 RAs, ARP), and a single descriptor we could not parse aborted the whole transfer. On a live network that is guaranteed to happen. A bad descriptor is now non-fatal: log the first few, resync RRP to the gateware's RWP, and keep waiting. Capped at 200 so a genuinely broken ring still terminates rather than spinning. This also removes the dependence on the ring being quiet when Z is pressed, which was never a reasonable assumption. The descriptor 00 00 00 01 persists after fixing both dropped-write bugs, and grep confirms no bare `ready` tests remain, so it is NOT that. Rather than theorise a fourth time, the error path now dumps 32 bytes from the head of the offending page. That distinguishes the two remaining possibilities outright: an ethernet header at offset 4 means the descriptor length is simply wrong, while a header at 3 or 5 -- or at 0 -- means we are reading the wrong place. Also dumps the first frame whose ethertype does not match, with the ethertype read at offset 12, because a body written at the wrong offset would show the ethertype at 11 or 13. And the timeout now reports how many frames were seen and how many descriptors were rejected, so "no match" is distinguishable from "nothing arrived". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
86cd5d1abd |
Fix two dropped-write bugs in RXFrameAssembler's SPRAM handshake
Both from one mistake: eth_wr_valid is scheduled with m.d.sync and so is LOW
for the whole first cycle in a state, while eth_wr_ready was tested
immediately -- and `ready` is usually already high on entry. Both
valid.eq(1) and valid.eq(0) then fired in the same cycle, the later won, the
write was never issued, and it was dropped SILENTLY.
1. WRITE_LEN_HI dropped the descriptor's HIGH length byte, leaving it at its
placeholder 0x00. Hardware read a descriptor of 00 00 00 01 -- a 1-byte
frame length, impossible for ethernet -- and rejected the ring.
2. WRITE_PLACEHOLDER counted a phantom first write, advancing wr_addr without
issuing one. Only THREE placeholder bytes were written and wr_addr ended
at desc_base+3, so the frame body started at descriptor byte 3 and EVERY
RECEIVED FRAME WAS SHIFTED ONE BYTE EARLY. The GC's ethertype test then
read the wrong offset and silently dropped every frame: the host saw "no
ACK" while the ring filled and RWP advanced normally.
Both now advance on `valid & ready`, and the body offset is set explicitly
(desc_base + 4) rather than relying on an incremented pointer, so it cannot
drift with the handshake again.
WHY NO TEST CAUGHT THIS, which is the more important half:
- The testbench drove the RX stream OPEN-LOOP -- one byte per tick, never
checking rx_ready, which is low through IDLE and the whole placeholder
write. It silently lost bytes, including the SOF byte. A real master
must hold each byte until accepted.
- It only ever asserted on rwp and rx_irq. It declared a spram_writes list
and never filled it, so NOTHING had ever checked what was written.
- Every frame it sent was short, and a short frame's high length byte is
legitimately zero -- so a dropped high byte was indistinguishable from a
correct one.
The testbench now respects back-pressure, records every write, and checks the
descriptor bytes and the body offset. T2 sends a 300-byte frame specifically
so the high length byte is non-zero. Mutation-tested: restoring either bug
fails T2 with got [0,0,0,48] want [0,0,1,48] -- the same shape as the
hardware's 00 00 00 01 -- while T1 (short frame) still passes, which is
exactly the blind spot that let this ship.
Bring-up's 342-byte DHCP frame read a correct 00 00 01 5A only because the
arbiter happened to hold `ready` low on that entry cycle; the domain merge
and the SPRAM response-FIFO removal changed that timing.
--seeds 8: clk 27.8-32.2 MHz, EXI 54.9-64.5 MHz, 8/8 PASS. Best seed 8
(clk 32.22, EXI 62.25), flashed. Gateware only -- the .dol is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|