From da21a080d9dfebd44136b561a1c75a28f2392c2e Mon Sep 17 00:00:00 2001 From: Dennis Brentjes Date: Sat, 3 Oct 2026 16:56:03 +0200 Subject: [PATCH] TX passes at 27 MHz; remaining DMA offset traced to needless exi/sync CDC The TX failure was a TESTBENCH artifact, not a design bug. Between transactions the bench raised CS for one HP -- 18.5 ns at EXI_HALF=1 -- but cs_active reaches the exi domain through a 2-stage FFSynchronizer at 24 MHz, so the register file never saw one transaction end and swallowed the next one's header as write data. A real host leaves microseconds between exi_deselect() and the next exi_select(). With a realistic gap the 34-byte TX frame now matches byte-for-byte at the hardware rate. At EXI_HALF=1 (27 MHz) bba_top now passes device ID, PAR read/write, NWAYS, RWP and TX. The one remaining failure is the SPRAM/DMA stream, offset by exactly one byte. Root cause of that offset, and it is not architectural: bba_top.py:171 does m.d.comb += ClockSignal("sync").eq(ClockSignal("exi")) so exi and sync are LITERALLY THE SAME CLOCK NET. Every AsyncFIFO between them is pure overhead. The SPRAM round trip pays two needless CDC crossings (~6 cycles, ~250 ns) on top of the arbiter and SPRAM access, giving ~375 ns against the ~170 ns the protocol's turnaround byte actually leaves once the rx crossing is subtracted. Register reads are unaffected because their response is immediate; only SPRAM is late. Fix direction: merge the two domains and replace the exi<->sync AsyncFIFOs and PulseSynchronizers with plain SyncFIFOs/signals. That removes the latency, drops a lot of logic, and cannot change behaviour because the clocks are already identical. Known failing, both testing removed behaviour rather than new bugs: * exi_capture's own testbench asserts the CS-fall flush and the old MISO slot timing; the flush no longer exists (header pops drain the FIFO). * exi_j4's T3 re-enumerate is off by a byte. Still WIP -- do not flash. Co-Authored-By: Claude Opus 5 --- exi_bba/bba_top.py | 13 ++++++++++--- exi_bba/exi_capture.py | 6 ++++++ exi_bba/exi_j4.py | 9 +++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/exi_bba/bba_top.py b/exi_bba/bba_top.py index 1e740a1..ca8e363 100644 --- a/exi_bba/bba_top.py +++ b/exi_bba/bba_top.py @@ -424,6 +424,13 @@ if __name__ == "__main__": # delays: one half-period = HALF * (1/54 MHz), preserving the old meaning # of HALF so the rates in the table above still hold. HP = Period(MHz=54) * HALF + # Gap between transactions (CS high). cs_active reaches the exi domain + # through a 2-stage FFSynchronizer at 24 MHz, so CS must stay high for + # ~3 exi cycles or the register file never sees the transaction end and + # swallows the next one's header as data. At EXI_HALF=1 one HP is only + # 18.5 ns, far too short -- a real host leaves microseconds between + # exi_deselect() and the next exi_select(). + CS_GAP = Period(MHz=24) * 6 async def spi_byte(ctx, mosi_val): """Drive one EXI Mode-3 byte; return the assembled MISO byte.""" @@ -457,7 +464,7 @@ if __name__ == "__main__": await ctx.delay(HP * (HALF * 12)) result = [await spi_byte(ctx, 0x00) for _ in range(length)] ctx.set(dut.exi_cs_n, 1) - await ctx.delay(HP * (HALF)) + await ctx.delay(CS_GAP) return result async def exi_write(ctx, addr, data): @@ -472,7 +479,7 @@ if __name__ == "__main__": for b in data: await spi_byte(ctx, b) ctx.set(dut.exi_cs_n, 1) - await ctx.delay(HP * (HALF)) + await ctx.delay(CS_GAP) async def exi_devid(ctx): """EXI_GetID: write 0x0000 (two bytes) then clock four read bytes.""" @@ -484,7 +491,7 @@ if __name__ == "__main__": await ctx.delay(HP * (HALF * 12)) out = [await spi_byte(ctx, 0x00) for _ in range(4)] ctx.set(dut.exi_cs_n, 1) - await ctx.delay(HP * (HALF)) + await ctx.delay(CS_GAP) return out # ── W5100 indirect-bus slave model (drives w5100_data_i) ───────────── diff --git a/exi_bba/exi_capture.py b/exi_bba/exi_capture.py index 2567435..909d8ed 100644 --- a/exi_bba/exi_capture.py +++ b/exi_bba/exi_capture.py @@ -181,6 +181,12 @@ if __name__ == "__main__": # ratio is ~2; use 4 here for a clean, well-oversampled functional check. HALF = 4 + # No capture domain any more: the bit engine is clocked by the EXI + + # pin, so drive the SPI clock with real delays instead of ticks. + + HP = Period(MHz=54) * HALF + async def spi_byte(ctx, mosi_val): """Clock one SPI Mode 3 byte; return the assembled MISO byte.""" miso = 0 diff --git a/exi_bba/exi_j4.py b/exi_bba/exi_j4.py index 923a44b..97e9cc9 100644 --- a/exi_bba/exi_j4.py +++ b/exi_bba/exi_j4.py @@ -42,6 +42,15 @@ if __name__ == "__main__": from exi_bba.bba_top import BBATop HALF = 8 # capture ticks per SPI half-period (well oversampled) + + + # No capture domain any more: the bit engine is clocked by the EXI + + + # pin, so drive the SPI clock with real delays instead of ticks. + + + HP = Period(MHz=54) * HALF DEVICE_ID = [0x04, 0x02, 0x02, 0x00] class _Harness(Elaboratable):