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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Reverts the 0x8000/0xC000 change from 162dbf7, which was WRONG and actively
broke transmission. The W5100S datasheet pseudo-code quotes 0x8000 (TX) /
0xC000 (RX) for the memory blocks, but the PARALLEL/indirect (IDM) interface
only decodes a 15-bit offset. Bench-proven with an address-aliasing probe:
writing distinct values to each candidate base and reading them back showed
0xC000 and 0x4000 are the SAME cell (0xC000 & 0x7FFF == 0x4000) while 0x6000
holds its own value.
Consequence of the bad base: every payload byte written to 0x9800+i aliased
down onto 0x0000+i, landing on SHAR (0x09-0x0E) and SIPR (0x0F-0x12) and
overwriting them with the payload. The chip then transmitted frames with a
garbage source MAC/IP, which the peer NIC drops without counting -- the
"SEND_OK but rx_packets=0" symptom, and the bogus SIPR0 readback (0x50 was
literally payload byte 15, 'P').
The NETLCKR unlock from 9c4bc7a is a genuine W5100S requirement and stays.
Also:
- w5100_udptest.py: self-contained configure+send+readback probe (and the
address-aliasing probe that found this), reports over UART.
- scripts/build-flash.sh: sweep N seeds and flash the best (capture timing is
seed-dependent, so the sweep is mandatory).
- scripts/uart-console.py: interactive UART console, plus --send/--read
one-shot modes for scripting.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reads socket-3 / status registers over the indirect bus and streams them on the
UART, without resetting the chip, to inspect what a UDP send left behind. First
hardware run was inconclusive (socket regs read 0x00 but SIPR/PHYSR didn't match
either "state persisted" or "clean reset") -- the FPGA reflash appears to glitch
RST_N, so a self-contained configure+send+readback is the reliable next step.
Committed as a starting point for that.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second W5100S-vs-W5100 gotcha found at bring-up: the W5100S has a Network Lock
Register (NETLCKR, 0x0071) the original W5100 lacks. It defaults to 0x00 (locked),
and while locked, ALL writes to GWR/SUBR/SHAR/SIPR are silently dropped -- so the
chip has no source IP/MAC and the UDP/MACRAW send builds nothing (chip reports
SEND_OK but transmits nothing; socket registers aren't locked, which is why an
indirect write/readback of Sn_PORT succeeded). Fix: write the unlock value 0x3A
to NETLCKR right after MR enables indirect mode, before the network-config writes.
On the bench this flipped the board from silent to actually keying frames onto
the wire (magjack activity LED now blinks).
Also:
- bba_top.py test model: RX buffer base 0x6000 -> 0xC000 to match the W5100S
buffer-base fix (the model hardcoded the old W5100 address).
- w5100_selftest.py: added an indirect-mode (IDM) write/readback probe of a
socket register + IND= field in the UART report, to isolate direct-vs-indirect
access (confirmed indirect works on the real chip).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
From the physical V2 bring-up session (2026-09-20):
- exi_bba/w5100_selftest.py (NEW): standalone FPGA<->W5100 bus diagnostic.
Clock-independent data-bus walk (direct-mode GAR0 read-back, all 8 lines) +
MR software-reset self-clear (needs the 25 MHz crystal). Reports 4 verdicts on
D6/D7 LEDs AND over UART (115200): GOOD / data-line-N / clock-dead / no-response.
Boot wait is 85 ms (> the W5100S 60.3 ms init). On this board: GOOD -- bus +
all 8 data lines + 25 MHz clock all verified (unlike V1's broken D3).
- REVIEW.md / TODO.md: eFuse U13 OVLO is tied to GND, which on the TPS25961
selects the internal FIXED ~6 V overvoltage cutoff -- so it blocks the 12 V
rail entirely (bench-confirmed: 12V_EXI=0, GC_ON=0, W5100 unpowered). FAB-
BLOCKER for V2.1: fix with an OVLO divider (~1.1M/100k -> trip ~15 V). Bench
workaround: lift U13 pin 2 (OVLO) off GND. Also closed the eFuse-placement,
GND-via/3V3-zone, and CC-ESD TODO items.
- hardware/re-bba-rb/exi_devid_spi.py (NEW): Pi hardware-SPI (~25 MHz) EXI
device-ID host, companion to the bit-bang exi_devid_rpi.py.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The W5100ParallelMaster used the original W5100's TX (0x4000) / RX (0x6000)
memory-block base addresses. This board's chip is a W5100S, whose TX/RX memory
blocks live at 0x8000 / 0xC000 (datasheet: TX 0x8000-0xBFFF, RX 0xC000-0xFFFF).
Consequence found at hardware bring-up (2026-09-20): every frame was written to
dead address space, so SEND transmitted the (empty) real TX buffer -- the chip
reported "sent" but nothing valid reached the wire (peer rx_packets stayed 0,
link up). The sim bus-model used these same constants, so it never caught it --
exactly the "register addresses are datasheet-from-memory, confirm at bring-up"
risk. All socket register offsets were verified correct against the W5100S
datasheet; only the two memory bases were wrong. Fixes both the socket-N UDP
test and the socket-0 MACRAW (GC) path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>