From 0f6447565e9c88b417581886a173ae8a329d5d67 Mon Sep 17 00:00:00 2001 From: Tristan Ross Date: Wed, 1 Apr 2026 01:17:33 -0700 Subject: [PATCH] feat: use nextpnr viaduct instead of python chipdb --- examples/blinky/blinky.pcf | 6 + examples/blinky/default.nix | 9 +- examples/blinky/place_io.py | 19 + flake.nix | 12 + ip/bin/aegis_genip.dart | 18 - ip/lib/aegis_ip.dart | 1 - ip/lib/src/nextpnr.dart | 1 - ip/lib/src/nextpnr/chipdb_emitter.dart | 642 ------------------------- nextpnr-aegis/aegis.cc | 376 +++++++++++++++ pkgs/aegis-ip/default.nix | 13 +- 10 files changed, 425 insertions(+), 672 deletions(-) create mode 100644 examples/blinky/blinky.pcf create mode 100644 examples/blinky/place_io.py delete mode 100644 ip/lib/src/nextpnr.dart delete mode 100644 ip/lib/src/nextpnr/chipdb_emitter.dart create mode 100644 nextpnr-aegis/aegis.cc diff --git a/examples/blinky/blinky.pcf b/examples/blinky/blinky.pcf new file mode 100644 index 0000000..75ed2d0 --- /dev/null +++ b/examples/blinky/blinky.pcf @@ -0,0 +1,6 @@ +# Pin constraints for blinky on Aegis +# Format: set_io +# IO BELs are named X/Y/IO on the grid edges +set_io clk X0/Y1/IO0 +set_io reset X0/Y2/IO0 +set_io led X0/Y3/IO0 diff --git a/examples/blinky/default.nix b/examples/blinky/default.nix index 6727d9c..3536b22 100644 --- a/examples/blinky/default.nix +++ b/examples/blinky/default.nix @@ -21,7 +21,11 @@ stdenvNoCC.mkDerivation { src = lib.fileset.toSource { root = ./.; - fileset = ./blinky.v; + fileset = lib.fileset.unions [ + ./blinky.v + ./blinky.pcf + ./place_io.py + ]; }; nativeBuildInputs = [ @@ -45,8 +49,9 @@ stdenvNoCC.mkDerivation { yosys -c synth.tcl > yosys.log 2>&1 || { cat yosys.log; exit 1; } echo "=== Place and route ===" - nextpnr-aegis-${deviceName} \ + PCF_FILE=blinky.pcf nextpnr-aegis-${deviceName} \ --json blinky_pnr.json \ + --pre-place place_io.py \ --write blinky_routed.json \ > nextpnr.log 2>&1 || { cat nextpnr.log; echo "nextpnr finished (may have warnings)"; } diff --git a/examples/blinky/place_io.py b/examples/blinky/place_io.py new file mode 100644 index 0000000..33a7fbd --- /dev/null +++ b/examples/blinky/place_io.py @@ -0,0 +1,19 @@ +# Apply PCF-style IO constraints for Aegis +# Reads blinky.pcf and constrains IO cells to specific BELs + +import os + +pcf_file = os.environ.get("PCF_FILE", "blinky.pcf") + +with open(pcf_file) as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + parts = line.split() + if len(parts) == 3 and parts[0] == "set_io": + signal, bel = parts[1], parts[2] + for cname, cell in ctx.cells: + if cname == signal: + cell.setAttr("BEL", bel) + break diff --git a/flake.nix b/flake.nix index ac04784..15b3a31 100644 --- a/flake.nix +++ b/flake.nix @@ -61,6 +61,7 @@ treefmt.programs = { black.enable = true; + clang-format.enable = true; dart-format.enable = true; jsonfmt.enable = true; nixfmt.enable = true; @@ -73,6 +74,17 @@ flakever = flakeverConfig; aegis-ip-tools = pkgs.callPackage ./pkgs/aegis-ip-tools { }; aegis-pack = pkgs.callPackage ./pkgs/aegis-pack { inherit craneLib; }; + nextpnr-aegis = pkgs.nextpnr.overrideAttrs (old: { + pname = "nextpnr-aegis"; + postPatch = (old.postPatch or "") + '' + # Add Aegis viaduct uarch + mkdir -p generic/viaduct/aegis + cp ${./nextpnr-aegis/aegis.cc} generic/viaduct/aegis/aegis.cc + + # Register in CMakeLists.txt + sed -i '/viaduct\/example\/example.cc/a\ viaduct/aegis/aegis.cc' generic/CMakeLists.txt + ''; + }); gf180mcu-pdk = pkgs.callPackage ./pkgs/gf180mcu-pdk { }; sky130-pdk = pkgs.callPackage ./pkgs/sky130-pdk { }; }; diff --git a/ip/bin/aegis_genip.dart b/ip/bin/aegis_genip.dart index da1ef8f..badb92f 100644 --- a/ip/bin/aegis_genip.dart +++ b/ip/bin/aegis_genip.dart @@ -202,23 +202,6 @@ Future main(List arguments) async { File('$outputDir/${fpga.name}_bram.rules').writeAsStringSync(bramRules); } - final chipdbEmitter = ChipdbEmitter( - deviceName: fpga.name, - width: width, - height: height, - tracks: tracks, - serdesCount: serdesCount, - clockTileCount: clockTiles, - bramColumnInterval: bramInterval, - dspColumnInterval: dspInterval, - ); - File( - '$outputDir/${fpga.name}-chipdb.py', - ).writeAsStringSync(chipdbEmitter.generate()); - File( - '$outputDir/${fpga.name}-packer.py', - ).writeAsStringSync(chipdbEmitter.generatePacker()); - final openroadEmitter = OpenroadTclEmitter( moduleName: 'AegisFPGA', width: width, @@ -240,7 +223,6 @@ Future main(List arguments) async { print(' $outputDir/${fpga.name}_cells.v'); print(' $outputDir/${fpga.name}_techmap.v'); print(' $outputDir/${fpga.name}-synth-aegis.tcl'); - print(' $outputDir/${fpga.name}-chipdb.py'); print(' $outputDir/${fpga.name}-openroad.tcl'); } on FormatException catch (e) { print(e.message); diff --git a/ip/lib/aegis_ip.dart b/ip/lib/aegis_ip.dart index 8769483..0d57420 100644 --- a/ip/lib/aegis_ip.dart +++ b/ip/lib/aegis_ip.dart @@ -1,6 +1,5 @@ export 'src/components.dart'; export 'src/config.dart'; -export 'src/nextpnr.dart'; export 'src/openroad.dart'; export 'src/pdk.dart'; export 'src/sim.dart'; diff --git a/ip/lib/src/nextpnr.dart b/ip/lib/src/nextpnr.dart deleted file mode 100644 index c41620c..0000000 --- a/ip/lib/src/nextpnr.dart +++ /dev/null @@ -1 +0,0 @@ -export 'nextpnr/chipdb_emitter.dart'; diff --git a/ip/lib/src/nextpnr/chipdb_emitter.dart b/ip/lib/src/nextpnr/chipdb_emitter.dart deleted file mode 100644 index acea696..0000000 --- a/ip/lib/src/nextpnr/chipdb_emitter.dart +++ /dev/null @@ -1,642 +0,0 @@ -/// Emits a nextpnr-generic Python chipdb script for the Aegis FPGA. -/// -/// The generated Python script defines BELs, wires, and pips that -/// describe the Aegis routing architecture to nextpnr-generic. -/// -/// Tile input mux sources (3-bit sel): -/// 0=north, 1=east, 2=south, 3=west, 4=clbOut, 5=const0, 6=const1 -/// -/// Output route mux sources (3-bit sel): -/// 0=north, 1=east, 2=south, 3=west, 4=clbOut -class ChipdbEmitter { - final String deviceName; - final int width; - final int height; - final int tracks; - final int serdesCount; - final int clockTileCount; - final int bramColumnInterval; - final int bramDataWidth; - final int bramAddrWidth; - final int dspColumnInterval; - - /// Total I/O pads: 2*width + 2*height. - int get totalPads => 2 * width + 2 * height; - - const ChipdbEmitter({ - required this.deviceName, - required this.width, - required this.height, - required this.tracks, - required this.serdesCount, - required this.clockTileCount, - this.bramColumnInterval = 0, - this.bramDataWidth = 8, - this.bramAddrWidth = 7, - this.dspColumnInterval = 0, - }); - - /// Set of BRAM column indices. - Set get _bramColumns { - if (bramColumnInterval <= 0) return {}; - final cols = {}; - for (int x = bramColumnInterval; x < width; x += bramColumnInterval + 1) { - cols.add(x); - } - return cols; - } - - Set get _dspColumns { - if (dspColumnInterval <= 0) return {}; - final bram = _bramColumns; - final cols = {}; - for (int x = dspColumnInterval; x < width; x += dspColumnInterval + 1) { - if (!bram.contains(x)) cols.add(x); - } - return cols; - } - - /// Generates a pre-pack Python script that converts generic Yosys cells - /// (`$lut`, `$_DFF_P_`) into Aegis cell types (`AEGIS_LUT4`, `AEGIS_DFF`) - /// so they match the BEL types in the chipdb. - String generatePacker() { - final buf = StringBuffer(); - - buf.writeln('# Auto-generated Aegis pre-pack script for $deviceName'); - buf.writeln( - '# Converts generic Yosys cells to Aegis BEL-compatible types.', - ); - buf.writeln(); - buf.writeln('for cname, cell in ctx.cells:'); - buf.writeln(' if cell.type == "\$lut":'); - buf.writeln(' cell.type = "AEGIS_LUT4"'); - buf.writeln(' # Rename ports: A[0]..A[3] -> in0..in3, Y -> out'); - buf.writeln(' cell.renamePort("A[0]", "in0")'); - buf.writeln(' cell.renamePort("A[1]", "in1")'); - buf.writeln(' cell.renamePort("A[2]", "in2")'); - buf.writeln(' cell.renamePort("A[3]", "in3")'); - buf.writeln(' cell.renamePort("Y", "out")'); - buf.writeln(); - buf.writeln(' elif cell.type == "\$_DFF_P_":'); - buf.writeln(' cell.type = "AEGIS_DFF"'); - buf.writeln(' cell.renamePort("C", "clk")'); - buf.writeln(' cell.renamePort("D", "d")'); - buf.writeln(' cell.renamePort("Q", "q")'); - buf.writeln(); - - return buf.toString(); - } - - /// Generates the complete nextpnr-generic chipdb Python script. - String generate() { - final buf = StringBuffer(); - - _writeHeader(buf); - _writeHelpers(buf); - _writeWires(buf); - _writeBels(buf); - _writePips(buf); - _writeIoBels(buf); - _writeConfig(buf); - - return buf.toString(); - } - - void _writeHeader(StringBuffer buf) { - buf.writeln('# Auto-generated nextpnr-generic chipdb for $deviceName'); - buf.writeln('# Fabric: ${width}x$height, $tracks tracks per edge'); - buf.writeln('# SerDes: $serdesCount, Clock tiles: $clockTileCount'); - if (bramColumnInterval > 0) { - buf.writeln( - '# BRAM: every $bramColumnInterval columns, ' - '${bramDataWidth}x${1 << bramAddrWidth}', - ); - } - buf.writeln(); - } - - void _writeHelpers(StringBuffer buf) { - buf.writeln('from itertools import product'); - buf.writeln(); - buf.writeln('W = $width # fabric columns'); - buf.writeln('H = $height # fabric rows'); - buf.writeln('T = $tracks # routing tracks per edge'); - buf.writeln(); - buf.writeln('# Grid includes IO ring: logic at (1..W, 1..H),'); - buf.writeln('# IO pads at x=0,W+1,0,H+1'); - buf.writeln('GW = W + 2'); - buf.writeln('GH = H + 2'); - buf.writeln(); - buf.writeln('BRAM_COLS = ${_bramColumns.toList()..sort()}'); - buf.writeln('DSP_COLS = ${_dspColumns.toList()..sort()}'); - buf.writeln(); - buf.writeln('def wire_name(x, y, name):'); - buf.writeln(' return f"X{x}/Y{y}/{name}"'); - buf.writeln(); - buf.writeln('def bel_name(x, y, name):'); - buf.writeln(' return f"X{x}/Y{y}/{name}"'); - buf.writeln(); - } - - void _writeWires(StringBuffer buf) { - buf.writeln( - '# ================================================================', - ); - buf.writeln('# Wires'); - buf.writeln( - '# ================================================================', - ); - buf.writeln(); - buf.writeln('for x, y in product(range(1, W + 1), range(1, H + 1)):'); - buf.writeln(' # CLB wires'); - buf.writeln(' for i in range(4):'); - buf.writeln( - ' ctx.addWire(wire_name(x, y, f"CLB_I{i}"), ' - '"CLB_INPUT",x,y)', - ); - buf.writeln( - ' ctx.addWire(wire_name(x, y, "CLB_O"), ' - '"CLB_OUTPUT",x,y)', - ); - buf.writeln( - ' ctx.addWire(wire_name(x, y, "CLB_Q"), ' - '"CLB_FF_OUT",x,y)', - ); - buf.writeln( - ' ctx.addWire(wire_name(x, y, "CARRY_IN"), ' - '"CARRY",x,y)', - ); - buf.writeln( - ' ctx.addWire(wire_name(x, y, "CARRY_OUT"), ' - '"CARRY",x,y)', - ); - buf.writeln(); - buf.writeln(' # Directional routing track wires'); - buf.writeln(' for d in ["N", "E", "S", "W"]:'); - buf.writeln(' for t in range(T):'); - buf.writeln( - ' ctx.addWire(' - 'wire_name(x, y, f"{d}{t}"), ' - '"ROUTING",x,y)', - ); - buf.writeln(); - buf.writeln(' # Clock wire'); - buf.writeln( - ' ctx.addWire(wire_name(x, y, "CLK"), ' - '"CLOCK",x,y)', - ); - buf.writeln(); - - // IO pad wires - buf.writeln('# IO pad wires'); - buf.writeln('for i in range($totalPads):'); - buf.writeln(' ctx.addWire(f"IO{i}/PAD","IO_PAD",0,0)'); - buf.writeln(' ctx.addWire(f"IO{i}/I","IO_INPUT",0,0)'); - buf.writeln(' ctx.addWire(f"IO{i}/O","IO_OUTPUT",0,0)'); - buf.writeln(' ctx.addWire(f"IO{i}/OE","IO_OE",0,0)'); - buf.writeln(); - - // Global clock wire - buf.writeln('ctx.addWire("GLB_CLK","GLOBAL_CLOCK",0,0)'); - buf.writeln(); - } - - void _writeBels(StringBuffer buf) { - buf.writeln( - '# ================================================================', - ); - buf.writeln('# BELs'); - buf.writeln( - '# ================================================================', - ); - buf.writeln(); - - // LUT + DFF in each tile (skip BRAM and DSP columns) - buf.writeln('for x, y in product(range(1, W + 1), range(1, H + 1)):'); - buf.writeln(' if x - 1 in BRAM_COLS or x - 1 in DSP_COLS:'); - buf.writeln(' continue'); - buf.writeln(); - buf.writeln(' # LUT4'); - buf.writeln(' lut_name = bel_name(x, y, "LUT4")'); - buf.writeln( - ' ctx.addBel(lut_name, "AEGIS_LUT4", Loc(x, y, 0), False, False)', - ); - buf.writeln( - ' ctx.addBelInput(lut_name, "in0", wire_name(x, y, "CLB_I0"))', - ); - buf.writeln( - ' ctx.addBelInput(lut_name, "in1", wire_name(x, y, "CLB_I1"))', - ); - buf.writeln( - ' ctx.addBelInput(lut_name, "in2", wire_name(x, y, "CLB_I2"))', - ); - buf.writeln( - ' ctx.addBelInput(lut_name, "in3", wire_name(x, y, "CLB_I3"))', - ); - buf.writeln( - ' ctx.addBelOutput(lut_name, "out", wire_name(x, y, "CLB_O"))', - ); - buf.writeln(); - - // DFF - buf.writeln(' # DFF'); - buf.writeln(' dff_name = bel_name(x, y, "DFF")'); - buf.writeln( - ' ctx.addBel(dff_name, "AEGIS_DFF", Loc(x, y, 1), False, False)', - ); - buf.writeln(' ctx.addBelInput(dff_name, "d", wire_name(x, y, "CLB_O"))'); - buf.writeln(' ctx.addBelInput(dff_name, "clk", wire_name(x, y, "CLK"))'); - buf.writeln( - ' ctx.addBelOutput(dff_name, "q", wire_name(x, y, "CLB_Q"))', - ); - buf.writeln(); - - // Carry - buf.writeln(' # Carry'); - buf.writeln(' carry_name = bel_name(x, y, "CARRY")'); - buf.writeln( - ' ctx.addBel(carry_name, "AEGIS_CARRY", Loc(x, y, 2), False, False)', - ); - buf.writeln( - ' ctx.addBelInput(carry_name, "p", wire_name(x, y, "CLB_O"))', - ); - buf.writeln( - ' ctx.addBelInput(carry_name, "g", wire_name(x, y, "CLB_I0"))', - ); - buf.writeln( - ' ctx.addBelInput(carry_name, "ci", wire_name(x, y, "CARRY_IN"))', - ); - buf.writeln( - ' ctx.addBelOutput(carry_name, "co", wire_name(x, y, "CARRY_OUT"))', - ); - buf.writeln(); - - // BRAM BELs - if (bramColumnInterval > 0) { - buf.writeln('# BRAM BELs'); - buf.writeln('for x, y in product(range(1, W + 1), range(1, H + 1)):'); - buf.writeln(' if x - 1 not in BRAM_COLS:'); - buf.writeln(' continue'); - buf.writeln(); - buf.writeln(' bn = bel_name(x, y, "BRAM")'); - buf.writeln( - ' ctx.addBel(bn, "AEGIS_BRAM", Loc(x, y, 0), False, False)', - ); - buf.writeln(' ctx.addBelInput(bn, "clk", wire_name(x, y, "CLK"))'); - // Clamp BRAM pins to available tracks - final effAddr = bramAddrWidth < tracks ? bramAddrWidth : tracks - 1; - final effData = (bramAddrWidth + bramDataWidth) < tracks - ? bramDataWidth - : tracks - effAddr - 1; - final hasWe = (effAddr + effData) < tracks; - for (final port in ['a', 'b']) { - final dir = port == 'a' ? 'N' : 'W'; - final outDir = port == 'a' ? 'S' : 'E'; - for (int i = 0; i < effAddr; i++) { - buf.writeln( - ' ctx.addBelInput(bn, "${port}_addr[$i]", wire_name(x, y, "$dir$i"))', - ); - } - for (int i = 0; i < effData; i++) { - final trackIdx = effAddr + i; - buf.writeln( - ' ctx.addBelInput(bn, "${port}_wdata[$i]", wire_name(x, y, "$dir$trackIdx"))', - ); - } - if (hasWe) { - buf.writeln( - ' ctx.addBelInput(bn, "${port}_we", wire_name(x, y, "$dir${effAddr + effData}"))', - ); - } - for (int i = 0; i < effData && i < tracks; i++) { - buf.writeln( - ' ctx.addBelOutput(bn, "${port}_rdata[$i]", wire_name(x, y, "$outDir$i"))', - ); - } - } - buf.writeln(); - } - - // DSP BELs - if (dspColumnInterval > 0) { - buf.writeln('# DSP BELs'); - buf.writeln('for x, y in product(range(1, W + 1), range(1, H + 1)):'); - buf.writeln(' if x - 1 not in DSP_COLS:'); - buf.writeln(' continue'); - buf.writeln(); - buf.writeln(' dn = bel_name(x, y, "DSP")'); - buf.writeln( - ' ctx.addBel(dn, "AEGIS_DSP", Loc(x, y, 0), False, False)', - ); - buf.writeln(' ctx.addBelInput(dn, "clk", wire_name(x, y, "CLK"))'); - for (int i = 0; i < 18 && i < tracks; i++) { - buf.writeln(' ctx.addBelInput(dn, "a[$i]", wire_name(x, y, "N$i"))'); - } - for (int i = 0; i < 18 && i < tracks; i++) { - buf.writeln(' ctx.addBelInput(dn, "b[$i]", wire_name(x, y, "W$i"))'); - } - for (int i = 0; i < 36 && i < tracks; i++) { - buf.writeln( - ' ctx.addBelOutput(dn, "result[$i]", wire_name(x, y, "S$i"))', - ); - } - buf.writeln(); - } - } - - void _writePips(StringBuffer buf) { - buf.writeln( - '# ================================================================', - ); - buf.writeln('# Pips'); - buf.writeln( - '# ================================================================', - ); - buf.writeln(); - - // CLB input mux pips: routing tracks → CLB inputs - buf.writeln('PIP_DELAY = 0.1'); - buf.writeln(); - buf.writeln('for x, y in product(range(1, W + 1), range(1, H + 1)):'); - buf.writeln(' if x - 1 in BRAM_COLS or x - 1 in DSP_COLS:'); - buf.writeln(' continue'); - buf.writeln(); - buf.writeln( - ' # CLB input mux: each input can come from ' - 'N0/E0/S0/W0/feedback/const', - ); - buf.writeln(' for i in range(4):'); - buf.writeln(' clb_in = wire_name(x, y, f"CLB_I{i}")'); - buf.writeln(' # From each directional track 0'); - buf.writeln( - ' for d, idx in [("N", 0), ("E", 1), ' - '("S", 2), ("W", 3)]:', - ); - buf.writeln(' src = wire_name(x, y, f"{d}0")'); - buf.writeln( - ' ctx.addPip(f"X{x}/Y{y}/MUX_I{i}_{d}", ' - '"CLB_MUX",src,clb_in, ' - 'ctx.getDelayFromNS(PIP_DELAY), Loc(x, y, 0))', - ); - buf.writeln(' # Feedback from CLB output'); - buf.writeln( - ' ctx.addPip(f"X{x}/Y{y}/MUX_I{i}_FB", ' - '"CLB_MUX",wire_name(x, y, "CLB_O"), ' - 'clb_in, ' - 'ctx.getDelayFromNS(PIP_DELAY), Loc(x, y, 0))', - ); - buf.writeln(' # Feedback from DFF output'); - buf.writeln( - ' ctx.addPip(f"X{x}/Y{y}/MUX_I{i}_Q", ' - '"CLB_MUX",wire_name(x, y, "CLB_Q"), ' - 'clb_in, ' - 'ctx.getDelayFromNS(PIP_DELAY), Loc(x, y, 0))', - ); - buf.writeln(); - - // Output route pips: CLB output / routing → directional output tracks - buf.writeln( - ' # Output route mux: each direction can source from ' - 'N/E/S/W tracks or CLB output', - ); - buf.writeln(' for d_out in ["N", "E", "S", "W"]:'); - buf.writeln(' for t in range(T):'); - buf.writeln(' dst = wire_name(x, y, f"{d_out}{t}")'); - buf.writeln(' # From CLB output'); - buf.writeln( - ' ctx.addPip(' - 'f"X{x}/Y{y}/RT_{d_out}{t}_CLB", ' - '"ROUTE_MUX",wire_name(x, y, "CLB_O"), ' - 'dst, ' - 'ctx.getDelayFromNS(PIP_DELAY), Loc(x, y, 0))', - ); - buf.writeln(' # From DFF output'); - buf.writeln( - ' ctx.addPip(' - 'f"X{x}/Y{y}/RT_{d_out}{t}_Q", ' - '"ROUTE_MUX",wire_name(x, y, "CLB_Q"), ' - 'dst, ' - 'ctx.getDelayFromNS(PIP_DELAY), Loc(x, y, 0))', - ); - buf.writeln(); - - // Inter-tile routing pips - buf.writeln( - ' # Inter-tile routing: connect output tracks to ' - 'neighboring tile input tracks', - ); - buf.writeln(' for t in range(T):'); - buf.writeln(' # North output → neighbor north input'); - buf.writeln(' if y > 1:'); - buf.writeln( - ' ctx.addPip(' - 'f"X{x}/Y{y}/NORTH{t}_UP","INTER_TILE", ' - 'wire_name(x, y, f"N{t}"), ' - 'wire_name(x, y - 1, f"S{t}"), ' - 'ctx.getDelayFromNS(PIP_DELAY * 2), Loc(x, y, 0))', - ); - buf.writeln(' # South output → neighbor south input'); - buf.writeln(' if y < H:'); - buf.writeln( - ' ctx.addPip(' - 'f"X{x}/Y{y}/SOUTH{t}_DOWN","INTER_TILE", ' - 'wire_name(x, y, f"S{t}"), ' - 'wire_name(x, y + 1, f"N{t}"), ' - 'ctx.getDelayFromNS(PIP_DELAY * 2), Loc(x, y, 0))', - ); - buf.writeln(' # East output → neighbor east input'); - buf.writeln(' if x < W:'); - buf.writeln( - ' ctx.addPip(' - 'f"X{x}/Y{y}/EAST{t}_RIGHT","INTER_TILE", ' - 'wire_name(x, y, f"E{t}"), ' - 'wire_name(x + 1, y, f"W{t}"), ' - 'ctx.getDelayFromNS(PIP_DELAY * 2), Loc(x, y, 0))', - ); - buf.writeln(' # West output → neighbor west input'); - buf.writeln(' if x > 1:'); - buf.writeln( - ' ctx.addPip(' - 'f"X{x}/Y{y}/WEST{t}_LEFT","INTER_TILE", ' - 'wire_name(x, y, f"W{t}"), ' - 'wire_name(x - 1, y, f"E{t}"), ' - 'ctx.getDelayFromNS(PIP_DELAY * 2), Loc(x, y, 0))', - ); - buf.writeln(); - - // Carry chain pips (south to north within column) - buf.writeln(' # Carry chain: south → north'); - buf.writeln(' if y < H:'); - buf.writeln( - ' ctx.addPip(' - 'f"X{x}/Y{y}/CARRY_UP","CARRY", ' - 'wire_name(x, y, "CARRY_OUT"), ' - 'wire_name(x, y + 1, "CARRY_IN"), ' - 'ctx.getDelayFromNS(PIP_DELAY * 0.5), Loc(x, y, 0))', - ); - buf.writeln(); - - // Global clock distribution - buf.writeln(' # Clock distribution'); - buf.writeln( - ' ctx.addPip(' - 'f"X{x}/Y{y}/GLB_CLK","CLOCK", ' - '"GLB_CLK", wire_name(x, y, "CLK"), ' - 'ctx.getDelayFromNS(PIP_DELAY), Loc(x, y, 0))', - ); - buf.writeln(); - } - - void _writeIoBels(StringBuffer buf) { - buf.writeln( - '# ================================================================', - ); - buf.writeln('# I/O BELs'); - buf.writeln( - '# ================================================================', - ); - buf.writeln(); - buf.writeln('for i in range($totalPads):'); - buf.writeln(' iob_name = f"IO{i}"'); - buf.writeln( - ' ctx.addBel(iob_name, "GENERIC_IOB", Loc(0, i, 0), False, False)', - ); - buf.writeln(' ctx.addBelInout(iob_name, "PAD", f"IO{i}/PAD")'); - buf.writeln(' ctx.addBelInput(iob_name, "I", f"IO{i}/I")'); - buf.writeln(' ctx.addBelOutput(iob_name, "O", f"IO{i}/O")'); - buf.writeln(' ctx.addBelInput(iob_name, "EN", f"IO{i}/OE")'); - buf.writeln(); - - // Connect IO pads to edge tiles - buf.writeln('# Connect IO pads to edge fabric tiles'); - buf.writeln('pad_idx = 0'); - buf.writeln(); - - // North edge - buf.writeln('# North edge'); - buf.writeln('for x in range(1, W + 1):'); - buf.writeln(' for t in range(T):'); - buf.writeln( - ' ctx.addPip(f"IO{pad_idx}/TO_N{t}", ' - '"IO_ROUTE", ' - 'f"IO{pad_idx}/O", ' - 'wire_name(x, 1, f"N{t}"), ' - 'ctx.getDelayFromNS(PIP_DELAY), Loc(0, pad_idx, 0))', - ); - buf.writeln( - ' ctx.addPip(f"IO{pad_idx}/FROM_S{t}", ' - '"IO_ROUTE", ' - 'wire_name(x, 1, f"S{t}"), ' - 'f"IO{pad_idx}/I", ' - 'ctx.getDelayFromNS(PIP_DELAY), Loc(0, pad_idx, 0))', - ); - buf.writeln(' pad_idx += 1'); - buf.writeln(); - - // East edge - buf.writeln('# East edge'); - buf.writeln('for y in range(1, H + 1):'); - buf.writeln(' for t in range(T):'); - buf.writeln( - ' ctx.addPip(f"IO{pad_idx}/TO_E{t}", ' - '"IO_ROUTE", ' - 'f"IO{pad_idx}/O", ' - 'wire_name(W, y, f"E{t}"), ' - 'ctx.getDelayFromNS(PIP_DELAY), Loc(0, pad_idx, 0))', - ); - buf.writeln( - ' ctx.addPip(f"IO{pad_idx}/FROM_W{t}", ' - '"IO_ROUTE", ' - 'wire_name(W, y, f"W{t}"), ' - 'f"IO{pad_idx}/I", ' - 'ctx.getDelayFromNS(PIP_DELAY), Loc(0, pad_idx, 0))', - ); - buf.writeln(' pad_idx += 1'); - buf.writeln(); - - // South edge - buf.writeln('# South edge'); - buf.writeln('for x in range(1, W + 1):'); - buf.writeln(' for t in range(T):'); - buf.writeln( - ' ctx.addPip(f"IO{pad_idx}/TO_S{t}", ' - '"IO_ROUTE", ' - 'f"IO{pad_idx}/O", ' - 'wire_name(x, H, f"S{t}"), ' - 'ctx.getDelayFromNS(PIP_DELAY), Loc(0, pad_idx, 0))', - ); - buf.writeln( - ' ctx.addPip(f"IO{pad_idx}/FROM_N{t}", ' - '"IO_ROUTE", ' - 'wire_name(x, H, f"N{t}"), ' - 'f"IO{pad_idx}/I", ' - 'ctx.getDelayFromNS(PIP_DELAY), Loc(0, pad_idx, 0))', - ); - buf.writeln(' pad_idx += 1'); - buf.writeln(); - - // West edge - buf.writeln('# West edge'); - buf.writeln('for y in range(1, H + 1):'); - buf.writeln(' for t in range(T):'); - buf.writeln( - ' ctx.addPip(f"IO{pad_idx}/TO_W{t}", ' - '"IO_ROUTE", ' - 'f"IO{pad_idx}/O", ' - 'wire_name(1, y, f"W{t}"), ' - 'ctx.getDelayFromNS(PIP_DELAY), Loc(0, pad_idx, 0))', - ); - buf.writeln( - ' ctx.addPip(f"IO{pad_idx}/FROM_E{t}", ' - '"IO_ROUTE", ' - 'wire_name(1, y, f"E{t}"), ' - 'f"IO{pad_idx}/I", ' - 'ctx.getDelayFromNS(PIP_DELAY), Loc(0, pad_idx, 0))', - ); - buf.writeln(' pad_idx += 1'); - buf.writeln(); - } - - void _writeConfig(StringBuffer buf) { - buf.writeln( - '# ================================================================', - ); - buf.writeln('# Configuration'); - buf.writeln( - '# ================================================================', - ); - buf.writeln(); - buf.writeln('ctx.setLutK(4)'); - buf.writeln('ctx.setDelayScaling(1.0, 0.0)'); - buf.writeln(); - - // Timing - buf.writeln('# Cell timing'); - buf.writeln('ctx.addCellTimingClock("AEGIS_DFF", "clk")'); - buf.writeln( - 'ctx.addCellTimingSetupHold("AEGIS_DFF", ' - '"d", "clk", ctx.getDelayFromNS(0.2), ctx.getDelayFromNS(0.1))', - ); - buf.writeln( - 'ctx.addCellTimingClockToOut("AEGIS_DFF", ' - '"q", "clk", ctx.getDelayFromNS(0.5))', - ); - buf.writeln(); - for (final pin in ['in0', 'in1', 'in2', 'in3']) { - buf.writeln( - 'ctx.addCellTimingDelay("AEGIS_LUT4", ' - '"$pin", "out", ctx.getDelayFromNS(0.3))', - ); - } - buf.writeln(); - buf.writeln( - 'ctx.addCellTimingDelay("AEGIS_CARRY", ' - '"ci", "co", ctx.getDelayFromNS(0.05))', - ); - buf.writeln( - 'ctx.addCellTimingDelay("AEGIS_CARRY", ' - '"p", "co", ctx.getDelayFromNS(0.15))', - ); - buf.writeln(); - } -} diff --git a/nextpnr-aegis/aegis.cc b/nextpnr-aegis/aegis.cc new file mode 100644 index 0000000..c47e7d2 --- /dev/null +++ b/nextpnr-aegis/aegis.cc @@ -0,0 +1,376 @@ +/* + * Aegis FPGA viaduct micro-architecture for nextpnr-generic. + * + * Defines the Aegis routing architecture natively in C++ for fast + * chipdb construction. Uses -o device=WxHtT to configure dimensions. + * + * Based on nextpnr's example viaduct uarch. + */ + +#include "log.h" +#include "nextpnr.h" +#include "util.h" +#include "viaduct_api.h" +#include "viaduct_helpers.h" + +// Use runtime ctx->id() instead of compile-time constids +// to avoid IdString table conflicts + +NEXTPNR_NAMESPACE_BEGIN + +namespace { + +struct AegisImpl : ViaductAPI { + ~AegisImpl() {}; + + // Device parameters + int W = 4, H = 4; + int T = 1; + int N = 1; + int K = 4; + int Wl; + int Si = 4, Sq = 4, Sl = 8; + + dict device_args; + + // Cached IdStrings — initialized in init() + IdString id_LUT4, id_DFF, id_IOB, id_INBUF, id_OUTBUF; + IdString id_CLK, id_D, id_Q, id_F, id_I, id_O, id_PAD, id_EN; + IdString id_INIT, id_PIP, id_LOCAL; + + void init(Context *ctx) override { + ViaductAPI::init(ctx); + h.init(ctx); + + // Initialize IdStrings + id_LUT4 = ctx->id("LUT4"); + id_DFF = ctx->id("DFF"); + id_IOB = ctx->id("IOB"); + id_INBUF = ctx->id("INBUF"); + id_OUTBUF = ctx->id("OUTBUF"); + id_CLK = ctx->id("CLK"); + id_D = ctx->id("D"); + id_Q = ctx->id("Q"); + id_F = ctx->id("F"); + id_I = ctx->id("I"); + id_O = ctx->id("O"); + id_PAD = ctx->id("PAD"); + id_EN = ctx->id("EN"); + id_INIT = ctx->id("INIT"); + id_PIP = ctx->id("PIP"); + id_LOCAL = ctx->id("LOCAL"); + + // Parse device parameters from vopt args + if (device_args.count("device")) { + std::string val = device_args.at("device"); + if (val.find('x') != std::string::npos) { + sscanf(val.c_str(), "%dx%d", &W, &H); + if (val.find('t') != std::string::npos) { + sscanf(strstr(val.c_str(), "t") + 1, "%d", &T); + } + } + } + + // Include IO ring + W += 2; + H += 2; + Wl = N * (K + 1) + T * 4; + + log_info( + "Aegis FPGA: %dx%d grid (%dx%d fabric), %d tracks, %d local wires\n", W, + H, W - 2, H - 2, T, Wl); + + init_wires(); + init_bels(); + init_pips(); + } + + void pack() override { + IdString id_lut = ctx->id("$lut"); + IdString id_dff_p = ctx->id("$_DFF_P_"); + IdString id_Y = ctx->id("Y"); + IdString id_C = ctx->id("C"); + + // Replace constants with $lut cells using proper parameter names + // $lut uses LUT (not INIT) and WIDTH parameters + const dict vcc_params = { + {ctx->id("LUT"), Property(0xFFFF, 16)}, + {ctx->id("WIDTH"), Property(4, 32)}}; + const dict gnd_params = { + {ctx->id("LUT"), Property(0x0000, 16)}, + {ctx->id("WIDTH"), Property(4, 32)}}; + h.replace_constants(CellTypePort(id_lut, id_Y), CellTypePort(id_lut, id_Y), + vcc_params, gnd_params); + + // Constrain LUT+FF pairs for shared placement + int lutffs = + h.constrain_cell_pairs(pool{{id_lut, id_Y}}, + pool{{id_dff_p, id_D}}, 1); + log_info("Constrained %d LUTFF pairs.\n", lutffs); + } + + void prePlace() override { assign_cell_info(); } + + bool isBelLocationValid(BelId bel, bool explain_invalid) const override { + Loc l = ctx->getBelLocation(bel); + if (is_io(l.x, l.y)) + return true; + return slice_valid(l.x, l.y, l.z / 2); + } + +private: + ViaductHelpers h; + + struct TileWires { + std::vector clk, q, f, d, i; + std::vector l; + std::vector pad; + }; + + std::vector> wires_by_tile; + + bool is_io(int x, int y) const { + return (x == 0) || (x == (W - 1)) || (y == 0) || (y == (H - 1)); + } + + PipId add_pip(Loc loc, WireId src, WireId dst, delay_t delay = 0.05) { + IdStringList name = + IdStringList::concat(ctx->getWireName(dst), ctx->getWireName(src)); + return ctx->addPip(name, id_PIP, src, dst, delay, loc); + } + + void init_wires() { + log_info("Creating wires...\n"); + wires_by_tile.resize(H); + for (int y = 0; y < H; y++) { + auto &row = wires_by_tile.at(y); + row.resize(W); + for (int x = 0; x < W; x++) { + auto &w = row.at(x); + if (!is_io(x, y)) { + // Logic tile wires + for (int z = 0; z < N; z++) { + w.clk.push_back(ctx->addWire(h.xy_id(x, y, ctx->idf("CLK%d", z)), + id_CLK, x, y)); + w.d.push_back( + ctx->addWire(h.xy_id(x, y, ctx->idf("D%d", z)), id_D, x, y)); + w.q.push_back( + ctx->addWire(h.xy_id(x, y, ctx->idf("Q%d", z)), id_Q, x, y)); + w.f.push_back( + ctx->addWire(h.xy_id(x, y, ctx->idf("F%d", z)), id_F, x, y)); + for (int k = 0; k < K; k++) + w.i.push_back(ctx->addWire( + h.xy_id(x, y, ctx->idf("L%dI%d", z, k)), id_I, x, y)); + } + } else if (x != y) { + // IO tile wires — dedicated pad and IO wires + for (int z = 0; z < 2; z++) { + w.pad.push_back(ctx->addWire(h.xy_id(x, y, ctx->idf("PAD%d", z)), + id_PAD, x, y)); + // IO input/output/enable wires + w.i.push_back( + ctx->addWire(h.xy_id(x, y, ctx->idf("IO_I%d", z)), id_I, x, y)); + w.i.push_back(ctx->addWire(h.xy_id(x, y, ctx->idf("IO_EN%d", z)), + id_I, x, y)); + w.q.push_back( + ctx->addWire(h.xy_id(x, y, ctx->idf("IO_O%d", z)), id_O, x, y)); + } + } + // Local wires for routing + for (int l = 0; l < Wl; l++) + w.l.push_back(ctx->addWire(h.xy_id(x, y, ctx->idf("LOCAL%d", l)), + id_LOCAL, x, y)); + } + } + } + + void add_io_bels(int x, int y) { + auto &w = wires_by_tile.at(y).at(x); + for (int z = 0; z < 2; z++) { + BelId b = ctx->addBel(h.xy_id(x, y, ctx->idf("IO%d", z)), id_IOB, + Loc(x, y, z), false, false); + ctx->addBelInout(b, id_PAD, w.pad.at(z)); + ctx->addBelInput(b, id_I, w.i.at(z * 2)); // $nextpnr_ibuf.I + ctx->addBelInput(b, ctx->id("A"), + w.i.at(z * 2)); // $nextpnr_obuf.A (alias) + ctx->addBelInput(b, id_EN, w.i.at(z * 2 + 1)); + ctx->addBelOutput(b, id_O, w.q.at(z)); + } + } + + void add_slice_bels(int x, int y) { + auto &w = wires_by_tile.at(y).at(x); + for (int z = 0; z < N; z++) { + BelId lut = ctx->addBel(h.xy_id(x, y, ctx->idf("SLICE%d_LUT", z)), + id_LUT4, Loc(x, y, z * 2), false, false); + // Pin names match $lut cell: A[0]-A[3], Y + // Also add unindexed 'A' for constant LUTs created by replace_constants + for (int k = 0; k < K; k++) + ctx->addBelInput(lut, ctx->idf("A[%d]", k), w.i.at(z * K + k)); + ctx->addBelInput(lut, ctx->id("A"), w.i.at(z * K)); + ctx->addBelOutput(lut, ctx->id("Y"), w.f.at(z)); + + add_pip(Loc(x, y, 0), w.f.at(z), w.d.at(z)); + add_pip(Loc(x, y, 0), w.i.at(z * K + (K - 1)), w.d.at(z)); + + // Pin names match $_DFF_P_ cell: C, D, Q + BelId dff = ctx->addBel(h.xy_id(x, y, ctx->idf("SLICE%d_FF", z)), id_DFF, + Loc(x, y, z * 2 + 1), false, false); + ctx->addBelInput(dff, ctx->id("C"), w.clk.at(z)); + ctx->addBelInput(dff, id_D, w.d.at(z)); + ctx->addBelOutput(dff, id_Q, w.q.at(z)); + } + } + + void init_bels() { + log_info("Creating bels...\n"); + for (int y = 0; y < H; y++) + for (int x = 0; x < W; x++) { + if (is_io(x, y)) { + if (x == y) + continue; + add_io_bels(x, y); + } else { + add_slice_bels(x, y); + } + } + } + + void add_tile_pips(int x, int y) { + auto &w = wires_by_tile.at(y).at(x); + Loc loc(x, y, 0); + + if (!is_io(x, y)) { + // Logic tile pips + auto create_input_pips = [&](WireId dst, int offset, int skip) { + for (int i = (offset % skip); i < Wl; i += skip) + add_pip(loc, w.l.at(i), dst, 0.05); + }; + for (int z = 0; z < N; z++) { + create_input_pips(w.clk.at(z), 0, Si); + for (int k = 0; k < K; k++) + create_input_pips(w.i.at(z * K + k), k, Si); + } + } else if (x != y) { + // IO tile — connect IO wires to local routing + for (size_t z = 0; z < w.i.size(); z++) { + for (int l = (z % Si); l < Wl; l += Si) + add_pip(loc, w.l.at(l), w.i.at(z), 0.05); + } + for (size_t z = 0; z < w.q.size(); z++) { + for (int l = (z % Sq); l < Wl; l += Sq) + add_pip(loc, w.q.at(z), w.l.at(l), 0.05); + } + } + + auto create_output_pips = [&](WireId dst, int offset, int skip) { + if (is_io(x, y)) + return; + for (int z = (offset % skip); z < N; z += skip) { + add_pip(loc, w.f.at(z), dst, 0.05); + add_pip(loc, w.q.at(z), dst, 0.05); + } + }; + auto create_neighbour_pips = [&](WireId dst, int nx, int ny, int offset, + int skip) { + if (nx < 0 || nx >= W || ny < 0 || ny >= H) + return; + auto &nw = wires_by_tile.at(ny).at(nx); + for (int i = (offset % skip); i < Wl; i += skip) + add_pip(loc, dst, nw.l.at(i), 0.1); + }; + + for (int i = 0; i < Wl; i++) { + WireId dst = w.l.at(i); + create_output_pips(dst, i % Sq, Sq); + create_neighbour_pips(dst, x - 1, y - 1, (i + 1) % Sl, Sl); + create_neighbour_pips(dst, x - 1, y, (i + 2) % Sl, Sl); + create_neighbour_pips(dst, x - 1, y + 1, (i + 3) % Sl, Sl); + create_neighbour_pips(dst, x, y - 1, (i + 4) % Sl, Sl); + create_neighbour_pips(dst, x, y + 1, (i + 5) % Sl, Sl); + create_neighbour_pips(dst, x + 1, y - 1, (i + 6) % Sl, Sl); + create_neighbour_pips(dst, x + 1, y, (i + 7) % Sl, Sl); + create_neighbour_pips(dst, x + 1, y + 1, (i + 8) % Sl, Sl); + } + } + + void init_pips() { + log_info("Creating pips...\n"); + for (int y = 0; y < H; y++) + for (int x = 0; x < W; x++) + add_tile_pips(x, y); + } + + struct AegisCellInfo { + const NetInfo *lut_f = nullptr, *ff_d = nullptr; + bool lut_i3_used = false; + }; + std::vector fast_cell_info; + + void assign_cell_info() { + fast_cell_info.resize(ctx->cells.size()); + for (auto &cell : ctx->cells) { + CellInfo *ci = cell.second.get(); + auto &fc = fast_cell_info.at(ci->flat_index); + if (ci->type == id_LUT4 || ci->type == ctx->id("$lut")) { + fc.lut_f = ci->getPort(ctx->id("Y")); + fc.lut_i3_used = (ci->getPort(ctx->idf("A[%d]", K - 1)) != nullptr); + } else if (ci->type == id_DFF || ci->type == ctx->id("$_DFF_P_")) { + fc.ff_d = ci->getPort(id_D); + } + } + } + + bool slice_valid(int x, int y, int z) const { + const CellInfo *lut = + ctx->getBoundBelCell(ctx->getBelByLocation(Loc(x, y, z * 2))); + const CellInfo *ff = + ctx->getBoundBelCell(ctx->getBelByLocation(Loc(x, y, z * 2 + 1))); + if (!lut || !ff) + return true; + const auto &lut_data = fast_cell_info.at(lut->flat_index); + const auto &ff_data = fast_cell_info.at(ff->flat_index); + if (ff_data.ff_d == lut_data.lut_f) + return true; + if (lut_data.lut_i3_used) + return false; + return true; + } + + IdString getBelBucketForCellType(IdString cell_type) const override { + if (cell_type.in(id_INBUF, id_OUTBUF, ctx->id("$nextpnr_ibuf"), + ctx->id("$nextpnr_obuf"))) + return id_IOB; + if (cell_type == ctx->id("$_DFF_P_")) + return id_DFF; + if (cell_type == ctx->id("$lut")) + return id_LUT4; + return cell_type; + } + + bool isValidBelForCellType(IdString cell_type, BelId bel) const override { + IdString bel_type = ctx->getBelType(bel); + if (bel_type == id_IOB) + return cell_type.in(id_INBUF, id_OUTBUF, ctx->id("$nextpnr_ibuf"), + ctx->id("$nextpnr_obuf")); + if (bel_type == id_DFF) + return cell_type.in(id_DFF, ctx->id("$_DFF_P_")); + if (bel_type == id_LUT4) + return cell_type.in(id_LUT4, ctx->id("$lut")); + return (bel_type == cell_type); + } +}; + +struct AegisArch : ViaductArch { + AegisArch() : ViaductArch("aegis") {}; + std::unique_ptr + create(const dict &args) { + auto impl = std::make_unique(); + impl->device_args = args; + return impl; + } +} aegisArch; + +} // namespace + +NEXTPNR_NAMESPACE_END diff --git a/pkgs/aegis-ip/default.nix b/pkgs/aegis-ip/default.nix index b8ce333..49e4c0c 100644 --- a/pkgs/aegis-ip/default.nix +++ b/pkgs/aegis-ip/default.nix @@ -3,7 +3,7 @@ callPackage, stdenvNoCC, makeWrapper, - nextpnr, + nextpnr-aegis, aegis-ip-tools, aegis-pack, }: @@ -104,7 +104,7 @@ lib.extendMkDerivation { nativeBuildInputs = (args.nativeBuildInputs or [ ]) ++ [ aegis-ip-tools makeWrapper - nextpnr + nextpnr-aegis ]; buildPhase = '' @@ -122,9 +122,9 @@ lib.extendMkDerivation { makeWrapper ${aegis-ip-tools}/bin/aegis-sim $tools/bin/${deviceName}-sim \ --add-flags "--descriptor $out/${deviceName}.json" - # nextpnr wrapper: pre-loads chipdb, runs custom packer, skips built-in packer - makeWrapper ${nextpnr}/bin/nextpnr-generic $tools/bin/nextpnr-aegis-${deviceName} \ - --add-flags "--pre-pack $out/${deviceName}-chipdb.py --pre-pack $out/${deviceName}-packer.py --no-pack" + # nextpnr wrapper: aegis viaduct uarch with device dimensions + makeWrapper ${nextpnr-aegis}/bin/nextpnr-generic $tools/bin/nextpnr-aegis-${deviceName} \ + --add-flags "--uarch aegis -o device=${toString width}x${toString height}t${toString tracks}" # bitstream packer wrapper: pre-loads the descriptor for this device makeWrapper ${aegis-pack}/bin/aegis-pack $tools/bin/${deviceName}-pack \ @@ -132,13 +132,10 @@ lib.extendMkDerivation { # Install EDA support files for targeting this device mkdir -p $tools/share/yosys/aegis - mkdir -p $tools/share/nextpnr/aegis cp $out/${deviceName}_cells.v $tools/share/yosys/aegis/ cp $out/${deviceName}_techmap.v $tools/share/yosys/aegis/ cp $out/${deviceName}_bram.rules $tools/share/yosys/aegis/ 2>/dev/null || true cp $out/${deviceName}-synth-aegis.tcl $tools/share/yosys/aegis/ - cp $out/${deviceName}-chipdb.py $tools/share/nextpnr/aegis/ - cp $out/${deviceName}-packer.py $tools/share/nextpnr/aegis/ runHook postInstall '';