diff --git a/exi_bba/synth.py b/exi_bba/synth.py index aa1a16e..dbe35df 100644 --- a/exi_bba/synth.py +++ b/exi_bba/synth.py @@ -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. _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): device = "iCE40UP5K" @@ -116,7 +153,16 @@ class IceBreakerPlatform(LatticeICE40Platform): Subsignal("miso", Pins("3", dir="o")), Subsignal("cs_n", Pins("45", dir="i")), 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 Resource("w5100", 0, @@ -176,10 +222,26 @@ class BBATopSynth(BBATop): exi = platform.request("exi", 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 += [ - self.exi_clk .eq(exi.clk.i), - self.exi_mosi .eq(exi.mosi.i), - self.exi_cs_n .eq(exi.cs_n.i), + 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),