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>
This commit is contained in:
@@ -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),
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user