Commit Graph

103 Commits

Author SHA1 Message Date
Roflin 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
Roflin 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
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 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
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 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
Roflin 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
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 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
Roflin 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
Roflin 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
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 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
Roflin 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
Roflin 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
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 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
Roflin 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
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 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
Roflin 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
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 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
Roflin 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
Roflin 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
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 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
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 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
Roflin 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
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 c0abd45518 synth: optional EXI mirror on the J4 debug header (EXI_J4=1)
Lets a bench host drive EXI without the SP1 edge connector, for bring-up.

Merges a second set of EXI pads onto the existing ones rather than repinning,
so the SP1 connector stays live and the default build is untouched.  The three
inputs are AND-merged, which is correct because every EXI input is inactive
HIGH (CPOL=1 clk, active-low cs_n, mosi only matters while cs_n is low): the
unplugged side idles at 1 through its SB_IO PULLUP and passes the other side
through, so no select signal is needed.  Outputs drive both pads.

All five J4 pads are on the same bottom die edge as the EXI pads (x=13..18 vs
x=5..9), so the capture domain keeps its anchor.  It still adds 3 LUTs ahead of
the input synchronisers, so EXI_J4 builds must sweep seeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 17:54:24 +00:00
Roflin fff16a85c4 uart-console: document the venv interpreter needed under sudo
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 17:39:32 +00:00
Roflin 48cd3555f6 D9 USB LED: loose contact reflowed, now lights correctly
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 17:38:17 +00:00
Roflin 951c7bc3f3 Bring-up: D9 'USB' LED is an open solder joint, not a circuit fault
Anode-side pad at 3V3, cathode at 0V, LED dark — the full rail standing
across it means no current and no drop across R42, so Q2 and R42 are both
doing their job.  Probe pressure on the anode pad lights it at full
brightness: the terminal is sitting on the pad unwetted.  Reflow fixes it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 17:35:52 +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