Support BOTH command-space forms: Swiss's 4-byte and libogc's 2-byte
Reading Swiss's own driver (cube/patches/bba/bba.c) shows the previous commit
fixed libogc and BROKE Swiss. The two disagree, and both are real:
Swiss bba_cmd_out8: exi_imm_read_write(0x40<<24 | reg<<24 | val, 4)
-> ONE 4-byte transfer [0x40|reg, 00, 00, val], and bba_cmd_in8
returns the LOW byte, so a read response must be in BYTE 3.
libogc2 bba_cmd_outsnosel: req = (reg<<8)|0x4000;
EXI_ImmEx(&req, 2, WRITE); EXI_ImmEx(val, len, WRITE)
-> THREE bytes, value in byte 2, and a read takes byte 2.
stock libogc: identical to libogc2 (read out of libbba.a).
They share bytes 0 and 1 and differ only in how many bytes follow, so one
decoder serves both: take the LAST data byte before CS rise as the write value
(Swiss sends [00, val], libogc sends [val] -- last is val either way), and push
a read response TWICE so bytes 2 and 3 both carry it. Swiss clocks 4 bytes and
pops both; libogc clocks 3 and leaves one, which the next transaction's header
pops drain -- a 2-byte header gives exactly one drain slot, so the leftover is
bounded at one and cannot accumulate.
Critically this means NO auto-increment of cmd_reg across the data phase: that
applied Swiss's padding 00 to the register and val to reg+1, which is precisely
how the libogc-only version broke Swiss.
T12 covers the Swiss form and asserts bytes 2 AND 3; T10 keeps the libogc form.
Both mutation-tested: re-adding the auto-increment fails T12 with 0x00 in both
slots, and pushing the response once fails T12's byte 3. gc_bba_test gains
"cmd-space Swiss" alongside "cmd-space write", with bba_cmd_write4/read4.
Also corrects two things in CLAUDE.md now checkable against three drivers:
- NO driver writes TWD -- not Swiss, not libogc, not libogc2. The "GC sets
TWD before streaming" note was self-confirming: t_tx writes TWD because we
wrote t_tx. The counted TXDATA length is the primary path, not a fallback.
- Swiss's exi_dma_write rounds the length UP to 32 bytes (OSRoundUp32B), so
TXDATA gets up to 31 trailing bytes past the frame and a counted length sends
that much extra. Real hardware must do the same since Swiss works on it;
trailing padding past the IP total length is ignored. A 1514-byte frame
rounds to 1536 though, over the 1518 maximum.
Both drivers trigger TX with NCRA ST1, which the existing any-ST test accepts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+104
-32
@@ -257,6 +257,8 @@ class BBARegisterFile(Elaboratable):
|
||||
cmd_space = Signal() # this transaction is a command access
|
||||
cmd_reg = Signal(6)
|
||||
cmd_wreg = Signal(6) # target of a pending command-space write
|
||||
cmd_have = Signal() # at least one data byte seen this txn
|
||||
cmd_pushes = Signal(2) # command-read responses pushed
|
||||
byte_ctr = Signal(3) # response-byte index (device-ID sequence)
|
||||
cmd_wr = Signal() # command-space write pending
|
||||
prefetched = Signal() # first response byte already pushed
|
||||
@@ -378,7 +380,8 @@ class BBARegisterFile(Elaboratable):
|
||||
# PREVIOUS transaction would be pushed as this
|
||||
# one's first response and shift the whole
|
||||
# read by one.
|
||||
sp_pending.eq(0)]
|
||||
sp_pending.eq(0),
|
||||
cmd_have.eq(0), cmd_pushes.eq(0)]
|
||||
with m.If(self.rx_data[7]): # register space
|
||||
m.d.sync += cmd_space.eq(0)
|
||||
m.next = "RHDR_HI"
|
||||
@@ -448,25 +451,39 @@ class BBARegisterFile(Elaboratable):
|
||||
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)]
|
||||
|
||||
# ── command space: a 2-BYTE header, then a data phase ─────────
|
||||
# ── command space: 2-byte header, then a data phase ───────────
|
||||
#
|
||||
# [0x40|reg, 0x00] + N data bytes written, or
|
||||
# [reg, 0x00] + N data bytes read.
|
||||
# ⚠️ THE TWO REAL DRIVERS DISAGREE ON THE LENGTH, and both must
|
||||
# work. Checked against the source, not guessed:
|
||||
#
|
||||
# ⚠️ 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:
|
||||
# Swiss (cube/patches/bba/bba.c, bba_cmd_out8 / bba_cmd_in8):
|
||||
# exi_imm_read_write(0x40<<24 | reg<<24 | val, 4)
|
||||
# -> ONE 4-byte transfer [0x40|reg, 00, 00, val], and a read
|
||||
# takes the LOW byte of the 32-bit result, so the response
|
||||
# must land in BYTE 3.
|
||||
#
|
||||
# EXI_Imm (len=2, write) -> 42 00 the whole header
|
||||
# EXI_Sync
|
||||
# EXI_ImmEx(len=1, write) -> 00 the data
|
||||
# libogc (libbba.a, gcif.o, __bba_init):
|
||||
# EXI_Imm (len=2, write) -> 42 00 the whole header
|
||||
# EXI_ImmEx(len=1, write) -> 00 the data
|
||||
# -> THREE bytes, data in byte 2, and a read takes byte 2.
|
||||
#
|
||||
# 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.
|
||||
# Both share bytes 0 and 1, so one decoder serves both:
|
||||
#
|
||||
# WRITE: latch every data byte and apply the LAST one at CS
|
||||
# rise. Swiss sends [00, val] so the last is val;
|
||||
# libogc sends [val] so the last is val. Hence NO
|
||||
# auto-increment of cmd_reg -- that would apply Swiss's
|
||||
# padding 00 to reg and val to reg+1.
|
||||
# READ : push the response TWICE so byte 2 AND byte 3 carry it.
|
||||
# Swiss clocks 4 bytes and pops both; libogc clocks 3 and
|
||||
# leaves one, which the next transaction's header pops
|
||||
# drain. A 2-byte header gives exactly one drain slot,
|
||||
# so the leftover is bounded at one and cannot build up.
|
||||
#
|
||||
# This file previously had only the 4-byte form (Swiss's), and the
|
||||
# decoder waited for a fourth byte -- so libogc's writes were
|
||||
# dropped. Changing it to libogc's form alone then broke Swiss.
|
||||
# Neither driver is "the" driver; support both.
|
||||
with m.State("CHDR_1"):
|
||||
with m.If(self.rx_rdy):
|
||||
m.d.comb += self.rx_en.eq(1)
|
||||
@@ -479,29 +496,37 @@ class BBARegisterFile(Elaboratable):
|
||||
with m.Elif(is_write):
|
||||
m.next = "CMD_WRITE"
|
||||
with m.Else():
|
||||
# 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):
|
||||
# Ordinary command read: push during header byte 1 so the
|
||||
# byte-boundary pop presents it in data slot 0.
|
||||
m.next = "CMD_READ"
|
||||
with m.Elif(~is_write & self.tx_rdy & (cmd_pushes == 0)):
|
||||
# Push #1, during header byte 1, so the byte-boundary pop
|
||||
# presents it in byte 2 -- what libogc reads.
|
||||
m.d.comb += [self.tx_data.eq(rd_val), self.tx_en.eq(1)]
|
||||
m.d.sync += prefetched.eq(1)
|
||||
m.d.sync += cmd_pushes.eq(1)
|
||||
|
||||
with m.State("CMD_READ"):
|
||||
# Push #2 lands in byte 3 -- what Swiss reads. It has a whole
|
||||
# byte-time to get there, unlike push #1.
|
||||
with m.If(self.tx_rdy & (cmd_pushes < 2)):
|
||||
m.d.comb += [self.tx_data.eq(rd_val), self.tx_en.eq(1)]
|
||||
m.d.sync += cmd_pushes.eq(cmd_pushes + 1)
|
||||
with m.If(self.rx_rdy):
|
||||
m.d.comb += self.rx_en.eq(1) # Swiss's padding bytes
|
||||
with m.If(~self.cs_active):
|
||||
m.next = "HEADER0"
|
||||
|
||||
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.
|
||||
# Data phase. Latch each byte; the LAST one before CS rise is
|
||||
# the value, which is what lets Swiss's 4-byte form and
|
||||
# libogc's 3-byte form both decode correctly.
|
||||
with m.If(self.rx_rdy):
|
||||
m.d.comb += self.rx_en.eq(1)
|
||||
m.d.sync += [cmd_wr.eq(1),
|
||||
cmd_wdata.eq(self.rx_data),
|
||||
m.d.sync += [cmd_wdata.eq(self.rx_data),
|
||||
cmd_wreg.eq(cmd_reg),
|
||||
cmd_reg.eq(cmd_reg + 1)]
|
||||
cmd_have.eq(1)]
|
||||
with m.If(~self.cs_active):
|
||||
m.next = "TRAILER"
|
||||
with m.If(cmd_have):
|
||||
m.d.sync += cmd_wr.eq(1)
|
||||
m.next = "HEADER0"
|
||||
|
||||
with m.State("TRAILER"):
|
||||
# Soak up anything else the GC clocks, then wait for CS to rise.
|
||||
@@ -882,6 +907,32 @@ if __name__ == "__main__":
|
||||
await ctx.tick("sync").repeat(6)
|
||||
return result
|
||||
|
||||
async def cmd_write_swiss(ctx, creg, val):
|
||||
"""Swiss's command write: ONE 4-byte transfer
|
||||
[0x40|reg, 0x00, 0x00, val] -- exi_imm_read_write(..., 4)."""
|
||||
ctx.set(reg.cs_active, 1)
|
||||
await push_rx(ctx, 0x40 | (creg & 0x3F))
|
||||
await push_rx(ctx, 0x00)
|
||||
await push_rx(ctx, 0x00) # padding, NOT the value
|
||||
await push_rx(ctx, val) # the value is the LAST byte
|
||||
ctx.set(reg.cs_active, 0)
|
||||
await ctx.tick("sync").repeat(6)
|
||||
|
||||
async def cmd_read_swiss(ctx, creg):
|
||||
"""Swiss's command read: a 4-byte read/write whose LOW byte (byte 3)
|
||||
is the response. Returns (byte2, byte3)."""
|
||||
tx_log.clear()
|
||||
ctx.set(reg.cs_active, 1)
|
||||
await push_rx(ctx, creg & 0x3F)
|
||||
for _ in range(4):
|
||||
sample_tx(ctx)
|
||||
await ctx.tick("sync")
|
||||
await push_rx(ctx, 0x00)
|
||||
got = await collect_tx(ctx, 2)
|
||||
ctx.set(reg.cs_active, 0)
|
||||
await ctx.tick("sync").repeat(6)
|
||||
return got
|
||||
|
||||
# SPRAM contents the streaming-read test reads back (byte i = 0xA0+i).
|
||||
spram_mem = {0x100 + i: (0xA0 + i) & 0xFF for i in range(64)}
|
||||
|
||||
@@ -1146,6 +1197,27 @@ if __name__ == "__main__":
|
||||
ctx.set(reg.tx_irq_i, 0)
|
||||
await ctx.tick("sync").repeat(2)
|
||||
|
||||
# ── T12: Swiss's command-space form, which DIFFERS from libogc's ──
|
||||
# Swiss sends ONE 4-byte transfer with the value as byte 3 and reads
|
||||
# the response from byte 3; libogc sends three bytes with the value as
|
||||
# byte 2 and reads byte 2. Both are real drivers, so both must work:
|
||||
# taking the LAST data byte as the value serves both, and pushing the
|
||||
# read response twice puts it in byte 2 AND byte 3.
|
||||
#
|
||||
# Supporting only one of them is not a theoretical risk -- this file
|
||||
# had Swiss's form only (libogc's writes were dropped), then briefly
|
||||
# had libogc's form only (Swiss's writes would have been dropped).
|
||||
await cmd_write_swiss(ctx, 0x02, 0x5A)
|
||||
both = await cmd_read_swiss(ctx, 0x02)
|
||||
print(f"T12 Swiss 4-byte cmd write 0x5A -> bytes 2,3 = "
|
||||
f"{[hex(x) for x in both]}")
|
||||
if len(both) < 2 or both[1] != 0x5A:
|
||||
errors.append(f"T12 Swiss cmd form: byte 3 = "
|
||||
f"{[hex(x) for x in both]}, want 0x5A")
|
||||
if len(both) >= 1 and both[0] != 0x5A:
|
||||
errors.append(f"T12 libogc byte-2 slot also wrong: {hex(both[0])}")
|
||||
await cmd_write_swiss(ctx, 0x02, 0x00) # restore
|
||||
|
||||
frame11 = [0x40 + i for i in range(24)]
|
||||
await exi_write(ctx, _TXDATA, frame11) # the frame, in ONE write
|
||||
await exi_write(ctx, _NCRA, [0x04]) # ST trigger, no TWD
|
||||
|
||||
Reference in New Issue
Block a user