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:
2026-10-10 10:27:45 +02:00
parent 5ee1b9d07b
commit d83f7beef8
6 changed files with 273 additions and 79 deletions
+62
View File
@@ -1219,6 +1219,68 @@ parallel master is the default — see "W5100 vs W5500".)
--- ---
## RX Ring Descriptor — the format GC software actually expects
✅ **Corrected 2026-10-10 from libogc's own driver.** The 4 bytes at each ring
page start are a **packed 32-bit little-endian word**, not a byte-wise layout:
| bits | field |
|---|---|
| 0–11 | `next_packet_ptr` — the PAGE the NEXT frame starts at |
| 12–23 | `packet_len` — total bytes, **including** these 4 |
| 24–31 | `status` — `0x80` and `0x04` are errors; `0x00` is good |
```
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
```
⚠️ **This was WRONG until 2026-10-10 and nothing we had built could have
caught it.** The assembler wrote `00 00 len[11:8] len[7:0]` and every reader we
had — `netboot`, `t_dma`, the testbenches — was written by us to match it, so
the whole stack was self-consistent and green while being incompatible with the
only driver that matters. The error was found by disassembling
`/opt/devkitpro/libogc/lib/cube/libbba.a` (`gcif.o`, `bba_interrupt`):
```
lwbrx r9,0,r9 # 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 # ... must be <= 1540
srwi r9,r9,24 # status = word >> 24
and. r9,r9,r18 # r18 = 0x..84 -> errors are 0x80 | 0x04
lwz r9,cur_descr # and the low 12 bits go STRAIGHT to RRP:
# byte 0 -> 0x0018 (RRP_LO)
rlwinm r9,r31,24,28,31 # bits 11:8 -> 0x0019 (RRP_HI)
```
Under the old format a 342-byte frame (`00 00 01 5A`) decodes as
`packet_len = 16` → an 12-byte frame, with `next_packet_ptr = 0` pointing at
the reserved page. The length semantics and addressing were already right
(descriptor at `page<<8`, body at `+4`, length includes the descriptor); only
the packing was wrong, and `next_packet_ptr` was never written at all.
**Lesson worth keeping:** a green suite proves self-consistency, not
compatibility. For anything an external driver parses, check it against that
driver — the binary is on disk and `objdump` settles in minutes what guessing
could not. This is the third time a *guessed* interface cost real hardware
debugging (after the EXI header format and the Mode 2/3 mix-up).
`rx_frame_assembler`'s testbench now decodes descriptors with
`libogc_decode()`, which reproduces the instruction sequence above, so the
tests check the driver's algorithm rather than our own packing.
⚠️ **The status byte must be written EXPLICITLY** (`WRITE_D3`). The placeholder
issues four writes but the first lands twice, so `desc_base+3` is never
written; that was harmless while the length write covered byte 3. Left
unwritten it hands the driver whatever was in that SPRAM cell, and a stale
`0x80` or `0x04` discards the frame as an error. Simulation flags it as an
unwritten cell; real SPRAM has no such tell.
---
## SPRAM Notes ## SPRAM Notes
- iCE40UP5K has 128 KB SPRAM (SB_SPRAM256KA, 16-bit wide). - iCE40UP5K has 128 KB SPRAM (SB_SPRAM256KA, 16-bit wide).
+19 -5
View File
@@ -553,14 +553,28 @@ Each page is 256 bytes. A received frame may span multiple pages.
**Frame descriptor (first 4 bytes of first page):** **Frame descriptor (first 4 bytes of first page):**
A **packed 32-bit little-endian word**, not a byte-wise layout:
``` ```
Byte 0: LRPS value (Last Received Packet Status — set to 0x00 or actual status) bits 0–11 : next_packet_ptr — the PAGE at which the NEXT frame starts
Byte 1: 0x00 bits 12–23 : packet_len — total bytes, INCLUDING these 4
Byte 2: frame_length[15:8] (big-endian, includes descriptor bytes) bits 24–31 : status — 0x80 and 0x04 are errors; 0x00 means good
Byte 3: frame_length[7:0]
Bytes 4+: raw ethernet frame data (DA, SA, EtherType, payload, FCS) 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 data (DA, SA, EtherType, payload)
``` ```
⚠️ Read out of libogc's own BBA driver (`libbba.a`, `gcif.o`,
`bba_interrupt`) — `lwbrx` loads the four bytes as a little-endian word,
`rlwinm r4,r9,20,20,31` takes `packet_len = (word >> 12) & 0xFFF`, `srwi
r9,r9,24` takes the status, and the low 12 bits are written straight back to
`RRP`. This spec previously said `00 00 len_hi len_lo`, which that driver
decodes as a 12-byte frame with a next-pointer of 0 for a 342-byte frame. Do
not "simplify" it back to a plain big-endian length.
**Flow:** **Flow:**
1. Wait for `W5500SPIMaster` to signal frame available (`rx_sof` pulse). 1. Wait for `W5500SPIMaster` to signal frame available (`rx_sof` pulse).
+6 -1
View File
@@ -626,8 +626,13 @@ if __name__ == "__main__":
rwp = await exi_read(ctx, 0x16, 1) # RX write pointer (page) rwp = await exi_read(ctx, 0x16, 1) # RX write pointer (page)
total_len = len(RX_FRAME) + 4 total_len = len(RX_FRAME) + 4
RX_NEXT_PAGE = 2 # one-page frame written at page 1
got = await exi_read(ctx, 0x0100, total_len) # descriptor + frame got = await exi_read(ctx, 0x0100, total_len) # descriptor + frame
want = [0x00, 0x00, (total_len >> 8) & 0xFF, total_len & 0xFF] + RX_FRAME # The descriptor is a packed little-endian word, as libogc's driver
# reads it: next_packet_ptr[11:0] | packet_len[23:12] | status[31:24].
# See the format note in rx_frame_assembler.py.
_desc_word = ((total_len & 0xFFF) << 12) | (RX_NEXT_PAGE & 0xFFF)
want = [(_desc_word >> (8 * i)) & 0xFF for i in range(4)] + RX_FRAME
print(f"T6 RWP=0x{rwp[0]:02X} (want 0x02)") print(f"T6 RWP=0x{rwp[0]:02X} (want 0x02)")
print(f"T6 SPRAM[0x100]: {[f'0x{b:02X}' for b in got]}") print(f"T6 SPRAM[0x100]: {[f'0x{b:02X}' for b in got]}")
print(f"T6 expected : {[f'0x{b:02X}' for b in want]}") print(f"T6 expected : {[f'0x{b:02X}' for b in want]}")
+136 -32
View File
@@ -11,17 +11,50 @@ Page wrap: after 0x0F → 0x01 (skip 0x00).
Frame descriptor (4 bytes at page start) Frame descriptor (4 bytes at page start)
----------------------------------------------- -----------------------------------------------
Byte 0: LRPS (last received packet status) — 0x00 A PACKED 32-BIT LITTLE-ENDIAN WORD, not a byte-wise layout:
Byte 1: 0x00
Byte 2: total_length[15:8] (big-endian; includes 4 descriptor bytes) bits 0–11 : next_packet_ptr — the PAGE at which the NEXT frame starts
Byte 3: total_length[7:0] bits 12–23 : packet_len — total bytes, INCLUDING these 4
Bytes 4+: raw ethernet frame 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 Write sequence
-------------- --------------
1. Issue 4 SPRAM writes of 0x00 (placeholder descriptor). 1. Issue 4 SPRAM writes of 0x00 (placeholder descriptor).
2. For each byte received from W5500, issue one SPRAM write. 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. 4. Advance RWP, push to rx_wptr FIFO, pulse rx_irq.
""" """
@@ -112,6 +145,10 @@ class RXFrameAssembler(Elaboratable):
data_ctr = Signal(12) data_ctr = Signal(12)
# Total length = data_ctr + 4 # Total length = data_ctr + 4
total_len = Signal(12) 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 # Descriptor base (rwp*256) — saved when frame starts
desc_base = Signal(16) desc_base = Signal(16)
@@ -207,7 +244,7 @@ class RXFrameAssembler(Elaboratable):
# RWP advanced, but the GC's ethertype test read the wrong # RWP advanced, but the GC's ethertype test read the wrong
# offset and silently dropped every frame -- "no ACK" from the # offset and silently dropped every frame -- "no ACK" from the
# host with no other sign of trouble. The matching # 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. # descriptor of 00 00 00 01.
# #
# The first byte is written twice as a consequence of # 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) # Compute pages used: ceil((data_ctr + 4) / 256)
# = (total_len + 255) >> 8 = total_len[11:8] + (total_len[7:0] != 0) # = (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.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 # ⚠️ All three descriptor writes must advance on `valid & ready`,
# `ready` alone. `valid` is scheduled with m.d.sync and so is low # 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 # for the whole first cycle in the state, while `ready` is tested
# immediately -- and coming out of WAIT_LAST_WRITE `ready` is # immediately -- and coming out of WAIT_LAST_WRITE `ready` is
# usually already high. Testing `ready` alone therefore fired both # 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 # 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 # bring-up's 342-byte DHCP frame read a correct 00 00 01 5A and
# this went unnoticed. # this went unnoticed.
with m.State("WRITE_LEN_HI"): with m.State("CALC_NEXT"):
# Overwrite descriptor byte 2 with total_len[15:8] # The descriptor carries next_packet_ptr, so the next page must
m.d.sync += self.eth_wr_addr.eq(desc_base + 2) # be known BEFORE the descriptor is written -- this state used
m.d.sync += self.eth_wr_data.eq(total_len[8:12]) # 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) m.d.sync += self.eth_wr_valid.eq(1)
with m.If(self.eth_wr_valid & self.eth_wr_ready): with m.If(self.eth_wr_valid & self.eth_wr_ready):
m.d.sync += self.eth_wr_valid.eq(0) m.d.sync += self.eth_wr_valid.eq(0)
m.next = "WRITE_LEN_LO" m.next = "WRITE_D1"
with m.State("WRITE_LEN_LO"): with m.State("WRITE_D1"):
# Overwrite descriptor byte 3 with total_len[7:0] # 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_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) m.d.sync += self.eth_wr_valid.eq(1)
with m.If(self.eth_wr_valid & self.eth_wr_ready): with m.If(self.eth_wr_valid & self.eth_wr_ready):
m.d.sync += self.eth_wr_valid.eq(0) m.d.sync += self.eth_wr_valid.eq(0)
m.next = "ADVANCE_RWP" m.next = "ADVANCE_RWP"
with m.State("ADVANCE_RWP"): with m.State("ADVANCE_RWP"):
# next_rwp = ((rwp - 1 + pages_used) % 15) + 1 m.d.sync += rwp.eq(next_page)
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.next = "PUSH_WPT" m.next = "PUSH_WPT"
with m.State("PUSH_WPT"): with m.State("PUSH_WPT"):
@@ -465,6 +542,27 @@ if __name__ == "__main__":
base = page << 8 base = page << 8
return [mem.get(base + i) for i in range(4)] 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): def body(page, n):
base = (page << 8) + 4 base = (page << 8) + 4
return [mem.get(base + i) for i in range(n)] return [mem.get(base + i) for i in range(n)]
@@ -479,8 +577,9 @@ if __name__ == "__main__":
await send_frame(f1) await send_frame(f1)
check("T1 rwp advance", await wait_irq(), 2) check("T1 rwp advance", await wait_irq(), 2)
total1 = len(f1) + 4 total1 = len(f1) + 4
check("T1 descriptor", descriptor(1), check("T1 descriptor", descriptor(1), pack_desc(total1, 2))
[0x00, 0x00, (total1 >> 8) & 0xFF, total1 & 0xFF]) 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 # The body MUST start at base+4. This is the check that catches a
# placeholder write being dropped: with only three placeholder bytes # placeholder write being dropped: with only three placeholder bytes
# written the body landed at base+3 and every frame was shifted one # written the body landed at base+3 and every frame was shifted one
@@ -500,8 +599,9 @@ if __name__ == "__main__":
await send_frame(f2) await send_frame(f2)
rwp2 = await wait_irq() rwp2 = await wait_irq()
total2 = len(f2) + 4 total2 = len(f2) + 4
check("T2 descriptor (len > 255)", descriptor(2), check("T2 descriptor (len > 255)", descriptor(2), pack_desc(total2, 4))
[0x00, 0x00, (total2 >> 8) & 0xFF, total2 & 0xFF]) 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) check("T2 body at base+4", body(2, len(f2)), f2)
# 304 bytes spans two pages, so page 2 -> page 4. # 304 bytes spans two pages, so page 2 -> page 4.
check("T2 rwp advance (2 pages)", rwp2, 4) check("T2 rwp advance (2 pages)", rwp2, 4)
@@ -564,8 +664,11 @@ if __name__ == "__main__":
check("T5 stored despite stalls", rwp5, "stored") check("T5 stored despite stalls", rwp5, "stored")
pg5 = (ctx.get(dut.rx_wptr_w_data) - 2) & 0xFF # page it was written to pg5 = (ctx.get(dut.rx_wptr_w_data) - 2) & 0xFF # page it was written to
total5 = len(f5) + 4 total5 = len(f5) + 4
next5 = ctx.get(dut.rx_wptr_w_data)
check("T5 descriptor under stall", descriptor(pg5), 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) check("T5 body at base+4 under stall", body(pg5, len(f5)), f5)
stall[0] = 1 stall[0] = 1
await tick(4) await tick(4)
@@ -586,8 +689,9 @@ if __name__ == "__main__":
await send_frame(f6) await send_frame(f6)
check("T6 rwp back to page 1 after NCRA reset", await wait_irq(), 2) check("T6 rwp back to page 1 after NCRA reset", await wait_irq(), 2)
total6 = len(f6) + 4 total6 = len(f6) + 4
check("T6 frame lands at page 1", descriptor(1), check("T6 frame lands at page 1", descriptor(1), pack_desc(total6, 2))
[0x00, 0x00, (total6 >> 8) & 0xFF, total6 & 0xFF]) 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) check("T6 body at page 1 base+4", body(1, len(f6)), f6)
await tick(4) await tick(4)
+30 -31
View File
@@ -32,7 +32,7 @@ static u8 s_last_desc[4];
/* How often the descriptor was found at each page offset. The assembler is /* How often the descriptor was found at each page offset. The assembler is
* supposed to put it at +0 always; hardware shows it sometimes landing at +1, * supposed to put it at +0 always; hardware shows it sometimes landing at +1,
* varying frame to frame, so count both rather than assume either. */ * varying frame to frame, so count both rather than assume either. */
static u32 s_shift_hits[2]; static u32 s_desc_ok;
/* ACKs where TI never set -- the frame did not reach the W5100. */ /* ACKs where TI never set -- the frame did not reach the W5100. */
static u32 s_tx_timeouts; static u32 s_tx_timeouts;
static u8 s_last_dump[32]; static u8 s_last_dump[32];
@@ -60,7 +60,7 @@ static int ring_next(u8 *buf, u32 cap)
{ {
u8 wp[2], hdr[8]; u8 wp[2], hdr[8];
u32 rpage, wpage, dlen, flen, addr; u32 rpage, wpage, dlen, flen, addr;
u32 shift; u32 desc, nextp;
if (bba_read(REG_RWP_LO, wp, 2) < 0) return -1; if (bba_read(REG_RWP_LO, wp, 2) < 0) return -1;
rpage = s_rrp; rpage = s_rrp;
@@ -87,25 +87,24 @@ static int ring_next(u8 *buf, u32 cap)
memcpy(s_last_desc, hdr, 4); memcpy(s_last_desc, hdr, 4);
s_last_page = rpage; s_last_page = rpage;
/* LOCATE the descriptor rather than assume it is at +0. /* The descriptor is a PACKED 32-BIT LITTLE-ENDIAN WORD -- the format
* libogc's own BBA driver reads, so this is what GC software expects:
* *
* It should always be at +0: four bytes [00 00 len_hi len_lo] followed * bits 0-11 next_packet_ptr : the PAGE the NEXT frame starts at
* immediately by the frame. On hardware it sometimes sits at +1 instead, * bits 12-23 packet_len : total bytes, INCLUDING these 4
* and which one varies frame to frame -- 18 of 23 frames were rejected * bits 24-31 status : 0x80 and 0x04 are errors
* for an implausible length while 5 parsed fine, in the same run. That
* is a race in the assembler, not a constant offset, and the gateware bug
* behind it is not yet found.
* *
* Taking the length from whichever offset yields a plausible one lets the * See the note in exi_bba/rx_frame_assembler.py for the disassembly this
* transfer proceed either way AND measures the split, which is the * was taken from. The old reader assumed [00 00 len_hi len_lo] and
* measurement needed to find the real cause. A length is a strong check: * probed offsets +0 and +1 to work around the tx-FIFO-underrun bug that
* it must exceed the 4 descriptor bytes and fit a legal frame. */ * shifted descriptors; that bug is fixed (confirmed on console, +1=0) and
for (shift = 0; shift <= 1; shift++) { * the probe is gone -- under the packed format a +1 read is not a shifted
dlen = ((u32)hdr[shift + 2] << 8) | hdr[shift + 3]; * descriptor, just garbage. */
if (dlen > 4 && dlen <= 1518 + 4) break; desc = (u32)hdr[0] | ((u32)hdr[1] << 8) |
} ((u32)hdr[2] << 16) | ((u32)hdr[3] << 24);
if (shift > 1) { dlen = (desc >> 12) & 0xFFF;
dlen = ((u32)hdr[2] << 8) | hdr[3]; /* report the nominal one */ nextp = desc & 0xFFF;
if (((desc >> 24) & 0x84) || dlen <= 4 || dlen > 1518 + 4) {
/* Only now is a hex dump worth an EXI transaction. Doing it for /* Only now is a hex dump worth an EXI transaction. Doing it for
* EVERY frame (as an earlier version did) added a round trip to the * EVERY frame (as an earlier version did) added a round trip to the
* poll loop that is already losing the race against the writer -- * poll loop that is already losing the race against the writer --
@@ -113,7 +112,7 @@ static int ring_next(u8 *buf, u32 cap)
bba_stream((u16)addr, s_last_dump, sizeof(s_last_dump)); bba_stream((u16)addr, s_last_dump, sizeof(s_last_dump));
return -3; return -3;
} }
s_shift_hits[shift]++; s_desc_ok++;
flen = dlen - 4; flen = dlen - 4;
if (flen > cap) flen = cap; if (flen > cap) flen = cap;
@@ -122,16 +121,18 @@ static int ring_next(u8 *buf, u32 cap)
* one DMA read covers it. */ * one DMA read covers it. */
{ {
static u8 tmp[1536 + 4]; static u8 tmp[1536 + 4];
u32 want = shift + dlen; u32 want = dlen;
if (want > sizeof(tmp)) want = sizeof(tmp); if (want > sizeof(tmp)) want = sizeof(tmp);
if (bba_stream((u16)addr, tmp, want) < 0) return -1; if (bba_stream((u16)addr, tmp, want) < 0) return -1;
memcpy(buf, tmp + shift + 4, flen); memcpy(buf, tmp + 4, flen);
} }
/* Advance RRP by the pages this frame occupied, wrapping 1..15. */ /* Advance RRP to the descriptor's OWN next_packet_ptr, which is what
* libogc does, rather than recomputing the page span here. The writer
* put that value in the descriptor, so the two cannot disagree. */
{ {
u32 pages = (shift + dlen + 255) >> 8; u32 next = nextp;
u32 next = ((rpage - 1 + pages) % 15) + 1; if (next < 1 || next > 15) next = 1;
if (bba_w8(REG_RRP_LO, (u8)next) < 0) return -1; if (bba_w8(REG_RRP_LO, (u8)next) < 0) return -1;
s_rrp = next; s_rrp = next;
} }
@@ -381,11 +382,10 @@ int netboot_receive(void *dst, u32 maxlen, void (*status)(const char *msg))
if (idle > (got ? 5000u : 30000u)) { if (idle > (got ? 5000u : 30000u)) {
if (status) { if (status) {
snprintf(line, sizeof(line), snprintf(line, sizeof(line),
"%s (%u seen, %u bad, desc+0=%u +1=%u, txto=%u)", "%s (%u seen, %u bad, desc ok=%u, txto=%u)",
got ? "timeout mid-transfer" : "timeout: no match", got ? "timeout mid-transfer" : "timeout: no match",
(unsigned)seen, (unsigned)bad, (unsigned)seen, (unsigned)bad,
(unsigned)s_shift_hits[0], (unsigned)s_desc_ok,
(unsigned)s_shift_hits[1],
(unsigned)s_tx_timeouts); (unsigned)s_tx_timeouts);
status(line); status(line);
} }
@@ -433,9 +433,8 @@ int netboot_receive(void *dst, u32 maxlen, void (*status)(const char *msg))
* W5100) versus sent-but-lost. The host reports the * W5100) versus sent-but-lost. The host reports the
* retries; only the GC knows which kind they were. */ * retries; only the GC knows which kind they were. */
snprintf(line, sizeof(line), snprintf(line, sizeof(line),
"desc@+0=%u +1=%u bad=%u lapped=%u txto=%u", "desc ok=%u bad=%u lapped=%u txto=%u",
(unsigned)s_shift_hits[0], (unsigned)s_desc_ok,
(unsigned)s_shift_hits[1],
(unsigned)bad, (unsigned)lapped, (unsigned)bad, (unsigned)lapped,
(unsigned)s_tx_timeouts); (unsigned)s_tx_timeouts);
status(line); status(line);
+19 -9
View File
@@ -394,7 +394,7 @@ static int t_rx(char *m, size_t n)
static int t_dma(char *m, size_t n) static int t_dma(char *m, size_t n)
{ {
u8 buf[64], rp[2], wp[2]; u8 buf[64], rp[2], wp[2];
u32 dlen, page, addr; u32 dlen, page, addr, desc, nextp;
if (bba_read(REG_RRP_LO, rp, 2) < 0) { snprintf(m, n, "RRP read failed"); return 1; } if (bba_read(REG_RRP_LO, rp, 2) < 0) { snprintf(m, n, "RRP read failed"); return 1; }
if (bba_read(REG_RWP_LO, wp, 2) < 0) { snprintf(m, n, "RWP read failed"); return 1; } if (bba_read(REG_RWP_LO, wp, 2) < 0) { snprintf(m, n, "RWP read failed"); return 1; }
page = ((u32)rp[1] << 8) | rp[0]; page = ((u32)rp[1] << 8) | rp[0];
@@ -417,11 +417,18 @@ static int t_dma(char *m, size_t n)
snprintf(m, n, "stream read at 0x%04X failed", (unsigned)addr); snprintf(m, n, "stream read at 0x%04X failed", (unsigned)addr);
return 1; return 1;
} }
/* 4-byte ring descriptor; its length field INCLUDES the descriptor. */ /* The 4-byte ring descriptor is a PACKED 32-BIT LITTLE-ENDIAN WORD, the
dlen = ((u32)buf[2] << 8) | buf[3]; * format libogc's own driver reads:
if (dlen > 4 && dlen <= 1518) { * bits 0-11 next_packet_ptr (page), 12-23 packet_len (INCLUDING these
snprintf(m, n, "p%u len 0x%04X = %u bytes, etype %02X%02X", * 4 bytes), 24-31 status (0x80 and 0x04 are errors).
(unsigned)page, (unsigned)dlen, (unsigned)(dlen - 4), * Decoded here exactly as bba_interrupt does. */
desc = (u32)buf[0] | ((u32)buf[1] << 8) |
((u32)buf[2] << 16) | ((u32)buf[3] << 24);
dlen = (desc >> 12) & 0xFFF;
nextp = desc & 0xFFF;
if (((desc >> 24) & 0x84) == 0 && dlen > 4 && dlen <= 1518 + 4) {
snprintf(m, n, "p%u len %u B, next p%u, etype %02X%02X",
(unsigned)page, (unsigned)(dlen - 4), (unsigned)nextp,
buf[16], buf[17]); buf[16], buf[17]);
return 0; return 0;
} }
@@ -442,9 +449,12 @@ static int t_dma(char *m, size_t n)
map[pg - 1] = '?'; map[pg - 1] = '?';
continue; continue;
} }
dl = ((u32)d[2] << 8) | d[3]; u32 w = (u32)d[0] | ((u32)d[1] << 8) |
if (d[0] == 0 && d[1] == 0 && dl == 0) map[pg - 1] = '-'; ((u32)d[2] << 16) | ((u32)d[3] << 24);
else if (dl > 4 && dl <= 1518) map[pg - 1] = 'o'; dl = (w >> 12) & 0xFFF;
if (w == 0) map[pg - 1] = '-';
else if (((w >> 24) & 0x84) == 0 &&
dl > 4 && dl <= 1518 + 4) map[pg - 1] = 'o';
else map[pg - 1] = 'x'; else map[pg - 1] = 'x';
} }
map[15] = 0; map[15] = 0;