"""Synthesis script for BBATop → iCEbreaker (iCE40UP5K SG48). Run from workspace root: python -m exi_bba.synth # synthesize only python -m exi_bba.synth --flash # synthesize and flash This file re-declares IceBreakerPlatform inline so that importing rebbarb/rebbarb.py (which has a module-level platform.build() call) is avoided. """ import os import subprocess import sys from amaranth import * from amaranth.build import * from amaranth.vendor import LatticeICE40Platform from exi_bba.bba_top import BBATop # ── Platform definition ─────────────────────────────────────────────────── # Pin assignments use the iCEbreaker PMOD connectors as placeholders. # Replace with actual SP1-interposer pin numbers once PCB is finalised. # # PMOD1A (J2): pins 4 2 47 45 / 3 48 46 44 (top/bottom) # PMOD1B (J3): pins 43 38 34 31 / 42 36 32 28 # PMOD2 (J4): pins 27 25 21 19 / 26 23 20 18 # # EXI : CLK=4 MOSI=2 MISO=47 CS_N=45 INT_N=3 (PMOD1A) # W5100 : indirect parallel bus — 15 pins across PMOD1B + PMOD2. # ADDR[1:0]=43 38 DATA[7:0]=34 31 42 36 32 28 27 25 # CS_N=21 RD_N=19 WR_N=26 INT_N=23 RST_N=20 (pin 18 free) # Board: tie the W5100's upper address lines A[14:2] to 0 (only A[1:0] wired); # DATA[7:0] is bidirectional (SB_IO tristate, single shared output-enable). class IceBreakerPlatform(LatticeICE40Platform): device = "iCE40UP5K" package = "SG48" default_clk = "clk12" resources = [ Resource("clk12", 0, Pins("35", dir="i"), Clock(12e6), Attrs(GLOBAL=True, IO_STANDARD="SB_LVCMOS")), # EXI interface (GC side, SPI Mode 3) — PMOD1A FPGA pins Resource("exi", 0, Subsignal("clk", Pins("4", dir="i")), Subsignal("mosi", Pins("2", dir="i")), Subsignal("miso", Pins("47", dir="o")), Subsignal("cs_n", Pins("45", dir="i")), Subsignal("int_n", Pins("3", dir="o")), Attrs(IO_STANDARD="SB_LVCMOS")), # W5100 indirect parallel bus — PMOD1B + PMOD2 FPGA pins Resource("w5100", 0, Subsignal("addr", Pins("43 38", dir="o")), Subsignal("data", Pins("34 31 42 36 32 28 27 25", dir="io")), Subsignal("cs_n", Pins("21", dir="o")), Subsignal("rd_n", Pins("19", dir="o")), Subsignal("wr_n", Pins("26", dir="o")), Subsignal("int_n", Pins("23", dir="i")), Subsignal("rst_n", Pins("20", dir="o")), Attrs(IO_STANDARD="SB_LVCMOS")), # Bring-up status panel → iCEbreaker ONBOARD parts (dedicated pins, not # on any PMOD, so they coexist with EXI + W5100). LEDR/LEDG are # active-low discrete LEDs; BTN_N is the user button. # (The onboard RGB LED on pins 39/40/41 needs an SB_RGBA_DRV instance # wired to raw pads — board/version-specific — left as a future add-on # to expose rx/tx/ready as colours; the 2 discrete LEDs cover bring-up.) Resource("ledr", 0, Pins("11", dir="o"), Attrs(IO_STANDARD="SB_LVCMOS")), Resource("ledg", 0, Pins("37", dir="o"), Attrs(IO_STANDARD="SB_LVCMOS")), Resource("btn", 0, Pins("10", dir="i"), Attrs(IO_STANDARD="SB_LVCMOS")), ] connectors = [] def toolchain_program(self, products, name): iceprog = os.environ.get("ICEPROG", "iceprog") with products.extract(f"{name}.bin") as bitstream_filename: subprocess.check_call([iceprog, bitstream_filename]) # ── BBATop with platform resource wiring ───────────────────────────────── class BBATopSynth(BBATop): """BBATop with platform pin connections added in elaborate().""" def elaborate(self, platform): m = super().elaborate(platform) if platform is not None: exi = platform.request("exi", 0) w5100 = platform.request("w5100", 0) 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) w5100.addr.o .eq(self.w5100_addr), w5100.data.o .eq(self.w5100_data_o), w5100.data.oe .eq(self.w5100_data_oe), self.w5100_data_i.eq(w5100.data.i), w5100.cs_n.o .eq(self.w5100_cs_n), w5100.rd_n.o .eq(self.w5100_rd_n), w5100.wr_n.o .eq(self.w5100_wr_n), self.w5100_int_n .eq(w5100.int_n.i), w5100.rst_n.o .eq(self.w5100_rst_n), ] # ── Bring-up status panel → onboard LEDs / button ────────────── # Two discrete LEDs answer the #1 bring-up question on a real GC: # LEDG = heartbeat (clock alive) LEDR = EXI activity (GC talking) # The one onboard button → panel btn[1] (manual re-init). if self._status_panel: ledr = platform.request("ledr", 0) ledg = platform.request("ledg", 0) btn = platform.request("btn", 0) led = self.panel_led m.d.comb += [ ledg.o.eq(~led[0]), # heartbeat (active-low LED) ledr.o.eq(~led[1]), # EXI activity (active-low LED) # btn[0]/[2] held released (active-low idle = 1) self.panel_btn.eq(Cat(C(1, 1), btn.i, C(1, 1))), ] return m # ── Entry point ─────────────────────────────────────────────────────────── # # Seed sweep: nextpnr placement is stochastic. With ~22% LC utilisation # routing dominates timing, so different seeds can vary fmax by ±20%. # Pass --seeds N to try N seeds (default 1, i.e. seed 1 only). # The build directory is reused across seeds; the final artefact in # build/top.bin is the result of the last (or best) seed tried. if __name__ == "__main__": do_flash = "--flash" in sys.argv n_seeds = next((int(sys.argv[i+1]) for i, a in enumerate(sys.argv) if a == "--seeds"), 1) platform = IceBreakerPlatform() print(f"Synthesizing BBATop for {platform.device}-{platform.package} " f"(do_program={do_flash}, seeds=1..{n_seeds})") best_seed = 1 best_fmax = 0.0 for seed in range(1, n_seeds + 1): print(f"\n{'='*60}") print(f" Seed {seed}/{n_seeds}") print(f"{'='*60}") opts = (f"--opt-timing --seed {seed} --timing-allow-fail") try: platform.build(BBATopSynth(status_panel=True), do_program=False, verbose=True, nextpnr_opts=opts) except Exception as exc: # nextpnr exits non-zero even with --timing-allow-fail on some # versions; treat as non-fatal timing failure. print(f" [seed {seed}] build exception (timing?): {exc}") # Parse fmax from nextpnr log in build/top.tim (if present) import glob, re tim_files = glob.glob("build/top.tim") + glob.glob("build/*.tim") fmax_exi = 0.0 for tf in tim_files: try: with open(tf) as f: for line in f: m_ = re.search( r"Max frequency.*exi.*?:\s*([\d.]+)\s*MHz", line) if m_: fmax_exi = float(m_.group(1)) except OSError: pass print(f" [seed {seed}] exi fmax extracted: {fmax_exi:.1f} MHz") if fmax_exi > best_fmax: best_fmax = fmax_exi best_seed = seed print(f"\nBest seed: {best_seed} exi fmax: {best_fmax:.1f} MHz") if do_flash: print(f"\nFlashing with seed {best_seed}...") opts = f"--opt-timing --seed {best_seed} --timing-allow-fail" platform.build(BBATopSynth(status_panel=True), do_program=True, verbose=True, nextpnr_opts=opts) print("Done.")