219 lines
9.3 KiB
Python
219 lines
9.3 KiB
Python
"""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 ──────────────
|
|
# All 5 panel LEDs mapped:
|
|
# LEDG (pin 37) = led[0] heartbeat
|
|
# LEDR (pin 11) = led[1] EXI activity
|
|
# RGB (pins 39/40/41) = led[2] rx / led[3] tx / led[4] ready
|
|
# 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))),
|
|
]
|
|
|
|
# iCEbreaker RGB LED has no series resistors — must use
|
|
# SB_RGBA_DRV (raw pad driver with built-in current source).
|
|
# RGB0=red→rx_act RGB1=green→tx_act RGB2=blue→ready
|
|
# Verify colour-to-element mapping against schematic at bring-up.
|
|
m.submodules.rgb_drv = Instance("SB_RGBA_DRV",
|
|
p_CURRENT_MODE="0b1",
|
|
p_RGB0_CURRENT="0b000001",
|
|
p_RGB1_CURRENT="0b000001",
|
|
p_RGB2_CURRENT="0b000001",
|
|
i_CURREN=Const(1, 1),
|
|
i_RGBLEDEN=Const(1, 1),
|
|
i_RGB0PWM=led[2],
|
|
i_RGB1PWM=led[3],
|
|
i_RGB2PWM=led[4],
|
|
o_RGB0=Signal(name="rgb_r"),
|
|
o_RGB1=Signal(name="rgb_g"),
|
|
o_RGB2=Signal(name="rgb_b"),
|
|
)
|
|
|
|
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.")
|