Fix the RX ring descriptor to the format libogc actually parses
The assembler wrote 00 00 len[11:8] len[7:0] with bytes 0 and 1 zero. The real format, read out of libogc's own BBA driver, is a PACKED 32-BIT LITTLE-ENDIAN word: next_packet_ptr in bits 0-11, packet_len in bits 12-23, status in bits 24-31. Under that driver our descriptor for a 342-byte frame decoded as a 12-byte frame with a next-pointer of 0 into the reserved page, so Swiss and anything else built on libogc would have initialised perfectly and then misparsed every received frame. Found by disassembling /opt/devkitpro/libogc/lib/cube/libbba.a (gcif.o, bba_interrupt) rather than by guessing: lwbrx r9,0,r9 # 4 bytes as a little-endian word rlwinm r4,r9,20,20,31 # packet_len = (word >> 12) & 0xFFF addi r4,r4,-4 # payload = packet_len - 4 cmpwi r4,1540 srwi r9,r9,24 # status = word >> 24 and. r9,r9,r18 # r18 = 0x..84 -> errors are 0x80 | 0x04 and the low 12 bits are written straight back to RRP (0x0018/0x0019), which is why next_packet_ptr has to be there at all -- it is the driver's read pointer. The length semantics and addressing were already correct (descriptor at page<<8, body at +4, length including the descriptor); only the packing was wrong and next_packet_ptr was never written. RXFrameAssembler now computes the next page BEFORE writing the descriptor (CALC_NEXT, formerly ADVANCE_RWP, moved ahead of the backfill) and writes bytes 0-3 as WRITE_D0..WRITE_D3. WRITE_D3 fixes a latent bug this exposed: WRITE_PLACEHOLDER issues four writes but the first lands twice, so desc_base+3 was NEVER written. That was harmless while the length write covered byte 3. As the status byte it would have been whatever the previous frame left in that cell, and a stale 0x80 or 0x04 makes the driver discard the frame. Readers updated to decode identically: netboot (which also now takes RRP from the descriptor's own next_packet_ptr, as libogc does, instead of recomputing the page span) and t_dma, including its page map. netboot's +0/+1 offset probe is removed -- the underrun that shifted descriptors is fixed and confirmed on console, and under a packed format a +1 read is not a shifted descriptor but garbage. The assembler's testbench now decodes with libogc_decode(), reproducing the instruction sequence above, so the tests check the DRIVER'S algorithm rather than our own packing. Every module testbench and bba_top at EXI_HALF=1/2/8 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+136
-32
@@ -11,17 +11,50 @@ Page wrap: after 0x0F → 0x01 (skip 0x00).
|
||||
|
||||
Frame descriptor (4 bytes at page start)
|
||||
-----------------------------------------------
|
||||
Byte 0: LRPS (last received packet status) — 0x00
|
||||
Byte 1: 0x00
|
||||
Byte 2: total_length[15:8] (big-endian; includes 4 descriptor bytes)
|
||||
Byte 3: total_length[7:0]
|
||||
Bytes 4+: raw ethernet frame
|
||||
A PACKED 32-BIT LITTLE-ENDIAN WORD, not a byte-wise layout:
|
||||
|
||||
bits 0–11 : next_packet_ptr — the PAGE at which the NEXT frame starts
|
||||
bits 12–23 : packet_len — total bytes, INCLUDING these 4
|
||||
bits 24–31 : status — errors are 0x80 and 0x04; 0x00 means good
|
||||
|
||||
so the four bytes at the page start are
|
||||
|
||||
Byte 0: next_packet_ptr[7:0]
|
||||
Byte 1: packet_len[3:0] << 4 | next_packet_ptr[11:8]
|
||||
Byte 2: packet_len[11:4]
|
||||
Byte 3: status
|
||||
Bytes 4+: raw ethernet frame
|
||||
|
||||
⚠️ This is NOT a guess and must not be "simplified" back to a plain
|
||||
big-endian length. It was read out of libogc's own BBA driver
|
||||
(`/opt/devkitpro/libogc/lib/cube/libbba.a`, `gcif.o`, `bba_interrupt`), which
|
||||
is the driver GC software actually runs:
|
||||
|
||||
lwbrx r9,0,r9 # load the 4 bytes as a LITTLE-ENDIAN word
|
||||
rlwinm r4,r9,20,20,31 # packet_len = (word >> 12) & 0xFFF
|
||||
addi r4,r4,-4 # payload = packet_len - 4
|
||||
cmpwi r4,1540 # ... which must be <= 1540
|
||||
srwi r9,r9,24 # status = word >> 24
|
||||
and. r9,r9,r18 # r18 = 0x...84, so errors are 0x80 | 0x04
|
||||
|
||||
and it writes the low 12 bits straight back to RRP as the next read pointer:
|
||||
|
||||
lwz r9,cur_descr # byte 0 -> register 0x0018 (RRP_LO)
|
||||
rlwinm r9,r31,24,28,31 # bits 11:8 -> register 0x0019 (RRP_HI)
|
||||
|
||||
The length semantics and addressing already matched (descriptor at page<<8,
|
||||
body at +4, length including the descriptor); the PACKING did not, and
|
||||
`next_packet_ptr` was never written at all. Our previous
|
||||
`00 00 len[11:8] len[7:0]` decodes under that driver as packet_len = 16 and
|
||||
next_packet_ptr = 0 for a 342-byte frame — a 12-byte frame and a read pointer
|
||||
into the reserved page.
|
||||
|
||||
Write sequence
|
||||
--------------
|
||||
1. Issue 4 SPRAM writes of 0x00 (placeholder descriptor).
|
||||
2. For each byte received from W5500, issue one SPRAM write.
|
||||
3. After EOF: rewrite descriptor bytes 2 and 3 with actual length.
|
||||
3. After EOF: compute the next page, then rewrite descriptor bytes 0, 1 and 2
|
||||
with the packed next_packet_ptr + packet_len. Byte 3 (status) stays 0x00.
|
||||
4. Advance RWP, push to rx_wptr FIFO, pulse rx_irq.
|
||||
"""
|
||||
|
||||
@@ -112,6 +145,10 @@ class RXFrameAssembler(Elaboratable):
|
||||
data_ctr = Signal(12)
|
||||
# Total length = data_ctr + 4
|
||||
total_len = Signal(12)
|
||||
# The page the NEXT frame will start at. Computed before the
|
||||
# descriptor is written, because the descriptor carries it as
|
||||
# next_packet_ptr -- libogc writes those 12 bits straight to RRP.
|
||||
next_page = Signal(12)
|
||||
|
||||
# Descriptor base (rwp*256) — saved when frame starts
|
||||
desc_base = Signal(16)
|
||||
@@ -207,7 +244,7 @@ class RXFrameAssembler(Elaboratable):
|
||||
# RWP advanced, but the GC's ethertype test read the wrong
|
||||
# offset and silently dropped every frame -- "no ACK" from the
|
||||
# host with no other sign of trouble. The matching
|
||||
# descriptor bug (same cause, in WRITE_LEN_HI) showed up as a
|
||||
# descriptor bug (same cause, in the length write) showed up as a
|
||||
# descriptor of 00 00 00 01.
|
||||
#
|
||||
# The first byte is written twice as a consequence of
|
||||
@@ -247,10 +284,10 @@ class RXFrameAssembler(Elaboratable):
|
||||
# Compute pages used: ceil((data_ctr + 4) / 256)
|
||||
# = (total_len + 255) >> 8 = total_len[11:8] + (total_len[7:0] != 0)
|
||||
m.d.sync += pages_used.eq(total_len[8:12] + (total_len[:8] != 0))
|
||||
m.next = "WRITE_LEN_HI"
|
||||
m.next = "CALC_NEXT"
|
||||
|
||||
# ⚠️ Both length writes must advance on `valid & ready`, NOT on
|
||||
# `ready` alone. `valid` is scheduled with m.d.sync and so is low
|
||||
# ⚠️ All three descriptor writes must advance on `valid & ready`,
|
||||
# NOT on `ready` alone. `valid` is scheduled with m.d.sync and so is low
|
||||
# for the whole first cycle in the state, while `ready` is tested
|
||||
# immediately -- and coming out of WAIT_LAST_WRITE `ready` is
|
||||
# usually already high. Testing `ready` alone therefore fired both
|
||||
@@ -264,32 +301,72 @@ class RXFrameAssembler(Elaboratable):
|
||||
# happens to hold `ready` high on the entry cycle, which is why
|
||||
# bring-up's 342-byte DHCP frame read a correct 00 00 01 5A and
|
||||
# this went unnoticed.
|
||||
with m.State("WRITE_LEN_HI"):
|
||||
# Overwrite descriptor byte 2 with total_len[15:8]
|
||||
m.d.sync += self.eth_wr_addr.eq(desc_base + 2)
|
||||
m.d.sync += self.eth_wr_data.eq(total_len[8:12])
|
||||
with m.State("CALC_NEXT"):
|
||||
# The descriptor carries next_packet_ptr, so the next page must
|
||||
# be known BEFORE the descriptor is written -- this state used
|
||||
# to come after it (as ADVANCE_RWP).
|
||||
# next_rwp = ((rwp - 1 + pages_used) % 15) + 1
|
||||
next_rwp_raw = Signal(8)
|
||||
m.d.comb += next_rwp_raw.eq(rwp + pages_used)
|
||||
with m.If(next_rwp_raw > _RX_PAGE_LAST):
|
||||
m.d.sync += next_page.eq(next_rwp_raw - _PAGES_TOTAL)
|
||||
with m.Else():
|
||||
m.d.sync += next_page.eq(next_rwp_raw)
|
||||
m.next = "WRITE_D0"
|
||||
|
||||
with m.State("WRITE_D0"):
|
||||
# Descriptor byte 0 = next_packet_ptr[7:0]
|
||||
m.d.sync += self.eth_wr_addr.eq(desc_base + 0)
|
||||
m.d.sync += self.eth_wr_data.eq(next_page[0:8])
|
||||
m.d.sync += self.eth_wr_valid.eq(1)
|
||||
with m.If(self.eth_wr_valid & self.eth_wr_ready):
|
||||
m.d.sync += self.eth_wr_valid.eq(0)
|
||||
m.next = "WRITE_LEN_LO"
|
||||
m.next = "WRITE_D1"
|
||||
|
||||
with m.State("WRITE_LEN_LO"):
|
||||
# Overwrite descriptor byte 3 with total_len[7:0]
|
||||
with m.State("WRITE_D1"):
|
||||
# Descriptor byte 1 = packet_len[3:0] << 4 | next_ptr[11:8].
|
||||
# Cat puts its FIRST argument in the low bits.
|
||||
m.d.sync += self.eth_wr_addr.eq(desc_base + 1)
|
||||
m.d.sync += self.eth_wr_data.eq(
|
||||
Cat(next_page[8:12], total_len[0:4]))
|
||||
m.d.sync += self.eth_wr_valid.eq(1)
|
||||
with m.If(self.eth_wr_valid & self.eth_wr_ready):
|
||||
m.d.sync += self.eth_wr_valid.eq(0)
|
||||
m.next = "WRITE_D2"
|
||||
|
||||
with m.State("WRITE_D2"):
|
||||
# Descriptor byte 2 = packet_len[11:4]. Byte 3 (status) keeps
|
||||
# the placeholder 0x00, which libogc reads as "no error".
|
||||
m.d.sync += self.eth_wr_addr.eq(desc_base + 2)
|
||||
m.d.sync += self.eth_wr_data.eq(total_len[4:12])
|
||||
m.d.sync += self.eth_wr_valid.eq(1)
|
||||
with m.If(self.eth_wr_valid & self.eth_wr_ready):
|
||||
m.d.sync += self.eth_wr_valid.eq(0)
|
||||
m.next = "WRITE_D3"
|
||||
|
||||
with m.State("WRITE_D3"):
|
||||
# Descriptor byte 3 = status = 0x00 ("no error": libogc tests
|
||||
# it against 0x84).
|
||||
#
|
||||
# ⚠️ This MUST be written explicitly and cannot rely on the
|
||||
# placeholder. WRITE_PLACEHOLDER issues four writes but the
|
||||
# first lands twice (eth_wr_addr trails wr_addr by a cycle),
|
||||
# so desc_base+3 is NEVER written. That was harmless while
|
||||
# the length write covered byte 3; now byte 3 is the status
|
||||
# and leaving it would hand libogc whatever the previous
|
||||
# frame -- or power-on -- left in that SPRAM cell, and any
|
||||
# frame landing on a stale 0x80 or 0x04 would be discarded as
|
||||
# an error. The simulation model reports it as an unwritten
|
||||
# cell; real SPRAM has no such tell.
|
||||
m.d.sync += self.eth_wr_addr.eq(desc_base + 3)
|
||||
m.d.sync += self.eth_wr_data.eq(total_len[:8])
|
||||
m.d.sync += self.eth_wr_data.eq(0x00)
|
||||
m.d.sync += self.eth_wr_valid.eq(1)
|
||||
with m.If(self.eth_wr_valid & self.eth_wr_ready):
|
||||
m.d.sync += self.eth_wr_valid.eq(0)
|
||||
m.next = "ADVANCE_RWP"
|
||||
|
||||
with m.State("ADVANCE_RWP"):
|
||||
# next_rwp = ((rwp - 1 + pages_used) % 15) + 1
|
||||
next_rwp_raw = Signal(8)
|
||||
m.d.comb += next_rwp_raw.eq(rwp + pages_used)
|
||||
with m.If(next_rwp_raw > _RX_PAGE_LAST):
|
||||
m.d.sync += rwp.eq(next_rwp_raw - _PAGES_TOTAL)
|
||||
with m.Else():
|
||||
m.d.sync += rwp.eq(next_rwp_raw)
|
||||
m.d.sync += rwp.eq(next_page)
|
||||
m.next = "PUSH_WPT"
|
||||
|
||||
with m.State("PUSH_WPT"):
|
||||
@@ -465,6 +542,27 @@ if __name__ == "__main__":
|
||||
base = page << 8
|
||||
return [mem.get(base + i) for i in range(4)]
|
||||
|
||||
def pack_desc(total, next_ptr, status=0):
|
||||
"""The 4 descriptor bytes libogc's driver expects."""
|
||||
word = (status << 24) | ((total & 0xFFF) << 12) | (next_ptr & 0xFFF)
|
||||
return [(word >> (8 * i)) & 0xFF for i in range(4)]
|
||||
|
||||
def libogc_decode(d):
|
||||
"""Decode exactly as libogc's bba_interrupt does, so these tests
|
||||
check the driver's ALGORITHM rather than our own packing:
|
||||
|
||||
lwbrx -> little-endian word from the 4 bytes
|
||||
rlwinm r4,r9,20,20,31 -> packet_len = (word >> 12) & 0xFFF
|
||||
srwi r9,r9,24 -> status = word >> 24
|
||||
word & 0xFFF -> next_packet_ptr
|
||||
|
||||
Returns (payload_len, next_ptr, status_is_error)."""
|
||||
if any(b is None for b in d):
|
||||
return (None, None, None)
|
||||
word = d[0] | (d[1] << 8) | (d[2] << 16) | (d[3] << 24)
|
||||
return (((word >> 12) & 0xFFF) - 4, word & 0xFFF,
|
||||
bool((word >> 24) & 0x84))
|
||||
|
||||
def body(page, n):
|
||||
base = (page << 8) + 4
|
||||
return [mem.get(base + i) for i in range(n)]
|
||||
@@ -479,8 +577,9 @@ if __name__ == "__main__":
|
||||
await send_frame(f1)
|
||||
check("T1 rwp advance", await wait_irq(), 2)
|
||||
total1 = len(f1) + 4
|
||||
check("T1 descriptor", descriptor(1),
|
||||
[0x00, 0x00, (total1 >> 8) & 0xFF, total1 & 0xFF])
|
||||
check("T1 descriptor", descriptor(1), pack_desc(total1, 2))
|
||||
check("T1 descriptor via libogc decode", libogc_decode(descriptor(1)),
|
||||
(len(f1), 2, False))
|
||||
# The body MUST start at base+4. This is the check that catches a
|
||||
# placeholder write being dropped: with only three placeholder bytes
|
||||
# written the body landed at base+3 and every frame was shifted one
|
||||
@@ -500,8 +599,9 @@ if __name__ == "__main__":
|
||||
await send_frame(f2)
|
||||
rwp2 = await wait_irq()
|
||||
total2 = len(f2) + 4
|
||||
check("T2 descriptor (len > 255)", descriptor(2),
|
||||
[0x00, 0x00, (total2 >> 8) & 0xFF, total2 & 0xFF])
|
||||
check("T2 descriptor (len > 255)", descriptor(2), pack_desc(total2, 4))
|
||||
check("T2 descriptor via libogc decode", libogc_decode(descriptor(2)),
|
||||
(len(f2), 4, False))
|
||||
check("T2 body at base+4", body(2, len(f2)), f2)
|
||||
# 304 bytes spans two pages, so page 2 -> page 4.
|
||||
check("T2 rwp advance (2 pages)", rwp2, 4)
|
||||
@@ -564,8 +664,11 @@ if __name__ == "__main__":
|
||||
check("T5 stored despite stalls", rwp5, "stored")
|
||||
pg5 = (ctx.get(dut.rx_wptr_w_data) - 2) & 0xFF # page it was written to
|
||||
total5 = len(f5) + 4
|
||||
next5 = ctx.get(dut.rx_wptr_w_data)
|
||||
check("T5 descriptor under stall", descriptor(pg5),
|
||||
[0x00, 0x00, (total5 >> 8) & 0xFF, total5 & 0xFF])
|
||||
pack_desc(total5, next5))
|
||||
check("T5 descriptor via libogc decode", libogc_decode(descriptor(pg5)),
|
||||
(len(f5), next5, False))
|
||||
check("T5 body at base+4 under stall", body(pg5, len(f5)), f5)
|
||||
stall[0] = 1
|
||||
await tick(4)
|
||||
@@ -586,8 +689,9 @@ if __name__ == "__main__":
|
||||
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 frame lands at page 1", descriptor(1), pack_desc(total6, 2))
|
||||
check("T6 descriptor via libogc decode", libogc_decode(descriptor(1)),
|
||||
(len(f6), 2, False))
|
||||
check("T6 body at page 1 base+4", body(1, len(f6)), f6)
|
||||
|
||||
await tick(4)
|
||||
|
||||
Reference in New Issue
Block a user