diff --git a/CLAUDE.md b/CLAUDE.md index 0139dad..34415e4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -305,11 +305,36 @@ What has been ruled out: clearing `sp_pending` in `HEADER0` fixed a real latent bug but left the ratio unchanged. -⚠️ **THE MISSING TEST:** a `bba_top`-level test that performs an EXI DMA read -from the SPRAM ring **while** the ethernet side writes frames into it. ETH -preempts EXI reads, so only under that overlap can a read response be delayed -past the end of its transaction. No simulation does this, which is why the -offset is still unexplained. Write that before guessing again. +✅ **REPRODUCED IN SIMULATION 2026-10-09** — `bba_top` **T9** performs EXI DMA +reads from the ring **while** the ethernet side writes frames into it, which no +earlier test did (T6 waits for the frame to finish first, so reads and writes +never overlapped). + +``` +EXI_HALF=8 T9 concurrent DMA reads: 6/6 byte-exact +EXI_HALF=2 T9 concurrent DMA reads: 6/6 byte-exact +EXI_HALF=1 T9 concurrent DMA reads: 2/6 byte-exact <-- the hardware rate +``` + +2-of-6 good matches the console ratio closely (`desc@+0=100 +1=343`, 23% good), +and the corruption is exactly the hardware symptom — **one extra byte inserted +at the front**, shifting everything: + +``` +expected: 00 00 00 0C DE AD BE EF +got: 00 00 00 00 0C DE AD BE +``` + +An INSERTED byte means an extra push into the tx FIFO, not a corrupted one, so +the fault is a count mismatch somewhere in the request/response path — not the +SPRAM data. It only appears at 27 MHz because the read must finish inside the +protocol's turnaround window while ETH (which has absolute arbiter priority) is +preempting it. + +⚠️ **`bba_top` therefore FAILS at `EXI_HALF=1` until this is fixed.** That is +deliberate: the failure is real and the test is the reproduction. Do not +"fix" it by relaxing T9. `EXI_HALF=8` and `EXI_HALF=2` still pass, so use +those while working on unrelated changes. ### ⚠️ Simulation rate — always re-run at the hardware rate diff --git a/exi_bba/bba_top.py b/exi_bba/bba_top.py index dcc83d2..6a14e1c 100644 --- a/exi_bba/bba_top.py +++ b/exi_bba/bba_top.py @@ -644,6 +644,45 @@ if __name__ == "__main__": errors.append(f"T7 panel: EXI-activity LED not lit (led=0b{leds:05b})") print(f"T7 panel led=0b{leds:05b} (bit1=EXI activity, expect 1)") + # ── T9: EXI DMA READ **WHILE** THE ETHERNET SIDE WRITES THE RING ── + # This is the condition hardware actually runs in and that no test has + # ever modelled: T6 above waits for the frame to finish before reading, + # so reads and writes never overlap. + # + # ETH has ABSOLUTE priority in SPRAMArbiter -- eth_wr_ready is asserted + # combinationally whenever eth_wr_valid is high -- so an EXI read is + # preempted whenever a frame byte is being written. If a read request + # is lost, or a response is delayed past the end of its transaction, + # the GC's byte stream shifts. + # + # On console the ring descriptor lands at page offset +1 instead of +0 + # for ~77% of frames (desc@+0=100 +1=343), varying frame to frame, and + # netboot only works because its reader probes both offsets. Swiss + # does not probe: it would read a frame length of 1 and the RX path + # would be broken for real networking. So this test exists to + # reproduce that, not merely to add coverage. + # + # Page 1 still holds T6's completed frame, so its expected contents are + # known exactly while a SECOND frame is being written behind it. + ctx.set(dut.w5100_int_n, 0) # another frame arriving + mismatches = 0 + first_bad = None + for attempt in range(6): + again = await exi_read(ctx, 0x0100, total_len) + if again != want: + mismatches += 1 + if first_bad is None: + first_bad = again + ctx.set(dut.w5100_int_n, 1) + await ctx.delay(HP * (HALF * 4)) + + print(f"T9 concurrent DMA reads: {6 - mismatches}/6 byte-exact") + if mismatches: + print(f"T9 first bad read: {[f'0x{b:02X}' for b in first_bad[:8]]}") + print(f"T9 expected : {[f'0x{b:02X}' for b in want[:8]]}") + errors.append(f"T9 DMA read corrupted by concurrent ETH writes " + f"({mismatches}/6 reads wrong)") + # T8: GC → wire TX of a frame LONGER than the 16-deep tx_bytes FIFO. # This is the case that shipped broken: TXFrameDrain used to wait for # the length until the NCRA ST write, i.e. until after the GC had