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>
This commit is contained in:
2026-10-09 11:06:22 +02:00
parent 19044307bf
commit 3db4ab0851
+39 -2
View File
@@ -27,6 +27,10 @@ _RWP_LO = 0x16
_RWP_HI = 0x17
_RRP_LO = 0x18
_RRP_HI = 0x19
# First page of the SPRAM RX ring (pages 1-15; page 0 is the register region).
# Must match rx_frame_assembler._RX_PAGE_FIRST.
_RX_PAGE_FIRST = 0x01
_PAR0 = 0x20
_PAR1 = 0x21
_PAR2 = 0x22
@@ -183,8 +187,19 @@ class BBARegisterFile(Elaboratable):
r_ncra = Signal(8)
r_imr = Signal(8)
r_ir = Signal(8)
r_rwp_lo = Signal(8)
r_rrp_lo = Signal(8)
# Both ring pointers reset to the ring's FIRST PAGE, not to 0.
# The SPRAM ring is pages 1-15 (page 0 holds the register region), and
# RXFrameAssembler's own write pointer resets to page 1 -- so a 0 here
# disagreed with the assembler and, worse, told the GC driver to read
# its first frame from page 0. The frame is at page 1; page 0 is the
# register page. Hardware symptom: "RRP page 0 outside ring pages
# 1-15" from the test app's DMA descriptor check, on a board whose RX
# path was otherwise working.
# 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, and pointing at the start of the ring.
r_rwp_lo = Signal(8, init=_RX_PAGE_FIRST)
r_rrp_lo = Signal(8, init=_RX_PAGE_FIRST)
# PAR0–5 reset to a valid Nintendo OUI MAC (00:09:BF:00:00:01) so the
# device has a sane source MAC even before the GC driver programs its
# own. PAR0 is the first MAC octet.
@@ -720,6 +735,28 @@ if __name__ == "__main__":
if result != []:
errors.append(f"T1 device-ID query must push nothing, got {result}")
print(f"T1 device-ID query pushes nothing: {result == []}")
# T1b: both ring pointers must come out of reset at the ring's FIRST
# PAGE, not 0. RRP == RWP == 1 is "empty, pointing at the start of
# the ring"; 0 points at the register page, and a driver that read its
# first frame from RRP=0 would get the register region instead of the
# frame -- which is what the DMA descriptor check caught on hardware.
for name, a in (("RWP", _RWP_LO), ("RRP", _RRP_LO)):
got = await exi_read(ctx, a, 1)
if got != [_RX_PAGE_FIRST]:
errors.append(f"T1b {name} reset: expected "
f"[{_RX_PAGE_FIRST}], got {got}")
print(f"T1b {name} at reset: {got} (want [{_RX_PAGE_FIRST}])")
# ...and the constant must not drift from the assembler's, which owns
# the write pointer. Two copies of a page number is exactly how the
# off-by-one got in.
from exi_bba.rx_frame_assembler import _RX_PAGE_FIRST as _ASM_FIRST
if _RX_PAGE_FIRST != _ASM_FIRST:
errors.append(f"T1b ring-first-page mismatch: register file "
f"{_RX_PAGE_FIRST} vs assembler {_ASM_FIRST}")
print(f"T1b ring first page agrees with assembler: "
f"{_RX_PAGE_FIRST == _ASM_FIRST}")
await ctx.tick("sync").repeat(4)
# T2: Write and read back PAR0-PAR3