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)