WIP rewrite: clock the EXI bit engine from the EXI clock pin

Replaces 54 MHz oversampling with a slave clocked by EXI CLK itself:
  exiclk    posedge  sample MOSI (Mode 3 samples on edge 2)
  exiclk_n  negedge  drive MISO (must settle between the master's samples)
  CS_N      async reset for both
EXI CLK (pin 44) is at die (6,0) = global buffer net 6 on the 5k-sg48, so it
can drive a real clock network with no board change.

This removes the capture domain entirely.  Bit-level timing no longer
crosses a clock boundary -- MISO is clock-to-Q instead of 3 ticks of a foreign
clock -- and the only crossings left are the byte FIFOs, which have a whole
byte-time (~296 ns) of slack rather than a fraction of a bit.

AsyncFIFO needs a posedge clock for its internal reset CDC, so the FIFO sides
use reset-less POSEDGE domains on the same pin; the engine pops on the posedge
and hands the byte to the negedge output stage through a register.  The FIFOs
must not see CS: resetting one side of an AsyncFIFO desynchronises its gray
pointers.

Two bugs found and fixed along the way:
  * one pop per byte boundary is all that is needed -- pops during the header
    drain whatever the previous transaction left (replacing the CS-fall flush)
    and the turnaround-byte push lands in data slot 0.  No slot-threshold or
    header-length logic at all.
  * rx_byte/rx_valid had to become COMBINATIONAL.  Registered, they need one
    more clock edge, and after a transaction's final bit the clock stops and
    CS rises -- so the last byte of every transaction was lost (a 4-byte write
    landed as DE AD BE 00, a 34-byte TX as 15 bytes).

At the HARDWARE rate (EXI_HALF=1, 27 MHz) the device ID, NWAYS, PAR
read/write and the W5100->SPRAM RX path now all pass -- none of which has ever
worked at 27 MHz before.  Testbenches now drive the SPI clock with real
delays, since there is no capture domain to count ticks against.

STILL BROKEN: the SPRAM/DMA stream is offset by one byte (its round trip
exceeds the single turnaround byte), and TX lands 15 of 34 bytes.  Do not
flash this yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 16:47:42 +02:00
parent d95f2147a4
commit a3b3e052bc
4 changed files with 203 additions and 378 deletions
+11 -12
View File
@@ -82,10 +82,10 @@ if __name__ == "__main__":
for bit in range(7, -1, -1):
ctx.set(mosi, (mosi_val >> bit) & 1)
ctx.set(clk, 0) # falling: slave samples
await ctx.tick("capture").repeat(HALF)
await ctx.delay(HP * (HALF))
miso = (miso << 1) | ctx.get(dut.exi_miso)
ctx.set(clk, 1) # rising: slave drives
await ctx.tick("capture").repeat(HALF)
await ctx.delay(HP * (HALF))
return miso
async def exi_read(ctx, pads, addr, length):
@@ -93,14 +93,14 @@ if __name__ == "__main__":
clk, mosi, cs_n = pads
ctx.set(cs_n, 0)
ctx.set(clk, 1)
await ctx.tick("capture").repeat(HALF)
await ctx.delay(HP * (HALF))
await spi_byte(ctx, pads, 0x80)
await spi_byte(ctx, pads, (addr >> 8) & 0xFF)
await spi_byte(ctx, pads, addr & 0xFF)
await spi_byte(ctx, pads, 0x00) # turnaround byte
result = [await spi_byte(ctx, pads, 0x00) for _ in range(length)]
ctx.set(cs_n, 1)
await ctx.tick("capture").repeat(HALF)
await ctx.delay(HP * (HALF))
return result
async def exi_devid(ctx, pads):
@@ -108,16 +108,16 @@ if __name__ == "__main__":
clk, mosi, cs_n = pads
ctx.set(cs_n, 0)
ctx.set(clk, 1)
await ctx.tick("capture").repeat(HALF)
await ctx.delay(HP * (HALF))
await spi_byte(ctx, pads, 0x00)
await spi_byte(ctx, pads, 0x00)
out = [await spi_byte(ctx, pads, 0x00) for _ in range(4)]
ctx.set(cs_n, 1)
await ctx.tick("capture").repeat(HALF)
await ctx.delay(HP * (HALF))
return out
async def testbench(ctx):
await ctx.tick("capture").repeat(20)
await ctx.delay(HP * (20))
# T1: enumerate over the SP1 pads while J4 is unplugged (idle high).
unplug(ctx, top.j4)
@@ -125,7 +125,7 @@ if __name__ == "__main__":
print(f"T1 enumerate via SP1 (J4 idle): {[f'0x{b:02X}' for b in dev]}")
if dev != DEVICE_ID:
errors.append(f"T1 SP1 device ID: got {dev}")
await ctx.tick("capture").repeat(HALF * 4)
await ctx.delay(HP * (HALF * 4))
# T2: enumerate over the J4 pads while SP1 is unplugged (idle high).
unplug(ctx, top.sp1)
@@ -133,7 +133,7 @@ if __name__ == "__main__":
print(f"T2 enumerate via J4 (SP1 idle): {[f'0x{b:02X}' for b in dev]}")
if dev != DEVICE_ID:
errors.append(f"T2 J4 device ID: got {dev}")
await ctx.tick("capture").repeat(HALF * 4)
await ctx.delay(HP * (HALF * 4))
# T3: back to SP1, to prove using J4 left no sticky state behind.
unplug(ctx, top.j4)
@@ -141,7 +141,7 @@ if __name__ == "__main__":
print(f"T3 enumerate via SP1 again: {[f'0x{b:02X}' for b in dev]}")
if dev != DEVICE_ID:
errors.append(f"T3 SP1 re-enumerate: got {dev}")
await ctx.tick("capture").repeat(HALF * 4)
await ctx.delay(HP * (HALF * 4))
# T4: a register read through the merge, not just the ID ROM path.
unplug(ctx, top.sp1)
@@ -164,7 +164,7 @@ if __name__ == "__main__":
):
ctx.set(top.sp1[2], sp1_v)
ctx.set(top.j4[2], j4_v)
await ctx.tick("capture")
await ctx.delay(HP)
got = ctx.get(dut.exi_cs_n)
ok = "ok" if got == want else "MISMATCH"
print(f"T5 cs_n sp1={sp1_v} j4={j4_v} -> core {got} (want {want}) {ok} {note}")
@@ -174,7 +174,6 @@ if __name__ == "__main__":
unplug(ctx, top.j4)
sim = Simulator(top)
sim.add_clock(Period(MHz=54), domain="capture")
sim.add_clock(Period(MHz=24), domain="exi")
sim.add_clock(Period(MHz=24), domain="sync")
sim.add_testbench(testbench)