Commit Graph

50 Commits

Author SHA1 Message Date
Roflin f4e7e2c661 CLAUDE.md: refresh the seed table after the cleanup pass
Best seed is 3, not 6, and the flash skill points at build/seed<N>/top.bin --
so a stale seed number sends you to the wrong bitstream.  Also records the
area, which is the thing the cleanup actually moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-10 19:42:19 +02:00
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin ccc13d4302 docs: record EXI_J4 J4-header EXI merge + measured seed sweep
4/8 seeds pass capture, best seed 8 = 63.09 MHz (+17% margin) — same pass
rate as the default build, so the AND-merge costs no measurable timing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 18:00:20 +00:00
Roflin 3d37e4ba6d Hardware bring-up: first UDP packet transmitted and captured
The W5100S ethernet back-end now works end-to-end on real hardware: a complete,
correctly-checksummed UDP datagram (10.0.0.99:40000 -> 10.0.0.50:6464, 32-byte
payload) was transmitted by the FPGA-driven W5100S and captured on a PC, after a
full bidirectional ARP handshake.

Records the three W5100S-specific findings in CLAUDE.md (NETLCKR unlock, the
15-bit IDM address space putting the buffers at 0x4000/0x6000, and the 60.3 ms
reset-to-ready time), and notes that UDP unicast is the reliable bring-up path
while a 255.255.255.255 send sets SEND_OK without appearing on the wire.

w5100_udptest.py is the self-contained configure+send+readback probe that
isolated all of this.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 19:10:07 +00:00
Roflin 1a7f0689ab Enable in-container USB flashing + flash-re-bba-rb skill; find U7 EEPROM undersized
Devcontainer now grants libusb access to the FT2232H so iceprog (bitstream) and
pyftdi (FT2232H EEPROM) run inside the container:
- devcontainer.json: --privileged + /dev/bus/usb bind mount
- Dockerfile: sudo (NOPASSWD for vscode; USB nodes are root-owned), libusb-1.0-0,
  libftdi1-2, pyftdi

Add the flash-re-bba-rb skill documenting the procedure (bitstream via best swept
seed, optional EEPROM string, troubleshooting, guardrails) and
flash_ftdi_eeprom.py (pyftdi, backup-first, dry-run default) + ftdi_eeprom.conf
value spec.

Bring-up on the repaired V1 unit: bitstream flash VERIFY OK (iceprog
build/seed4/top.bin, capture 58.36 MHz). EEPROM product string CANNOT be
programmed on V1 — U7 is a 93LC46B (128 B), too small for the FT2232H, which
needs a 93LC56B (256 B); the 128 B chip mirrors and the config write fails
verify. This overturns the old REVIEW note "93LC46B = correct for FT2232H"
(that only checked ORG, not size). Board still works blank -> ROM defaults;
iceprog + UART unaffected. Documented in REVIEW.md + TODO.md (swap U7->93LC56B
for V2); flash_ftdi_eeprom.py detects the mirroring and refuses the write.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-22 14:44:43 +00:00
Roflin 64123014bf docs: record capture-domain bottleneck + future optimization levers
Board V1 ships as-is (capture closes full-speed on 4/8 seeds). Documents,
for a possible future push toward more capture margin (e.g. 64 MHz), that
the binding path is now the tx_fifo exi->capture Gray-code CDC pointer
loop (~6.4ns logic + ~10.7ns routing = 17.1ns / 58.4MHz), and the two
measured levers: a lightweight 2-deep ping-pong TX CDC (RTL, pinout-
independent, est. 67-80MHz - the reliable one) and EXI/W5100 pin
co-location on one die edge (respin, routing-half only, ~63MHz seen).
Both investigated but NOT applied.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 18:37:35 +00:00
Roflin c16afb6eea Add integrated UART UDP bring-up shell (socket 3) + capture critical-path fix
An interactive UART command shell drives a UDP send/receive test on the
W5100's socket 3, running ALONGSIDE the live BBA (socket-0 MACRAW) with
EXI keeping bus priority. MACRAW+UDP coexistence is W5100S-datasheet
confirmed (S4.6 + "4 independent SOCKETs"). Now the default flash build
(--console selects the old event-log console).

New / changed gateware:
- uart_shell.py (new): rebbarb> shell over FT2232H channel B. Commands:
  help; udp unicast <ip> [msg]; udp broadcast [msg]. After each send it
  waits (bounded, else "timeout") for a reply and prints "rx <payload>".
  Line buffer + message ROM live in block RAM with a sequential parser
  (LC-efficient); 1-deep RX holding reg keeps pastes intact. 8 sim tests.
- w5100_parallel_master.py: configurable UDP socket (default 3) with UDP
  send AND receive (IP-stack init, runtime dest IP, WIZnet UDP RX header
  + payload). Gated by enable_udp_test so the MACRAW path is unchanged
  when off. Tests U1-U4 + MACRAW T1-T5.
- exi_capture.py: CAPTURE-DOMAIN CRITICAL-PATH FIX. The TX byte-FIFO
  read-enable was gated by its own gray-coded ready
  (r_en = ... | (flushing & r_rdy)), forming a consume_ptr -> gray ->
  r_rdy -> flush -> r_en -> consume_ptr loop that capped capture_clk.
  Replaced the r_rdy-based "drain until empty" flush with a fixed-length
  drain counter (FIFO is only tx_depth deep), removing the pointer
  feedback from r_en. Path 24.3 -> 19.6 ns; flush behavior preserved.
- bba_top.py: wire shell <-> W5100 UDP (send + rx); shell additive.
- synth.py: shell default build; env-var UDP network config; documents a
  reverted PNR-timing-priority experiment.

Timing (--seeds 8, default shell build, 67% LC): capture closes on 4/8
seeds (best seed 4 = 58.36 MHz, +8%), clk passes on all. This is BETTER
than the pre-shell 2/8 baseline because the flush fix improved the
capture domain intrinsically. Flash build/seed4/top.bin.

Bring-up caveats (unchanged): W5100 socket register addresses / UDP
header format are datasheet-derived (confirm on hardware); UDP_SRC_IP /
subnet / gateway must match the LAN for unicast ARP.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 14:36:47 +00:00
Dennis Brentjes 7d7ab34bff Fixes review comments. 2026-07-31 22:06:21 +02:00
Dennis Brentjes 5828d6a388 Finished the board including the connector and mousebites 2026-07-24 14:04:04 +02:00
Roflin a86aa255f1 Add REVIEW.md: complete hardware-review coverage record
Documents everything examined in the 2026-07-17/18 review - not just
findings but full coverage with verdicts, so future sessions know what
not to re-review: per-sheet verification against datasheets and the
WIZnet reference design, the settled look-wrong-but-correct list
(VPHY/VPLL on 3.3V, D4/D5 floating VBUS + flow-through, EN at 12V,
RSET 12.4k, no CT cap, DO/DI direction evidence), PCB/JLC/BOM audit
state, explicit not-reviewed list (bench/gateware items), and a binding
rule to keep the file updated in the same commit as any change to a
reviewed circuit. Referenced from CLAUDE.md alongside TODO.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 20:21:39 +02:00
Roflin fe96876a80 Fix EXI DO/DI swap: rename nets + swap damping resistors, no copper changes
SP1 DO/DI naming is console-perspective (verified against SD Gecko and
SD2SP2 wiring): pin 6 DO = GC data out = adapter MOSI input, pin 9 DI =
GC data in = adapter MISO output. The design doc's SP1 table had the
roles reversed and the schematic followed it, putting the FPGA's MISO
driver on the GC-driven DO line (bus contention).

Fix keeps all PCB routing identical:
- Swap net names EXI_MOSI(_RAW) <-> EXI_MISO(_RAW) across exi/Fpga/root
  sheets and the PCB (also renames the D2 PUSB3AB6 channel assignments).
- Swap R21 (22R) <-> R25 (33R) identities between the two footprint
  positions so 33R stays on the FPGA-driven MISO line, 22R on the MOSI
  input.
- Correct the SP1 pin table + MISO-tristate note in
  docs/gc_bba_fpga_design.md (root cause).

Resulting board pin map (for the future board PCF): EXI_MOSI = FPGA
pin 4 (IOB_8a, input), EXI_MISO = pin 3 (IOB_9b, output).

Verified: ERC 0, DRC 0 errors with schematic parity 0, netlist shows
J3.6(DO)->R21->pin 4 and J3.9(DI)->R25->pin 3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 12:36:41 +00:00
Roflin 4704dfbb36 Clean up hardware repo: kill the resurrecting stale power sheet, restore datasheets
- Remove stale lowercase power.kicad_sch (pre-rework draft; live sheet is
  Power.kicad_sch). Both paths were git-tracked, which on a case-insensitive
  Windows checkout makes them one file on disk - deleting the stale one also
  deleted the live one, and restoring the live sheet resurrected the stale
  path at the next commit. Removing the path from the index breaks the loop.
- Remove re-bba-rb.kicad_pcb.old.* leftovers (recoverable from history).
- Restore hardware/re-bba-rb/datasheets/ (deleted accidentally by 3547302)
  and add the two missing datasheets: TPS22810 (power) and PUSB3AB6 (exi).
- Add hardware/re-bba-rb/TODO.md capturing the 2026-07-17 review findings
  (EXI DO/DI swap, missing W5100S MDI termination, FTDI EEPROM resistors,
  iceprog pin mismatch, PR1 margin, BOM/ordering notes); referenced from
  CLAUDE.md.

Verified after cleanup: ERC 0 violations, netlist identical (107 components).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:17:35 +00:00
Roflin a7c88109a9 Started on component selection of the final PCB. 2026-06-13 22:09:05 +02:00
Dennis Brentjes 8d0ab1d948 Added full design created with Claude 2026-06-13 18:35:38 +02:00