Commit Graph

9 Commits

Author SHA1 Message Date
Roflin 20d64ac416 rx_frame_assembler: document the ring and descriptor, drop stale references
844 -> 818 lines, 30% -> 12% comment-only.  The descriptor bit layout is kept
in FULL, because it is the one interface in this module an external driver
parses; what went was the disassembly listing that proves it (CLAUDE.md holds
that) and the per-bug narrative around each fix.

Stale references fixed:

  * The module claimed to receive frames "from W5500SPIMaster".  The W5100
    parallel master is the default back-end; the stream interface is
    deliberately identical, so the text now names neither.
  * "RWP FIFO write-side (sync->exi)" and "rx_irq : 1-cycle pulse ->
    PulseSynchronizer input" both described the old split clock domains.  The
    exi domain was merged into sync; that FIFO is a SyncFIFO and the pulse is
    a plain wire.
  * The module docstring's write sequence said descriptor "bytes 0, 1 and 2"
    are rewritten and "byte 3 (status) stays 0x00", and WRITE_D2's comment
    said byte 3 "keeps the placeholder".  Both predate WRITE_D3, which exists
    precisely because the placeholder NEVER reaches desc_base+3 -- so the
    file contradicted its own next state, and in the direction that sounds
    reassuring.

Two comment blocks were duplicated outright: the registered-ring-bounds
timing note appeared twice in a row with slightly different wording, and the
NCRA-reset rationale appeared both on the port and again at the logic.

Structure: elaborate() is now _ring_bounds, _free_space, _frame_fsm and
_ncra_reset, with _ncra_reset still elaborated last so it keeps priority over
ADVANCE_RWP.  The FSM's docstring lists all eleven states in visit order, and
the valid-&-ready rule that both 2026-10-09 handshake bugs violated is stated
once for every write state instead of twice at two of them.

Also fixed a real (if harmless) bug in the testbench: send_frame's docstring
sat after two statements, making it a no-op expression rather than a
docstring.

VERIFIED EQUIVALENT: generated RTLIL identical, 1930 lines, after stripping
\src attributes.  Full module suite passes, bba_top T9 6/6 at EXI_HALF=1, 2
and 8, and this module's own 25 checks all pass including T5's stalled-write
case and T7's small-ring wrap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-10 20:02:34 +02:00
Roflin 6b2e6724a2 Fix the 0/8 timing failure the ring-geometry change caused
The sweep after honouring BP/RHBP came back clk 16.5-17.6 MHz, 0/8 seeds
passing against the 24 MHz target -- from ~29 MHz before.  The build was
unusable.

Cause: the sanitised bounds were combinational, so

  bp/rhbp -> comparators -> Mux -> subtract -> pg_total -> add -> raw
          -> compare -> free -> compare -> have_room

became ONE path where it had been constant-folded.  Two fixes were needed, and
the first alone was not enough:

1. Register pg_first/pg_last/pg_total and rrp_s.  They change only when the GC
   writes BP/RHBP/RRP, so a cycle of staleness costs nothing, and `free`
   already reaches the FSM through the registered have_room.  This got it to
   ~20 MHz, still failing.
2. Narrow them from 8 bits to 4.  Ring pages are 1-15.  An 8-bit variable-vs-
   variable comparison is a carry chain where variable-vs-CONSTANT folds into
   a couple of LUTs -- precisely what making the bounds dynamic had thrown
   away.  geom_ok guarantees bp < rhbp <= 0x0F, which makes the narrow slice
   safe.

Now 26.90 and 28.50 MHz on seeds 1-2, against 28.00/29.21 for the build
without the geometry change.

Attribution needed an A/B: the register-space decode (item 4) landed in the
same stretch and had never been swept on its own.  Measured separately it
closes 28.0-29.2 MHz and costs nothing, so the whole regression was the
assembler.  The critical-path report named rx_frame_assembler.py:215 -- the
rrp clamp -- which is what pointed at the comparator width rather than at the
arithmetic everyone would suspect first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-10 14:16:43 +02:00
Roflin 147ef8fcfc Honour the GC's ring geometry (BP / RHBP) instead of hardcoding it
Compat TODO item 2, the last silent-corruption risk.  RXFrameAssembler took
the RX ring as a compile-time constant (pages 1-15), so a driver configuring a
SMALLER ring was ignored and we kept writing pages it would never read, with
nothing reporting an error.  The bounds now come from bp/rhbp, wired in BBATop
from the GC's BP (0x0A/0B) and RHBP (0x1A/1B) registers, and both the page wrap
and the free-space calculation use them.

Garbage is rejected rather than obeyed: the driver's values are used only when
bp >= 1 (page 0 is the TX buffer), rhbp <= 0x0F and rhbp > bp.  Otherwise the
hardcoded layout stands and geom_honoured goes low, so a mismatch is
observable.  Without that check BP=0 or an RHBP below BP gives a zero or
negative ring size and the assembler writes outside it.

⚠️ Recorded as a known limitation: a ring must be LARGER than
_MAX_FRAME_PAGES (6) or nothing is ever stored, because a frame's length is
unknown until EOF so the worst case (1518+4 = 6 pages) is reserved up front.
Found the honest way -- the test was written with RHBP=4 first and passed
VACUOUSLY, storing nothing at all.  Making that work needs a length-aware
reservation, which is a bigger change than this one.

T7 (8-page ring honoured, frames only in pages 1-8, and at least one actually
stored) and T7b (all three garbage cases rejected, fallback still stores).
Mutation-tested: wrapping at the hardcoded page 15 writes pages 9-11 and fails
T7; removing the sanity check fails all four T7b assertions.

Item 1 (RX filtering) remains the only open gap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-10 14:06:04 +02:00
Roflin d83f7beef8 Fix the RX ring descriptor to the format libogc actually parses
The assembler wrote 00 00 len[11:8] len[7:0] with bytes 0 and 1 zero.  The real
format, read out of libogc's own BBA driver, is a PACKED 32-BIT LITTLE-ENDIAN
word: next_packet_ptr in bits 0-11, packet_len in bits 12-23, status in bits
24-31.  Under that driver our descriptor for a 342-byte frame decoded as a
12-byte frame with a next-pointer of 0 into the reserved page, so Swiss and
anything else built on libogc would have initialised perfectly and then
misparsed every received frame.

Found by disassembling /opt/devkitpro/libogc/lib/cube/libbba.a (gcif.o,
bba_interrupt) rather than by guessing:

  lwbrx   r9,0,r9              # 4 bytes as a little-endian word
  rlwinm  r4,r9,20,20,31       # packet_len = (word >> 12) & 0xFFF
  addi    r4,r4,-4             # payload = packet_len - 4
  cmpwi   r4,1540
  srwi    r9,r9,24             # status = word >> 24
  and.    r9,r9,r18            # r18 = 0x..84 -> errors are 0x80 | 0x04

and the low 12 bits are written straight back to RRP (0x0018/0x0019), which is
why next_packet_ptr has to be there at all -- it is the driver's read pointer.

The length semantics and addressing were already correct (descriptor at
page<<8, body at +4, length including the descriptor); only the packing was
wrong and next_packet_ptr was never written.

RXFrameAssembler now computes the next page BEFORE writing the descriptor
(CALC_NEXT, formerly ADVANCE_RWP, moved ahead of the backfill) and writes bytes
0-3 as WRITE_D0..WRITE_D3.

WRITE_D3 fixes a latent bug this exposed: WRITE_PLACEHOLDER issues four writes
but the first lands twice, so desc_base+3 was NEVER written.  That was harmless
while the length write covered byte 3.  As the status byte it would have been
whatever the previous frame left in that cell, and a stale 0x80 or 0x04 makes
the driver discard the frame.

Readers updated to decode identically: netboot (which also now takes RRP from
the descriptor's own next_packet_ptr, as libogc does, instead of recomputing
the page span) and t_dma, including its page map.  netboot's +0/+1 offset probe
is removed -- the underrun that shifted descriptors is fixed and confirmed on
console, and under a packed format a +1 read is not a shifted descriptor but
garbage.

The assembler's testbench now decodes with libogc_decode(), reproducing the
instruction sequence above, so the tests check the DRIVER'S algorithm rather
than our own packing.  Every module testbench and bba_top at EXI_HALF=1/2/8
pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-10 10:27:45 +02:00
Roflin 2aa50a7cc3 Reset the assembler's own write pointer on NCRA reset too
Hardware: "timeout: no match (0 seen, 0 bad desc, 0 lapped)".  Zero frames
reached the ring at all -- a regression from 83a62d6, which reset the REGISTER
FILE's copy of RWP on an NCRA reset but not RXFrameAssembler's own `rwp`.

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

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 18:41:06 +02:00
Roflin f69b4d084a Model write-port stalls in the assembler testbench (T5)
eth_wr_ready was held constantly HIGH by every test this file ever had, which
is a serious blind spot: on hardware the SPRAM arbiter stalls that port
constantly, because the GC reads the ring over EXI while frames are written
into it.  Any address bookkeeping that depends on ready timing is invisible
until ready actually goes low.

T5 repeats T2's 300-byte frame with ready asserted 1 cycle in 3 and requires
the descriptor and body offset to be IDENTICAL to the unstalled case.

Result: contention does NOT reproduce the one-byte body offset seen on
hardware.  Under stalls the descriptor is still [0,0,1,48] and all 300 body
bytes land at base+4.  So that hypothesis is eliminated, which was the point
of writing it.

Two harness faults found and fixed while building it, both of which produced
false results rather than no result:

  - rx_irq and rx_overflow are 1-cycle pulses, and rx_overflow fires on the
    very tick that consumes the EOF byte -- inside send_frame.  Polling for it
    afterwards missed it entirely, so a correctly DROPPED frame looked like a
    stall.  Both are now latched in the tick helper.
  - send_frame read rx_ready BEFORE tick() applied the next eth_wr_ready.
    rx_ready is combinational on it, so the loop was a cycle out of phase with
    the accept the DUT actually made: it waited on a byte already taken, and
    reported "rx_ready never asserted" for a frame that had stored correctly.
    Ready is now applied, then read, then clocked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 17:18:04 +02:00
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 86cd5d1abd Fix two dropped-write bugs in RXFrameAssembler's SPRAM handshake
Both from one mistake: eth_wr_valid is scheduled with m.d.sync and so is LOW
for the whole first cycle in a state, while eth_wr_ready was tested
immediately -- and `ready` is usually already high on entry.  Both
valid.eq(1) and valid.eq(0) then fired in the same cycle, the later won, the
write was never issued, and it was dropped SILENTLY.

1. WRITE_LEN_HI dropped the descriptor's HIGH length byte, leaving it at its
   placeholder 0x00.  Hardware read a descriptor of 00 00 00 01 -- a 1-byte
   frame length, impossible for ethernet -- and rejected the ring.

2. WRITE_PLACEHOLDER counted a phantom first write, advancing wr_addr without
   issuing one.  Only THREE placeholder bytes were written and wr_addr ended
   at desc_base+3, so the frame body started at descriptor byte 3 and EVERY
   RECEIVED FRAME WAS SHIFTED ONE BYTE EARLY.  The GC's ethertype test then
   read the wrong offset and silently dropped every frame: the host saw "no
   ACK" while the ring filled and RWP advanced normally.

Both now advance on `valid & ready`, and the body offset is set explicitly
(desc_base + 4) rather than relying on an incremented pointer, so it cannot
drift with the handshake again.

WHY NO TEST CAUGHT THIS, which is the more important half:

  - The testbench drove the RX stream OPEN-LOOP -- one byte per tick, never
    checking rx_ready, which is low through IDLE and the whole placeholder
    write.  It silently lost bytes, including the SOF byte.  A real master
    must hold each byte until accepted.
  - It only ever asserted on rwp and rx_irq.  It declared a spram_writes list
    and never filled it, so NOTHING had ever checked what was written.
  - Every frame it sent was short, and a short frame's high length byte is
    legitimately zero -- so a dropped high byte was indistinguishable from a
    correct one.

The testbench now respects back-pressure, records every write, and checks the
descriptor bytes and the body offset.  T2 sends a 300-byte frame specifically
so the high length byte is non-zero.  Mutation-tested: restoring either bug
fails T2 with got [0,0,0,48] want [0,0,1,48] -- the same shape as the
hardware's 00 00 00 01 -- while T1 (short frame) still passes, which is
exactly the blind spot that let this ship.

Bring-up's 342-byte DHCP frame read a correct 00 00 01 5A only because the
arbiter happened to hold `ready` low on that entry cycle; the domain merge
and the SPRAM response-FIFO removal changed that timing.

--seeds 8: clk 27.8-32.2 MHz, EXI 54.9-64.5 MHz, 8/8 PASS.  Best seed 8
(clk 32.22, EXI 62.25), flashed.  Gateware only -- the .dol is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 12:04:00 +02:00
Dennis Brentjes 8d0ab1d948 Added full design created with Claude 2026-06-13 18:35:38 +02:00