Drive MISO from an SB_IO output register
Removing ~2 ns from the fabric MISO path (EXI_J4 merge + the CS mux) did NOT move the hardware result: the GC still returned a byte-identical 0B 80 at 27 MHz while reading correctly at 16 MHz and below. Identical rather than intermittent means the path is well outside the window, not marginally inside it, so trimming fabric delay further was not going to get there. MISO is source-synchronous -- driven on the falling edge, sampled by the GC on the next rising edge -- so the whole path must fit a HALF period, 18.5 ns at 27 MHz, minus global-buffer insertion, pad, connector and the GC's setup. Driving from fabric spent 8.4-11.5 ns of that on routing alone. An SB_IO output register sits in the IO tile: clock-to-pad is the IO flop's own delay with no fabric routing. nextpnr confirms the path left the fabric entirely -- miso_io.D_OUT_0 packs at die (9,0), the pad itself, SB_IO count is unchanged at 24, and no negedge -> <async> path is reported any more. What replaces it is a setup path INTO the IOB register (10.07 ns against a half-period), which is internal and has nothing stacked on top of it. PIN_TYPE was decoded from yosys's own SB_IO model rather than from memory, because wrong bits there produce a silently dead or mistimed output: [5:4]=10 tri-state, combinational OUTPUT_ENABLE [3]=0,[2]=1 dout = dout_q_0, single-rate REGISTERED output [1:0]=01 simple input (unused) NEG_TRIGGER=1 moves the IO registers to the FALLING edge. To keep simulation and silicon agreeing about MISO, the slave exposes miso_next -- the value the shift register WILL hold after the falling edge -- and spi_mode3_slave T6 proves it predicts spi_miso bit for bit. Without that the IOB register would be an untested sim/hardware divergence, which is the exact class of bug that has cost this design the most time. BEHAVIOUR CHANGE: MISO is now TRI-STATED when CS is high, held high by the pad's PULLUP, instead of actively driven high. EXI MISO is a SHARED bus -- SP1 is on channel 0 alongside memory-card slot A -- so driving it while deselected was wrong and could fight a card. --seeds 8: clk 28.5-30.7 MHz, EXI 41.6-49.7 MHz, 8/8 PASS. The EXI numbers fall (62 -> 49) because the posedge->negedge setup path into the IOB register is now charged to that domain at a half-period constraint; 41.6 MHz worst case is still 1.5x the 27 MHz target. Best seed 3 (clk 30.68, EXI 49.14), flashed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -86,6 +86,7 @@ class BBATop(Elaboratable):
|
||||
dst_port=udp_dst_port)
|
||||
|
||||
# EXI (GC side)
|
||||
self.exi_miso_next = Signal(init=1) # for an SB_IO output register
|
||||
self.exi_clk = Signal(init=1)
|
||||
self.exi_mosi = Signal()
|
||||
self.exi_cs_n = Signal(init=1)
|
||||
@@ -188,6 +189,7 @@ class BBATop(Elaboratable):
|
||||
cap.spi_cs_n.eq(self.exi_cs_n),
|
||||
# EXI outputs
|
||||
self.exi_miso.eq(cap.spi_miso),
|
||||
self.exi_miso_next.eq(cap.miso_next),
|
||||
self.int_n .eq(reg.exi_int_n),
|
||||
]
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ class ExiCapture(Elaboratable):
|
||||
self.spi_mosi = Signal()
|
||||
self.spi_cs_n = Signal(init=1)
|
||||
self.spi_miso = Signal()
|
||||
self.miso_next = Signal(init=1) # for an SB_IO output register
|
||||
|
||||
# Core-facing RX read side
|
||||
self.rx_data = Signal(8)
|
||||
@@ -150,6 +151,7 @@ class ExiCapture(Elaboratable):
|
||||
spi.spi_mosi.eq(self.spi_mosi),
|
||||
spi.spi_cs_n.eq(self.spi_cs_n),
|
||||
self.spi_miso.eq(spi.spi_miso),
|
||||
self.miso_next.eq(spi.miso_next),
|
||||
]
|
||||
|
||||
# ── RX: every received byte → rx_fifo ────────────────────────────
|
||||
|
||||
@@ -73,6 +73,12 @@ class SPIMode3Slave(Elaboratable):
|
||||
self.spi_cs_n = Signal(init=1) # active LOW
|
||||
|
||||
self.spi_miso = Signal(init=1)
|
||||
# The SAME bit as spi_miso, but combinational from the PRE-edge state,
|
||||
# so it can feed an SB_IO output register clocked on the falling edge.
|
||||
# Driving MISO from a flop inside the IO block removes the fabric
|
||||
# routing between the shift register and the pad, which is what the
|
||||
# 27 MHz GameCube read fails on -- see the note in synth.py.
|
||||
self.miso_next = Signal(init=1)
|
||||
|
||||
self.rx_byte = Signal(8)
|
||||
self.rx_valid = Signal()
|
||||
@@ -158,6 +164,9 @@ class SPIMode3Slave(Elaboratable):
|
||||
m.d.exiclk_n += tx_shift.eq(Cat(0, tx_shift[:-1]))
|
||||
|
||||
m.d.comb += self.spi_miso.eq(tx_shift[7])
|
||||
# What tx_shift[7] WILL be after the next falling edge. Mirrors the
|
||||
# load/shift choice above exactly; T6 proves the two agree.
|
||||
m.d.comb += self.miso_next.eq(Mux(rx_bit == 0, tx_hold[7], tx_shift[6]))
|
||||
|
||||
return m
|
||||
|
||||
@@ -306,6 +315,28 @@ if __name__ == "__main__":
|
||||
check("T5 rising-edge start, slot1", hex(s1), hex(0xA5))
|
||||
check("T5 rising-edge start, slot2", hex(s2), hex(0x3C))
|
||||
|
||||
# T6 — miso_next must equal what spi_miso BECOMES on the next falling
|
||||
# edge. The synthesised build drives the pad from an SB_IO register
|
||||
# clocked on that edge (synth.py), so if this ever diverges the
|
||||
# hardware and the simulation stop agreeing about MISO -- which is
|
||||
# precisely the class of bug that has cost this design the most time.
|
||||
ctx.set(dut.spi_clk, 1)
|
||||
ctx.set(dut.spi_cs_n, 0)
|
||||
await ctx.delay(HP)
|
||||
ctx.set(dut.tx_byte, 0x6B)
|
||||
mismatch = 0
|
||||
for _ in range(3):
|
||||
for _bit in range(8):
|
||||
predicted = ctx.get(dut.miso_next) # before the falling edge
|
||||
ctx.set(dut.spi_clk, 0)
|
||||
await ctx.delay(HP)
|
||||
if ctx.get(dut.spi_miso) != predicted:
|
||||
mismatch += 1
|
||||
ctx.set(dut.spi_clk, 1)
|
||||
await ctx.delay(HP)
|
||||
await cs_high(ctx)
|
||||
check("T6 miso_next predicts spi_miso", mismatch, 0)
|
||||
|
||||
sim = Simulator(top)
|
||||
sim.add_testbench(tb)
|
||||
with sim.write_vcd("SPIMode3Slave.vcd"):
|
||||
|
||||
+52
-4
@@ -190,7 +190,9 @@ class BBATopSynth(BBATop):
|
||||
m = super().elaborate(platform)
|
||||
|
||||
if platform is not None:
|
||||
exi = platform.request("exi", 0)
|
||||
# MISO is requested RAW so it can be driven by an SB_IO
|
||||
# output register instead of a plain buffer -- see below.
|
||||
exi = platform.request("exi", 0, dir={"miso": "-"})
|
||||
w5100 = platform.request("w5100", 0)
|
||||
|
||||
# The EXI clock pin now clocks the bit engine directly (see
|
||||
@@ -206,11 +208,52 @@ class BBATopSynth(BBATop):
|
||||
# that would miss bits on a console.
|
||||
platform.add_clock_constraint(exi.clk.i, _EXI_HZ)
|
||||
|
||||
# ── MISO driven from a register INSIDE the IO block ───────────
|
||||
# MISO is source-synchronous: the device drives it on the FALLING
|
||||
# edge and the GameCube samples it on the next RISING edge, so the
|
||||
# entire path must fit a HALF period -- 18.5 ns at 27 MHz -- minus
|
||||
# global-buffer insertion, the pad, the connector and the GC's own
|
||||
# setup. Driving it from fabric spends most of that budget on
|
||||
# routing from the shift register to the pad (measured 8.4-11.5 ns
|
||||
# across builds), and the console read every byte ONE BIT LATE at
|
||||
# 27 MHz while reading correctly at 16 MHz and below -- the
|
||||
# textbook signature of exactly this.
|
||||
#
|
||||
# An SB_IO output register sits in the IO tile, so clock-to-pad is
|
||||
# the IO flop's own delay with no fabric routing at all. It
|
||||
# captures `exi_miso_next` -- the value the shift register WILL
|
||||
# hold after the falling edge -- so the bit presented is identical
|
||||
# to the fabric version (spi_mode3_slave T6 proves the two agree
|
||||
# bit for bit).
|
||||
#
|
||||
# PIN_TYPE decoded from yosys's own SB_IO model, not from memory:
|
||||
# [5:4]=10 tri-state, combinational OUTPUT_ENABLE
|
||||
# [3]=0,[2]=1 dout = dout_q_0, single-rate REGISTERED output
|
||||
# [1:0]=01 simple input (unused)
|
||||
# NEG_TRIGGER=1 moves the IO registers to the FALLING edge.
|
||||
#
|
||||
# NOTE this also changes MISO's electrical behaviour: it is now
|
||||
# TRI-STATED when CS is high (held high by the pad's own PULLUP)
|
||||
# instead of being actively driven high. EXI MISO is a SHARED bus
|
||||
# -- SP1 sits on channel 0 alongside memory-card slot A -- so
|
||||
# driving it while deselected was wrong and could fight a card.
|
||||
m.submodules.miso_io = Instance(
|
||||
"SB_IO",
|
||||
p_PIN_TYPE = 0b100101,
|
||||
p_PULLUP = 1,
|
||||
p_NEG_TRIGGER = 1,
|
||||
p_IO_STANDARD = "SB_LVCMOS",
|
||||
io_PACKAGE_PIN = exi.miso.io,
|
||||
i_CLOCK_ENABLE = Const(1, 1),
|
||||
i_OUTPUT_CLK = exi.clk.i,
|
||||
i_OUTPUT_ENABLE = ~exi.cs_n.i,
|
||||
i_D_OUT_0 = self.exi_miso_next,
|
||||
)
|
||||
|
||||
m.d.comb += [
|
||||
self.exi_clk .eq(exi.clk.i),
|
||||
self.exi_mosi .eq(exi.mosi.i),
|
||||
self.exi_cs_n .eq(exi.cs_n.i),
|
||||
exi.miso.o .eq(self.exi_miso),
|
||||
exi.int_n.o .eq(self.int_n),
|
||||
|
||||
# W5100 parallel bus (DATA[7:0] bidirectional via SB_IO)
|
||||
@@ -490,7 +533,10 @@ if __name__ == "__main__":
|
||||
print(f" {'seed':>4} {'clk (≥24)':>10} {f'exi (≥{_EXI_TARGET})':>16}"
|
||||
f" {'MISO clk→pad':>13} verdict")
|
||||
for seed, fc, fexi, verdict, miso in results:
|
||||
ms = f"{miso:.2f} ns" if miso is not None else " n/a"
|
||||
# No negedge->async path means MISO is driven from the SB_IO
|
||||
# output register, so clock-to-pad never enters the fabric at all --
|
||||
# that is the GOOD case, not a parse failure.
|
||||
ms = f"{miso:.2f} ns" if miso is not None else "IOB (none)"
|
||||
print(f" {seed:>4} {fc:>10.2f} {fexi:>16.2f} {ms:>13} {verdict}")
|
||||
n_pass = sum(1 for r in results if r[3] == "PASS")
|
||||
if results:
|
||||
@@ -500,7 +546,9 @@ if __name__ == "__main__":
|
||||
overall = "PASS" if best_fmax >= 1.0 else "FAIL"
|
||||
if best_detail:
|
||||
ms = (f", MISO clk→pad {best_detail[2]:.2f} ns"
|
||||
if best_detail[2] is not None else "")
|
||||
if best_detail[2] is not None
|
||||
else ", MISO driven from the SB_IO output register "
|
||||
"(no fabric clock-to-pad path)")
|
||||
print(f"\nBest seed: {best_seed} clk {best_detail[0]:.2f} MHz, "
|
||||
f"exi {best_detail[1]:.2f} MHz{ms} — {overall}")
|
||||
if best_detail[2] is not None and best_detail[2] > 12.0:
|
||||
|
||||
Reference in New Issue
Block a user