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 <noreply@anthropic.com>
This commit is contained in:
+10
-3
@@ -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) ─────────────
|
||||
|
||||
Reference in New Issue
Block a user