TX: count the TXDATA bytes, because libogc never writes TWD
Third contract checked against libogc rather than our notes. __bba_link_tx is four transactions -- read NWAYS, ONE register-space write of the whole frame to 0x0048, read NCRA, write NCRA = 0x04 -- with no TWD_LO/TWD_HI anywhere. So the device has to count the bytes it was handed. The NCRA ST fallback queued tx_frame_len, which only TWD sets, so any driver that skips TWD would have transmitted a stale or zero length. BBARegisterFile now keeps txdata_cnt, incremented per byte accepted at 0x0048 and cleared whenever a frame is queued from either path, and the ST path queues that. T11 drives TX in libogc's shape with no TWD write and checks both the queued length and the drained bytes. Mutation-tested: switching the ST path back to tx_frame_len queues length 0, which is exactly the bug. gc_bba_test gains the hardware counterpart, "TX without TWD", which deliberately omits the TWD writes that t_tx makes. The TWD path stays and is still preferred when present: writing the length before streaming is what lets TXFrameDrain consume concurrently and keeps tx_bytes a rate-matching buffer rather than whole-frame storage. Swiss writes TWD, libogc does not, so both are supported. Also recorded: libogc streams the payload with EXI_Dma (32-byte-aligned bulk after PPCDCacheStoreAsync, then EXI_ImmEx for the remainder). CLAUDE.md said EXI DMA writes are avoided for the GC's 1-bit-shift bug; that is true of Swiss, not libogc. Nothing in the gateware changes -- a DMA write is just bytes clocked in -- but the path will be exercised, and if that bug is real it appears as corrupted TX frames that are not fixable here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -271,6 +271,17 @@ class BBARegisterFile(Elaboratable):
|
||||
# that the drain already has a length for this frame.
|
||||
twd_armed = Signal()
|
||||
tx_queued = Signal()
|
||||
# Bytes actually streamed into TXDATA since the last trigger.
|
||||
#
|
||||
# ⚠️ Needed because libogc DOES NOT WRITE TWD AT ALL. Its
|
||||
# __bba_link_tx is four transactions -- read NWAYS, one register-space
|
||||
# write of the whole frame to 0x0048 (EXI_Dma for the 32-byte-aligned
|
||||
# bulk plus EXI_ImmEx for the remainder), read NCRA, write NCRA = 0x04
|
||||
# -- with no TWD_LO/TWD_HI anywhere. The real device must therefore
|
||||
# count what it was given, which is what this does. Using
|
||||
# tx_frame_len on the ST fallback path would transmit a stale or zero
|
||||
# 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
|
||||
@@ -534,9 +545,13 @@ class BBARegisterFile(Elaboratable):
|
||||
# and we never double-queue one frame.
|
||||
with m.If(self.rx_data[1:3].any() & ~tx_queued):
|
||||
with m.If(tx_ctrl.w_rdy):
|
||||
m.d.sync += tx_ctrl.w_data.eq(tx_frame_len)
|
||||
# The COUNTED length, not tx_frame_len:
|
||||
# a driver reaching this path never wrote
|
||||
# TWD, so tx_frame_len is stale or zero.
|
||||
m.d.sync += tx_ctrl.w_data.eq(txdata_cnt)
|
||||
m.d.sync += tx_ctrl.w_en.eq(1)
|
||||
m.d.sync += tx_queued.eq(1)
|
||||
m.d.sync += txdata_cnt.eq(0)
|
||||
with m.Case(_IMR):
|
||||
m.d.sync += r_imr.eq(self.rx_data)
|
||||
with m.Case(_IR):
|
||||
@@ -567,6 +582,7 @@ class BBARegisterFile(Elaboratable):
|
||||
with m.If(tx_bytes.w_rdy):
|
||||
m.d.sync += tx_bytes.w_data.eq(self.rx_data)
|
||||
m.d.sync += tx_bytes.w_en.eq(1)
|
||||
m.d.sync += txdata_cnt.eq(txdata_cnt + 1)
|
||||
# All other addresses silently ignored
|
||||
|
||||
# No length field in the real protocol: consume data
|
||||
@@ -709,6 +725,7 @@ class BBARegisterFile(Elaboratable):
|
||||
# tx_ctrl has room so a full control FIFO cannot drop the trigger.
|
||||
with m.If(twd_armed & tx_ctrl.w_rdy):
|
||||
m.d.sync += tx_ctrl.w_data.eq(tx_frame_len)
|
||||
m.d.sync += txdata_cnt.eq(0)
|
||||
m.d.sync += tx_ctrl.w_en.eq(1)
|
||||
m.d.sync += twd_armed.eq(0)
|
||||
m.d.sync += tx_queued.eq(1)
|
||||
@@ -1111,6 +1128,38 @@ if __name__ == "__main__":
|
||||
if got2 != [0x00]:
|
||||
errors.append(f"T10b command-space rewrite failed: {got2}")
|
||||
|
||||
# ── T11: TX in libogc's shape -- NO TWD WRITE AT ALL ─────────────
|
||||
# __bba_link_tx is four transactions: read NWAYS, ONE register-space
|
||||
# write of the whole frame to 0x0048, read NCRA, write NCRA = 0x04.
|
||||
# There is no TWD_LO/TWD_HI anywhere, so the device has to count what
|
||||
# it was handed. The ST fallback used to queue tx_frame_len, which a
|
||||
# driver that skips TWD never sets -- so the frame would go out with a
|
||||
# stale or zero length.
|
||||
await drain_tx_bytes(ctx) # clear anything pending
|
||||
while ctx.get(reg.tx_ctrl_r_rdy): # and any queued length
|
||||
ctx.set(reg.tx_ctrl_r_en, 1)
|
||||
await ctx.tick("sync")
|
||||
ctx.set(reg.tx_ctrl_r_en, 0)
|
||||
await ctx.tick("sync")
|
||||
ctx.set(reg.tx_irq_i, 1) # TX done -> clears tx_queued
|
||||
await ctx.tick("sync")
|
||||
ctx.set(reg.tx_irq_i, 0)
|
||||
await ctx.tick("sync").repeat(2)
|
||||
|
||||
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
|
||||
qlen = ctx.get(reg.tx_ctrl_r_data) if ctx.get(reg.tx_ctrl_r_rdy) else None
|
||||
body = await drain_tx_bytes(ctx)
|
||||
print(f"T11 libogc-shape TX (no TWD): queued len={qlen} "
|
||||
f"bytes={len(body)}")
|
||||
if qlen != len(frame11):
|
||||
errors.append(f"T11 queued length {qlen}, want {len(frame11)} "
|
||||
f"(ST path must use the COUNTED length)")
|
||||
if body != frame11:
|
||||
errors.append(f"T11 frame bytes wrong: {len(body)} of "
|
||||
f"{len(frame11)}")
|
||||
|
||||
# NO T10 FOR THE LATE-RESPONSE LEAK -- and that is deliberate.
|
||||
#
|
||||
# The guard added to the response handler (outstanding != 0) and the
|
||||
|
||||
Reference in New Issue
Block a user