From 2aa50a7cc3ae2af4428d52cd7d29c12caebd7aa7 Mon Sep 17 00:00:00 2001 From: Dennis Brentjes Date: Fri, 9 Oct 2026 18:41:06 +0200 Subject: [PATCH] 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 --- exi_bba/bba_top.py | 1 + exi_bba/rx_frame_assembler.py | 51 +++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/exi_bba/bba_top.py b/exi_bba/bba_top.py index f7e543f..dcc83d2 100644 --- a/exi_bba/bba_top.py +++ b/exi_bba/bba_top.py @@ -195,6 +195,7 @@ class BBATop(Elaboratable): # pointer to know what it must not overwrite, and reports a # dropped frame back as RBFI (IR[7]). asm.rrp.eq(reg.rrp), + asm.ncra_rst.eq(reg.ncra_rst_o), reg.rx_overflow.eq(asm.rx_overflow), self.int_n .eq(reg.exi_int_n), ] diff --git a/exi_bba/rx_frame_assembler.py b/exi_bba/rx_frame_assembler.py index 1f2290b..c19d152 100644 --- a/exi_bba/rx_frame_assembler.py +++ b/exi_bba/rx_frame_assembler.py @@ -90,6 +90,16 @@ class RXFrameAssembler(Elaboratable): # Drives IR[7] (RBFI), which the GC's IMR enables (0x86). self.rx_overflow = Signal() + # The GC's NCRA reset. MUST reset `rwp` here as well as the register + # file's copy: they are two copies of the same pointer, and resetting + # only one leaves them disagreeing about where the ring is. A driver + # that then syncs RRP to the register file's value is pointing + # somewhere the writer is not, the free-space calculation shrinks with + # every frame, and once it falls below _MAX_FRAME_PAGES EVERY frame is + # dropped -- silently, because a dropped frame never reaches the ring + # to be counted. Hardware symptom: "0 seen, 0 bad desc, 0 lapped". + self.ncra_rst = Signal() + def elaborate(self, platform): m = Module() @@ -289,6 +299,25 @@ class RXFrameAssembler(Elaboratable): m.d.sync += self.rx_irq.eq(1) m.next = "IDLE" + # ── NCRA reset: the write pointer goes back to the ring start ────── + # Placed AFTER the FSM so it takes priority over ADVANCE_RWP in the + # same cycle. + # + # This MUST happen here as well as in the register file: they are two + # copies of the same pointer, and resetting only one leaves them + # disagreeing about where the ring is. A driver that then syncs RRP to + # the register file's value points somewhere the writer is not, the + # free-space calculation shrinks with every frame, and once it drops + # below _MAX_FRAME_PAGES EVERY frame is dropped -- silently, because a + # dropped frame never reaches the ring to be counted. Hardware + # symptom: "timeout: no match (0 seen, 0 bad desc, 0 lapped)". + # + # No in-flight frame needs abandoning: the same NCRA write that pulses + # this also clears the SR bit, so rx_enabled is low and the FSM is in + # IDLE. + with m.If(self.ncra_rst): + m.d.sync += rwp.eq(_RX_PAGE_FIRST) + return m @@ -541,6 +570,28 @@ if __name__ == "__main__": stall[0] = 1 await tick(4) + # ── T6: NCRA reset must return the WRITE pointer to page 1 ─────── + # The register file resets its own copy of RWP on an NCRA reset; if + # this module does not reset its own, the two disagree and the GC ends + # up pointing somewhere the writer is not. The free-space sum then + # shrinks with every frame until every frame is dropped -- silently, + # which on hardware read as "0 seen, 0 bad desc, 0 lapped". + ctx.set(dut.rrp, 1) + await tick(2) + ctx.set(dut.ncra_rst, 1) + await tick(1) + ctx.set(dut.ncra_rst, 0) + await tick(2) + f6 = [0xC0 | (i & 0x0F) for i in range(40)] + await send_frame(f6) + check("T6 rwp back to page 1 after NCRA reset", await wait_irq(), 2) + total6 = len(f6) + 4 + check("T6 frame lands at page 1", descriptor(1), + [0x00, 0x00, (total6 >> 8) & 0xFF, total6 & 0xFF]) + check("T6 body at page 1 base+4", body(1, len(f6)), f6) + + await tick(4) + # ── T3: RX disabled — SOF must be ignored ──────────────────────── before = len(mem) ctx.set(dut.rx_enabled, 0)