Fix command-space writes, and replay libogc's real init sequence
Second protocol mismatch found the same way as the descriptor: by disassembling libogc's own driver instead of trusting our notes. COMMAND SPACE IS A 2-BYTE HEADER + DATA PHASE, not a single 4-byte transfer. CLAUDE.md said [0x40|reg, 00, 00, data] and the decoder followed it, waiting for a fourth byte and consuming the DATA byte as header byte 2 -- so every command-space WRITE was silently dropped. libogc's __bba_init writes command register 2 (its interrupt mask) at both the start and the end of init, so none of that took effect. What it actually sends: EXI_Imm (len=2, write) -> 42 00 the whole header, 0x4000 | (reg << 8) EXI_Sync EXI_ImmEx(len=1, write) -> 00 the data Three bytes between CS fall and CS rise. The device-ID read survived the wrong decode only because SPIMode3Slave answers it on a fast path independent of this FSM -- which is precisely why no test caught it: the only command-space traffic under test bypassed the decoder being tested. CHDR_2/CHDR_3 are replaced by CMD_WRITE, which takes the data byte straight after header byte 1 and auto-increments the command register so a multi-byte command write behaves like the register-space path. cmd_wreg latches the target because the write applies a cycle later, by which time cmd_reg has moved on. bba_register_file T10 writes command register 2 and reads it back, and is mutation-tested: restoring the 4-byte decode fails it. Also replaces the GUESSED init sequence in CLAUDE.md with the real transaction list. The old one was wrong in three ways that matter: libogc READS the MAC out of PAR0-5 rather than writing one (so our reset value is what the console uses), IMR is 0xDF not 0x86, and it writes RWP (0x0016/17) and RHBP (0x001A/1B) which the register file does not accept. The latter is harmless today -- RHBP is 0x000F, matching our hardcoded 15-page ring -- but a driver configuring a smaller ring would be ignored while the assembler kept writing outside it. New gc_bba_test suite 7 "libogc init" replays the sequence on hardware: cmd-space write (2-byte header, read back), MAC readable (one 6-byte PAR read, Nintendo OUI), and init replay (all 30 writes in order, then NWAYS/HIPR/NCRA.SR must still read correctly -- the old guessed header format turned a NWAYS read into an NCRA reset, so a misdecoded write is a real risk). bba.c gains bba_cmd_write/bba_cmd_read in libogc's shape. 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:
@@ -1057,13 +1057,33 @@ There are TWO address spaces, selected by bit 7 of the first byte, and **no
|
|||||||
length field**:
|
length field**:
|
||||||
|
|
||||||
```
|
```
|
||||||
COMMAND space (6-bit reg) one 4-byte READWRITE transfer
|
COMMAND space (6-bit reg) 2-byte header, then a SEPARATE data phase
|
||||||
write: [0x40|reg, 00, 00, data]
|
write: [0x40|reg, 0x00] then N bytes written (libogc writes 1)
|
||||||
read : [reg, 00, 00, xx ] <- response is the 4th byte
|
read : [reg, 0x00] then N bytes read (device ID reads 4)
|
||||||
REGISTER space (16-bit reg) 4-byte header, then a SEPARATE data phase
|
REGISTER space (16-bit reg) 4-byte header, then a SEPARATE data phase
|
||||||
read : [0x80, reg>>8, reg&0xFF, 0x00] then N bytes read (Swiss uses DMA)
|
read : [0x80, reg>>8, reg&0xFF, 0x00] then N bytes read (Swiss uses DMA)
|
||||||
write: [0xC0, reg>>8, reg&0xFF, 0x00] then N bytes written
|
write: [0xC0, reg>>8, reg&0xFF, 0x00] then N bytes written
|
||||||
```
|
```
|
||||||
|
|
||||||
|
⚠️ **Command space is a 2-byte header + data phase — corrected 2026-10-10.**
|
||||||
|
This file previously said "one 4-byte READWRITE transfer,
|
||||||
|
`[0x40|reg, 00, 00, data]`", and the decoder followed it: it waited for a
|
||||||
|
fourth byte, consumed the DATA byte as header byte 2, and **silently dropped
|
||||||
|
every command-space write.** libogc's `__bba_init` writes command register 2
|
||||||
|
(its interrupt mask) at both the start and the end of init, so none of that
|
||||||
|
would have taken effect. The real shape, from the disassembly:
|
||||||
|
|
||||||
|
```
|
||||||
|
EXI_Imm (len=2, write) -> 42 00 the WHOLE header
|
||||||
|
EXI_Sync
|
||||||
|
EXI_ImmEx(len=1, write) -> 00 the data
|
||||||
|
```
|
||||||
|
|
||||||
|
Three bytes between CS fall and CS rise, not four. `0x4200 = 0x4000 |
|
||||||
|
(reg << 8)`. The device-ID read survived the wrong decode only because
|
||||||
|
`SPIMode3Slave` answers it on a fast path independent of the FSM — which is
|
||||||
|
exactly why no test caught it. `bba_register_file` **T10** now writes command
|
||||||
|
register 2 and reads it back, and is mutation-tested against the 4-byte decode.
|
||||||
NCRA, IR, NWAYS, RWP, RRP, PAR… all live in the REGISTER space
|
NCRA, IR, NWAYS, RWP, RRP, PAR… all live in the REGISTER space
|
||||||
(`bba_in8`/`bba_out8`/`bba_ins`/`bba_outs`). Only the device-ID query uses a
|
(`bba_in8`/`bba_out8`/`bba_ins`/`bba_outs`). Only the device-ID query uses a
|
||||||
short header — the single case where the two formats coincide.
|
short header — the single case where the two formats coincide.
|
||||||
@@ -1292,21 +1312,60 @@ unwritten cell; real SPRAM has no such tell.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## GC Initialisation Sequence (Swiss/BBA driver)
|
## GC Initialisation Sequence — read out of libogc, 2026-10-10
|
||||||
|
|
||||||
```
|
⚠️ **The sequence below replaces a GUESSED one.** The old version said the
|
||||||
1. Write 0x0000 × 2, read 4 B → must get 0x04020200 (device ID)
|
driver *writes* PAR0–5, writes `MAR0–7 = 0xFF`, `ANALOG = 0xD6`, `NWAYC`,
|
||||||
2. Write NCRA = 0x01 (reset, self-clears; resets W5500 + SPRAM ptrs)
|
`GCA` and `IMR = 0x86`. Disassembling libogc's `__bba_init`
|
||||||
3. Poll NCRA bit 0 until 0 (wait reset complete)
|
(`/opt/devkitpro/libogc/lib/cube/libbba.a`, `gcif.o`) shows otherwise. This is
|
||||||
4. Write PAR0–5 (MAC address)
|
the actual transaction list, in order:
|
||||||
5. Write MAR0–7 = 0xFF (promiscuous multicast)
|
|
||||||
6. Write ANALOG = 0xD6 (enable PHY — no FPGA effect, just store)
|
| # | transaction | note |
|
||||||
7. Write NWAYC (autoneg config — store only)
|
|---|---|---|
|
||||||
8. Write IMR = 0x86 (enable RBFI | TI | RI interrupts)
|
| 0 | **cmd** write reg 2 = `0x00` | interrupt mask (IRMASKALL) |
|
||||||
9. Write GCA (AUTOPUB bit)
|
| 1 | reg `0x0060` = `0x00` | |
|
||||||
10. Write NCRA SR bit = 0x08 (start receive)
|
| 2 | **cmd** read reg `0x0F` | |
|
||||||
11. Poll NWAYS until link up → return hardcoded 0x17 immediately
|
| 3 | reg `0x0000` = `0x01` | **NCRA reset** |
|
||||||
```
|
| 4 | reg `0x0000` = `0x00` | |
|
||||||
|
| 5 | **cmd** read reg `0x01` | |
|
||||||
|
| 6,7 | **cmd** writes reg `0x04`, `0x05` | |
|
||||||
|
| 8–15 | reg `0x0058`–`0x005F` = `80 00 03 83 32 FE 1F 1F` | PHY/analog block |
|
||||||
|
| 16,17 | reg `0x0001` = `0x52`, `0x0060` = `0x74` | |
|
||||||
|
| 18,19 | reg `0x0014` = `0x00`, `0x0015` = `0x06` | |
|
||||||
|
| 20 | reg `0x0050` = `0x80` | |
|
||||||
|
| 21–24 | reg `0x000C/0D` = `0x00`, `0x000A` = `0x01`, `0x000B` = `0x00` | |
|
||||||
|
| 25,26 | reg `0x001A/1B` = `0x000F` | **RHBP** — ring high boundary = page 15 |
|
||||||
|
| 27,28 | reg `0x0016/17` = `0x0001` | **writes RWP** |
|
||||||
|
| 29,30 | reg `0x0018/19` = `0x0001` | **writes RRP** |
|
||||||
|
| 31 | reg `0x0032` = `0x08` | |
|
||||||
|
| 32 | reg `0x0000` = `0x08` | **NCRA SR — start receive** |
|
||||||
|
| 33–36 | reg `0x0030` read, then writes `0x04`, `0x0C` | |
|
||||||
|
| 37 | reg `0x0020`, **6-byte READ** | **reads PAR0–5 — the MAC** |
|
||||||
|
| 38 | reg `0x0009` = `0xFF` | IR: clear all |
|
||||||
|
| 39 | reg `0x0008` = `0xDF` | **IMR — not `0x86`** |
|
||||||
|
| 40 | **cmd** write reg 2 = `0xF8` | IRMASKNONE (enable) |
|
||||||
|
|
||||||
|
Three corrections that matter:
|
||||||
|
- **The driver READS the MAC from PAR0–5; it does not write one.** So the
|
||||||
|
PAR0–5 reset value is what the console will use — keep a valid Nintendo OUI
|
||||||
|
there (`00:09:BF:00:00:01`). It is a single 6-byte register-space read.
|
||||||
|
- **`IMR` is `0xDF`**, and `IR` is cleared with `0xFF` first.
|
||||||
|
- **It writes `RWP` (0x0016/17) and `RHBP` (0x001A/1B)**, which the register
|
||||||
|
file does not accept. Harmless today — `RHBP` is `0x000F`, matching our
|
||||||
|
hardcoded 15-page ring, and the NCRA reset already puts `RWP` at page 1 —
|
||||||
|
but it is a real divergence: a driver that configured a SMALLER ring would
|
||||||
|
be ignored, and our assembler would write outside it.
|
||||||
|
- Registers `0x0058`–`0x005F`, `0x0060`, `0x0001`, `0x0014/15`, `0x0050`,
|
||||||
|
`0x000A`–`0x000D`, `0x0030`, `0x0032` are not decoded. Writes fall through
|
||||||
|
the `Switch` with no `Case`, so they are ignored rather than aliased —
|
||||||
|
which is the safe failure, but worth knowing when a driver expects one back.
|
||||||
|
|
||||||
|
`gc_bba_test` suite 7 (**libogc init**) replays this list on hardware and
|
||||||
|
checks the shapes: `cmd-space write` (2-byte header + data phase, read back),
|
||||||
|
`MAC readable` (one 6-byte PAR read, Nintendo OUI), and `init replay` (all 30
|
||||||
|
register writes in order, then NWAYS/HIPR/NCRA.SR must still be correct — the
|
||||||
|
old guessed header format turned a NWAYS read into an NCRA reset, so a
|
||||||
|
misdecoded write is a real risk, not a theoretical one).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -256,6 +256,7 @@ class BBARegisterFile(Elaboratable):
|
|||||||
is_write = Signal()
|
is_write = Signal()
|
||||||
cmd_space = Signal() # this transaction is a command access
|
cmd_space = Signal() # this transaction is a command access
|
||||||
cmd_reg = Signal(6)
|
cmd_reg = Signal(6)
|
||||||
|
cmd_wreg = Signal(6) # target of a pending command-space write
|
||||||
byte_ctr = Signal(3) # response-byte index (device-ID sequence)
|
byte_ctr = Signal(3) # response-byte index (device-ID sequence)
|
||||||
cmd_wr = Signal() # command-space write pending
|
cmd_wr = Signal() # command-space write pending
|
||||||
prefetched = Signal() # first response byte already pushed
|
prefetched = Signal() # first response byte already pushed
|
||||||
@@ -436,7 +437,25 @@ class BBARegisterFile(Elaboratable):
|
|||||||
m.d.comb += [self.tx_data.eq(rd_val), self.tx_en.eq(1)]
|
m.d.comb += [self.tx_data.eq(rd_val), self.tx_en.eq(1)]
|
||||||
m.d.sync += [eff_addr.eq(eff_addr + 1), prefetched.eq(1)]
|
m.d.sync += [eff_addr.eq(eff_addr + 1), prefetched.eq(1)]
|
||||||
|
|
||||||
# ── command space: [reg, 00, 00, data] in ONE 4-byte transfer ──
|
# ── command space: a 2-BYTE header, then a data phase ─────────
|
||||||
|
#
|
||||||
|
# [0x40|reg, 0x00] + N data bytes written, or
|
||||||
|
# [reg, 0x00] + N data bytes read.
|
||||||
|
#
|
||||||
|
# ⚠️ NOT a single 4-byte transfer. This file used to decode
|
||||||
|
# [reg, 00, 00, data] and wait for a fourth byte, taking the DATA
|
||||||
|
# byte as header byte 2 -- so every command-space WRITE was
|
||||||
|
# silently dropped. libogc's own driver (see the init map in
|
||||||
|
# CLAUDE.md) does, for a write of 0x00 to command register 2:
|
||||||
|
#
|
||||||
|
# EXI_Imm (len=2, write) -> 42 00 the whole header
|
||||||
|
# EXI_Sync
|
||||||
|
# EXI_ImmEx(len=1, write) -> 00 the data
|
||||||
|
#
|
||||||
|
# i.e. three bytes between CS fall and CS rise, not four. The
|
||||||
|
# device-ID read survived the old decode only because
|
||||||
|
# SPIMode3Slave answers it on the fast path, independently of
|
||||||
|
# this FSM, which is why nothing caught it.
|
||||||
with m.State("CHDR_1"):
|
with m.State("CHDR_1"):
|
||||||
with m.If(self.rx_rdy):
|
with m.If(self.rx_rdy):
|
||||||
m.d.comb += self.rx_en.eq(1)
|
m.d.comb += self.rx_en.eq(1)
|
||||||
@@ -446,24 +465,31 @@ class BBARegisterFile(Elaboratable):
|
|||||||
# 2-byte header has too few pops to drain.
|
# 2-byte header has too few pops to drain.
|
||||||
with m.If(~is_write & (cmd_reg == 0) & id_phase):
|
with m.If(~is_write & (cmd_reg == 0) & id_phase):
|
||||||
m.next = "DEVID"
|
m.next = "DEVID"
|
||||||
|
with m.Elif(is_write):
|
||||||
|
m.next = "CMD_WRITE"
|
||||||
with m.Else():
|
with m.Else():
|
||||||
m.next = "CHDR_2"
|
# Read: the response was prefetched below and the pop
|
||||||
|
# at the end of this byte lands it in data slot 0.
|
||||||
|
m.next = "TRAILER"
|
||||||
with m.Elif(~is_write & self.tx_rdy & ~prefetched):
|
with m.Elif(~is_write & self.tx_rdy & ~prefetched):
|
||||||
# Ordinary command read: response lands in byte slot 3.
|
# Ordinary command read: push during header byte 1 so the
|
||||||
|
# byte-boundary pop presents it in data slot 0.
|
||||||
m.d.comb += [self.tx_data.eq(rd_val), self.tx_en.eq(1)]
|
m.d.comb += [self.tx_data.eq(rd_val), self.tx_en.eq(1)]
|
||||||
m.d.sync += prefetched.eq(1)
|
m.d.sync += prefetched.eq(1)
|
||||||
|
|
||||||
with m.State("CHDR_2"):
|
with m.State("CMD_WRITE"):
|
||||||
|
# Data phase. Each byte writes the running command register,
|
||||||
|
# which auto-increments so a multi-byte command write behaves
|
||||||
|
# like the register-space path. cmd_wreg latches the target
|
||||||
|
# separately because the handler below applies the write a
|
||||||
|
# cycle later, by which time cmd_reg has already moved on.
|
||||||
with m.If(self.rx_rdy):
|
with m.If(self.rx_rdy):
|
||||||
m.d.comb += self.rx_en.eq(1)
|
m.d.comb += self.rx_en.eq(1)
|
||||||
m.next = "CHDR_3"
|
m.d.sync += [cmd_wr.eq(1),
|
||||||
|
cmd_wdata.eq(self.rx_data),
|
||||||
with m.State("CHDR_3"):
|
cmd_wreg.eq(cmd_reg),
|
||||||
with m.If(self.rx_rdy):
|
cmd_reg.eq(cmd_reg + 1)]
|
||||||
m.d.comb += self.rx_en.eq(1)
|
with m.If(~self.cs_active):
|
||||||
with m.If(is_write):
|
|
||||||
m.d.sync += cmd_wr.eq(1) # applied below
|
|
||||||
m.d.sync += cmd_wdata.eq(self.rx_data)
|
|
||||||
m.next = "TRAILER"
|
m.next = "TRAILER"
|
||||||
|
|
||||||
with m.State("TRAILER"):
|
with m.State("TRAILER"):
|
||||||
@@ -659,10 +685,10 @@ class BBARegisterFile(Elaboratable):
|
|||||||
m.d.comb += [self.tx_data.eq(sp_hold), self.tx_en.eq(1)]
|
m.d.comb += [self.tx_data.eq(sp_hold), self.tx_en.eq(1)]
|
||||||
m.d.sync += sp_pending.eq(0)
|
m.d.sync += sp_pending.eq(0)
|
||||||
|
|
||||||
# ── Command-space write, applied the cycle after CHDR_3 ──────────
|
# ── Command-space write, applied the cycle after the data byte ───
|
||||||
with m.If(cmd_wr):
|
with m.If(cmd_wr):
|
||||||
m.d.sync += cmd_wr.eq(0)
|
m.d.sync += cmd_wr.eq(0)
|
||||||
with m.Switch(cmd_reg):
|
with m.Switch(cmd_wreg):
|
||||||
with m.Case(0x02): m.d.sync += r_cmd_imr.eq(cmd_wdata)
|
with m.Case(0x02): m.d.sync += r_cmd_imr.eq(cmd_wdata)
|
||||||
with m.Case(0x03): m.d.sync += r_cmd_ir.eq(r_cmd_ir & ~cmd_wdata)
|
with m.Case(0x03): m.d.sync += r_cmd_ir.eq(r_cmd_ir & ~cmd_wdata)
|
||||||
|
|
||||||
@@ -807,6 +833,38 @@ if __name__ == "__main__":
|
|||||||
await ctx.tick("sync").repeat(6)
|
await ctx.tick("sync").repeat(6)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
async def cmd_write(ctx, creg, data):
|
||||||
|
"""Command-space write in libogc's REAL shape: a 2-byte header
|
||||||
|
[0x40|reg, 0x00] then the data bytes -- three bytes total for a
|
||||||
|
single-byte write, NOT a 4-byte transfer."""
|
||||||
|
ctx.set(reg.cs_active, 1)
|
||||||
|
await push_rx(ctx, 0x40 | (creg & 0x3F))
|
||||||
|
await push_rx(ctx, 0x00)
|
||||||
|
for b in data:
|
||||||
|
await push_rx(ctx, b)
|
||||||
|
ctx.set(reg.cs_active, 0)
|
||||||
|
await ctx.tick("sync").repeat(6)
|
||||||
|
|
||||||
|
async def cmd_read(ctx, creg, length=1):
|
||||||
|
"""Command-space read: 2-byte header, then clock `length` bytes."""
|
||||||
|
tx_log.clear()
|
||||||
|
ctx.set(reg.cs_active, 1)
|
||||||
|
await push_rx(ctx, creg & 0x3F)
|
||||||
|
# Idle while header byte 1 is "on the wire". The device must push its
|
||||||
|
# response DURING that byte: the engine's byte-boundary pop for data
|
||||||
|
# slot 0 happens at the end of byte 1, which is before the FSM has even
|
||||||
|
# seen byte 1 (the rx CDC is ~125 ns). A response pushed in reaction
|
||||||
|
# to consuming byte 1 would land in slot 1 instead, so pushing both
|
||||||
|
# header bytes back-to-back here would not model the real transfer.
|
||||||
|
for _ in range(4):
|
||||||
|
sample_tx(ctx) # the push lands HERE; a bare tick loses it
|
||||||
|
await ctx.tick("sync")
|
||||||
|
await push_rx(ctx, 0x00)
|
||||||
|
result = await collect_tx(ctx, length)
|
||||||
|
ctx.set(reg.cs_active, 0)
|
||||||
|
await ctx.tick("sync").repeat(6)
|
||||||
|
return result
|
||||||
|
|
||||||
# SPRAM contents the streaming-read test reads back (byte i = 0xA0+i).
|
# SPRAM contents the streaming-read test reads back (byte i = 0xA0+i).
|
||||||
spram_mem = {0x100 + i: (0xA0 + i) & 0xFF for i in range(64)}
|
spram_mem = {0x100 + i: (0xA0 + i) & 0xFF for i in range(64)}
|
||||||
|
|
||||||
@@ -1030,6 +1088,29 @@ if __name__ == "__main__":
|
|||||||
print(f"T9 second ST trigger queues: {bool(ctx.get(reg.tx_ctrl_r_rdy))} "
|
print(f"T9 second ST trigger queues: {bool(ctx.get(reg.tx_ctrl_r_rdy))} "
|
||||||
f"(was {bool(tx_ctrl_before)})")
|
f"(was {bool(tx_ctrl_before)})")
|
||||||
|
|
||||||
|
# ── T10: command-space write in libogc's real 3-byte shape ───────
|
||||||
|
# The decoder used to expect [reg, 00, 00, data] and wait for a fourth
|
||||||
|
# byte, consuming the DATA byte as header byte 2 -- so every
|
||||||
|
# command-space write was silently DROPPED. libogc writes command
|
||||||
|
# register 2 (its interrupt mask) at both the start and the end of
|
||||||
|
# init, so a dropped write means that mask is never applied.
|
||||||
|
#
|
||||||
|
# Nothing caught this before because no test issued a command-space
|
||||||
|
# write at all: the device-ID READ is answered by SPIMode3Slave's fast
|
||||||
|
# path, independently of this FSM, so the only command-space traffic
|
||||||
|
# under test bypassed the decoder being tested.
|
||||||
|
await cmd_write(ctx, 0x02, [0xF8]) # libogc's IRMASKNONE
|
||||||
|
got = await cmd_read(ctx, 0x02, 1)
|
||||||
|
print(f"T10 cmd write reg2=0xF8 -> read back {[hex(x) for x in got]}")
|
||||||
|
if got != [0xF8]:
|
||||||
|
errors.append(f"T10 command-space write dropped: read back {got}")
|
||||||
|
|
||||||
|
await cmd_write(ctx, 0x02, [0x00]) # ... and IRMASKALL
|
||||||
|
got2 = await cmd_read(ctx, 0x02, 1)
|
||||||
|
print(f"T10b cmd write reg2=0x00 -> read back {[hex(x) for x in got2]}")
|
||||||
|
if got2 != [0x00]:
|
||||||
|
errors.append(f"T10b command-space rewrite failed: {got2}")
|
||||||
|
|
||||||
# NO T10 FOR THE LATE-RESPONSE LEAK -- and that is deliberate.
|
# NO T10 FOR THE LATE-RESPONSE LEAK -- and that is deliberate.
|
||||||
#
|
#
|
||||||
# The guard added to the response handler (outstanding != 0) and the
|
# The guard added to the response handler (outstanding != 0) and the
|
||||||
|
|||||||
@@ -102,6 +102,43 @@ int bba_devid(u8 id[4])
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* COMMAND-space access: a 2-BYTE header then a data phase -- three bytes
|
||||||
|
* total for a single-byte write, NOT a 4-byte transfer. This is the shape
|
||||||
|
* libogc's own driver uses (gcif.o, __bba_init):
|
||||||
|
*
|
||||||
|
* EXI_Imm (len=2, write) -> [0x40|reg, 0x00] write, or [reg, 0x00] read
|
||||||
|
* EXI_Sync
|
||||||
|
* EXI_ImmEx(len=N, write/read)
|
||||||
|
*
|
||||||
|
* The device-ID query is the same form with reg 0 and a 4-byte read, which is
|
||||||
|
* why bba_devid above looks like this too. */
|
||||||
|
int bba_cmd_write(u8 creg, u8 val)
|
||||||
|
{
|
||||||
|
int rc;
|
||||||
|
u16 h = (u16)(((0x40 | (creg & 0x3F)) << 8));
|
||||||
|
if ((rc = begin()) < 0) return rc;
|
||||||
|
if (EXI_Imm(BBA_EXI_CHAN, &h, 2, EXI_WRITE, NULL) <= 0) { end(); return -3; }
|
||||||
|
if (EXI_Sync(BBA_EXI_CHAN) <= 0) { end(); return -4; }
|
||||||
|
gap();
|
||||||
|
if (EXI_ImmEx(BBA_EXI_CHAN, &val, 1, EXI_WRITE) <= 0) { end(); return -5; }
|
||||||
|
end();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bba_cmd_read(u8 creg)
|
||||||
|
{
|
||||||
|
int rc;
|
||||||
|
u8 v = 0;
|
||||||
|
u16 h = (u16)(((creg & 0x3F) << 8));
|
||||||
|
if ((rc = begin()) < 0) return rc;
|
||||||
|
if (EXI_Imm(BBA_EXI_CHAN, &h, 2, EXI_WRITE, NULL) <= 0) { end(); return -3; }
|
||||||
|
if (EXI_Sync(BBA_EXI_CHAN) <= 0) { end(); return -4; }
|
||||||
|
gap();
|
||||||
|
if (EXI_ImmEx(BBA_EXI_CHAN, &v, 1, EXI_READ) <= 0) { end(); return -5; }
|
||||||
|
end();
|
||||||
|
return (int)v;
|
||||||
|
}
|
||||||
|
|
||||||
int bba_r8(u16 addr)
|
int bba_r8(u16 addr)
|
||||||
{
|
{
|
||||||
u8 v;
|
u8 v;
|
||||||
|
|||||||
@@ -76,6 +76,12 @@ int bba_devid(u8 id[4]);
|
|||||||
int begin_raw(void);
|
int begin_raw(void);
|
||||||
void end_raw(void);
|
void end_raw(void);
|
||||||
|
|
||||||
|
/* COMMAND-space access: 2-byte header + data phase (3 bytes total for a
|
||||||
|
* single-byte write), the shape libogc uses. bba_cmd_read returns the byte
|
||||||
|
* or <0 on error. */
|
||||||
|
int bba_cmd_write(u8 creg, u8 val);
|
||||||
|
int bba_cmd_read(u8 creg);
|
||||||
|
|
||||||
/* Convenience single-byte helpers. bba_r8 returns the byte, or <0 on error. */
|
/* Convenience single-byte helpers. bba_r8 returns the byte, or <0 on error. */
|
||||||
int bba_r8(u16 addr);
|
int bba_r8(u16 addr);
|
||||||
int bba_w8(u16 addr, u8 val);
|
int bba_w8(u16 addr, u8 val);
|
||||||
|
|||||||
@@ -389,6 +389,109 @@ static int t_rx(char *m, size_t n)
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─────────────────── suite 7: libogc init sequence ─────────────────── */
|
||||||
|
/* These replay what libogc's own BBA driver does, in its real transaction
|
||||||
|
* shapes, rather than what our docs said it does. The whole sequence was
|
||||||
|
* read out of /opt/devkitpro/libogc/lib/cube/libbba.a (gcif.o, __bba_init) --
|
||||||
|
* see CLAUDE.md. Two of the mismatches it exposed were silent:
|
||||||
|
* - command space is a 2-byte header + data phase, not a 4-byte transfer,
|
||||||
|
* so every command-space write was being dropped; and
|
||||||
|
* - libogc READS the MAC out of PAR0-5 rather than writing it. */
|
||||||
|
|
||||||
|
static int t_cmd_write(char *m, size_t n)
|
||||||
|
{
|
||||||
|
int rc, rb;
|
||||||
|
/* Command register 2 is libogc's interrupt mask: it writes 0x00
|
||||||
|
* (IRMASKALL) at the start of init and 0xF8 (IRMASKNONE) at the end. */
|
||||||
|
if ((rc = bba_cmd_write(0x02, 0xF8)) < 0) {
|
||||||
|
snprintf(m, n, "cmd write failed: EXI error %d", rc); return 1;
|
||||||
|
}
|
||||||
|
if ((rb = bba_cmd_read(0x02)) < 0) {
|
||||||
|
snprintf(m, n, "cmd read failed: EXI error %d", rb); return 1;
|
||||||
|
}
|
||||||
|
if (rb != 0xF8) {
|
||||||
|
snprintf(m, n, "wrote 0xF8, read back 0x%02X "
|
||||||
|
"(2-byte cmd header not honoured?)", rb);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
/* Put it back the way libogc leaves it at the start of init. */
|
||||||
|
bba_cmd_write(0x02, 0x00);
|
||||||
|
rb = bba_cmd_read(0x02);
|
||||||
|
snprintf(m, n, "0xF8 then 0x00 read back (0x%02X)", rb);
|
||||||
|
return rb == 0x00 ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int t_mac_is_readable(char *m, size_t n)
|
||||||
|
{
|
||||||
|
u8 mac[6];
|
||||||
|
/* libogc does ONE 6-byte register read of 0x0020 and uses whatever it
|
||||||
|
* finds as the console's MAC -- it does not write one. So PAR0-5 must
|
||||||
|
* hold a sane address out of reset, and a 6-byte read must work. */
|
||||||
|
if (bba_read(REG_PAR0, mac, 6) < 0) {
|
||||||
|
snprintf(m, n, "6-byte PAR read failed"); return 1;
|
||||||
|
}
|
||||||
|
snprintf(m, n, "%02X:%02X:%02X:%02X:%02X:%02X%s",
|
||||||
|
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5],
|
||||||
|
(mac[0] == 0x00 && mac[1] == 0x09 && mac[2] == 0xBF)
|
||||||
|
? " (Nintendo OUI)" : " - NOT a Nintendo OUI");
|
||||||
|
if (mac[0] == 0xFF || (mac[0] | mac[1] | mac[2] | mac[3] | mac[4] | mac[5]) == 0)
|
||||||
|
return 1; /* all-ones or all-zero is unusable */
|
||||||
|
return (mac[0] == 0x00 && mac[1] == 0x09 && mac[2] == 0xBF) ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int t_init_sequence(char *m, size_t n)
|
||||||
|
{
|
||||||
|
/* The register writes libogc issues, in order, with the values it uses.
|
||||||
|
* Most are store-only (PHY/analog) and we ignore them; what matters is
|
||||||
|
* that none of them is misdecoded into something harmful -- the old
|
||||||
|
* guessed header format turned a NWAYS read into an NCRA reset, so this
|
||||||
|
* is a real risk, not a theoretical one. */
|
||||||
|
static const struct { u16 a; u8 v; } seq[] = {
|
||||||
|
{0x0060, 0x00},
|
||||||
|
{0x0000, 0x01}, /* NCRA reset */
|
||||||
|
{0x0000, 0x00},
|
||||||
|
{0x0058, 0x80}, {0x0059, 0x00}, {0x005A, 0x03}, {0x005B, 0x83},
|
||||||
|
{0x005C, 0x32}, {0x005D, 0xFE}, {0x005E, 0x1F}, {0x005F, 0x1F},
|
||||||
|
{0x0001, 0x52}, {0x0060, 0x74},
|
||||||
|
{0x0014, 0x00}, {0x0015, 0x06}, {0x0050, 0x80},
|
||||||
|
{0x000C, 0x00}, {0x000D, 0x00}, {0x000A, 0x01}, {0x000B, 0x00},
|
||||||
|
{0x001A, 0x0F}, {0x001B, 0x00}, /* RHBP = page 15 */
|
||||||
|
{0x0016, 0x01}, {0x0017, 0x00}, /* RWP = page 1 */
|
||||||
|
{0x0018, 0x01}, {0x0019, 0x00}, /* RRP = page 1 */
|
||||||
|
{0x0032, 0x08},
|
||||||
|
{0x0000, 0x08}, /* NCRA SR: start receive */
|
||||||
|
{0x0009, 0xFF}, /* IR: clear all */
|
||||||
|
{0x0008, 0xDF}, /* IMR (NOT 0x86) */
|
||||||
|
};
|
||||||
|
unsigned i;
|
||||||
|
int v;
|
||||||
|
for (i = 0; i < sizeof(seq) / sizeof(seq[0]); i++) {
|
||||||
|
if (bba_w8(seq[i].a, seq[i].v) < 0) {
|
||||||
|
snprintf(m, n, "write %u (0x%04X=0x%02X) failed",
|
||||||
|
i, seq[i].a, seq[i].v);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_ncra_reset_done = 1;
|
||||||
|
/* The sentinels must still read correctly afterwards -- if any write in
|
||||||
|
* that list had been misdecoded, the most likely casualty is the state
|
||||||
|
* these two report. */
|
||||||
|
if ((v = bba_r8(REG_NWAYS)) != NWAYS_LINKUP) {
|
||||||
|
snprintf(m, n, "NWAYS 0x%02X after init (want 0x%02X)",
|
||||||
|
v, NWAYS_LINKUP);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if ((v = bba_r8(REG_HIPR)) != HIPR_PRESENT) {
|
||||||
|
snprintf(m, n, "HIPR 0x%02X after init (want 0x%02X)",
|
||||||
|
v, HIPR_PRESENT);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
v = bba_r8(REG_NCRA);
|
||||||
|
snprintf(m, n, "%u writes replayed; NCRA=0x%02X NWAYS/HIPR ok",
|
||||||
|
(unsigned)(sizeof(seq) / sizeof(seq[0])), v);
|
||||||
|
return (v & 0x08) ? 0 : 1; /* SR must still be set */
|
||||||
|
}
|
||||||
|
|
||||||
/* ─────────────────────── suite 6: SPRAM / DMA ──────────────────────── */
|
/* ─────────────────────── suite 6: SPRAM / DMA ──────────────────────── */
|
||||||
|
|
||||||
static int t_dma(char *m, size_t n)
|
static int t_dma(char *m, size_t n)
|
||||||
@@ -518,6 +621,12 @@ static test_t s_spram[] = {
|
|||||||
{"Stream length", "reads >= 0x100 stream until CS, length-independent",t_dma_stream_len, T_PENDING, {0}},
|
{"Stream length", "reads >= 0x100 stream until CS, length-independent",t_dma_stream_len, T_PENDING, {0}},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static test_t s_init[] = {
|
||||||
|
{"cmd-space write","2-byte header + data phase, as libogc sends it", t_cmd_write, T_PENDING, {0}},
|
||||||
|
{"MAC readable", "libogc READS the MAC from PAR0-5; it must be sane", t_mac_is_readable, T_PENDING, {0}},
|
||||||
|
{"init replay", "libogc's 30 init writes, in order, do no harm", t_init_sequence, T_PENDING, {0}},
|
||||||
|
};
|
||||||
|
|
||||||
suite_t g_suites[] = {
|
suite_t g_suites[] = {
|
||||||
{"Identity", "device enumerates and reports itself", s_identity, 3},
|
{"Identity", "device enumerates and reports itself", s_identity, 3},
|
||||||
{"Registers", "register file is readable and writable", s_regs, 4},
|
{"Registers", "register file is readable and writable", s_regs, 4},
|
||||||
@@ -525,6 +634,7 @@ suite_t g_suites[] = {
|
|||||||
{"EXI integrity", "the EXI link is error-free, and how fast", s_exi, 6},
|
{"EXI integrity", "the EXI link is error-free, and how fast", s_exi, 6},
|
||||||
{"Ethernet", "frames get to and from the W5100", s_eth, 3},
|
{"Ethernet", "frames get to and from the W5100", s_eth, 3},
|
||||||
{"SPRAM/DMA", "bulk reads stream correctly from the ring", s_spram, 2},
|
{"SPRAM/DMA", "bulk reads stream correctly from the ring", s_spram, 2},
|
||||||
|
{"libogc init", "the real driver's init sequence and shapes", s_init, 3},
|
||||||
};
|
};
|
||||||
const int g_nsuites = sizeof(g_suites) / sizeof(g_suites[0]);
|
const int g_nsuites = sizeof(g_suites) / sizeof(g_suites[0]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user