Files

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.")