Remove the device-ID one-shot: it made Swiss see no adapter at all

id_phase was set at reset and cleared permanently by the GC's first NCRA reset,
after which a command-space read of register 0 returned 0x00000000.  Swiss
calls EXI_GetID from bba_exists() AND again from bba_requires_init(), long
after init, and matches the result against EXI_MX98730EC_ID -- so with the
one-shot spent no case matched and Swiss reported NO ADAPTER, not a broken one.
Running gc_bba_test was enough to spend it, so the adapter would "disappear"
after any test session.

The justification in the comment was wrong.  It claimed the fast path would
otherwise hijack address 0 so the GC could never read NCRA back -- but NCRA is
REGISTER space (0x80/0xC0 header) while the fast path requires byte 0 == 0x00,
i.e. COMMAND space.  They were never ambiguous, and no driver reads command
register 0 for anything else (libogc2 reads 0x01/0x03/0x0B/0x0F, Swiss 0x03).
Real hardware answers EXI_GetID whenever asked; that is how EXI enumerates
devices.

id_arm is now tied high, and the id_phase output, the FFSynchronizer that
crossed it into the pin domain, and the BBATop wiring are all gone.  The port
stays only so the PHASE_METER build can disarm the fast path, which would
otherwise send ID bytes instead of its constant 0x80.

bba_top T1b reads the device ID AFTER an NCRA reset, which T1 (before any
reset) structurally cannot catch.  Mutation-tested: disarming the fast path
returns 00 00 00 00 for both, exactly what Swiss saw.

gc_bba_test: the ID tests no longer skip.  A wrong ID is a FAILURE again, with
no iface_alive() discrimination and no ordering requirement between suites --
the ID is readable at any point in a run.  g_ncra_reset_done and iface_alive()
are removed outright rather than left as a dead flag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 14:33:12 +02:00
parent 991342c0b5
commit 90a583dd46
6 changed files with 104 additions and 99 deletions
+23 -16
View File
@@ -107,13 +107,6 @@ class BBARegisterFile(Elaboratable):
# (DMA) bulk reads, not just ≤4-byte immediate transfers.
self.cs_active = Signal()
# One-shot: set at reset, cleared by the GC's first NCRA reset. While
# set, a command-space read of register 0 returns the device ID -- but
# the REPLY is produced by SPIMode3Slave's fast path, not here, because
# a 2-byte command header leaves too little time for a round trip
# through both byte FIFOs. This output is what arms that path.
self.id_phase = Signal(init=1)
# ── Ring-overflow protection (RBFI) ───────────────────────────────
# RRP out to RXFrameAssembler so it can tell how much of the ring is
# still unread and DROP frames instead of overwriting them.
@@ -350,9 +343,6 @@ class BBARegisterFile(Elaboratable):
# length for any driver that skips TWD.
txdata_cnt = Signal(16)
# True until first NCRA reset write: return device ID on addr=0 reads
id_phase = self.id_phase
# Per-byte SPRAM read handshake (register-read path): sp_req marks a
# request in flight; drain_ctr counts the read-phase dummy bytes.
sp_req = Signal()
@@ -603,11 +593,29 @@ class BBARegisterFile(Elaboratable):
with m.State("CHDR_1"):
with m.If(self.rx_rdy):
m.d.comb += self.rx_en.eq(1)
# The device-ID query (command reg 0 while id_phase) is
# answered by SPIMode3Slave, not here: pushing anything
# for it would leave stale bytes in the tx FIFO that a
# 2-byte header has too few pops to drain.
with m.If(~is_write & (cmd_reg == 0) & id_phase):
# The device-ID query (command space, read, reg 0) is
# answered by SPIMode3Slave's fast path, not here: pushing
# anything for it would leave stale bytes in the tx FIFO
# that a 2-byte header has too few pops to drain.
#
# ⚠️ NO LONGER GATED ON A ONE-SHOT. This used to be
# `& id_phase`, a bit cleared by the GC's first NCRA
# reset, after which the device stopped identifying
# itself. Real hardware answers EXI_GetID whenever the
# GC asks -- that is how EXI enumerates devices -- and
# Swiss calls it from bba_exists() and again from
# bba_requires_init() long after init. With the one-shot
# spent, EXI_GetID returned 0x00000000 and Swiss reported
# NO ADAPTER AT ALL.
#
# The old justification was that the fast path would
# "hijack address 0 forever so the GC could never read
# NCRA back". That was wrong: NCRA is REGISTER space
# (0x80/0xC0 header) and this path requires byte 0 ==
# 0x00, i.e. COMMAND space. They were never ambiguous,
# and no driver reads command register 0 for anything
# else (libogc2 reads 0x01/0x03/0x0B/0x0F, Swiss 0x03).
with m.If(~is_write & (cmd_reg == 0)):
m.next = "DEVID"
with m.Elif(is_write):
m.next = "CMD_WRITE"
@@ -661,7 +669,6 @@ class BBARegisterFile(Elaboratable):
with m.If(self.rx_data[0]):
m.d.sync += r_ncra[0].eq(0) # RESET self-clears
m.d.sync += ncra_rst_pulse.eq(1)
m.d.sync += id_phase.eq(0)
# An NCRA reset resets the RX ring, so the
# pointers the GC reads MUST follow -- the
# assembler's own write pointer goes back to
+19 -1
View File
@@ -190,7 +190,6 @@ class BBATop(Elaboratable):
# EXI outputs
self.exi_miso.eq(cap.spi_miso),
self.exi_miso_next.eq(cap.miso_next),
cap.id_phase.eq(reg.id_phase),
# Ring-overflow protection: the assembler needs the GC's read
# pointer to know what it must not overwrite, and reports a
# dropped frame back as RBFI (IR[7]).
@@ -626,6 +625,25 @@ if __name__ == "__main__":
await ctx.delay(HP * (2000)) # let W5100 init run
await exi_write(ctx, 0x00, [0x08]) # NCRA SR bit → enable RX
await ctx.delay(HP * (HALF * 2))
# T1b: the device ID STILL ANSWERS after an NCRA reset.
#
# It used to be a one-shot: `id_phase` was cleared by the GC's first
# NCRA reset, after which a command-space read of reg 0 returned
# 0x00000000. Real hardware answers EXI_GetID whenever asked -- that
# is how EXI enumerates devices -- and Swiss calls it from
# bba_exists() AND again from bba_requires_init(), long after init.
# With the one-shot spent, Swiss reported NO ADAPTER AT ALL.
#
# T1 above reads the ID before any reset, so it cannot catch this;
# the reset two lines up is what makes this check meaningful.
dev2 = await exi_devid(ctx)
print(f"T1b device ID after NCRA reset: {[f'0x{b:02X}' for b in dev2]}")
if dev2 != [0x04, 0x02, 0x02, 0x00]:
errors.append(f"T1b device ID after NCRA reset: got {dev2} "
f"- the one-shot is back, Swiss would see no adapter")
await ctx.delay(HP * (HALF * 2))
ctx.set(dut.w5100_int_n, 0) # W5100: a packet was received
await ctx.delay(HP * (4000)) # let the W5100 RX + SPRAM write run
ctx.set(dut.w5100_int_n, 1)
+9 -11
View File
@@ -83,11 +83,6 @@ class ExiCapture(Elaboratable):
self.spi_cs_n = Signal(init=1)
self.spi_miso = Signal()
self.miso_next = Signal(init=1) # for an SB_IO output register
# Register-file id_phase (sync domain). Crossed into exiclk so the
# bit engine can answer the device-ID query itself -- see the fast
# path in SPIMode3Slave.
self.id_phase = Signal(init=1)
# Core-facing RX read side
self.rx_data = Signal(8)
self.rx_rdy = Signal()
@@ -133,12 +128,15 @@ class ExiCapture(Elaboratable):
]
spi = SPIMode3Slave(phase_meter=self._phase_meter)
# id_phase is a slow single bit: it starts set and clears once, on the
# GC's first NCRA reset, which happens inside a register-space
# transaction with the clock running -- so it propagates well before
# the next transaction needs it.
m.submodules.id_sync = FFSynchronizer(self.id_phase, spi.id_arm,
o_domain="exiclk", init=1)
# The device-ID fast path is ARMED PERMANENTLY. It used to be gated
# by the register file's `id_phase`, a one-shot cleared on the GC's
# first NCRA reset and crossed in here with an FFSynchronizer -- so
# after init the device stopped identifying itself and Swiss's
# EXI_GetID returned 0x00000000, i.e. "no adapter". Real hardware
# answers EXI_GetID whenever asked. `id_arm` stays a port because
# the PHASE_METER diagnostic build needs to turn the fast path off
# (it would otherwise send ID bytes instead of the constant 0x80).
m.d.comb += spi.id_arm.eq(0 if self._phase_meter else 1)
m.submodules.spi = spi
# RX bytes cross exiclk -> sync; TX bytes cross sync -> exififo_r.