From 3db4ab0851df0c19b5642ad55b7c103c2c4ab94f Mon Sep 17 00:00:00 2001 From: Dennis Brentjes Date: Fri, 9 Oct 2026 11:06:22 +0200 Subject: [PATCH] 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 --- exi_bba/bba_register_file.py | 41 ++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/exi_bba/bba_register_file.py b/exi_bba/bba_register_file.py index 33f90a2..8e16ac2 100644 --- a/exi_bba/bba_register_file.py +++ b/exi_bba/bba_register_file.py @@ -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