synth: optional EXI mirror on the J4 debug header (EXI_J4=1)

Lets a bench host drive EXI without the SP1 edge connector, for bring-up.

Merges a second set of EXI pads onto the existing ones rather than repinning,
so the SP1 connector stays live and the default build is untouched.  The three
inputs are AND-merged, which is correct because every EXI input is inactive
HIGH (CPOL=1 clk, active-low cs_n, mosi only matters while cs_n is low): the
unplugged side idles at 1 through its SB_IO PULLUP and passes the other side
through, so no select signal is needed.  Outputs drive both pads.

All five J4 pads are on the same bottom die edge as the EXI pads (x=13..18 vs
x=5..9), so the capture domain keeps its anchor.  It still adds 3 LUTs ahead of
the input synchronisers, so EXI_J4 builds must sweep seeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 17:54:24 +00:00
parent fff16a85c4
commit c0abd45518
+66 -4
View File
@@ -97,6 +97,43 @@ _UART_TX, _UART_RX = ("9", "10") if os.environ.get("UART_J4") else ("19", "18")
# full UART_J4 design. Default = 28 (real board). See BRINGUP.md. # full UART_J4 design. Default = 28 (real board). See BRINGUP.md.
_ETH_D3 = os.environ.get("ETH_D3_PIN", "28") _ETH_D3 = os.environ.get("ETH_D3_PIN", "28")
# EXI bring-up on the J4 debug header (set env EXI_J4=1). Rather than REPINNING
# the EXI resource onto J4 -- which moves where the capture domain anchors on the
# die -- this MERGES a second set of EXI pads onto the existing ones, so the SP1
# edge connector stays live and the normal build is one env var away.
#
# J4.1 = GND
# J4.2 = DBG0 = FPGA pin 9 -> EXI CLK (in)
# J4.3 = DBG1 = FPGA pin 10 -> EXI MOSI (in)
# J4.4 = DBG2 = FPGA pin 11 -> EXI MISO (out)
# J4.5 = DBG3 = FPGA pin 12 -> EXI CS_N (in)
# J4.6 = DBG4 = FPGA pin 6 -> EXI INT_N (out)
#
# The three INPUTS are merged with AND. That is correct precisely because every
# EXI input is idle/inactive HIGH: CPOL=1 so CLK idles high, CS_N is active-low,
# and MOSI only matters while CS_N is low. Whichever side is unplugged sits at 1
# through its SB_IO PULLUP and passes the other side through untouched, so no
# select signal is needed. Both pad sets therefore get PULLUP=1 in this build.
# DRIVE ONE SIDE AT A TIME -- a GameCube on SP1 and a host on J4 at the same time
# will fight, and the AND will hand the capture engine the mangled product.
# The two OUTPUTS are simply driven onto both pads (fanout 2, no added logic).
#
# Timing: all five J4 pads are on the SAME (bottom) die edge as the EXI pads
# (x=13..18 vs x=5..9, both y=0), so the capture domain keeps its bottom-edge
# anchor -- this does not recreate the cross-die span described in CLAUDE.md.
# It still adds 3 LUTs ahead of the input synchronisers and a second MISO load,
# and capture has little margin, so ALWAYS sweep seeds on an EXI_J4 build.
# COLLIDES WITH: UART_J4 (pins 9/10) and ETH_D3_PIN if pointed at a J4 pad.
_EXI_J4 = bool(os.environ.get("EXI_J4"))
if _EXI_J4 and os.environ.get("UART_J4"):
raise SystemExit("EXI_J4 and UART_J4 both claim J4 pins 9/10 - pick one")
if _EXI_J4 and _ETH_D3 in ("9", "10", "11", "12", "6"):
raise SystemExit(f"EXI_J4 conflicts with ETH_D3_PIN={_ETH_D3} on the J4 header")
# With the merge active the unplugged side must idle high rather than float.
_EXI_ATTRS = (Attrs(IO_STANDARD="SB_LVCMOS", PULLUP=1) if _EXI_J4
else Attrs(IO_STANDARD="SB_LVCMOS"))
class IceBreakerPlatform(LatticeICE40Platform): class IceBreakerPlatform(LatticeICE40Platform):
device = "iCE40UP5K" device = "iCE40UP5K"
@@ -116,7 +153,16 @@ class IceBreakerPlatform(LatticeICE40Platform):
Subsignal("miso", Pins("3", dir="o")), Subsignal("miso", Pins("3", dir="o")),
Subsignal("cs_n", Pins("45", dir="i")), Subsignal("cs_n", Pins("45", dir="i")),
Subsignal("int_n", Pins("46", dir="o")), Subsignal("int_n", Pins("46", dir="o")),
Attrs(IO_STANDARD="SB_LVCMOS")), _EXI_ATTRS),
# EXI mirror on the J4 debug header (EXI_J4=1 only) -- see note above.
*([Resource("exi_j4", 0,
Subsignal("clk", Pins("9", dir="i")),
Subsignal("mosi", Pins("10", dir="i")),
Subsignal("miso", Pins("11", dir="o")),
Subsignal("cs_n", Pins("12", dir="i")),
Subsignal("int_n", Pins("6", dir="o")),
_EXI_ATTRS)] if _EXI_J4 else []),
# W5100 indirect parallel bus # W5100 indirect parallel bus
Resource("w5100", 0, Resource("w5100", 0,
@@ -176,10 +222,26 @@ class BBATopSynth(BBATop):
exi = platform.request("exi", 0) exi = platform.request("exi", 0)
w5100 = platform.request("w5100", 0) w5100 = platform.request("w5100", 0)
# EXI inputs: optionally AND-merged with the J4 header mirror so a
# bench host can drive EXI without the SP1 edge connector. AND is
# valid because all three inputs are inactive-HIGH and the unused
# side is held there by its SB_IO PULLUP -- see the _EXI_J4 note.
if _EXI_J4:
j4 = platform.request("exi_j4", 0)
exi_clk_i = exi.clk.i & j4.clk.i
exi_mosi_i = exi.mosi.i & j4.mosi.i
exi_cs_n_i = exi.cs_n.i & j4.cs_n.i
m.d.comb += [
j4.miso.o .eq(self.exi_miso),
j4.int_n.o .eq(self.int_n),
]
else:
exi_clk_i, exi_mosi_i, exi_cs_n_i = exi.clk.i, exi.mosi.i, exi.cs_n.i
m.d.comb += [ m.d.comb += [
self.exi_clk .eq(exi.clk.i), self.exi_clk .eq(exi_clk_i),
self.exi_mosi .eq(exi.mosi.i), self.exi_mosi .eq(exi_mosi_i),
self.exi_cs_n .eq(exi.cs_n.i), self.exi_cs_n .eq(exi_cs_n_i),
exi.miso.o .eq(self.exi_miso), exi.miso.o .eq(self.exi_miso),
exi.int_n.o .eq(self.int_n), exi.int_n.o .eq(self.int_n),