41ba716759d8d4cb5ce0baa9e9927f8abf950f19
174 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
41ba716759 |
spi_mode3_slave: document the engine, and fix three stale comments
418 -> 439 lines, 31% -> 14% comment-only. The reasoning that matters here is
kept in full, because the edge conventions and the TX launch phase are the two
things in this design that cost silicon bugs; what went was the history around
them and the console measurement logs, which CLAUDE.md already holds.
Three comments described mechanisms that no longer exist:
* `miso_next` was documented as feeding "an SB_IO output register clocked on
the FALLING edge". It is clocked on the RISING edge (NEG_TRIGGER=0) --
that is the whole point of the full-cycle-early launch. The file
contradicted itself, since the comment on the assignment 100 lines later
said "after the next RISING edge" correctly.
* `id_arm` was documented as "the register file's id_phase, synchronised
into this domain by the parent". The one-shot was removed; id_arm is tied
high and the register file has no id_phase. It is now driven low only by
the PHASE_METER build.
* T8's rationale said the gate exists so that "once the GC's first NCRA
reset clears id_phase, address 0 reads back NCRA". That justification was
not merely stale, it was WRONG -- NCRA is register space and the fast path
requires command space, so they were never ambiguous. T8's real job is to
prove a disarmed build passes the FIFO byte through, which is what
PHASE_METER depends on.
Structure: elaborate() now delegates to _rx_bytes, _device_id and _tx_bits,
each carrying the reasoning for its own block -- why rx_byte/rx_valid must be
combinational (a registered pulse loses the last byte of every transaction),
why the device ID is answered here rather than through the FIFOs, and the
output-path budget that forces the early launch.
VERIFIED EQUIVALENT: generated RTLIL is identical for BOTH build variants
(phase_meter False and True), 395 lines each, after stripping \src attributes.
Executable testbench code is unchanged apart from one trailing comment. The
suite still has teeth: mutating the TX load back to rx_bit == 0 (a cycle late)
fails 8 of the 12 checks. Full module suite passes and bba_top T9 is 6/6 at
EXI_HALF=1, 2 and 8.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
b5961d0731 |
synth: document the build flow, and fix comments that had gone stale
589 -> 602 lines but 42% -> 13% comment-only: the prose shrank by two thirds
while the structure and the docstrings grew. The module docstring now carries
the usage, both timing targets, the per-seed build layout and every environment
variable in one place.
SIX of the comments here were not merely verbose, they were WRONG, and three of
them described hardware behaviour that had been replaced:
* MISO was described in three places as leaving on the FALLING edge with the
path having to fit a HALF period (18.5 ns). It has not worked that way
since the full-cycle-early launch: NEG_TRIGGER=0 keeps the IO register on
the RISING edge and the bit is launched one edge before the host samples
it, so the budget is a FULL period. The >12 ns warning text said the same
wrong thing to anyone who tripped it.
* The MISO clock-to-pad parse had no note saying it is EXPECTED to find
nothing. "IOB (none)" is the good case -- a number there means the output
has regressed into the fabric -- and only the summary printer said so.
* Two comments sat adjacent at the ranking site giving DIFFERENT rules: one
said rank by worst relative fmax margin, the next said rank by smallest
MISO delay. The code does MISO first and falls back to margin; the
docstring now says that once.
* "~22% LC utilisation" -- it is ~81%, which inverts the point being made
(routing dominates because the part is FULL, not because it is empty).
* "the final artefact in build/top.bin is the result of the last (or best)
seed" was contradicted 150 lines later by "There is no top-level
build/top.bin". The latter is true.
* A comment described `results` as 4-tuples; the code appends 5.
Structure: the 240-line __main__ is now main() over _udp_config,
_make_dut_factory, _build_seed, _parse_timing, _score, _run_sweep and
_print_summary. elaborate() delegates to _drive_miso and
_drive_status_panel. Each keeps the reasoning that belongs to it -- why a
fresh platform and a separate build dir per seed (both were real mis-scoring
bugs), why the post-route number is the last occurrence, why an unparseable
seed must never be ranked.
Cut: the pin-35/PLL history, which ends in a conclusion that is all that
matters now (ETH_INT is on pin 13 because the board is built that way); the
reverted router-options experiment, down to one line; and a duplicated
paragraph about the clock constraint, plus a duplicate `import os`.
VERIFIED EQUIVALENT, not just tested:
* Generated RTLIL is identical across all 30659 lines after stripping the
\src source-location attributes -- those 44 lines are the only diff, so
the hardware is provably unchanged.
* The ranking logic was differential-tested against the old algorithm
transcribed verbatim: 4000 randomised sweeps including unparseable seeds
and the best_miso/best_score interaction, 0 mismatches.
* A real build of seed 1 reports clk 27.62 / exi 59.47 MHz, matching the
pre-refactor sweep's seed 1 exactly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
f4e7e2c661 |
CLAUDE.md: refresh the seed table after the cleanup pass
Best seed is 3, not 6, and the flash skill points at build/seed<N>/top.bin -- so a stale seed number sends you to the wrong bitstream. Also records the area, which is the thing the cleanup actually moved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
41d487e1a7 |
Cut dead signals and the unused W5100 debug peek
An audit for signals that are written but never read turned up three things no
longer connected to anything. All verified unreferenced across every module,
testbench and script before removal.
exi_capture cs_n_prev registered on sync, read by nothing.
w5100 udp_rx_sof driven in the UDP rx stream mux; no
consumer uses it. uart_shell needs only
data/valid/eof, because the request
frames the transfer. The MACRAW rx
interface keeps its sof -- the frame
assembler does need it.
w5100 the debug peek 4 ports, 2 FSM states (DBG_RD/DBG_RD_W)
and a branch in the IDLE arbiter's
priority chain. dbg_rd_req was driven by
nothing -- not even its own testbench --
so this was untested logic sitting in the
path that decides what the bus master
does next.
Kept deliberately, against the audit: the unused register-address constants in
both ethernet masters (_IR, _TMSR, _S0_SR, _S0_TX_FSR, _S0_TX_RD and the three
W5500 buffer ones). They cost no logic and they document the chip's register
map in one place; that map was the source of three hardware bugs, and a partial
map invites re-guessing the missing half. Same for status_panel's eth_rst_n,
which is a documented panel feature synth.py has not wired yet -- a gap, not
dead weight. And rx_frame_assembler's geom_honoured, which is one comb line
asserting a property the tests otherwise could not observe.
Also fixed the register file's opening docstring, which claimed the module did
"three jobs" and invited splitting it into three. It does not: the interrupt
level is IR & IMR, a frame arriving is a register side effect, and the read mux
feeds the TX stream combinationally in the cycle the FSM pushes it. Those are
facets of one job that share the register bank, not separable parts. _read_mux
now records why that path must stay unregistered -- one pipeline register there
is 42 ns against a 296 ns byte budget, which is the tx FIFO underrun that the
+1 descriptor hunt ended in.
Full suite passes; bba_top T9 6/6 at EXI_HALF=1, 2 and 8; W5100 elaborates with
enable_udp_test both ways. Sweep: 8/8 seeds PASS, best seed 3 at clk 29.08 MHz
(1.21x the 24 MHz target), and area down again:
ICESTORM_LC 4350 original -> 4328 after the rx_rptr cut -> 4305 now
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
dac9c0c065 |
bba_register_file: delete the dead rx_rptr FIFO and unused constants
An audit for signals that are written but never read, ports nothing wires, and
constants only the tests touch turned up something bigger than stray signals:
the whole rx_rptr SyncFIFO was dead.
asm.rrp is wired from reg.rrp -- the combinational tap -- and the FIFO's far
side (rx_rptr_r_data / _r_en / _r_rdy) is wired NOWHERE in BBATop. It is a
leftover from when the register file and the frame assembler sat in different
clock domains; when those merged the direct tap replaced it and the FIFO was
never removed. Gone: a 4x8 SyncFIFO, its write logic in the RRP case, its
default strobe and three ports.
Also removed _RWP_HI and _RRP_HI, which were never referenced. The register
table now says why instead: the ring is 15 pages, so the high bytes of both
pointers are always zero and the read mux's default already returns that.
NCRB_PR went the other way -- it was used only by a test while BBATop steered
the ethernet address filter from a bare `reg.ncrb[0]`. It is now public and
BBATop imports it, so the constant is live and the magic index is gone.
Kept deliberately: _NCRB_PM and _NCRB_AB are referenced only by T15, but they
document the filtering bits of a real register and make that assertion
readable, so they stay.
Area went DOWN, which is the evidence the FIFO was real logic rather than
something yosys had already pruned:
ICESTORM_LC 4350 original -> 4380 after the docs pass -> 4328 now
Full suite passes, bba_top passes at EXI_HALF=1/2/8.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
b175cb0e77 |
bba_register_file: document the gateware, drop the history and client talk
First file of the documentation cleanup. Comments 403 -> 83 lines (46% ->
12% of the module), total 1563 -> 1266, with the real documentation moved into
structured docstrings instead of inline prose.
What went:
* every account of how a bug was found, what it looked like on a console,
and which fix attempts were falsified -- all of that lives in CLAUDE.md
* every description of how Swiss or libogc is written internally; the file
now documents what the GATEWAY does and what a host observes, not how any
particular host is implemented. Zero occurrences of Swiss/libogc/gcif
remain, in comments, docstrings, helper names or test output
* the module docstring, which still described the GUESSED header format
(addr[12:6] plus a 2-bit length field) replaced long ago
What arrived:
* a register table giving each address its name AND its purpose, bit
meanings included (NCRA reset/ST0/ST1/SR, IR's RI/TI/RBFI, the page
pointers, which side owns RWP vs RRP)
* a command-register section, likewise
* named IR/IMR and NCRB bit constants instead of bare indices
* a class docstring grouping the ports into EXI stream / ethernet side /
register taps / events
* an FSM docstring listing all 13 states in visit order with what each one
is for, and the invariant that every data phase ends on ~cs_active
Structure: elaborate() is now a table of contents -- _make_fifos,
_make_registers, _register_taps, _interrupts, _read_mux, _transaction_fsm,
_spram_responses, _command_write, _ethernet_events -- with register storage,
transaction state and the FIFOs passed as three namespaces so each block's
inputs are visible. The register write decode is its own method rather than
80 lines nested inside a state.
Dead code removed: sp_req, byte_ctr, drain_ctr, SP_LIMIT and _DEVICE_ID were
all write-only or declaration-only.
Behaviour preserved, checked four ways: all 15 module tests pass, the full
module suite passes, bba_top passes at EXI_HALF=1/2/8, and the hardware is
materially identical -- flip-flops exactly equal (1177 SB_DFFE, 671 SB_CARRY),
RAM/IO/global buffers unchanged, ICESTORM_LC 4350 -> 4380 (+0.7%). That rise
is the read mux: its Switch cases are now in address order, which changes how
the mux tree maps. All cases are distinct constants so the decode is
semantically identical.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
c887ab32fa |
fsp-server: report capability separately from duty cycle
The rate line was bytes/elapsed, which is not the capability of the path: the
game reads in bursts, so any window containing idle time understates what the
link can do. Dennis spotted this -- 593 KB/s observed while the per-round-trip
arithmetic says ~700 -- and the fix belongs in the tool, not in the reader's
head.
Now reports, every two seconds:
average bytes / elapsed, including idle (what the game happened to want)
CAPABLE bytes / time ACTUALLY serving (what the path can do)
duty fraction of the window streaming
round trip median gap, split into total and this server's service time,
plus the implied ceiling (reply_size / RTT) at that round trip
Consecutive requests closer together than 10 ms count as streaming (the round
trip is ~2 ms); longer gaps are the game not asking and are excluded from the
capability figure. A synthetic burst model confirms the split: 1440 B every
2 ms in bursts with 200 ms idle reports 352 KB/s average but 703 KB/s capable
at 50% duty, which is the distinction that matters when asking whether the
adapter is the limit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
4e412c8b9b |
fsp-server: stop clamping GET_FILE replies to 1024, which capped streaming
Streaming measured 509 KB/s, BELOW the menu's 850 KB/s TCP dump, and the
arithmetic says exactly why.
I had this wrong: QUEUE_SIZE 2 is the number of queued GAME reads, not
concurrent network requests. fsp_read_queued() sends ONE fsp_get_file and
arms a 1 s alarm; fsp_pop_queue() issues the next only once the response
arrives. Streaming is therefore STRICTLY ONE REQUEST IN FLIGHT, so
throughput = bytes_per_reply / round_trip
The server clamped replies to FSP_SPACE (1024), giving 1024 B / ~2.01 ms =
509 KB/s -- the measured figure almost exactly. But the in-game client asks
for pmtu - 40 (1460 at a 1500 MTU) and accepts it; only fsplib has the 1024
limit, and fsplib never asks for more than that. So honouring the requested
size is safe for both clients, and at 1440 B (the 1500 MTU budget rounded
down to 32) the same round trip gives ~1.41x, about 700 KB/s.
Replies stay a multiple of 32 unless they are the file tail, because the
client computes data_size = MIN(requested, OSRoundDown32B(reply_len)) and a
non-aligned mid-file reply would be silently truncated.
Also instruments the round trip: with one request in flight the observed
interval IS the RTT, so the report now splits it into total and this server's
median service time and says SERVER-bound or console-bound. Without that,
"it is slow" has no owner. And the raw socket now binds ETH_P_IP rather than
ETH_P_ALL, so our own outgoing frames are no longer copied to userspace and
parsed in Python on the path we are trying to shorten.
T13 checks a 1460-byte request yields 1440 (32-aligned) while an fsplib-sized
1024 request still yields exactly 1024.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
2172a85f0e |
fsp-server: implement writes, which booting a game requires
Swiss reported "failed to write file" then "Failed to setup the file (too fragmented?)". The FSP device declares FEAT_READ|FEAT_WRITE|FEAT_BOOT_GCM| FEAT_PATCHES, so booting a disc image from FSP is supported -- but Swiss writes patch files to swiss/patches/ first, and deviceHandler_FSP_setupFile returns failure when that write fails, which the UI reports as the fragment message. Implements CC_UP_LOAD (0x43), CC_INSTALL (0x44), CC_MAKE_DIR (0x49), CC_DEL_FILE (0x45) and CC_DEL_DIR (0x46), all confined to --root. --writable (aliased as --claim-owner) enables them and reports FSP_DIR_OWNER, so the two can no longer be set inconsistently: previously --claim-owner told Swiss it could write to a server with no write support, which is exactly the combination that produced the error. ⚠️ CC_UP_LOAD CARRIES NO FILENAME. fsplib calls buildfilename() only on the read branch, so the data field is RAW FILE BYTES at `pos` and CC_INSTALL names the destination afterwards (temp-then-commit). Handling UP_LOAD after the path/password parse rejects every chunk as a bad password, so it is handled before it; the password is validated on INSTALL, which does carry the path. `pos` must also be echoed, because fsp_transaction checks it for UP_LOAD. Tests: MAKE_DIR creates the directory, two 1024-byte UP_LOADs plus INSTALL commit byte-exact content, writes outside --root are refused, and GET_PRO reports OWNER when writable. Mutation-tested: moving UP_LOAD back below the password parse fails T11 with CC_ERR on every chunk. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7b94da0be5 |
fsp-server: add a raw-ethernet transport, because UDP replies never arrived
The trace settled it: our reply satisfies EVERY check fsplib makes -- fsp_pkt_read's checksum (0x9a computed, 0x9a present), len <= recv_len, (reply.seq & 0xfff8) == s->seq, cmd match, and fsp_getpro's pos == FSP_PRO_BYTES with the flags byte at buf[len]. Yet the console retried the same transaction (seq 0x8ccb -> 0x8ccc, txn 0x8cc8 try 3 -> try 4). The decisive evidence is the key: fsplib calls client_set_key(rpkt->key) on any ACCEPTED reply, so the next request would carry our 0x1234. Every request still carries 0x8022. No reply has ever been accepted -- so they are not arriving, and the problem was never the protocol. What differs from the thing that worked: mini-dhcpd.py replies over AF_PACKET and got a DHCP lease immediately, while this server used a normal UDP socket, whose reply depends on the kernel having a route to the console, an ARP entry for it, and choosing the right source address. (AF_PACKET also never populates the kernel ARP table, so DHCP succeeding tells us nothing about those.) --iface <dev> now replies with raw ethernet frames that MIRROR the request's own addresses -- MACs swapped, IPs swapped, ports swapped -- which removes routing, ARP and source-address selection in one go. The UDP path is kept for when it is not needed. Frame construction is tested: the reply carries the console's MAC as destination and ours as source, source IP 10.0.0.1 and destination 10.0.0.64 taken from the request, ports 21 -> 4096, a valid IP checksum, correct UDP length, and padding to the 60-byte ethernet minimum. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e56f21341a |
fsp-server: log the transaction id and retry counter, add --trace
Swiss is still stuck on "setting up device" and the log shows five identical
GET_PRO lines -- but there was no way to tell whether that is ONE transaction
being retried (our reply rejected or lost) or five separate transactions
(Swiss looping at a higher level). Those have completely different causes and
the log could not distinguish them.
fsp_transaction puts a random value in seq & 0xfff8 per transaction and the
retry counter in seq & 0x7:
p->seq = (s->seq) | (retry & 0x7);
so the split is the diagnostic. Every request now logs
seq=0x.... [txn 0x.... try N]: same txn with climbing try means our reply is
being rejected; different txn each time means the client gave up and restarted.
--trace additionally hexdumps every request and reply with key/pos/len/xlen,
so a reply can be verified by hand against fsp_pkt_read instead of trusted.
All replies now go through one reply() helper rather than scattered sendto
calls, which is what makes tracing them possible at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
cb0df55699 |
fsp-server: reply checksums must use fsplib's VERIFIER formula, not its writer
Swiss hung on "setting up device" while the server logged perfectly reasonable
GET_PRO replies. fsplib was dropping every one and retrying -- visible in the
log as the same request repeating with the low 3 sequence bits counting up,
since fsp_transaction encodes the retry number there.
FSP has TWO checksum formulas and they differ by the packet length:
fsp_pkt_write (what a client SENDS):
checksum = sum(bytes, sum byte zeroed) + used
sum_byte = checksum + (checksum >> 8)
fsp_pkt_read (what a client ACCEPTS):
mysum = sum(bytes) - sum_byte <- no length term at all
ok if ((mysum + (mysum >> 8)) & 0xff) == sum_byte
cube/patches/bba/tcpip.c matches the WRITE form (`sum = size` to start) and
never verifies replies, so the in-game path tolerated our packets; the menu's
fsplib did not. Replies are now built the way fsp_pkt_read verifies.
Incoming requests are accepted under either formula, since a tolerated request
is far easier to diagnose than a rejected one.
⚠️ The self-test passed throughout the bug, and that is the part worth
remembering: it verified replies with the SERVER'S OWN checksum function, so
the two agreed with each other and disagreed with Swiss. Identical in shape to
the RX descriptor format, where our writer, our readers and our tests were all
self-consistent and all wrong. The test now transcribes fsp_pkt_read literally
as fsplib_verify() and checks every reply against that, and mutating the server
back to the with-length formula fails it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
f3fee56673 |
fsp-server: implement GET_PRO (0x47), which Swiss asks for first
Swiss sent CC_GET_PRO 'swiss/patches' and the server replied "unsupported".
Reply shape, from fsplib's fsp_getpro:
* pos MUST equal FSP_PRO_BYTES (1), or fsplib fails with ENOMSG
* the flags byte is at buf[len] -- the first byte of the EXTRA region,
after the echoed name, NOT inside the data field
* a missing directory must be CC_ERR so fsplib reports ENOENT, because
Swiss probes "swiss/patches", then "swiss", then "" and relies on that
fallback chain
Those bits are not cosmetic. deviceHandler-FSP.c does:
if ((pro & (MKDIR|ADD|DEL|GET)) == (MKDIR|ADD|DEL) || (pro & OWNER)) {
features |= FEAT_PATCHES; emulable |= EMU_ETHERNET;
} else { ... clears both ... }
so a plain read-only answer makes Swiss DISABLE patch support on this device.
Default is therefore the honest GET|LIST (0x50) with patches disabled, and
--claim-owner reports OWNER to turn them back on -- flagged in the help as
unsafe for now, because this server has no UP_LOAD or MAKE_DIR, so Swiss
believing it can write here would make a patch write fail rather than degrade.
Try without it first.
T9 checks pos == 1 and that the flags land at buf[len] with GET|LIST and
without OWNER; T10 checks a missing directory gives CC_ERR so the fallback
chain works.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
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
|