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
- iCE40UP5K has 128 KB SPRAM (SB_SPRAM256KA, 16-bit wide).