Commit Graph
100 Commits
Author SHA1 Message Date
RoflinandClaude Opus 5 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>
2026-10-10 15:32:45 +02:00
RoflinandClaude Opus 5 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>
2026-10-10 15:26:39 +02:00
RoflinandClaude Opus 5 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>
2026-10-10 15:21:47 +02:00
RoflinandClaude Opus 5 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>
2026-10-10 15:07:44 +02:00
RoflinandClaude Opus 5 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>
2026-10-10 15:02:34 +02:00
RoflinandClaude Opus 5 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>
2026-10-10 14:51:17 +02:00
RoflinandClaude Opus 5 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>
2026-10-10 14:33:12 +02:00
RoflinandClaude Opus 5 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>
2026-10-10 14:24:53 +02:00
RoflinandClaude Opus 5 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>
2026-10-10 14:22:42 +02:00
RoflinandClaude Opus 5 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>
2026-10-10 14:16:43 +02:00
RoflinandClaude Opus 5 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>
2026-10-10 14:06:04 +02:00
RoflinandClaude Opus 5 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>
2026-10-10 14:02:28 +02:00
RoflinandClaude Opus 5 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>
2026-10-10 11:40:55 +02:00
RoflinandClaude Opus 5 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>
2026-10-10 11:30:50 +02:00
RoflinandClaude Opus 5 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>
2026-10-10 11:24:23 +02:00
RoflinandClaude Opus 5 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>
2026-10-10 11:18:09 +02:00
RoflinandClaude Opus 5 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>
2026-10-10 11:11:55 +02:00
RoflinandClaude Opus 5 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>
2026-10-10 11:04:02 +02:00
RoflinandClaude Opus 5 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>
2026-10-10 11:01:33 +02:00
RoflinandClaude Opus 5 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>
2026-10-10 10:49:11 +02:00
RoflinandClaude Opus 5 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>
2026-10-10 10:42:17 +02:00
RoflinandClaude Opus 5 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>
2026-10-10 10:27:45 +02:00
RoflinandClaude Opus 5 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>
2026-10-10 10:17:41 +02:00
RoflinandClaude Opus 5 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>
2026-10-10 10:14:19 +02:00
RoflinandClaude Opus 5 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>
2026-10-10 10:09:27 +02:00
RoflinandClaude Opus 5 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>
2026-10-10 09:53:55 +02:00
RoflinandClaude Opus 5 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>
2026-10-10 09:18:50 +02:00
RoflinandClaude Opus 5 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>
2026-10-09 20:17:19 +02:00
RoflinandClaude Opus 5 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>
2026-10-09 20:05:48 +02:00
RoflinandClaude Opus 5 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>
2026-10-09 19:56:13 +02:00
RoflinandClaude Opus 5 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 92f7d71.  Inserting the RBFI handler split the TX-done
block, leaving these two lines attached to rx_overflow -- an RX event:

    m.d.sync += r_ncra[1:3].eq(0)  # clear ST bits
    m.d.sync += tx_queued.eq(0)    # ready for the next frame

So after the first transmit tx_queued stayed set forever.  The NCRA ST
fallback trigger is guarded on ~tx_queued, so software driving TX via ST
rather than TWD would transmit exactly ONCE and then silently stop, and the ST
bits were left asserted in NCRA.  Our own path arms via TWD, which is why it
kept working and why this was not noticed.

T9 covers it and is mutation-tested: with the clears misplaced, NCRA reads
0x0A with ST stuck set.

This does NOT explain the 1/3 ACK loss -- the TWD path was unaffected -- so
that remains open.  txto from the next run is the discriminator: near zero
means the frames reach the W5100 and are lost after that, which points at the
chip's TX handling rather than the EXI side.

--seeds 8: clk 27.5-29.4 MHz, EXI 54.7-62.3 MHz, 8/8 PASS.  Best seed 5
(clk 29.41, EXI 60.22), flashed.  The .dol is unchanged; only send-dol.py's
default timeout moved, and that is read from the repo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 19:54:23 +02:00
RoflinandClaude Opus 5 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>
2026-10-09 19:42:24 +02:00
RoflinandClaude Opus 5 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>
2026-10-09 19:36:21 +02:00
RoflinandClaude Opus 5 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>
2026-10-09 19:17:34 +02:00
RoflinandClaude Opus 5 1e370deb49 Bound the TXDATA chunk to half the tx_bytes depth
a2e8e5c batched TXDATA writes into 64-byte chunks and broke ACKs outright --
"no ACK after 20 tries".  My regression, and CLAUDE.md warns about exactly
this: tx_bytes is 16 deep and BBARegisterFile silently drops bytes when
w_rdy is low, because SPI cannot back-pressure.

Within a single transaction the bytes arrive back-to-back at EXI speed, far
faster than TXFrameDrain starts consuming, so a 64-byte chunk overran the FIFO,
the frame went out truncated, and no valid ACK ever reached the host.
Byte-by-byte worked only because the per-byte EXI overhead happened to pace
the drain -- which is also why it was slow.

Chunk is now 8, half the FIFO depth: it cannot overflow even if the drain
consumes nothing for the whole transaction, while still cutting a 60-byte ACK
from ~60 transactions to 8.  Noted in both the source and CLAUDE.md not to
raise it without deepening tx_bytes or giving the TX path real back-pressure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 19:16:28 +02:00
RoflinandClaude Opus 5 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>
2026-10-09 19:13:28 +02:00
RoflinandClaude Opus 5 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>
2026-10-09 19:09:05 +02:00
RoflinandClaude Opus 5 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>
2026-10-09 19:04:52 +02:00
RoflinandClaude Opus 5 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>
2026-10-09 19:00:35 +02:00
RoflinandClaude Opus 5 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>
2026-10-09 18:50:35 +02:00
RoflinandClaude Opus 5 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>
2026-10-09 18:45:00 +02:00
RoflinandClaude Opus 5 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 83a62d6, which reset the REGISTER
FILE's copy of RWP on an NCRA reset but not RXFrameAssembler's own `rwp`.

They are two copies of the same pointer.  Resetting one leaves them
disagreeing about where the ring is: a driver that syncs RRP to the register
file's value (page 1) points somewhere the writer is not (wherever it left
off), the free-space sum shrinks with every frame, and once it falls below
_MAX_FRAME_PAGES the new overflow protection drops EVERY frame.  Silently,
because a dropped frame never reaches the ring to be counted -- hence 0 seen
rather than an error.

So fixing one half of a two-copy pointer created exactly the inconsistency the
other half had just been fixed for.  That is the third time in this session
that two copies of one piece of state has caused a hardware fault: the W5100's
init, then RRP, now RWP.  When adding state that the GC can also observe,
reset BOTH copies from the same event or do not keep two.

The reset is placed AFTER the FSM so it takes priority over ADVANCE_RWP in the
same cycle.  No in-flight frame needs abandoning: the same NCRA write that
pulses ncra_rst also clears the SR bit, so rx_enabled is low and the FSM is
already in IDLE.

T6 covers it and is mutation-tested.  Without the reset the frame lands on the
wrong page entirely and carries a previous frame's bytes:
    rwp back to page 1: got 4, want 2
    descriptor:         got [0,0,1,48], want [0,0,0,44]
    body index 0:       got 9, want 192

--seeds 8: clk 28.9-30.8 MHz, EXI 61.0-65.7 MHz, 8/8 PASS.  Best seed 2
(clk 30.83, EXI 65.72), flashed.  Gateware only -- the .dol is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 18:41:06 +02:00
RoflinandClaude Opus 5 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>
2026-10-09 17:18:04 +02:00
RoflinandClaude Opus 5 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>
2026-10-09 12:36:07 +02:00
RoflinandClaude Opus 5 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>
2026-10-09 12:20:14 +02:00
RoflinandClaude Opus 5 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>
2026-10-09 12:13:29 +02:00
RoflinandClaude Opus 5 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>
2026-10-09 12:07:13 +02:00
RoflinandClaude Opus 5 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>
2026-10-09 12:04:00 +02:00
RoflinandClaude Opus 5 77cd00a63c netboot: hold the screen on failure, dump the descriptor, shrink the block
Two separate problems, one of which was hiding the other.

1. The diagnostics were unreadable.  do_netboot() printed them and returned,
   and the caller's draw_main() redrew immediately and wiped the screen -- so
   two consecutive failures were reported only as "a short flash" with no
   detail, and the error code alone was not enough to act on.  nb_pause() now
   holds the screen until a button is pressed, on every failure path.

   This cost two blind round trips, each of which needs a hand copy of the
   .dol to the SD card.  Printing a diagnosis and then erasing it is worse
   than not printing one.

2. The default block was 1024 bytes, making ~1050-byte frames that span FIVE
   of the SPRAM ring's 15 pages.  The only multi-page frame ever proven on
   this hardware is the 342-byte DHCP broadcast from bring-up, which spans
   two.  Five-page frames are untested territory, and a transfer is the wrong
   place to discover a wrap bug.

   Default is now 256 bytes -> ~296-byte frame -> two pages, deliberately
   close to the proven case.  --block raises it once small blocks work; the GC
   still caps at NB_BLOCK_MAX = 1024.

ring_next() also keeps the four raw descriptor bytes and the page it read
them from, and the error path prints them.  When a length is rejected those
bytes ARE the diagnosis: 00 00 xx xx tells us the assembler wrote a
descriptor and the length is simply unexpected, whereas garbage tells us we
are reading the wrong page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 11:48:39 +02:00
RoflinandClaude Opus 5 83a62d60a0 Reset the register file's ring pointers on an NCRA reset
An NCRA reset returns RXFrameAssembler's write pointer to page 1, but the
register file's COPY of RWP was never reset -- it kept whatever the last frame
left.  So after any reset the GC reads a stale RWP pointing at a page the
assembler is not using, and its first RX after a reset reads the wrong page.

CLAUDE.md has stated the intent all along ("NCRA = 0x01 ... resets W5500 +
SPRAM ptrs"); the register file simply never did it.

This is NOT only a netboot problem.  Any driver that resets and then trusts
RWP hits it, Swiss included.  It stayed invisible because the suite's RX test
only checks that RWP CHANGES, never that it points at the right page, and
t_dma used to read a hardcoded 0x100 so it never consulted RWP either.  It
surfaced through netboot, whose RRP = RWP sync landed on a stale page and
found garbage where a descriptor should be.

T1c covers it, and BOTH halves have teeth.  My first version did not: RWP read
[1] with or without the fix because nothing in the testbench had moved it.  It
now drives the assembler's RWP update path to page 9 and writes RRP to 7
first, so the negative control fails on both:

    T1c RWP after NCRA reset: expected [1], got [9]
    T1c RRP after NCRA reset: expected [1], got [7]

A test that passes whether or not the fix is present is worse than no test.

Gateware only -- the test app needs no change and the existing .dol is
compatible: after its reset it reads RWP, now gets 1, and writes RRP = 1.

--seeds 8: clk 28.5-32.2 MHz, EXI 51.3-63.4 MHz, 8/8 PASS.  Best seed 2
(clk 32.19, EXI 63.23), flashed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 11:44:03 +02:00
RoflinandClaude Opus 5 8493c0ce9d netboot: sync RRP to RWP at init instead of trusting what we inherited
Pressing Z returned "net-update failed (-2)" -- ring_next() rejecting an RRP
outside the ring (pages 1-15) before a single frame was read.

The pointer was never ours to assume.  Swiss initialises the BBA before
launching us and writes RRP itself, and an NCRA reset does NOT restore the
register file's copy of either pointer: their init values only apply at FPGA
configuration.  So RRP can legitimately be anything on entry, including 0,
and the previous code treated that as a fatal error instead of a state to
establish.

netboot_receive() now reads RWP and writes RRP = RWP after the reset, which
declares the ring empty and consistent whatever it was, so the first frame
read is genuinely the host's first block.  RWP itself is still checked,
because that one IS the gateware's own pointer and an out-of-range value there
would be a real fault rather than inherited state.

The ring-error message now reports RRP and RWP and spells out which of the
three failures it was (EXI access, RRP out of range, implausible descriptor).
Each test costs an SD card copy by hand, so a bare error code is not worth the
round trip.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 11:36:06 +02:00
RoflinandClaude Opus 5 7cd0071e7c netboot: reset and enable the adapter before waiting for frames
Pressing Z straight after launch is the normal case, and in that case NOTHING
has issued the NCRA reset that triggers the W5100's own init.  MACRAW would
not be running, no frames would be stored, and the transfer would sit in
"timeout: no frames" for a reason with nothing to do with netboot -- the most
confusing possible first-run failure.

Swiss may have initialised the adapter, or an earlier test may have, or
neither.  An NCRA reset here makes all three cases identical: poll until the
RESET bit self-clears, wait out the W5100's 60.3 ms settle (twice over, hence
400 ms), then set the SR bit so RXFrameAssembler actually writes the ring.

Resetting also clears the SPRAM ring pointers, which is wanted rather than
incidental: a clean ring with RRP == RWP == page 1 means the first frame we
see is genuinely the host's first block, not a leftover from a test.

PAR0-5 is now read AFTER the reset, so the source address in our ACKs is the
one the adapter is actually filtering on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 11:28:35 +02:00
RoflinandClaude Opus 5 7c9488b617 Keep one generation back, and verify before touching the existing build
Without this, a successful transfer of a BROKEN build stranded you: the only
copy of a working test program had already been overwritten, so there was no
way to attempt another transfer without an SD card reader.

Write order is now temp -> verify -> rotate, and the ordering is the point:

  - The image goes to "<name>.tmp" and is read back and compared BEFORE
    anything existing is touched.  If the transfer, the write or the verify
    fails, the build you are running is still on the card untouched and you
    can simply try again.  Previously a failed write left the destination
    half-overwritten.
  - On success the existing build is rotated to "<name>.prev.dol" and the temp
    file takes its place.  So if the NEW build turns out to be broken, boot
    the previous one from Swiss and transfer again from there.  One generation
    back is enough never to need a card reader.

The two extensions are chosen deliberately:
  - the temp file has NO .dol extension, because Swiss lists .dol files and a
    half-written image Swiss offers to boot is exactly what this avoids;
  - the backup KEEPS .dol, because booting it from Swiss is the whole point of
    keeping it.

remove()/rename() on a file that is not there is ignored -- that is just the
first update on a fresh card.  If the final rename somehow fails, the message
names the .prev.dol path rather than leaving the user to guess where their
working build went.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 11:26:58 +02:00
RoflinandClaude Opus 5 30563b166d Self-update writes the new build to SD, then boots known-good /boot.dol
Previous version launched the image it had just received, which is the one
thing that cannot be made safe: a broken build hangs the console and the
checks can only catch a truncated or non-DOL image, never a plausible but
broken one.

The flow is now receive -> write to SD -> boot /boot.dol:
  - netboot_sd_write() mounts the card (auto-detecting SD2SP2, then SD Gecko
    slot B, then slot A), writes the image to the path the host asked for,
    and READS IT BACK AND COMPARES.  A half-written .dol that still parses is
    exactly the failure this design exists to avoid, and the card is the one
    component here with no other integrity check.
  - netboot_boot_file() then loads /boot.dol -- a known-good Swiss -- and
    launches that instead.

So the only image this ever jumps into is one that was already known good.  A
broken test build costs nothing: Swiss comes up and can load anything,
including the previous build.  If the SD write or verify fails, nothing is
launched at all and the menu is still there.

The destination path travels in the DONE frame rather than being baked in, so
the host decides where the build lands (send-dol.py --dest, default
/gc_bba_test.dol).

libfat is unmounted before the jump: leaving the card mounted hands the next
image a filesystem libfat still thinks it owns, on an EXI channel it is about
to reinitialise.

Note the SD Gecko in memory-card slot A shares EXI channel 0 with the BBA
(channel 0, device 2).  libogc arbitrates, but SD2SP2 is on its own channel
and avoids the question -- hence the detection order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 11:23:38 +02:00
RoflinandClaude Opus 5 4fd337fb24 Fix the netboot launcher: relocate the copier, reserve the staging region
The launcher committed in b438960 would have hung the console, and reviewing
it against Dennis's suggestion to test with Swiss's boot.dol is what surfaced
it -- Swiss is known-good, so a hang could only have been the launcher, but I
would not have been able to tell that from "the transfer is broken".

Two distinct defects, both from the same oversight -- a DOL's sections load
from 0x80003100 upward, which is exactly where this program is running:

1. The copier was an ordinary C function in .text, so it overwrote its OWN
   instructions partway through the copy.
2. The image was staged in a memalign'd buffer, which the destination sections
   can overwrite while it is still being read from.

Fixes:
  - netboot_stub.S: the copy/zero/cache-flush/jump sequence in position
    independent assembly, copied into reserved memory and executed THERE.  It
    has no globals, no literal pool, no TOC references and no calls, because
    any of those would read from the address it was LINKED at rather than the
    one it is RUNNING at.  It also does the dcbf/icbi pass the C version was
    doing through DCFlushRange -- without it the CPU executes stale icache
    lines at the entry point.
  - netboot_reserve(): carves the top of MEM1 and hands it to
    SYS_SetArena1Hi() so neither libogc nor malloc allocates there.  Called
    FIRST in main(), before the framebuffer.
  - netboot_launch(): verifies every text/data section AND bss against the
    reserved region and refuses (-3) if any would land on it, instead of
    corrupting the staged image or the copier mid-flight.  Also hands the new
    image a stack inside the reserved tail.

The app now tells you on screen to send Swiss's boot.dol first, and the README
explains why: the launcher and the transfer can each be broken independently
and a hang looks identical, so the first send should be an image whose success
is unmistakable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 11:13:32 +02:00
RoflinandClaude Opus 5 b438960eb5 Add self-update over the BBA: receive a .dol and relaunch into it
Copying to SD and rebooting is now the slowest part of the loop.  Z in the
test app pulls a new .dol over the adapter and launches it.

Deliberately NOT built on an IP stack.  Frames carry a private ethertype
(0x88B5, IEEE 802 local experimental 1) and a 12-byte header, so there is no
ARP, no IP, no checksums, and no libogc network driver competing with our raw
driver for the EXI channel.  It reuses exactly the two paths the suite already
proves on hardware: bba_tx_bytes() for TX and the SPRAM ring at RRP for RX.
A side benefit is that each update is a few hundred round trips through that
path -- a soak test we did not have.

STOP-AND-WAIT is load-bearing, not laziness.  The gateware does not consume
RRP, so RXFrameAssembler can overwrite ring pages the GC has not read yet;
there is no overflow protection.  One block in flight means the ring never
holds more than one unread frame, so the missing protection cannot bite.  Both
the header and the README say not to make this a sliding window without adding
RBFI-style protection first.

Failure behaviour, so a bad update never costs a power cycle:
  - 30 s idle with no frames, or 5 s mid-transfer, aborts to the menu rather
    than hanging.
  - A duplicate block is RE-ACKed rather than ignored, because a duplicate
    means our previous ACK was lost and re-ACKing is what lets the host move
    on.
  - netboot_receive() re-asserts the NCRA SR bit first: an earlier test may
    have left receive disabled, and then this would wait forever for frames
    the gateware is not storing.
  - netboot_launch() refuses an image whose entry point or first text section
    is not in MEM1 instead of jumping into a mis-parsed header.

The jump itself is the one unavoidable risk: once sections are copied and
interrupts are off, a bad image hangs the console hard enough to need a power
cycle.  The MEM1 check is what stands between a truncated transfer and that.

Host side is scripts/send-dol.py (AF_PACKET, needs sudo).  The GC's MAC is
discovered from its first ACK, so neither side needs an address configured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 11:08:48 +02:00
RoflinandClaude Opus 5 3db4ab0851 Reset both SPRAM ring pointers to page 1, not 0
The DMA descriptor test reported "RRP page 0 outside ring pages 1-15" on a
board whose RX path was working (t_rx passed: RWP advanced and RI fired), so
the pointer itself was wrong.

RXFrameAssembler's write pointer resets to _RX_PAGE_FIRST = page 1 -- the ring
is pages 1-15 and page 0 holds the register region -- but the register file's
r_rwp_lo and r_rrp_lo were plain Signal(8) and reset to 0.  Two consequences:
the register file and the assembler disagreed about where the ring starts, and
a driver reading its first frame from RRP=0 gets the REGISTER page instead of
the frame, which is at page 1.

It hid because RRP == RWP == 0 looks self-consistently empty, so nothing
complained until something actually read the pointer.  RRP is driver-owned
(the GC writes it after consuming frames) but its RESET value is ours, and
RRP == RWP == 1 is the correct initial state: empty, pointing at the ring
start.

T1b locks down both reset values AND imports the assembler's constant to
assert it matches, because two copies of a page number in two modules is
exactly how the off-by-one got in.

--seeds 8: clk 28.x-32.2 MHz, EXI 56-60 MHz, 8/8 PASS.  Best seed 8, flashed.

NOT fixed here, and worth separate attention: nothing in the gateware consumes
RRP, so RXFrameAssembler can overwrite pages the GC has not read yet -- there
is no ring-overflow protection (the real BBA has RBFI for this).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 11:06:22 +02:00
RoflinandClaude Opus 5 19044307bf Make the DMA descriptor test follow RRP instead of hardcoding 0x100
t_dma reported "descriptor len implausible (ring empty?)" and skipped on a
board that had just PASSED the RX test, so a frame had certainly arrived --
RWP advanced and RI fired.  The test was looking in the wrong place.

RWP/RRP are PAGE pointers into the 15-page ring, so the next unconsumed
frame's descriptor lives at RRP*256.  Reading SPRAM_BASE (0x0100)
unconditionally only works while nothing has been consumed and the ring has
not wrapped; once RRP has moved, 0x100 holds a stale or overwritten page.

t_dma now reads RRP, streams from RRP*256, and distinguishes the cases it used
to lump together:
  - RRP == RWP      -> genuinely empty, SKIP, and says to send it a frame
  - page outside 1-15 -> FAIL, with the page number
  - implausible len  -> FAIL, and reports RWP too so it is diagnosable

The old message could not tell "no frame yet" from "looked in the wrong
place", which is exactly why the skip was mysterious.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 10:55:15 +02:00
RoflinandClaude Opus 5 a5ca8b0cda Skip the raw ID dump once id_phase is consumed; record the full console pass
The whole gc_bba_test suite now passes on a real GameCube: Identity (device
ID 04 02 02 00, HIPR, NWAYS), Registers, Reset/control, EXI integrity (speed
sweep ok=[1,1,1,1,1,1], per-speed dump clean at all six indices, raw reg dump
0x17 in slot 0), Ethernet, and SPRAM/DMA -- at the real 27 MHz EXI rate.

The one reported failure was t_raw_id, and it was not a fault: an earlier
suite had already issued the NCRA reset that clears id_phase, after which
address 0 correctly reads back NCRA.  t_devid already guarded for this; t_raw_id
did not, so a full run cried wolf.  It now returns SKIP like t_devid, and both
messages say reflash/reset the FPGA rather than power-cycle the console, which
is what actually restores id_phase.

CLAUDE.md records the device-ID fix and, importantly, that simulation CANNOT
reproduce the bug it fixed -- bba_top passed the ID at EXI_HALF=1 beforehand,
because sim's sync-to-EXI latency ratio does not match silicon closely enough
to miss a 296 ns window.  Anyone "simplifying" the ID back into the register
file will keep a green suite and break hardware.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 10:53:59 +02:00
RoflinandClaude Opus 5 7587636b48 Answer the device-ID query from the bit engine, not the register file
bba_devid fails on a console with 00 04 02 02 -- the ID correct but one byte
late -- and it fails in its REAL shape, with the 16 us gap and exactly four
bytes clocked, so this is not the t_raw_id probe artefact.  The register suite
passes, so register-space reads are fine; only command space is affected.

Cause is latency, not phase.  A command-space header is two bytes, so the
reply must be in the tx FIFO by the LAST RISING EDGE of the header transfer:
one byte time, 296 ns at 27 MHz.  The round trip is rx AsyncFIFO (~125 ns) +
a sync cycle + tx AsyncFIFO (~110 ns) = 280-320 ns, marginal by construction.
HEADER0 already pushed in the same cycle it consumed byte 0, so the FSM side
was already as early as it can be.

The inter-transfer gap cannot rescue it: the load happens on that last rising
edge and the clock is PAUSED afterwards, so no edge exists during the gap for
anything to update.  That is why bba_devid's 16 us gap makes no difference.

Pre-pushing the ID at CS-fall does not work either -- the engine's
unconditional per-byte pops would eat it, and the header length is not known
until byte 0 has been decoded, which is the latency in question.

So take the round trip out of the path.  The device ID is a hardwired constant
and belongs to the physical interface rather than to the register file, and
byte 0 == 0x00 identifies the query exactly (bit 7 clear = command space, bit
6 clear = read, reg 0).  SPIMode3Slave now recognises it and answers
04 02 02 00 from the EXI clock domain, with no CDC at all.  The only thing it
needs from sync is id_phase, a single bit that changes once, crossed with an
FFSynchronizer.

The register file's ID path is REMOVED rather than kept as a fallback: left
in, it would push bytes that a 2-byte header has too few pops to drain, which
is exactly how an earlier version made the NEXT enumeration come back a slot
late.

Tests follow the responsibility:
  - spi_mode3_slave T7: the query returns 04 02 02 00 with the tx FIFO
    deliberately feeding 0x00, so only the fast path can be answering.
  - spi_mode3_slave T8: and it is GATED -- with id_arm low, 0x5A comes
    through from the FIFO.  Without this the fast path would hijack address 0
    forever and the GC could never read NCRA back after its own reset.
  - bba_register_file T1: retargeted to assert the FSM pushes NOTHING for an
    ID query, which is the property that stops stale bytes corrupting the
    following transaction.

Note what these tests do NOT establish: bba_top already passed the device ID
at EXI_HALF=1 before this change, because simulation cannot reproduce a
296 ns window miss -- the sync domain's latency relative to the EXI clock in
sim does not match silicon closely enough.  T7/T8 prove the fast path works;
the evidence that it is NECESSARY is the hardware result plus the paused-clock
argument above.

--seeds 8: clk 28.4-31.0 MHz, EXI 56.7-64.4 MHz, 8/8 PASS.  Best seed 2
(clk 30.98, EXI 59.47), flashed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 10:51:37 +02:00
RoflinandClaude Opus 5 5db3b7751a docs: MISO timing solved on hardware; command-space ID latency still open
Console confirms the full-cycle-early launch: speed sweep ok=[1,1,1,1,1,1],
per-speed dump 0:1700 through 5:1700, raw reg dump 0x17 in slot 0.  Register
reads are correct at every EXI speed index including 27 MHz.

Records what the PHASE_METER measurement showed, why three rounds of
delay-cutting could not work (9 ns removed, byte-identical result each time,
so the remaining cost is pad + global clock insertion + IOB + pad, which no
RTL change reaches), and the testbench rule that falls out of it: a master
must sample MISO in the CLK-LOW phase or a zero-delay simulation reads the
NEXT bit and reports everything one bit early while hardware is correct.

Also records the one remaining failure as OPEN rather than fixed: the
command-space device-ID response is one byte late at 27 MHz because a 2-byte
header leaves only one byte time (296 ns) for a round trip that costs
~280-320 ns, and the inter-transfer gap cannot help because the clock is
paused across it.  HEADER0 already pushes in the same cycle it consumes byte
0, so the FSM side is already as early as possible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 10:42:12 +02:00
RoflinandClaude Opus 5 7ac0cbe66c Launch MISO a full cycle early instead of chasing delay
PHASE_METER on a console settled what two rounds of delay-cutting could not.
Per-speed dump, first two slots per EXI speed index, with the register file
and both byte FIFOs out of the MISO path:

  0:8080  1:8080  2:8080  3:8080  4:8080  5:C040

Perfect to 16 MHz, one bit late at 27 MHz.  The 8-slot dump at 27 MHz read
C0 40 40 40 C0 40 40 40 -- a UNIFORM one-bit lag (ones at bits 1, 9, 17, 25,
33, ...), with extra highs at bits 0 and 32 where libogc splits the read into
two 4-byte transfers and MISO tri-states to its pull-up in the gap.  The ID
dump agreed exactly.

Decisive detail: ~9 ns had already been removed from the fabric output path
(the EXI_J4 pad merge, then the CS mux, then the entire fabric route by moving
the output into an SB_IO register) and the hardware result stayed BYTE
IDENTICAL each time.  Nine nanoseconds out of an 18.5 ns budget cannot change
nothing if propagation were the whole story.  What is left is the part no RTL
change can reach: input pad + GLOBAL CLOCK NETWORK INSERTION + IOB clock-to-Q
+ output pad, plus the host's own setup.  There was no fabric delay left to
cut, so cutting delay was the wrong move.

So double the budget instead.  MISO is now launched on the RISING edge
carrying the bit the host will sample at the NEXT rising edge, so the whole
path gets a FULL period (37 ns at 27 MHz) rather than half.  This is safe in
both directions: the insertion delay that caused the problem now covers the
host's hold time on the previous bit, and a full period covers its setup.

  - tx_out is clocked on the rising edge and loads at rx_bit == 7, one
    bit-time earlier than a falling-edge shifter loading at rx_bit == 0.
  - tx_hold and the entire exiclk_n domain are gone: the engine now has ONE
    clock edge and ONE phase reference.
  - synth.py's SB_IO output register moves to NEG_TRIGGER=0.
  - Byte alignment is unchanged -- T2/T5 still land A5 and 3C in the same
    slots.  Only the sub-bit launch timing moved.

Testbench consequence, and it matters: bba_top's master sampled MISO AFTER
raising the clock.  With a zero-delay simulation and a rising-edge launch that
reads the NEXT bit, so the suite would have reported everything one bit early
while hardware was correct.  Both masters now sample in the CLK-LOW phase --
the value the host latches ON the rising edge -- which is also exactly what
the hardware notes say a probe must do.

T2 slot 0 now reads 0xFF (the CS-reset parking value that keeps MISO high
while deselected) instead of 0x00; it is a header byte the host ignores.
T6 re-anchored to the rising edge and still proves miso_next predicts
spi_miso bit for bit, so the SB_IO register stays equivalent to the fabric
model rather than an untested divergence.

--seeds 8: clk 28.9-32.1 MHz, EXI 62.1-65.4 MHz, 8/8 PASS.  The EXI numbers
rise sharply (41.6-49.7 -> 62.1-65.4) because the setup path into the IOB
register is now posedge->posedge against a FULL period instead of
posedge->negedge against a half -- the same doubling, visible internally.
Best seed 3 (clk 32.09, EXI 62.78), flashed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 10:35:32 +02:00
RoflinandClaude Opus 5 a5b4d5bafd Add a per-speed raw dump to the GC test app
Removing ~9 ns from the MISO path (EXI_J4 merge, the CS mux, then the whole
fabric route via an SB_IO output register) left the hardware result BYTE
IDENTICAL: 0B 80 at index 5, ok=[1,1,1,1,1,0].  A ~9 ns reduction against an
18.5 ns half-period budget cannot change nothing if propagation were the
cause, so it is not, despite being speed-dependent.

t_raw_sweep reads the same 8 slots at EVERY speed index and reports the first
two bytes per index, so the shift can be seen as a curve rather than a single
point: a shift that appears or grows with clock rate is a timing problem, one
that is constant across indices is structural.  Run against a PHASE_METER
bitstream it reads the bit engine's byte phase directly at each speed, with
the register file and both byte FIFOs out of the path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 10:23:41 +02:00
RoflinandClaude Opus 5 2f081a3e69 Drive MISO from an SB_IO output register
Removing ~2 ns from the fabric MISO path (EXI_J4 merge + the CS mux) did NOT
move the hardware result: the GC still returned a byte-identical 0B 80 at
27 MHz while reading correctly at 16 MHz and below.  Identical rather than
intermittent means the path is well outside the window, not marginally inside
it, so trimming fabric delay further was not going to get there.

MISO is source-synchronous -- driven on the falling edge, sampled by the GC on
the next rising edge -- so the whole path must fit a HALF period, 18.5 ns at
27 MHz, minus global-buffer insertion, pad, connector and the GC's setup.
Driving from fabric spent 8.4-11.5 ns of that on routing alone.

An SB_IO output register sits in the IO tile: clock-to-pad is the IO flop's
own delay with no fabric routing.  nextpnr confirms the path left the fabric
entirely -- miso_io.D_OUT_0 packs at die (9,0), the pad itself, SB_IO count is
unchanged at 24, and no negedge -> <async> path is reported any more.  What
replaces it is a setup path INTO the IOB register (10.07 ns against a
half-period), which is internal and has nothing stacked on top of it.

PIN_TYPE was decoded from yosys's own SB_IO model rather than from memory,
because wrong bits there produce a silently dead or mistimed output:
  [5:4]=10     tri-state, combinational OUTPUT_ENABLE
  [3]=0,[2]=1  dout = dout_q_0, single-rate REGISTERED output
  [1:0]=01     simple input (unused)
  NEG_TRIGGER=1 moves the IO registers to the FALLING edge.

To keep simulation and silicon agreeing about MISO, the slave exposes
miso_next -- the value the shift register WILL hold after the falling edge --
and spi_mode3_slave T6 proves it predicts spi_miso bit for bit.  Without that
the IOB register would be an untested sim/hardware divergence, which is the
exact class of bug that has cost this design the most time.

BEHAVIOUR CHANGE: MISO is now TRI-STATED when CS is high, held high by the
pad's PULLUP, instead of actively driven high.  EXI MISO is a SHARED bus --
SP1 is on channel 0 alongside memory-card slot A -- so driving it while
deselected was wrong and could fight a card.

--seeds 8: clk 28.5-30.7 MHz, EXI 41.6-49.7 MHz, 8/8 PASS.  The EXI numbers
fall (62 -> 49) because the posedge->negedge setup path into the IOB register
is now charged to that domain at a half-period constraint; 41.6 MHz worst case
is still 1.5x the 27 MHz target.  Best seed 3 (clk 30.68, EXI 49.14), flashed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 19:26:08 +02:00
RoflinandClaude Opus 5 5f7f20db43 Cut the MISO output path: drop EXI_J4 and the CS mux; rank seeds by it
Remaining after the bit-phase fix was a SPEED-DEPENDENT failure -- the GC read
correctly at 1/2/4/8/16 MHz and failed only at index 5 (27 MHz), returning
0B 80 = 0x17 late by exactly ONE bit.  Opposite signature to the phase slip it
was hiding under, and this one really is propagation delay.

MISO is source-synchronous: driven on the falling edge, sampled by the GC on
the next rising edge, so the whole path must fit a HALF period -- 18.5 ns at
27 MHz -- minus global-buffer insertion, pad, connector and the GC's setup.
The failing build measured ~10.5 ns of that internally.  Critical path:

  clk-to-q  1.39  tx_shift            (3,12)
  routing   3.60  tx_shift[7]  (3,12) -> (5,5)     to the CS mux
  logic     1.21  Mux(cs_n, 1, ...) LUT
  routing   4.79  miso  (5,5) -> (17,0)            to the J4 MISO pad
            10.99 ns   (2.60 logic, 8.39 routing)

Two removals, both of things that were pure cost here:

1. EXI_J4 is GONE entirely (exi_j4.py, the resource, the AND merge, the docs
   section, scripts/pi-exi-wirecheck.py) -- recoverable from git history.  The
   AND put a LUT in the CLOCK path, denying the EXI clock pad its dedicated
   global-buffer route, and MISO fanned out to the J4 pad at die (17,0) which
   was the far end of the critical output path: 4.79 ns of routing to a header
   with nothing plugged into it.  10.2-11.5 ns -> 9.3-9.6 ns.

2. The Mux(cs_n, 1, ...) LUT is gone from the MISO data path.  It only parked
   MISO high when deselected, which the flop's own reset value does for free:
   tx_shift now has init=0xFF and CS is that domain's async reset, so MISO is
   tx_shift[7] with nothing in between.  Removes 1.21 ns of logic plus the
   3.60 ns route feeding it and lets the placer sit the flop by the pad.

synth.py now PARSES AND RANKS BY the MISO clock-to-pad delay.  Ranking by fmax
was how a board that failed at 27 MHz passed every seed at >=1.29x margin --
fmax was never the binding constraint for this design.

--seeds 8: clk 28.7-32.5 MHz, EXI 49.6-62.1 MHz, MISO 8.38-10.22 ns, 8/8 PASS.
Best seed 8 (clk 30.35, EXI 49.56, MISO 8.38 ns), flashed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 19:16:58 +02:00
RoflinandClaude Opus 5 d35688390e Take the TX bit phase from the rising-edge counter, not a falling-edge one
The bit engine had TWO independent phase references: rx_bit counting rising
edges and tx_bit counting falling edges.  Those only stay in step while the
host emits exactly as many falls as rises, and it does not -- a transfer that
begins on a RISING edge (CS asserted while CLK is already low, or the clock
gating between two back-to-back host transfers) delivers a rise with no
matching fall.  tx_bit then slips one bit and never recovers.

RX never notices, because the rising count always equals the host's bit count.
So the header decodes perfectly, the right register is selected, and ONLY MISO
is wrong -- which is what made this look like an output-edge or output-delay
problem for two rounds of debugging.

Measured on a GameCube: every MISO byte shifted right by 2 bits, identically
at every EXI speed index.  NWAYS read 05 C0 (= 0x17 with two leading zero
bits), device ID 01 00 80 80, speed sweep ok=[0,0,0,0,0,0].

Two diagnoses were wrong before this one, and both were ruled out by evidence
rather than argument:
  - NOT propagation delay.  Index 0 is 1 MHz, a 1000 ns bit time; no output
    path fails there.  Speed-independence is the tell.
  - NOT the output edge.  The old rising-edge build and the new falling-edge
    build returned BYTE-IDENTICAL 05 C0.  Flipping the edge would have moved
    it; it did not, so the slip is upstream of the output stage.

What settled it was a measurement.  PHASE_METER=1 builds a diagnostic,
non-functional bitstream whose engine transmits a constant 0x80 per byte
instead of the FIFO head, taking the register file and both byte FIFOs out of
the MISO path so the host reads the engine's byte PHASE directly: 0x80
aligned, 0x40 one bit late, 0x20 two bits late.  Hardware read 0x20 in all
eight slots, localising the fault to the bit engine in a single run.

Fix: the shift register loads on rx_bit == 0.  One phase reference, on the
edge the host counts, so the two cannot drift.

Regression test (spi_mode3_slave T5) asserts CS while CLK is LOW so the
transaction begins on a rising edge.  The old counter fails it by exactly one
bit: A5 3C comes back as 52 9E.  EVERY earlier testbench asserted CS with CLK
high, so falls and rises always matched and the slip was structurally
invisible -- the same blind spot as the Mode 2/3 mix-up, which was invisible
to a master that parked MOSI early.  Vary the starting edge in SPI tests.

Timing after the fix, EXI_J4=1, --seeds 8: clk 26.9-31.1 MHz, EXI 45.8-61.7
MHz, 8/8 PASS.  Best seed 4 (clk 31.07, EXI 56.70), flashed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 19:04:52 +02:00
RoflinandClaude Opus 5 91c3bf0ece Clock the EXI bit engine from the EXI pin: timing closes on all 8 seeds
Replaces the PLL-fed 54 MHz `capture` domain, which oversampled the GC's
27 MHz EXI clock 2x and so put BIT-level timing across a CDC boundary: the
engine's view of the clock lagged the pin by 2 FFSynchronizer stages plus a
register (~55 ns) against a 37 ns bit time, so MISO came out ~2 bits late and
the GC read 0x17 as 05 C0.  No amount of oversampling fixes that -- 54 MHz
gives only 2 ticks per bit and the part cannot close the 81 MHz that 3x needs.

The engine is now clocked by the pin itself (pin 44 = global buffer input net
6, no board change), making MISO clock-to-Q.  ExiCapture creates four domains:
exiclk (posedge, samples MOSI), exiclk_n (negedge, drives MISO), both
async-reset by CS, plus reset-less exififo_w/exififo_r for the byte FIFO sides
-- reset-less because resetting one side of an AsyncFIFO desynchronises its
gray pointers, and posedge because AsyncFIFO has a posedge-only reset CDC
internally and cannot host a negedge side.

Also removes the SPRAM response FIFO.  The DMA read was one byte late because
the round trip did not fit the protocol's turnaround byte; the arbiter already
exposed valid/ready, so the FIFO bought a cycle of latency and nothing else.
The response is handled outside the FSM (the reply lands in RHDR_PAD, not
SPRAM_STREAM) with a one-entry hold for when tx_fifo is momentarily full.

The PLL was left instantiated but unused after the rewrite; removing it frees
the hard macro and pin 35 (which stays unconnected only because the board is
already built that way).  This is what lifted the pass rate:

  domain            target    before        after
  clk (sync)        24 MHz    28.1-30.2     26.4-32.2   8/8
  capture_clk       54 MHz    2 of 8 PASS   (gone)
  EXI pin domain    27 MHz    --            45.6-64.4   8/8

Best seed 3: clk 32.18 MHz, EXI 55.86 MHz.  The binding constraint has FLIPPED
-- `clk` (1.10-1.34x) is now tighter than the EXI domain (1.69-2.39x) -- so
synth.py ranks seeds by worst RELATIVE margin, not by the EXI number.

synth.py could not have scored this design at all: it required the vanished
capture_clk, and its regex '(\w+)' cannot match nextpnr's exi_clk_$glb_clk
(no $ in \w).  add_clock_constraint was also landing on the wrong net -- under
EXI_J4 the clock is the AND of two pads, not either pad -- so nextpnr silently
fell back to a 12 MHz default and under-optimised the whole EXI domain.

Testbenches:
- spi_mode3_slave had lost its testbench entirely in the rewrite.  Restored
  with a strict Mode 3 master (changes MOSI on the falling edge, samples MISO
  in the clock-low phase) and MUTATION-TESTED: flipping either clock edge in
  the RTL makes it fail.  Both of those bugs previously reached silicon
  against a lax master that parked MOSI well before the edge.
- exi_j4 T2/T3 returned [0,4,2,2].  Real bug: the engine latches the FIFO head
  at each byte boundary, so a 2-byte command header leaves only ONE drain
  slot.  DEVID streamed zeros until CS and left two stale bytes -- one too
  many -- so the NEXT enumeration came back a slot late.  DEVID now pushes
  exactly four bytes and stops.  Register-space reads may still over-push:
  their 4-byte header gives three drain slots.
- Dead ports removed from SPIMode3Slave (hdr_bytes, frame_start, cs_active,
  byte_idx) and the mode2 parameter is gone -- there is one correct edge
  convention and it is now enforced by tests.

All 14 module suites pass, plus bba_top at EXI_HALF=1/2/8.

CLAUDE.md: rewrote the clocking, CDC inventory and module tables; marked the
2026-09-30 capture_clk measurements historical; and corrected the line in
"Critical Protocol Notes" that still stated the INVERTED edge convention
("MOSI sampled on falling, MISO driven on rising" is Mode 2) -- the original
implementation followed that doc error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 18:36:51 +02:00
RoflinandClaude Opus 5 02239466fb Merge the exi and sync domains into one
bba_top wired ClockSignal("sync") to ClockSignal("exi"), so the two "domains"
were always the same physical clock net.  Everything between them -- six
AsyncFIFOs and three PulseSynchronizers -- was pure latency and logic for a
crossing that did not exist.

  * the six AsyncFIFOs become plain SyncFIFOs
  * the three PulseSynchronizers become wires
  * BBARegisterFile and ExiCapture move from m.d.exi to m.d.sync
  * the exi ClockDomain is gone; HFOSC drives sync directly

The SPRAM request write port is also combinational now.  A registered w_en
costs a 24 MHz cycle, and the protocol's turnaround byte (~296 ns at 27 MHz)
has no cycles to spare.

Passing: bba_register_file, spram_arbiter, rx_frame_assembler, tx_frame_drain,
w5100_parallel_master, uart_shell, status_panel.  bba_top at EXI_HALF=1
(27 MHz, the hardware rate) passes device ID, PAR read/write, NWAYS, RWP and
the 34-byte TX frame.

STILL OPEN: the SPRAM/DMA read is one byte late.  Removing the CDC was not
enough -- the remaining round trip is the rx crossing (exiclk->sync, a REAL
CDC, ~125 ns) plus FSM decode plus the spram_req FIFO, arbiter, SPRAM access
and spram_rsp FIFO, which still exceeds the turnaround byte.  Next step: now
that it is all one domain, drop the two SPRAM FIFOs entirely and give the
register file a direct request/grant port on the arbiter -- that should take
the round trip to ~2-3 cycles and fit.

exi_capture and exi_j4 testbenches still assert removed behaviour (the CS-fall
flush and the old MISO slot timing) and need rewriting for the clocked engine.
Still WIP -- do not flash.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 17:02:58 +02:00
RoflinandClaude Opus 5 da21a080d9 TX passes at 27 MHz; remaining DMA offset traced to needless exi/sync CDC
The TX failure was a TESTBENCH artifact, not a design bug.  Between
transactions the bench raised CS for one HP -- 18.5 ns at EXI_HALF=1 -- but
cs_active reaches the exi domain through a 2-stage FFSynchronizer at 24 MHz,
so the register file never saw one transaction end and swallowed the next
one's header as write data.  A real host leaves microseconds between
exi_deselect() and the next exi_select().  With a realistic gap the 34-byte TX
frame now matches byte-for-byte at the hardware rate.

At EXI_HALF=1 (27 MHz) bba_top now passes device ID, PAR read/write, NWAYS,
RWP and TX.  The one remaining failure is the SPRAM/DMA stream, offset by
exactly one byte.

Root cause of that offset, and it is not architectural: bba_top.py:171 does
    m.d.comb += ClockSignal("sync").eq(ClockSignal("exi"))
so exi and sync are LITERALLY THE SAME CLOCK NET.  Every AsyncFIFO between
them is pure overhead.  The SPRAM round trip pays two needless CDC crossings
(~6 cycles, ~250 ns) on top of the arbiter and SPRAM access, giving ~375 ns
against the ~170 ns the protocol's turnaround byte actually leaves once the rx
crossing is subtracted.  Register reads are unaffected because their response
is immediate; only SPRAM is late.

Fix direction: merge the two domains and replace the exi<->sync AsyncFIFOs and
PulseSynchronizers with plain SyncFIFOs/signals.  That removes the latency,
drops a lot of logic, and cannot change behaviour because the clocks are
already identical.

Known failing, both testing removed behaviour rather than new bugs:
  * exi_capture's own testbench asserts the CS-fall flush and the old MISO
    slot timing; the flush no longer exists (header pops drain the FIFO).
  * exi_j4's T3 re-enumerate is off by a byte.
Still WIP -- do not flash.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 16:56:03 +02:00
RoflinandClaude Opus 5 a3b3e052bc WIP rewrite: clock the EXI bit engine from the EXI clock pin
Replaces 54 MHz oversampling with a slave clocked by EXI CLK itself:
  exiclk    posedge  sample MOSI (Mode 3 samples on edge 2)
  exiclk_n  negedge  drive MISO (must settle between the master's samples)
  CS_N      async reset for both
EXI CLK (pin 44) is at die (6,0) = global buffer net 6 on the 5k-sg48, so it
can drive a real clock network with no board change.

This removes the capture domain entirely.  Bit-level timing no longer
crosses a clock boundary -- MISO is clock-to-Q instead of 3 ticks of a foreign
clock -- and the only crossings left are the byte FIFOs, which have a whole
byte-time (~296 ns) of slack rather than a fraction of a bit.

AsyncFIFO needs a posedge clock for its internal reset CDC, so the FIFO sides
use reset-less POSEDGE domains on the same pin; the engine pops on the posedge
and hands the byte to the negedge output stage through a register.  The FIFOs
must not see CS: resetting one side of an AsyncFIFO desynchronises its gray
pointers.

Two bugs found and fixed along the way:
  * one pop per byte boundary is all that is needed -- pops during the header
    drain whatever the previous transaction left (replacing the CS-fall flush)
    and the turnaround-byte push lands in data slot 0.  No slot-threshold or
    header-length logic at all.
  * rx_byte/rx_valid had to become COMBINATIONAL.  Registered, they need one
    more clock edge, and after a transaction's final bit the clock stops and
    CS rises -- so the last byte of every transaction was lost (a 4-byte write
    landed as DE AD BE 00, a 34-byte TX as 15 bytes).

At the HARDWARE rate (EXI_HALF=1, 27 MHz) the device ID, NWAYS, PAR
read/write and the W5100->SPRAM RX path now all pass -- none of which has ever
worked at 27 MHz before.  Testbenches now drive the SPI clock with real
delays, since there is no capture domain to count ticks against.

STILL BROKEN: the SPRAM/DMA stream is offset by one byte (its round trip
exceeds the single turnaround byte), and TX lands 15 of 34 bytes.  Do not
flash this yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 16:47:55 +02:00
RoflinandClaude Opus 5 d95f2147a4 docs: EXI edge fix confirmed on hardware; MISO delay is the one open fault
Hardware speed sweep after the Mode 3 fix: ok=[1,1,1,0,0,0] -- register reads
are CORRECT up to 4 MHz and fail from 8 MHz.  The MOSI edge fix works and the
gateware is functionally correct; what remains is a fixed MISO output delay of
~3 capture ticks (~55 ns: 2 stages of input FFSynchronizer plus 1 tick to
register the shift).  At 27 MHz that is ~2 bits, which is exactly the observed
NWAYS read of 05 C0 = 0x17 >> 2, and the 250ns-pass / 125ns-fail threshold
brackets the delay at 60-125 ns.

Also records that lowering the EXI clock is not available: Swiss hardcodes
EXI_SPEED_32MHZ in exi_select().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 16:33:33 +02:00
RoflinandClaude Opus 5 afc10d7e5c fix(spi): sample MOSI on the RISING edge -- real Mode 3, not Mode 2
The docs call this 'Mode 3 (CPOL=1, CPHA=1)' and then describe sampling MOSI
on the FALLING edge.  Idle-high plus sample-on-falling is Mode 2.  Real Mode 3
samples on the second edge of each cycle, which with an idle-high clock is the
RISING one, and that is what the GameCube does.

Sampling on the same edge the GC drives MOSI latched the PREVIOUS bit, so
every byte arrived one bit late in both directions, at every clock speed:
the 0x80 register-space header byte was read as 0x00, which decodes as a
command-space device-ID query -- so NWAYS reads returned the device ID and the
speed sweep failed at every index.  Hardware raw dump, first transaction after
an FPGA reset:

    00 00 02 01 01 00 00 00      02 01 01 00 = 04 02 02 00 >> 1

The Pi probe never caught it because that script was written to match this
device, and every simulation testbench drives MOSI many capture ticks before
toggling the clock, so MOSI is stable at BOTH edges and the convention cannot
be distinguished.  The bba_top master now models a true Mode 3 master (drive
on falling, sample on rising) so the suite can tell the difference.

SPIMode3Slave(mode2=True) restores the old behaviour for the Pi/J4 probe.

Known remaining issue: at EXI_HALF=1 (27 MHz, 2x oversampling) reads are still
shifted, because the 2-stage input FFSynchronizer costs 2 capture ticks which
at 27 MHz is a whole bit.  That is a separate fault and only bites at 2x.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 13:00:49 +02:00
RoflinandClaude Opus 5 6f151cd1ff gc_bba_test: add raw slot dumps to locate the response
Reading one byte and getting 0x00 cannot distinguish 'decode is wrong' from
'reply is late' -- a one-byte window has nowhere for a late answer to show up.
These two probes clock EIGHT data bytes and print every slot, so the position
of 0x17 (NWAYS) or 0x04 (device ID) gives the latency directly, the same way
bit-shift analysis turned 00 00 00 10 into 'correct, 22 bits late'.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 12:46:46 +02:00
RoflinandClaude Opus 5 e682fded4a gc_bba_test: disambiguate the speed sweep result
'highest good = 0' meant both 'only index 0 works' and 'nothing works', which
are opposite diagnoses -- the first says oversampling, the second rules it
out.  Now reports the per-index map and says NONE explicitly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 12:43:11 +02:00
RoflinandClaude Opus 5 7df0c97c78 sim: make the EXI clock rate configurable; default was 8x too slow
bba_top derived its EXI clock from HALF, defaulting to 3.4 MHz against the
real GC's ~27 MHz.  At that rate the whole CDC response latency fits inside
one bit time, so the suite passed green on a design that failed on console --
this is how both the wrong-header-format and the TX-pop-threshold bugs
reached hardware with everything green.

HALF is now EXI_HALF-configurable (default 8 for fast regression).  At the
real rate, EXI_HALF=1, every read comes back shifted right by ONE BIT:
04 02 02 00 -> 02 01 01 00, NWAYS 0x17 -> 0x0B.  That is a bit-level sampling
misalignment, not the byte-level latency chased earlier: at 54 MHz capture
there are only 2 ticks per EXI bit and the 2-stage FFSynchronizer on EXI CLK
costs 2 ticks, a whole bit.

Recorded as an open bug in CLAUDE.md with the measurements.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 12:36:50 +02:00
RoflinandClaude Opus 5 399fbf02a4 fix(exi_capture): TX pop threshold must follow the header length
tx_fifo.r_en popped from txld_cnt >= 2, hard-coded for the old invented
2-byte header where the data phase began at slot 2.  The real protocol's
register space uses a 4-byte header, so data starts at slot 4 -- two bytes
were being consumed before the GC clocked any data, and every register read
came back wrong.  On hardware: HIPR and NWAYS both read 0x00, and the device
ID returned 01 80 c0 c0 (stale bytes, including our own header command bytes
0x80/0xC0 echoing back out of the FIFO).

The threshold is now derived from byte 0 bit 7, which is exactly what selects
the header form:  register space -> 4 slots, command space -> 2 slots.

Also removes the pad-slot filler byte the register file was pushing.  That was
compensating for this bug rather than fixing it -- it made the simulation pass
while hardware still failed, which is how it slipped through.  With the pop
threshold correct the filler is wrong, and both the unit and integration
testbenches agree without it.

All suites pass: bba_register_file, bba_top, exi_capture, exi_j4,
spi_mode3_slave, tx_frame_drain, spram_arbiter, rx_frame_assembler,
w5100_parallel_master.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 12:27:31 +02:00
RoflinandClaude Opus 5 2458884160 Rework the EXI decoder to the real GC<->BBA protocol
Replaces the invented 2-byte header (addr split across both bytes, 2-bit
length field) with the protocol libogc and Swiss actually speak:

  command  (bit7=0, 6-bit reg)   [reg, 00, 00, data]  one 4-byte transfer
  register (bit7=1, 16-bit reg)  [0x80|w<<6, ah, al, 00] + data until CS

This was not an optimisation: the old decoder read Swiss's NWAYS read
(80 00 31 00) as 'write 0x31 to NCRA', and bit 0 of 0x31 is RESET, so every
register read would have reset the device.

Changes:
  * 16-bit address; no length field -- register reads and writes now stream
    until CS, like the SPRAM path already did
  * RHDR_LO starts the SPRAM fetch as soon as the address completes, using
    the trailing header byte as the prefetch window the protocol provides
  * command space serves EXI regs 0x02/0x03 (interrupt mask/status), the only
    two Swiss uses
  * EXI_GetID gets its own path: it offers just ONE header byte of warning, so
    the first ID byte is pushed the instant byte 0 identifies the query

Two timing subtleties, both found by simulation and commented in place:
  * MISO is driven LIVE from the tx FIFO head, so a byte pushed while the pad
    byte is on the wire appears in the pad slot.  A filler byte is parked
    there (MISO is don't-care through the header) so the first real response
    lands in data slot 0.
  * the prefetch must be one-shot; re-firing while waiting for the pad byte
    consumed the register stream early and shifted reads by a byte.

All testbenches updated to the real format and passing: bba_register_file
(7), bba_top (8 incl. the full W5100->SPRAM->GC loop and TX), exi_j4,
exi_capture, spi_mode3_slave, tx_frame_drain, spram_arbiter,
rx_frame_assembler, w5100_parallel_master.  gc_bba_test rebuilt against the
new protocol.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 11:58:52 +02:00
RoflinandClaude Opus 5 f2762f613d docs: record the REAL GC<->BBA EXI protocol; current header format is wrong
Read from the two drivers that will actually talk to this device: libogc
(libdb/uIP/bba.c) and Swiss (cube/patches/bba/bba.c).  The implemented 2-byte
header with a 2-bit length field does not match either.

The real protocol has two address spaces selected by bit 7 of byte 0, and no
length field at all -- the data phase runs until CS:

  COMMAND  (6-bit reg)   one 4-byte readwrite; response in byte 3
  REGISTER (16-bit reg)  [0x80|w<<6, reg>>8, reg&0xFF, 0x00] + data phase

NCRA, IR, NWAYS, RWP, RRP and PAR all live in the register space.  The
mismatch is destructive rather than merely non-functional: Swiss reading
NWAYS sends 80 00 31 00, which the current decoder reads as 'write 0x31 to
NCRA' -- and bit 0 of 0x31 is RESET.  Every register read would reset the
device.  The device-ID query is the one case where the two formats coincide,
which is exactly why enumeration worked on hardware and nothing else would
have.

Also explains the 22-bit response latency measured on console: the trailing
0x00 byte in the real header is the device's prefetch window, and the current
format spends those bits on the length field, leaving zero slack.  Adopting
the real format is required for compatibility anyway and recovers ~296 ns of
the ~815 ns deficit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 11:47:50 +02:00
RoflinandClaude Opus 5 abc61f3e66 gc_bba_test: add header->data idle gap; device replies 22 bit-times late
First on-console run: the Identity suite failed with the device ID reading
00 00 00 10 instead of 04 02 02 00.  Bit analysis shows that is an EXACT
match for the correct stream delayed by 22 bits -- the device answers
correctly, just too late for a 32-clock window to catch more than its first
set bit.  Not corruption, and not a broken link: response latency.

At EXI index 5 (~27 MHz) 22 bit-times is ~815 ns, which is about what the CDC
chain costs (rx AsyncFIFO capture->exi, register-file decode, tx AsyncFIFO
exi->capture, SPI engine) at a 24 MHz exi clock.  The design expects the GC to
hold the clock idle between the header write and the data read so the register
file can prefetch the response; libogc's inter-call overhead alone is not
enough at full speed.

Adds bba_gap_us (default 16 us, busy-waited with CS asserted since we hold the
EXI lock) and a Gap sweep test that reports the minimum clean gap at the
current speed -- which measures the latency rather than guessing at it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 11:37:00 +02:00
RoflinandClaude Opus 5 b363e76ec4 scripts: add gc-testlink.sh to configure the PC side of the test link
Sets the PC to 10.0.0.50/24 so the GC test suite's RX test can pass: its TX
test ARPs for that address and the kernel answers ARP for an address it
actually holds, so no listener process is needed.  Without it RX fails
legitimately for lack of traffic.

Handles the NetworkManager trap -- NM silently strips a hand-added address
within seconds, so the device is set unmanaged first and handed back on
'down'.  Warns that this drops any existing connection on that NIC.

up / down / status / watch, with wired-interface auto-detection (prefers one
with a carrier) and a status readout that flags no-carrier along with the
likely cause: the W5100S rail is gated off GC_ON, so it is dead whenever the
console is off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 11:26:52 +02:00
RoflinandClaude Opus 5 d8725392a5 gc_bba_test: fix inverted EXI error checks; builds clean
libogc's EXI functions return '1 on success, <=0 on error', but every call
site tested '== 0'.  A -1 error return therefore read as success, so a failed
EXI_Lock or EXI_Select would fall through and the subsequent transfer would
produce garbage reported as a misleading test failure rather than an EXI
error.  All six call sites now test <= 0.

Also retries EXI_Lock for ~100 ms: it fails outright rather than blocking when
another subsystem holds the channel, and Swiss or a libogc driver may hold it
briefly -- without the retry a transient collision surfaces as a test failure.

Builds clean with devkitPPC 16.1.0 / GCC 16, no warnings: 172 KB .dol.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 11:24:47 +02:00
RoflinandClaude Opus 5 ccf4eb4996 Add on-console GameCube test suite + devkitPPC in the devcontainer
gc_bba_test/ builds a .dol that drives the adapter over raw EXI from the
console and reports pass/fail per test on the TV, navigable with the pad.

Uses libogc's EXI API against EXI0:2 (Serial Port 1) rather than its bba
network driver: driving the register file directly gives each check its own
verdict, so a failure points at a specific register instead of at
'networking'.  Six suites - identity, registers, reset/control, EXI link
integrity (including an EXI clock-index sweep), ethernet TX/RX, SPRAM/DMA
streaming.

Encodes the hardware lessons from bring-up so the app cannot mislead:
  * the device ID is a one-shot cleared by the first NCRA reset, so it is
    reported SKIP on a re-run rather than FAIL
  * TI means the frame reached the W5100, NOT the wire - the TX test says so
  * register reads clock exactly the requested length (over-clocking
    desynchronises the next transaction)
  * TXDATA is written one byte per transaction (eff_addr auto-increments)

.devcontainer/Dockerfile gains the devkitPro gamecube-dev group.  devkitPro
publishes no .deb on GitHub, so this uses their installer script against
apt.devkitpro.org -- which is 403 from inside the running container's egress
but reachable from a host-side docker build.  Rebuild the container to get it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 10:59:37 +02:00
RoflinandClaude Opus 5 f2a721cb1f docs: record the two CPL export steps the recipe was missing
Regenerating the production files for the V2.1 eFuse rework surfaced two
steps that neither the skill nor CPL_ROTATIONS.md documented, both caught
only by diffing against the previously-shipped CPL:

- --exclude-dnp does not drop 'in_bom no' parts, so J4 and TP1-TP5 landed in
  the raw export although the shipped CPL excluded them.  Filter to
  designators that have a BOM line; the two sets should end up equal.
- kicad-cli emits signed rotations while the correction step's % 360 only
  normalises corrected parts.  Mixing the two made the diff show 34 phantom
  rotation changes (270.0 vs -90.0 is the same angle), burying the 2 real
  ones.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 15:20:56 +02:00
RoflinandClaude Opus 5 c32ae49f28 fix(efuse): connect U13's exposed thermal pad to GND
The TPS25961's exposed pad is GND and the datasheet requires it be connected
to the GND plane (Table 5-1; package note 3 adds a mechanical requirement).
REVIEW.md claimed this was honoured because the footprint is the _ThermalVias
variant -- but the variant alone connects nothing.  hardware/TPS25961.kicad_sym
defined pins 1-6 only, with no pin for the exposed pad, so KiCad had nothing to
map onto footprint pad 7 and left it netless: an isolated copper island under
the chip with its thermal vias going nowhere.

Adds pin 7 (GND, power_in, hidden, stacked on pin 4) to the symbol library, the
cached symbol in exi.kicad_sch, and the U13 instance.

PCB updated and verified: U13 pad 7 carries GND on all four elements (both SMD
rects and both thermal vias), R48 placed, EFUSE_OVLO routed, R49 gone.  DRC
reports 0 violations, 0 unconnected pads and 0 schematic parity issues -- the
0-unconnected result is what confirms pad 7 is tied into the GND network rather
than merely labelled.  The 4 DRC errors previously dismissed as zone-fill
phantoms on this pad are gone, though zones were refilled in the same pass so
that is not proof of cause either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 15:15:19 +02:00
RoflinandClaude Opus 5 20a8a27e71 fix(bom): correct Tolerance fields that contradicted their MPN
13 resistors carried Tolerance=5% while their MPN is a UNI-ROYAL 0402WG*F*
part -- F is the EIA code for +/-1% (J is 5%), and LCSC confirms the 1% rating.
The parts are 1%; only the metadata was wrong.

Worth fixing rather than leaving: the same part number appearing with two
different tolerances splits into two BOM lines under any grouping that
includes the field, and it misleads anyone sizing a divider.  R48 in the eFuse
ladder was one of these (fixed in a9052c6) and there it was load-bearing --
at a genuine 5% the OVLO worst case reaches 17.05 V, over U3's 17 V limit,
versus 14.42-15.83 V at the correct 1%.

Metadata only; no circuit or part number changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 09:14:22 +00:00
RoflinandClaude Opus 5 a9052c6a6b Rework the eFuse schematic for readability; 3-resistor OVLO divider
Redraws the eFuse area with proper wires instead of labels-on-pins, and
simplifies the divider that 228ff03 added.

The first draft split R3 into 12 k + 2.2 k to hit 14.2 k, purely to preserve
the original ~8.9 V UVLO.  That bought a fourth resistor and a named junction
net (EFUSE_R3MID) that nothing else touched.  The ~9 V UVLO turned out to be
an assumption carried over from the original design rather than a requirement:
U3's minimum input is 4.5 V, so relaxing it to 6.78 V costs nothing and is in
fact less likely to chatter if the GC rail sags.

  R46  620 k -> 100 k   (C25741)   R1: 12V_RAW -> EN/UVLO
  R47  100 k -> 12.4 k  (C11692)   R2: EN/UVLO -> OVLO
  R48  NEW      10 k    (C25744)   R3: OVLO -> GND
  R49  removed
  R45  100 k -> 33 k    (C25779)   ILIM 0.52 A -> 1.52 A

OVLO is unchanged at 15.18 V typ (14.47-15.78 worst case) against U3's 17 V
limit, so the simplification costs nothing on the parameter that matters.

Verified: 12V_RAW(R46.2) / EFUSE_EN(R46.1,R47.1,U13.5) / EFUSE_OVLO(R47.2,
R48.1,U13.2) / GND(R48.2,R45.2,U13.4); 152 -> 151 components with only R49
removed; ERC 0 violations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 09:14:04 +00:00
RoflinandClaude Opus 5 228ff03cb6 schematic: eFuse OVLO ladder + ILIM fix (V2.1)
Implements the values worked out in 6ce7657.

  R45  100k -> 33k    ILIM 0.52 A -> 1.52 A (clears the 621 mA inrush)
  R46  620k -> 150k   ladder R1: 12V_RAW -> EN/UVLO
  R47  100k -> 10k    ladder R2: EN/UVLO -> OVLO
  R48  NEW    12k  \  ladder R3 = 14.2k: OVLO -> GND
  R49  NEW    2.2k /
  U13 pin 2 (OVLO) taken off GND and onto the new EFUSE_OVLO net

Verified: netlist shows 12V_RAW(R46.1) / EFUSE_EN(R46.2,R47.1,U13.5) /
EFUSE_OVLO(R47.2,R48.1,U13.2) / EFUSE_R3MID(R48.2,R49.1) / GND(R49.2,R45.2,
U13.4); 150 -> 152 components with none dropped (the malformed-property trap
from the kicad-bom-edit skill); ERC 0 violations.

PCB placement and routing still to do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 08:52:34 +00:00
RoflinandClaude Opus 5 6ce7657f05 eFuse V2.1 fix: datasheet-derived OVLO ladder + ILIM correction
Works the TPS25961 fix out from the datasheet rather than from memory, and
re-solves it against resistors already on the BOM.

OVLO: TI's 3-resistor ladder (eq 7/8) -> R46 620k->150k, R47 100k->10k, new
R48 12k + R49 2.2k (=14.2k).  UVLO 8.93 V typ, unchanged from today; OVLO
15.21 V typ, 15.81 V worst case.  The OVLO ceiling is set by U3 (TPS562201,
4.5-17 V input), not by the eFuse -- that rules out a tempting 3-resistor
solution tripping at 17.24 V.

ILIM: R45 100k -> 33k.  0.516 A sat BELOW the board's own 621 mA inrush, so
the part entered current limiting on every power-on (-> thermal shutdown ->
retries -> latch-off).  Inrush control comes from the fixed 5.17 V/ms slew
rate, not ILIM, so raising ILIM costs no inrush protection; it is pure fault
protection at ~27x the measured 56 mA load.

Every value is already stocked and 620k loses its only user: one FEWER unique
part, +2 placements.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 08:44:48 +00:00
RoflinandClaude Opus 5 b0e8c32619 Full ARP round trip on hardware: 5 requests out, 5 replies in
The PC's replies are unicast to our own MAC, which it could only learn from
our requests, so the loop is closed independently of any packet capture.

Records the four W5100 fixes it took and the caveat that TI does not mean the
chip transmitted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:25:58 +00:00
RoflinandClaude Opus 5 5f60c011cb fix(w5100): clear stale Sn_IR bits when INT_N asserts with no RX data
RX_RSR_CHK returned to IDLE when RSR==0 without clearing Sn_IR.  SEND_OK
(bit 4) fires after every successful transmit and the RX path only ever
cleared RECV (bit 2), so INT_N stayed asserted; because IDLE tests
~w5100_int_n BEFORE tx_valid, the FSM span IDLE->RX_CHECK->RX_RSR_CHK->IDLE
and TX was starved permanently.  Observed on hardware as the first frame
transmitting and every later one never setting TI while RX kept working.

Now clears every Sn_IR bit except RECV, so a frame arriving between the RSR
read and the write still raises INT_N and is served next pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:16:51 +00:00
RoflinandClaude Opus 5 e80b7485d5 fix(w5100): actually drive RSTn on init so a wedged chip can recover
w5100_rst_n was declared and never driven anywhere -- the pin sat at its init
value of 1 forever.  The only reset the chip ever got was the MR software
reset, which arrives over the very bus a wedged chip has stopped answering, so
a wedged W5100 could not be recovered at all: not by NCRA reset, not by
reflashing, not even by reconfiguring the FPGA (RSTn just floats to its R36
pull-up while unconfigured).  Only a 12 V power cycle worked.

Init now pulses RSTn low (1 ms, datasheet minimum is ~2 us) and waits the full
post-reset settle before touching the bus.  RSTn low time is clamped to
reset_cycles so the testbenches' small values stay reachable by wait_ctr.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:09:25 +00:00
RoflinandClaude Opus 5 b42dc65796 fix(w5100): wait 83ms after software reset, not 1ms
The init does MR=0x80 (software reset) then waits reset_cycles before writing
NETLCKR/SHAR/SIPR/socket registers.  That wait was 24_000 cycles = 1 ms at
24 MHz, but the W5100S needs the datasheet's 60.3 ms -- our own bus self-test
needed ~85 ms before the chip answered at all.

1 ms is marginal rather than simply wrong, which is why this presented as
intermittent: an NCRA reset sometimes left the chip still resetting while the
init writes landed on it and were silently dropped, leaving it misconfigured
and the bus master blocked.  Symptom was a W5100 that wedged after an NCRA
reset -- TX never completing and the UART shell's own 'udp unicast' hanging --
recoverable only by a 12 V power cycle.  Note RST_N is never asserted by the
gateware; only the software reset is used, so this wait is the only settle
time the chip gets.

Default is now 2_000_000 = 83 ms.  Testbenches pass small values explicitly and
are unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:02:52 +00:00
RoflinandClaude Opus 5 6621ce9d0d Both directions proven on hardware: 60-byte ARP out, DHCP frame in
TX: the TWD fix lands a full 60-byte ARP on the wire, byte-for-byte correct
(previously truncated at 16).  TI sets.

RX: a real 342-byte DHCP broadcast from the PC was received in MACRAW mode,
assembled into the SPRAM ring and DMA-read back over EXI.  The descriptor
length 0x015A cross-checks exactly against the frame's own IP total-length
field plus the ethernet header, so the assembler's length is confirmed, not
merely plausible.

Also replaces the full-path probe's 'is 08 and 06 somewhere in the buffer'
guess with a real descriptor decode -- it called a DHCP frame an ARP reply.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 18:59:26 +00:00
RoflinandClaude Opus 5 d156df1dda docs: record post-fix timing sweep (2/8 seeds, best 60.03 MHz)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 18:53:36 +00:00
RoflinandClaude Opus 5 80957f35db fix(tx): queue the frame length at TWD_HI so the drain runs concurrently
TXFrameDrain sat in IDLE until it had a length, and the length only reached
tx_ctrl when the NCRA ST bits were written -- after the GC had streamed the
whole frame.  So the entire frame had to fit in the 16-deep tx_bytes FIFO, and
BBARegisterFile silently drops bytes when w_rdy is low (SPI cannot
back-pressure).  On hardware this truncated frames at exactly 16 bytes: a
captured ARP had 16 correct bytes followed by the frame restarting over itself.

The length is now handed to the drain one cycle after the TWD_HI (0x35) write,
which the GC issues BEFORE streaming data.  The drain starts early and blocks
on tx_bytes_r_rdy, consuming bytes as they arrive, so tx_bytes becomes a
rate-matching buffer instead of whole-frame storage.  The ST write remains a
fallback trigger for software that skips TWD, guarded by tx_queued so one frame
is never queued twice.

Adds BBATop T8: streams a 34-byte frame (> the 16-deep FIFO) over EXI and
checks the W5100 model received it byte-for-byte.  Verified to FAIL without the
fix with exactly the hardware symptom -- 16/34 bytes, first bad byte at index
16.  The gap that let this ship: TXFrameDrain's own tests drive the FIFO
signals directly, and the BBATop test only covered the RX loop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 18:49:04 +00:00
RoflinandClaude Opus 5 d669917cfe docs: correct the TX diagnosis - it IS tx_bytes depth
A captured frame proves the full EXI->FPGA->W5100->wire path works: the first
16 bytes are byte-perfect (broadcast dst, PAR0-5 source MAC, ARP ethertype).
It truncates at exactly 16 = the tx_bytes FIFO depth, because TXFrameDrain
waits for a length that only arrives at the ST trigger, after the whole frame
has been streamed.

Supersedes the previous note, which concluded it was NOT depth-related; that
came from a clean-reset run whose result was invalid because the W5100 was
already wedged, so nothing transmitted regardless of length.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 18:45:08 +00:00
RoflinandClaude Opus 5 2b210dc5e3 EXI bring-up: full Swiss init runs on hardware; W5100 TX blocked
The whole GC init sequence now runs over EXI on the real board: device ID,
NCRA reset self-clear, PAR0-5, MAR, ANALOG, NWAYC, IMR, NCRA SR, NWAYS 0x17.

GC->wire TX does not complete.  Not an EXI fault and not tx_bytes depth --
8-byte frames fail too, and the UART shell's own UDP send hangs the same way,
so the W5100 bus master is stuck and TXFrameDrain is waiting on it.

Also records that id_phase is a one-shot cleared by the first NCRA reset, so
a re-run probe reads 00 00 00 00 for the device ID and looks broken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 18:42:16 +00:00
RoflinandClaude Opus 5 a082e15c4f EXI bring-up: device enumerates on hardware over the J4 header
First silicon proof of the EXI capture path.  A Pi bit-banging EXI into J4
reads the device ID 04 02 02 00 21/21, NWAYS 0x17, HIPR 0x01, writes and reads
back PAR0-3, writes IMR, and sees NCRA's reset bit self-clear.

Fixes two host-side bugs found getting there:
- exi_devid_rpi.py sampled MISO just after the rising edge, racing the slave's
  own update and landing one bit early (04 came back as 08).  Sample in the
  CLK-low phase, as the simulation does.
- pi-exi-wirecheck.py's bridge scan drove HIGH against pull-downs, but the
  FPGA's SB_IO pull-up out-pulls the Pi's internal pull-down, so every pin
  reported as bridged.  The aggressor now drives LOW against pull-ups.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 18:32:18 +00:00
RoflinandClaude Opus 5 28b7665b33 scripts: Pi<->J4 EXI wiring checker
Verifies the harness before attempting a real EXI transfer.  Keyed on the
EXI_J4 build's SB_IO pull-ups: an FPGA input with PULLUP=1 reads HIGH when
connected, follows the Pi's pull when open, and reads a hard 0 only when
shorted -- so all three cases are distinguishable from the Pi alone.

Includes a T0 control on known-unwired pins; without it a broken pull
mechanism would turn every 'connected' verdict into a false pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 18:18:34 +00:00
RoflinandClaude Opus 5 9a7eb54801 exi_j4: shared AND-merge helper + enumeration testbench
Factors the EXI_J4 pad merge out of synth.py so the synthesis path and the
test exercise the same code.  Verifies the device ID (04 02 02 00) enumerates
over SP1 with J4 idle, over J4 with SP1 idle, and over SP1 again afterwards,
plus a register read via J4 and the full cs_n truth table.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 18:12:26 +00:00