Commit Graph

10 Commits

Author SHA1 Message Date
Roflin 92f7d716d9 Add RBFI ring-overflow protection: drop frames instead of overwriting
RXFrameAssembler ignored RRP entirely, so it lapped the reader: a traffic
burst overwrote pages the GC had not read, and the "descriptor" the driver
then parsed was frame data from whatever had replaced it.  That is where the
hardware's 00 00 00 01 came from -- not a dropped write.

This is a product bug, not just a netboot one.  Until now ANY burst could
corrupt a frame Swiss was midway through reading, with nothing to indicate it
had happened.

The assembler now takes the GC's read pointer and computes free space as
(rrp - rwp - 1) mod 15.  On a frame start with fewer than 6 free pages -- the
worst case for 1518 bytes plus a 4-byte descriptor -- it enters DROP_FRAME,
discards to EOF, and pulses rx_overflow, which sets IR[7] (RBFI).  That is the
bit the GC's IMR = 0x86 already enables, so a driver that loses a frame is
told so instead of reading corruption.

Deliberate details:
  - rx_ready stays HIGH while dropping.  Stalling the ethernet master instead
    would back-pressure into the W5100's own buffer and turn a dropped frame
    into a wedged receiver.
  - Six pages are reserved up front because a frame's length is not known
    until EOF.  Starting with less risks running into unread data partway
    through, which is worse than refusing cleanly.
  - have_room is REGISTERED, not combinational.  Feeding a mod-15 subtract and
    comparator into the FSM branch cost real depth in `sync`, the domain with
    the least margin: clk fell 27.8-32.2 -> 27.0-28.0 MHz.  Registering it
    restored 28.4-31.4.  One cycle of staleness is harmless -- rwp moves only
    at end-of-frame and rrp only when the GC writes it.

T4 proves the property that matters, and the negative control REPRODUCES THE
HARDWARE FAULT IN SIMULATION: with the check removed the unread page reads
back as [245, 248, 251, 254, ...] -- frame data where a descriptor belongs.
First time one of today's RX faults has been reproducible on this side rather
than inferred from the console.  T4 also checks the receiver un-latches once
RRP advances, so a full ring cannot kill RX permanently.

Fixed a test artifact in the same pass: rx_overflow fires on the very tick
that consumes the EOF byte, inside send_frame, so polling for a 1-cycle pulse
afterwards missed it and a correctly-dropped frame looked like a stall.  Both
pulses are now latched in the tick helper.

netboot also clears IR (including RBFI) at init so a stale bit is not
inherited from an earlier run.

--seeds 8: clk 28.4-31.4 MHz, EXI 55.2-62.7 MHz, 8/8 PASS.  Best seed 4
(clk 31.39, EXI 61.85), flashed.

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 11:44:03 +02:00
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 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 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 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 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
Dennis Brentjes 8d0ab1d948 Added full design created with Claude 2026-06-13 18:35:38 +02:00