From 7f137a46637b2f7879e40340d85db0e6c4968575 Mon Sep 17 00:00:00 2001 From: Tristan Ross Date: Sat, 4 Apr 2026 23:23:02 -0700 Subject: [PATCH] feat: improve tapeout performance --- ip/bin/aegis_genip.dart | 60 ++ .../digital/fabric_config_loader.dart | 151 ++-- ip/lib/src/components/digital/fpga.dart | 18 +- ip/lib/src/components/digital/io_fabric.dart | 86 +-- ip/lib/src/openroad.dart | 1 + ip/lib/src/openroad/tcl_emitter.dart | 644 ++++++++++++++---- ip/lib/src/openroad/tile_tcl_emitter.dart | 187 +++++ ip/lib/src/yosys/tcl_emitter.dart | 98 ++- pkgs/aegis-tapeout/default.nix | 478 ++++++++++--- pkgs/aegis-tapeout/scripts/def2gds.py | 50 +- pkgs/aegis-tapeout/scripts/render_layout.py | 18 +- pkgs/aegis-tapeout/scripts/stamp_text.py | 91 +++ pkgs/gf180mcu-pdk/default.nix | 4 + pkgs/nextpnr-aegis/default.nix | 5 +- pkgs/sky130-pdk/default.nix | 4 + 15 files changed, 1464 insertions(+), 431 deletions(-) create mode 100644 ip/lib/src/openroad/tile_tcl_emitter.dart create mode 100644 pkgs/aegis-tapeout/scripts/stamp_text.py diff --git a/ip/bin/aegis_genip.dart b/ip/bin/aegis_genip.dart index badb92f..f80198e 100644 --- a/ip/bin/aegis_genip.dart +++ b/ip/bin/aegis_genip.dart @@ -174,9 +174,50 @@ Future main(List arguments) async { serdesCount: serdesCount, clockTileCount: clockTiles, ); + // Top-level assembly script (reads pre-synthesized tile macros) File( '$outputDir/${fpga.name}-yosys.tcl', ).writeAsStringSync(yosysEmitter.generate()); + // Determine which macro modules actually exist in the generated SV + final svContent = File('$outputDir/${fpga.name}.sv').readAsStringSync(); + final presentModules = YosysTclEmitter.macroModules + .where((mod) => svContent.contains('module $mod (')) + .toList(); + + // Per-tile synthesis scripts (only for modules that exist) + for (final mod in presentModules) { + File( + '$outputDir/${fpga.name}-yosys-$mod.tcl', + ).writeAsStringSync(yosysEmitter.generateTileSynth(mod)); + } + final stubs = StringBuffer(); + stubs.writeln( + '// Auto-generated tile blackbox stubs for hierarchical synthesis', + ); + stubs.writeln( + '// These are empty modules with matching ports so the top-level', + ); + stubs.writeln( + '// synthesis can elaborate without processing tile internals.', + ); + stubs.writeln(); + for (final mod in presentModules) { + final modStart = svContent.indexOf('module $mod ('); + if (modStart == -1) continue; + + // Extract from "module X (" to the closing ");" + final portEnd = svContent.indexOf(');', modStart); + if (portEnd == -1) continue; + + final decl = svContent.substring(modStart, portEnd + 2); + stubs.writeln('(* blackbox *)'); + stubs.writeln(decl); + stubs.writeln('endmodule'); + stubs.writeln(); + } + File( + '$outputDir/${fpga.name}_tile_stubs.v', + ).writeAsStringSync(stubs.toString()); final techmapEmitter = YosysTechmapEmitter( deviceName: fpga.name, @@ -208,22 +249,41 @@ Future main(List arguments) async { height: height, serdesCount: serdesCount, clockTileCount: clockTiles, + bramColumnInterval: bramInterval, + dspColumnInterval: dspInterval, hasConfigClk: results.flag('config-clk'), ); File( '$outputDir/${fpga.name}-openroad.tcl', ).writeAsStringSync(openroadEmitter.generate()); + // Per-tile OpenROAD PnR scripts (tile macro hardening) + final tileOpenroadEmitter = OpenroadTileTclEmitter( + deviceName: fpga.name, + tracks: tracks, + ); + for (final mod in presentModules) { + File( + '$outputDir/${fpga.name}-openroad-$mod.tcl', + ).writeAsStringSync(tileOpenroadEmitter.generateTilePnr(mod)); + } + print('Generated:'); print(' $outputDir/${fpga.name}.sv'); print(' $outputDir/${fpga.name}.json'); print(' $outputDir/${fpga.name}-xschem.tcl'); print(' $outputDir/${fpga.name}-xschem.sch'); print(' $outputDir/${fpga.name}-yosys.tcl'); + for (final mod in presentModules) { + print(' $outputDir/${fpga.name}-yosys-$mod.tcl'); + } print(' $outputDir/${fpga.name}_cells.v'); print(' $outputDir/${fpga.name}_techmap.v'); print(' $outputDir/${fpga.name}-synth-aegis.tcl'); print(' $outputDir/${fpga.name}-openroad.tcl'); + for (final mod in presentModules) { + print(' $outputDir/${fpga.name}-openroad-$mod.tcl'); + } } on FormatException catch (e) { print(e.message); print(''); diff --git a/ip/lib/src/components/digital/fabric_config_loader.dart b/ip/lib/src/components/digital/fabric_config_loader.dart index cec38eb..97482c4 100644 --- a/ip/lib/src/components/digital/fabric_config_loader.dart +++ b/ip/lib/src/components/digital/fabric_config_loader.dart @@ -1,6 +1,7 @@ import 'package:rohd/rohd.dart'; import 'package:rohd_hcl/rohd_hcl.dart'; +/// Streams configuration bits into the FPGA fabric config chain. class FabricConfigLoader extends Module { Logic get clk => input('clk'); Logic get start => input('start'); @@ -37,92 +38,81 @@ class FabricConfigLoader extends Module { final wordWidth = readPort.data.width; final wordsNeeded = (totalBits + wordWidth - 1) ~/ wordWidth; - // Use rohd_hcl Deserializer to collect words from memory into a - // flat LogicArray, then a Serializer to shift them out bit-by-bit. final active = Logic(name: 'active'); final wordAddr = Logic(width: readPort.addr.width, name: 'wordAddr'); - final wordCount = Logic(width: wordsNeeded.bitLength, name: 'wordCount'); - final fetchDone = Logic(name: 'fetchDone'); - - // --- Word fetch FSM: reads words from memory sequentially --- - Sequential(clk, [ - If( - start, - then: [ - active < Const(1), - wordAddr < Const(0, width: wordAddr.width), - wordCount < Const(0, width: wordCount.width), - fetchDone < Const(0), - ], - orElse: [ - If( - active & ~fetchDone, - then: [ - wordCount < wordCount + 1, - wordAddr < wordAddr + 1, - If( - wordCount.eq(Const(wordsNeeded - 1, width: wordCount.width)), - then: [fetchDone < Const(1)], - ), - ], - ), - ], - ), - ]); - - readPort.en <= active & ~fetchDone; - readPort.addr <= wordAddr; - - // --- Deserializer: collects words into a wide register --- - final deser = Deserializer( - readPort.data, - wordsNeeded, - clk: clk, - reset: start, - enable: active & ~fetchDone, + final wordBuf = Logic(width: wordWidth, name: 'wordBuf'); + final bitIdx = Logic(width: wordWidth.bitLength, name: 'bitIdx'); + final totalShifted = Logic( + width: totalBits.bitLength, + name: 'totalShifted', ); - - // --- Serializer: shifts the collected words out bit-by-bit --- - // We need to serialize the deserialized array one bit at a time. - // Use a simple bit counter + shift approach on the deserialized output. - final bitCounter = Logic(width: totalBits.bitLength, name: 'bitCounter'); final shifting = Logic(name: 'shifting'); - final shiftDone = Logic(name: 'shiftDone'); - - // Flatten the deserialized array into a single wide bus - final flatBits = Logic(width: wordsNeeded * wordWidth, name: 'flatBits'); - flatBits <= - deser.deserialized.elements - .map((e) => e) - .toList() - .reversed - .toList() - .swizzle(); - - // Latch the flat bits when deserialization completes - final latchedBits = Logic( - width: wordsNeeded * wordWidth, - name: 'latchedBits', - ); + final wordReady = Logic(name: 'wordReady'); + final allDone = Logic(name: 'allDone'); Sequential( clk, [ If( - deser.done & active, + start, then: [ - latchedBits < flatBits, - shifting < Const(1), - bitCounter < Const(0, width: bitCounter.width), + active < Const(1), + wordAddr < Const(0, width: wordAddr.width), + bitIdx < Const(0, width: bitIdx.width), + totalShifted < Const(0, width: totalShifted.width), + shifting < Const(0), + wordReady < Const(0), + allDone < Const(0), ], orElse: [ If( - shifting, + active & ~allDone, then: [ - bitCounter < bitCounter + 1, If( - bitCounter.eq(Const(totalBits - 1, width: bitCounter.width)), - then: [shifting < Const(0), shiftDone < Const(1)], + ~shifting & ~wordReady, + then: [ + wordBuf < readPort.data, + wordReady < Const(1), + bitIdx < Const(0, width: bitIdx.width), + ], + orElse: [ + If( + wordReady & ~shifting, + then: [shifting < Const(1)], + orElse: [ + If( + shifting, + then: [ + bitIdx < bitIdx + 1, + totalShifted < totalShifted + 1, + If( + totalShifted.eq( + Const(totalBits - 1, width: totalShifted.width), + ), + then: [ + // All bits shifted + allDone < Const(1), + shifting < Const(0), + ], + orElse: [ + If( + bitIdx.eq( + Const(wordWidth - 1, width: bitIdx.width), + ), + then: [ + // Word exhausted, fetch next + shifting < Const(0), + wordReady < Const(0), + wordAddr < wordAddr + 1, + ], + ), + ], + ), + ], + ), + ], + ), + ], ), ], ), @@ -131,15 +121,22 @@ class FabricConfigLoader extends Module { ], reset: start, resetValues: { + active: Const(0), shifting: Const(0), - shiftDone: Const(0), - bitCounter: Const(0, width: bitCounter.width), - latchedBits: Const(0, width: latchedBits.width), + wordReady: Const(0), + allDone: Const(0), + wordAddr: Const(0, width: wordAddr.width), + wordBuf: Const(0, width: wordWidth), + bitIdx: Const(0, width: bitIdx.width), + totalShifted: Const(0, width: totalShifted.width), }, ); - cfgIn <= mux(shifting, latchedBits[bitCounter], Const(0)); - cfgLoad <= shiftDone; - done <= shiftDone & ~active; + readPort.en <= active & ~allDone & ~shifting & ~wordReady; + readPort.addr <= wordAddr; + + cfgIn <= mux(shifting, wordBuf[bitIdx], Const(0)); + cfgLoad <= allDone; + done <= allDone; } } diff --git a/ip/lib/src/components/digital/fpga.dart b/ip/lib/src/components/digital/fpga.dart index 735552d..a783494 100644 --- a/ip/lib/src/components/digital/fpga.dart +++ b/ip/lib/src/components/digital/fpga.dart @@ -54,10 +54,12 @@ class AegisFPGA extends Module { addOutput('padOut', width: pads); addOutput('padOutputEnable', width: pads); - serialIn = addInput('serialIn', serialIn, width: serdesCount); - addOutput('serialOut', width: serdesCount); - addOutput('txReady', width: serdesCount); - addOutput('rxValid', width: serdesCount); + if (serdesCount > 0) { + serialIn = addInput('serialIn', serialIn, width: serdesCount); + addOutput('serialOut', width: serdesCount); + addOutput('txReady', width: serdesCount); + addOutput('rxValid', width: serdesCount); + } addOutput('clkOut', width: clockTileCount * ClockTile.numOutputs); addOutput('clkLocked', width: clockTileCount); @@ -149,9 +151,11 @@ class AegisFPGA extends Module { output('padOut') <= ioFabric.padOut; output('padOutputEnable') <= ioFabric.padOutputEnable; - output('serialOut') <= ioFabric.serialOut; - output('txReady') <= ioFabric.txReady; - output('rxValid') <= ioFabric.rxValid; + if (serdesCount > 0) { + output('serialOut') <= ioFabric.serialOut!; + output('txReady') <= ioFabric.txReady!; + output('rxValid') <= ioFabric.rxValid!; + } } /// Generates a JSON-serializable descriptor of the device, suitable for diff --git a/ip/lib/src/components/digital/io_fabric.dart b/ip/lib/src/components/digital/io_fabric.dart index 4ef15ac..2ac85ba 100644 --- a/ip/lib/src/components/digital/io_fabric.dart +++ b/ip/lib/src/components/digital/io_fabric.dart @@ -16,10 +16,10 @@ class IOFabric extends Module { Logic get padOut => output('padOut'); Logic get padOutputEnable => output('padOutputEnable'); - Logic get serialIn => input('serialIn'); - Logic get serialOut => output('serialOut'); - Logic get txReady => output('txReady'); - Logic get rxValid => output('rxValid'); + Logic? get serialIn => serdesCount > 0 ? input('serialIn') : null; + Logic? get serialOut => serdesCount > 0 ? output('serialOut') : null; + Logic? get txReady => serdesCount > 0 ? output('txReady') : null; + Logic? get rxValid => serdesCount > 0 ? output('rxValid') : null; final int width; final int height; @@ -61,10 +61,12 @@ class IOFabric extends Module { addOutput('padOut', width: pads); addOutput('padOutputEnable', width: pads); - serialIn = addInput('serialIn', serialIn, width: serdesCount); - addOutput('serialOut', width: serdesCount); - addOutput('txReady', width: serdesCount); - addOutput('rxValid', width: serdesCount); + if (serdesCount > 0) { + serialIn = addInput('serialIn', serialIn, width: serdesCount); + addOutput('serialOut', width: serdesCount); + addOutput('txReady', width: serdesCount); + addOutput('rxValid', width: serdesCount); + } // ---- IO tiles ---- // Order: north (L-to-R), east (T-to-B), south (L-to-R), west (T-to-B) @@ -137,33 +139,35 @@ class IOFabric extends Module { )); } - for (int i = 0; i < serdesCount; i++) { - serdesTiles[i].$2 <= serialIn[i]; - serdesTiles[i].$5 <= cfgLoad; - } + if (serdesCount > 0) { + for (int i = 0; i < serdesCount; i++) { + serdesTiles[i].$2 <= serialIn![i]; + serdesTiles[i].$5 <= cfgLoad; + } - // Collect serial outputs - serialOut <= - serdesTiles - .map((t) => t.$1.serialOut) - .toList() - .reversed - .toList() - .swizzle(); - txReady <= - serdesTiles - .map((t) => t.$1.txReady) - .toList() - .reversed - .toList() - .swizzle(); - rxValid <= - serdesTiles - .map((t) => t.$1.rxValid) - .toList() - .reversed - .toList() - .swizzle(); + // Collect serial outputs + serialOut! <= + serdesTiles + .map((t) => t.$1.serialOut) + .toList() + .reversed + .toList() + .swizzle(); + txReady! <= + serdesTiles + .map((t) => t.$1.txReady) + .toList() + .reversed + .toList() + .swizzle(); + rxValid! <= + serdesTiles + .map((t) => t.$1.rxValid) + .toList() + .reversed + .toList() + .swizzle(); + } // ---- Config chain: IO tiles -> SerDes tiles -> fabric ---- ioTiles[0].$4 <= cfgIn; @@ -171,13 +175,17 @@ class IOFabric extends Module { ioTiles[i].$4 <= ioTiles[i - 1].$1.cfgOut; } - serdesTiles[0].$4 <= ioTiles.last.$1.cfgOut; - for (int i = 1; i < serdesTiles.length; i++) { - serdesTiles[i].$4 <= serdesTiles[i - 1].$1.cfgOut; + Logic fabricCfgIn; + if (serdesTiles.isNotEmpty) { + serdesTiles[0].$4 <= ioTiles.last.$1.cfgOut; + for (int i = 1; i < serdesTiles.length; i++) { + serdesTiles[i].$4 <= serdesTiles[i - 1].$1.cfgOut; + } + fabricCfgIn = serdesTiles.last.$1.cfgOut; + } else { + fabricCfgIn = ioTiles.last.$1.cfgOut; } - final fabricCfgIn = serdesTiles.last.$1.cfgOut; - // Collect pad outputs padOut <= ioTiles.map((t) => t.$1.padOut).toList().reversed.toList().swizzle(); diff --git a/ip/lib/src/openroad.dart b/ip/lib/src/openroad.dart index a782a68..49e87d4 100644 --- a/ip/lib/src/openroad.dart +++ b/ip/lib/src/openroad.dart @@ -1 +1,2 @@ export 'openroad/tcl_emitter.dart'; +export 'openroad/tile_tcl_emitter.dart'; diff --git a/ip/lib/src/openroad/tcl_emitter.dart b/ip/lib/src/openroad/tcl_emitter.dart index f6f8c1d..8a2ea27 100644 --- a/ip/lib/src/openroad/tcl_emitter.dart +++ b/ip/lib/src/openroad/tcl_emitter.dart @@ -1,25 +1,43 @@ -/// Emits an OpenROAD TCL script for place-and-route. +import '../yosys/tcl_emitter.dart'; + +/// Emits an OpenROAD TCL script for top-level macro-based place-and-route. +/// +/// Tile modules are pre-hardened as macros (with LEF abstracts). +/// This script places them in a grid and routes the inter-tile wiring +/// on upper metal layers. class OpenroadTclEmitter { final String moduleName; final int width; final int height; final int serdesCount; final int clockTileCount; + final int bramColumnInterval; + final int dspColumnInterval; final bool hasConfigClk; - /// Total I/O pads: 2*width + 2*height. int get totalPads => 2 * width + 2 * height; + /// Spacing between tile macros (um). Must be large enough for + /// clock buffer and standard cell placement in routing channels. + final double macroHaloUm; + + /// Margin around the macro grid edge (um). Room for IO pins + /// and glue logic placement. + final double gridMarginUm; + const OpenroadTclEmitter({ required this.moduleName, required this.width, required this.height, required this.serdesCount, required this.clockTileCount, + this.bramColumnInterval = 0, + this.dspColumnInterval = 0, this.hasConfigClk = false, + this.macroHaloUm = 100, + this.gridMarginUm = 200, }); - /// Generates the complete OpenROAD TCL script. String generate() { final buf = StringBuffer(); @@ -44,29 +62,55 @@ class OpenroadTclEmitter { '$clockTileCount clock tiles', ); buf.writeln('#'); - buf.writeln('# Shell variables are substituted at build time.'); + buf.writeln('# Macro-based hierarchical flow:'); + buf.writeln('# - Tile types are pre-hardened macros (LEF abstracts)'); + buf.writeln('# - Top level places macros and routes inter-tile wiring'); + buf.writeln('#'); + buf.writeln('# Shell variables substituted at build time.'); + buf.writeln(); + // Helper: discover routing layers from loaded tech LEF + buf.writeln('# Discover routing layers from the PDK tech LEF'); + buf.writeln('proc get_routing_layers {} {'); + buf.writeln(' set layers {}'); + buf.writeln(' set tech [[ord::get_db] getTech]'); + buf.writeln(' foreach layer [\$tech getLayers] {'); + buf.writeln(' if {[\$layer getType] eq "ROUTING"} {'); + buf.writeln(' lappend layers [\$layer getName]'); + buf.writeln(' }'); + buf.writeln(' }'); + buf.writeln(' return \$layers'); + buf.writeln('}'); buf.writeln(); } void _writeReadInputs(StringBuffer buf) { - buf.writeln( - '# ================================================================', - ); - buf.writeln('# Read inputs'); - buf.writeln( - '# ================================================================', - ); - buf.writeln(); + buf.writeln(_section('Read inputs')); + buf.writeln('read_liberty \$LIB_FILE'); buf.writeln('read_lef \$TECH_LEF'); buf.writeln(); - buf.writeln('# Read cell LEFs'); + + // Read standard cell LEFs + buf.writeln('# Read standard cell LEFs'); buf.writeln('foreach lef [glob -directory \$CELL_LEF_DIR *.lef] {'); buf.writeln(' if {![string match "*tech*" \$lef]} {'); buf.writeln(' read_lef \$lef'); buf.writeln(' }'); buf.writeln('}'); buf.writeln(); + + // Read tile macro LEFs and liberty timing models + buf.writeln('# Read tile macro LEF abstracts and timing models'); + for (final mod in YosysTclEmitter.tileModules) { + buf.writeln('if {[file exists \${DEVICE_NAME}_${mod}.lef]} {'); + buf.writeln(' read_lef \${DEVICE_NAME}_${mod}.lef'); + buf.writeln('}'); + buf.writeln('if {[file exists \${DEVICE_NAME}_${mod}.lib]} {'); + buf.writeln(' read_liberty \${DEVICE_NAME}_${mod}.lib'); + buf.writeln('}'); + } + buf.writeln(); + buf.writeln('read_verilog \$SYNTH_V'); buf.writeln('link_design $moduleName'); buf.writeln('read_sdc \$SDC_FILE'); @@ -74,31 +118,88 @@ class OpenroadTclEmitter { } void _writeFloorplan(StringBuffer buf) { - buf.writeln( - '# ================================================================', - ); - buf.writeln('# Floorplan'); - buf.writeln( - '# ================================================================', - ); - buf.writeln(); + buf.writeln(_section('Floorplan')); + + // Compute die area from macro sizes if available + buf.writeln('# Determine die area: use explicit setting, or compute from'); + buf.writeln('# tile macro dimensions × grid size'); buf.writeln('if {[info exists DIE_AREA]} {'); buf.writeln(' initialize_floorplan \\'); buf.writeln(' -die_area \$DIE_AREA \\'); - buf.writeln(' -core_space 2 \\'); + buf.writeln( + ' -core_area "[expr {[lindex \$DIE_AREA 0] + 1}] [expr {[lindex \$DIE_AREA 1] + 1}] [expr {[lindex \$DIE_AREA 2] - 1}] [expr {[lindex \$DIE_AREA 3] - 1}]" \\', + ); buf.writeln(' -site \$SITE_NAME'); buf.writeln('} else {'); - buf.writeln(' initialize_floorplan \\'); - buf.writeln(' -utilization \$UTILIZATION \\'); - buf.writeln(' -core_space 2 \\'); - buf.writeln(' -site \$SITE_NAME'); + buf.writeln(' # Find largest macro to size the die'); + buf.writeln(' set tw 0'); + buf.writeln(' set th 0'); + buf.writeln(' foreach inst [[ord::get_db_block] getInsts] {'); + buf.writeln(' if {[[\$inst getMaster] isBlock]} {'); + buf.writeln( + ' set mw [ord::dbu_to_microns [[\$inst getMaster] getWidth]]', + ); + buf.writeln( + ' set mh [ord::dbu_to_microns [[\$inst getMaster] getHeight]]', + ); + buf.writeln(' if {\$mw > \$tw} { set tw \$mw }'); + buf.writeln(' if {\$mh > \$th} { set th \$mh }'); + buf.writeln(' }'); + buf.writeln(' }'); + buf.writeln(' if {\$tw > 0} {'); + buf.writeln(' # Count total macro instances to size the die'); + buf.writeln(' set macro_n 0'); + buf.writeln(' foreach inst [[ord::get_db_block] getInsts] {'); + buf.writeln( + ' if {[[\$inst getMaster] isBlock]} { incr macro_n }', + ); + buf.writeln(' }'); + buf.writeln(' # Compute grid dimensions from macro count'); + buf.writeln( + ' set grid_cols [expr {int(sqrt(\$macro_n * \$tw / \$th)) + 1}]', + ); + buf.writeln( + ' set grid_rows [expr {(\$macro_n + \$grid_cols - 1) / \$grid_cols}]', + ); + buf.writeln( + ' puts "Die grid: \$grid_cols cols x \$grid_rows rows for \$macro_n macros"', + ); + buf.writeln(' set mfg 0.005'); + buf.writeln( + ' if {![info exists MACRO_HALO]} { set MACRO_HALO $macroHaloUm }', + ); + buf.writeln( + ' if {![info exists GRID_MARGIN]} { set GRID_MARGIN $gridMarginUm }', + ); + buf.writeln( + ' set die_w [expr {int(((\$tw + \$MACRO_HALO) * \$grid_cols + 2 * \$GRID_MARGIN) / \$mfg) * \$mfg}]', + ); + buf.writeln( + ' set die_h [expr {int(((\$th + \$MACRO_HALO) * \$grid_rows + 2 * \$GRID_MARGIN) / \$mfg) * \$mfg}]', + ); + buf.writeln( + ' puts "Computed die area: \${die_w}um x \${die_h}um ' + '(tile: \${tw}um x \${th}um)"', + ); + buf.writeln(' set margin \$GRID_MARGIN'); + buf.writeln(' initialize_floorplan \\'); + buf.writeln(' -die_area "0 0 \$die_w \$die_h" \\'); + buf.writeln( + ' -core_area "\$margin \$margin ' + '[expr {\$die_w - \$margin}] [expr {\$die_h - \$margin}]" \\', + ); + buf.writeln(' -site \$SITE_NAME'); + buf.writeln(' } else {'); + buf.writeln(' # Fallback: utilization-based'); + buf.writeln(' initialize_floorplan \\'); + buf.writeln(' -utilization \$UTILIZATION \\'); + buf.writeln(' -core_space 2 \\'); + buf.writeln(' -site \$SITE_NAME'); + buf.writeln(' }'); buf.writeln('}'); buf.writeln(); - // Generate routing tracks - pitches are read from tech LEF layer defs - buf.writeln('# Generate routing tracks'); - buf.writeln('# Pitches should match the tech LEF layer definitions'); - buf.writeln('foreach layer {Metal1 Metal2 Metal3 Metal4 Metal5 Metal6} {'); + buf.writeln('foreach layer [get_routing_layers] {'); buf.writeln( ' if {![catch {set tech_layer ' '[[[ord::get_db] getTech] findLayer \$layer]}]} {', @@ -124,32 +225,16 @@ class OpenroadTclEmitter { } void _writePinPlacement(StringBuffer buf) { - buf.writeln( - '# ================================================================', - ); - buf.writeln('# Pin placement'); - buf.writeln( - '# ================================================================', - ); - buf.writeln(); + buf.writeln(_section('Pin placement')); - // Group pins by function and edge: - // - North: pad I/O for the north edge of the FPGA fabric - // - South: pad I/O for the south edge - // - East: SerDes pins - // - West: clock, reset, config - - // Clock and control pins on west edge final westPins = ['clk', 'reset', 'configDone']; if (hasConfigClk) westPins.add('configClk'); - // Config read port westPins.addAll([ 'configRead_en', 'configRead_addr\\[*\\]', 'configRead_data\\[*\\]', ]); - // SerDes pins on east edge final eastPins = []; if (serdesCount > 0) { eastPins.addAll([ @@ -159,115 +244,364 @@ class OpenroadTclEmitter { 'rxValid\\[*\\]', ]); } - - // Clock outputs on east edge too if (clockTileCount > 0) { eastPins.addAll(['clkOut\\[*\\]', 'clkLocked\\[*\\]']); } - // Pad I/O split between north and south - final northSouthPins = [ - 'padIn\\[*\\]', - 'padOut\\[*\\]', - 'padOutputEnable\\[*\\]', - ]; - - buf.writeln('# West edge: clock, reset, config'); - buf.writeln('set west_pins [list \\'); - for (final pin in westPins) { - buf.writeln(' $pin \\'); - } - buf.writeln(']'); + _writeLayerDetection(buf); + buf.writeln('place_pins -hor_layers \$hor_layer -ver_layers \$ver_layer'); buf.writeln(); + } - buf.writeln('# East edge: SerDes, clock outputs'); - buf.writeln('set east_pins [list \\'); - for (final pin in eastPins) { - buf.writeln(' $pin \\'); - } - buf.writeln(']'); + void _writePowerGrid(StringBuffer buf) { + buf.writeln(_section('Power grid')); + + buf.writeln('add_global_connection -net VDD -pin_pattern VDD -power'); + buf.writeln('add_global_connection -net VSS -pin_pattern VSS -ground'); + buf.writeln('global_connect'); buf.writeln(); + } - buf.writeln('# North/South edges: pad I/O'); - buf.writeln('set ns_pins [list \\'); - for (final pin in northSouthPins) { - buf.writeln(' $pin \\'); - } - buf.writeln(']'); + void _writePlacement(StringBuffer buf) { + buf.writeln(_section('Placement')); + + // Count and report unplaced macros + buf.writeln('# Detect macros'); + buf.writeln('set macro_count 0'); + buf.writeln('foreach inst [[ord::get_db_block] getInsts] {'); + buf.writeln(' if {[[\$inst getMaster] isBlock]} {'); + buf.writeln(' incr macro_count'); + buf.writeln(' }'); + buf.writeln('}'); + buf.writeln('puts "Detected \$macro_count macro instances"'); buf.writeln(); - // Use place_pins with pin groups for edge assignment - // Find available routing layers - buf.writeln('# Find routing layers for pin placement'); - buf.writeln('set hor_layer ""'); - buf.writeln('set ver_layer ""'); - buf.writeln('foreach layer {Metal2 Metal3 Metal4} {'); + buf.writeln('if {\$macro_count > 0} {'); buf.writeln( - ' if {![catch {set tl ' - '[[[ord::get_db] getTech] findLayer \$layer]}]} {', + ' if {![info exists MACRO_HALO]} { set MACRO_HALO $macroHaloUm }', ); - buf.writeln(' if {\$tl ne "NULL"} {'); - buf.writeln(' set dir [\$tl getDirection]'); buf.writeln( - ' if {\$dir eq "HORIZONTAL" && \$hor_layer eq ""} {', + ' if {![info exists GRID_MARGIN]} { set GRID_MARGIN $gridMarginUm }', ); - buf.writeln(' set hor_layer \$layer'); - buf.writeln(' }'); - buf.writeln(' if {\$dir eq "VERTICAL" && \$ver_layer eq ""} {'); - buf.writeln(' set ver_layer \$layer'); - buf.writeln(' }'); + buf.writeln(' set halo \$MACRO_HALO'); + buf.writeln(' set margin \$GRID_MARGIN'); + buf.writeln(); + + // Group macros by master name + buf.writeln(' # Group macros by type'); + buf.writeln(' array set macro_groups {}'); + buf.writeln(' foreach inst [[ord::get_db_block] getInsts] {'); + buf.writeln(' if {[[\$inst getMaster] isBlock]} {'); + buf.writeln(' set mname [[\$inst getMaster] getName]'); + buf.writeln(' lappend macro_groups(\$mname) \$inst'); buf.writeln(' }'); buf.writeln(' }'); - buf.writeln('}'); buf.writeln(); - buf.writeln('if {\$hor_layer eq ""} { set hor_layer Metal1 }'); - buf.writeln('if {\$ver_layer eq ""} { set ver_layer Metal2 }'); + buf.writeln(' foreach type [array names macro_groups] {'); + buf.writeln(' set n [llength \$macro_groups(\$type)]'); + buf.writeln(' set m [[lindex \$macro_groups(\$type) 0] getMaster]'); + buf.writeln(' set w [ord::dbu_to_microns [\$m getWidth]]'); + buf.writeln(' set h [ord::dbu_to_microns [\$m getHeight]]'); + buf.writeln(' puts " \$type: \$n instances (\${w}um x \${h}um)"'); + buf.writeln(' }'); buf.writeln(); - buf.writeln('place_pins -hor_layers \$hor_layer -ver_layers \$ver_layer'); + + // Get die dimensions + buf.writeln(' set die_rect [[ord::get_db_block] getDieArea]'); + buf.writeln(' set die_w [ord::dbu_to_microns [\$die_rect xMax]]'); + buf.writeln(' set die_h [ord::dbu_to_microns [\$die_rect yMax]]'); buf.writeln(); - } - void _writePowerGrid(StringBuffer buf) { + // Place fabric tiles (Tile, BramTile, DspBasicTile) in main grid + // Each type uses its own height, packed into columns + buf.writeln(' # Place fabric tiles in a grid'); + buf.writeln(' # Get Tile dimensions for the main grid pitch'); + buf.writeln(' set tile_w 0'); + buf.writeln(' set tile_h 0'); + buf.writeln(' if {[info exists macro_groups(Tile)]} {'); buf.writeln( - '# ================================================================', + ' set tile_w [ord::dbu_to_microns [[[lindex \$macro_groups(Tile) 0] getMaster] getWidth]]', ); - buf.writeln('# Power/ground connections'); buf.writeln( - '# ================================================================', + ' set tile_h [ord::dbu_to_microns [[[lindex \$macro_groups(Tile) 0] getMaster] getHeight]]', ); + buf.writeln(' }'); + buf.writeln(' set tile_px [expr {\$tile_w + \$halo}]'); + buf.writeln(' set tile_py [expr {\$tile_h + \$halo}]'); buf.writeln(); - buf.writeln('add_global_connection -net VDD -pin_pattern VDD -power'); - buf.writeln('add_global_connection -net VSS -pin_pattern VSS -ground'); - buf.writeln('global_connect'); + + // Compute fabric grid columns from die width + // Reserve right edge for non-fabric macros (Clock, SerDes, etc.) + buf.writeln(' set edge_reserve 250'); + buf.writeln( + ' set fabric_w [expr {\$die_w - 2 * \$margin - \$edge_reserve}]', + ); + buf.writeln(' set fabric_cols [expr {int(\$fabric_w / \$tile_px)}]'); + buf.writeln(' if {\$fabric_cols < 1} { set fabric_cols 1 }'); buf.writeln(); - } - void _writePlacement(StringBuffer buf) { + // Place Tile, BramTile, DspBasicTile in fabric grid + buf.writeln(' set cur_x \$margin'); + buf.writeln(' set cur_y \$margin'); + buf.writeln(' set col 0'); + buf.writeln(' set placed 0'); + buf.writeln(); + + // Place fabric tiles column by column, matching the FPGA fabric layout. + // BRAM columns at bramColumnInterval, DSP at dspColumnInterval, + // remaining columns are LUT tiles. Each column type uses its own width. buf.writeln( - '# ================================================================', + ' # Build column-major placement matching FPGA fabric layout', ); - buf.writeln('# Placement'); + buf.writeln(' # Queues for each tile type'); buf.writeln( - '# ================================================================', + ' set tile_q [expr {[info exists macro_groups(Tile)] ? \$macro_groups(Tile) : {}}]', + ); + buf.writeln( + ' set bram_q [expr {[info exists macro_groups(BramTile)] ? \$macro_groups(BramTile) : {}}]', + ); + buf.writeln( + ' set dsp_q [expr {[info exists macro_groups(DspBasicTile)] ? \$macro_groups(DspBasicTile) : {}}]', ); buf.writeln(); - buf.writeln('global_placement -density \$UTILIZATION'); - buf.writeln('detailed_placement'); + buf.writeln(' # Compute column types and x positions'); + buf.writeln(' set cur_x \$margin'); + buf.writeln(' set fabric_height ${height}'); + buf.writeln(' set col_x_list {}'); + buf.writeln(' set col_type_list {}'); + buf.writeln(' for {set c 0} {\$c < ${width}} {incr c} {'); + + // Determine column type using same logic as Dart fabric + buf.writeln(' set ctype "Tile"'); + if (bramColumnInterval > 0) { + buf.writeln( + ' if {$bramColumnInterval > 0 && ' + '(\$c - $bramColumnInterval) >= 0 && ' + '((\$c - $bramColumnInterval) % ($bramColumnInterval + 1)) == 0} {', + ); + buf.writeln(' set ctype "BramTile"'); + buf.writeln(' }'); + } + if (dspColumnInterval > 0) { + buf.writeln( + ' if {\$ctype eq "Tile" && $dspColumnInterval > 0 && ' + '(\$c - $dspColumnInterval) >= 0 && ' + '((\$c - $dspColumnInterval) % ($dspColumnInterval + 1)) == 0} {', + ); + buf.writeln(' set ctype "DspBasicTile"'); + buf.writeln(' }'); + } + + buf.writeln(' lappend col_type_list \$ctype'); + buf.writeln(' lappend col_x_list \$cur_x'); + buf.writeln(); + buf.writeln(' # Advance x by this column type\'s width'); + buf.writeln(' switch \$ctype {'); + buf.writeln(' Tile { set cw \$tile_w }'); + buf.writeln(' BramTile {'); + buf.writeln(' if {[llength \$bram_q] > 0} {'); + buf.writeln( + ' set cw [ord::dbu_to_microns [[[lindex \$bram_q 0] getMaster] getWidth]]', + ); + buf.writeln(' } else { set cw \$tile_w }'); + buf.writeln(' }'); + buf.writeln(' DspBasicTile {'); + buf.writeln(' if {[llength \$dsp_q] > 0} {'); + buf.writeln( + ' set cw [ord::dbu_to_microns [[[lindex \$dsp_q 0] getMaster] getWidth]]', + ); + buf.writeln(' } else { set cw \$tile_w }'); + buf.writeln(' }'); + buf.writeln(' }'); + buf.writeln(' set cur_x [expr {\$cur_x + \$cw + \$halo}]'); + buf.writeln(' }'); buf.writeln(); - } - void _writeCts(StringBuffer buf) { + // Now place tiles column by column, row by row + buf.writeln(' # Place tiles in fabric grid'); + buf.writeln(' set placed 0'); + buf.writeln(' for {set c 0} {\$c < ${width}} {incr c} {'); + buf.writeln(' set ctype [lindex \$col_type_list \$c]'); + buf.writeln(' set cx [lindex \$col_x_list \$c]'); + buf.writeln(' for {set r 0} {\$r < \$fabric_height} {incr r} {'); + buf.writeln(' set cy [expr {\$margin + \$r * \$tile_py}]'); + buf.writeln(' switch \$ctype {'); + buf.writeln(' Tile {'); + buf.writeln(' if {[llength \$tile_q] > 0} {'); + buf.writeln(' set inst [lindex \$tile_q 0]'); + buf.writeln(' set tile_q [lrange \$tile_q 1 end]'); + buf.writeln( + ' \$inst setLocation [ord::microns_to_dbu \$cx] [ord::microns_to_dbu \$cy]', + ); + buf.writeln(' \$inst setPlacementStatus FIRM'); + buf.writeln(' incr placed'); + buf.writeln(' }'); + buf.writeln(' }'); + buf.writeln(' BramTile {'); + buf.writeln(' if {[llength \$bram_q] > 0} {'); + buf.writeln(' set inst [lindex \$bram_q 0]'); + buf.writeln(' set bram_q [lrange \$bram_q 1 end]'); + buf.writeln( + ' \$inst setLocation [ord::microns_to_dbu \$cx] [ord::microns_to_dbu \$cy]', + ); + buf.writeln(' \$inst setPlacementStatus FIRM'); + buf.writeln(' incr placed'); + buf.writeln(' }'); + buf.writeln(' }'); + buf.writeln(' DspBasicTile {'); + buf.writeln(' if {[llength \$dsp_q] > 0} {'); + buf.writeln(' set inst [lindex \$dsp_q 0]'); + buf.writeln(' set dsp_q [lrange \$dsp_q 1 end]'); + buf.writeln( + ' \$inst setLocation [ord::microns_to_dbu \$cx] [ord::microns_to_dbu \$cy]', + ); + buf.writeln(' \$inst setPlacementStatus FIRM'); + buf.writeln(' incr placed'); + buf.writeln(' }'); + buf.writeln(' }'); + buf.writeln(' }'); + buf.writeln(' }'); + buf.writeln(' }'); + buf.writeln( + ' set fabric_top [expr {\$margin + \$fabric_height * \$tile_py}]', + ); + buf.writeln( + ' puts "Placed \$placed fabric tiles in ${width} cols x \$fabric_height rows"', + ); + buf.writeln(); + + // Place IO tiles along the edges of the fabric grid + buf.writeln(' # Place IO tiles along fabric perimeter'); + buf.writeln(' if {[info exists macro_groups(IOTile)]} {'); + buf.writeln( + ' set io_h [ord::dbu_to_microns [[[lindex \$macro_groups(IOTile) 0] getMaster] getHeight]]', + ); + buf.writeln( + ' set io_w [ord::dbu_to_microns [[[lindex \$macro_groups(IOTile) 0] getMaster] getWidth]]', + ); + buf.writeln(' set io_px [expr {\$io_w + \$halo}]'); + buf.writeln(' set io_idx 0'); + buf.writeln(' set io_count [llength \$macro_groups(IOTile)]'); + buf.writeln(' # Place along bottom edge'); + buf.writeln(' set io_y [expr {\$margin - \$io_h - \$halo}]'); + buf.writeln(' if {\$io_y < 0} { set io_y 0 }'); + buf.writeln( + ' for {set i 0} {\$i < \$fabric_cols && \$io_idx < \$io_count} {incr i} {', + ); + buf.writeln(' set io_x [expr {\$margin + \$i * \$tile_px}]'); + buf.writeln( + ' [lindex \$macro_groups(IOTile) \$io_idx] setLocation [ord::microns_to_dbu \$io_x] [ord::microns_to_dbu \$io_y]', + ); + buf.writeln( + ' [lindex \$macro_groups(IOTile) \$io_idx] setPlacementStatus FIRM', + ); + buf.writeln(' incr io_idx'); + buf.writeln(' }'); + buf.writeln(' # Place along top edge'); + buf.writeln(' set io_y \$fabric_top'); buf.writeln( - '# ================================================================', + ' for {set i 0} {\$i < \$fabric_cols && \$io_idx < \$io_count} {incr i} {', ); - buf.writeln('# Clock tree synthesis'); + buf.writeln(' set io_x [expr {\$margin + \$i * \$tile_px}]'); buf.writeln( - '# ================================================================', + ' [lindex \$macro_groups(IOTile) \$io_idx] setLocation [ord::microns_to_dbu \$io_x] [ord::microns_to_dbu \$io_y]', ); + buf.writeln( + ' [lindex \$macro_groups(IOTile) \$io_idx] setPlacementStatus FIRM', + ); + buf.writeln(' incr io_idx'); + buf.writeln(' }'); + buf.writeln(' # Place remaining IO tiles along right edge'); + buf.writeln( + ' set io_x [expr {\$margin + \$fabric_cols * \$tile_px}]', + ); + buf.writeln(' set io_row 0'); + buf.writeln(' while {\$io_idx < \$io_count} {'); + buf.writeln( + ' set io_y [expr {\$margin + \$io_row * (\$io_h + \$halo)}]', + ); + buf.writeln( + ' [lindex \$macro_groups(IOTile) \$io_idx] setLocation [ord::microns_to_dbu \$io_x] [ord::microns_to_dbu \$io_y]', + ); + buf.writeln( + ' [lindex \$macro_groups(IOTile) \$io_idx] setPlacementStatus FIRM', + ); + buf.writeln(' incr io_idx'); + buf.writeln(' incr io_row'); + buf.writeln(' }'); + buf.writeln(' puts "Placed \$io_count IO tiles around perimeter"'); + buf.writeln(' }'); + buf.writeln(); + + // Place edge macros (Clock, SerDes, FabricConfigLoader) along right edge + // Place SerDes above the fabric grid + buf.writeln(' # Place SerDes tiles above the fabric grid'); + buf.writeln(' if {[info exists macro_groups(SerDesTile)]} {'); + buf.writeln(' set serdes_x \$margin'); + buf.writeln(' set serdes_y [expr {\$fabric_top + \$halo}]'); + buf.writeln(' foreach inst \$macro_groups(SerDesTile) {'); + buf.writeln( + ' set mw [ord::dbu_to_microns [[\$inst getMaster] getWidth]]', + ); + buf.writeln( + ' set mh [ord::dbu_to_microns [[\$inst getMaster] getHeight]]', + ); + buf.writeln( + ' \$inst setLocation [ord::microns_to_dbu \$serdes_x] [ord::microns_to_dbu \$serdes_y]', + ); + buf.writeln(' \$inst setPlacementStatus FIRM'); + buf.writeln( + ' puts "Placed SerDesTile at \${serdes_x}um x \${serdes_y}um"', + ); + buf.writeln(' set serdes_x [expr {\$serdes_x + \$mw + \$halo}]'); + buf.writeln(' }'); + buf.writeln(' }'); buf.writeln(); + + // Place Clock and ConfigLoader on the right edge + buf.writeln(' # Place Clock and ConfigLoader on right edge'); + buf.writeln(' set edge_x [expr {\$die_w - \$margin}]'); + buf.writeln(' set edge_y \$margin'); + buf.writeln(' foreach type {ClockTile FabricConfigLoader} {'); + buf.writeln(' if {[info exists macro_groups(\$type)]} {'); + buf.writeln(' foreach inst \$macro_groups(\$type) {'); + buf.writeln( + ' set mw [ord::dbu_to_microns [[\$inst getMaster] getWidth]]', + ); + buf.writeln( + ' set mh [ord::dbu_to_microns [[\$inst getMaster] getHeight]]', + ); + buf.writeln(' set x [expr {\$edge_x - \$mw}]'); + buf.writeln( + ' \$inst setLocation [ord::microns_to_dbu \$x] [ord::microns_to_dbu \$edge_y]', + ); + buf.writeln(' \$inst setPlacementStatus FIRM'); + buf.writeln(' set edge_y [expr {\$edge_y + \$mh + \$halo}]'); + buf.writeln( + ' puts "Placed \$type at \${x}um x [expr {\$edge_y - \$mh - \$halo}]um"', + ); + buf.writeln(' }'); + buf.writeln(' }'); + buf.writeln(' }'); + buf.writeln(); + + buf.writeln(' # Cut standard cell rows around placed macros'); + buf.writeln(' cut_rows'); + buf.writeln('}'); + buf.writeln(); + + // Then place remaining standard cells (glue logic) + buf.writeln('# Place remaining standard cells'); + buf.writeln('global_placement -density \$UTILIZATION'); + buf.writeln('detailed_placement'); + buf.writeln(); + } + + void _writeCts(StringBuffer buf) { + buf.writeln(_section('Clock tree synthesis')); + buf.writeln('estimate_parasitics -placement'); buf.writeln(); - buf.writeln('# Use larger clock buffers for routable CTS'); buf.writeln('set cts_bufs {}'); buf.writeln('foreach sz {2 4 8 16} {'); buf.writeln(' set cell_name \${CELL_LIB}__clkbuf_\$sz'); @@ -275,35 +609,46 @@ class OpenroadTclEmitter { buf.writeln('}'); buf.writeln('clock_tree_synthesis -buf_list \$cts_bufs'); buf.writeln(); - buf.writeln('# Post-CTS legalization'); buf.writeln('detailed_placement'); buf.writeln(); } void _writeRouting(StringBuffer buf) { + buf.writeln(_section('Routing')); + + // Use upper metal layers for top-level routing + // Metal1-Metal2 may be used internally by tile macros + // Auto-detect the highest available routing layer + buf.writeln('set top_route ""'); + buf.writeln('foreach layer [lreverse [get_routing_layers]] {'); buf.writeln( - '# ================================================================', + ' if {![catch {set tl ' + '[[[ord::get_db] getTech] findLayer \$layer]}]} {', ); - buf.writeln('# Routing'); + buf.writeln(' if {\$tl ne "NULL" && \$top_route eq ""} {'); + buf.writeln(' set top_route \$layer'); + buf.writeln(' }'); + buf.writeln(' }'); + buf.writeln('}'); + buf.writeln('if {\$top_route eq ""} { set top_route Metal2 }'); + buf.writeln('puts "Routing layers: Metal1-\$top_route"'); + buf.writeln('set_routing_layers -signal Metal1-\$top_route'); + buf.writeln('global_route -allow_congestion'); + buf.writeln(); + buf.writeln('# Save global-routed DEF (guaranteed output)'); + buf.writeln('write_def \${DEVICE_NAME}_grouted.def'); + buf.writeln(); buf.writeln( - '# ================================================================', + '# Detailed route may fail on offgrid pin shapes from place_pins.', ); - buf.writeln(); - buf.writeln('set_routing_layers -signal Metal1-Metal4'); - buf.writeln('global_route -allow_congestion'); + buf.writeln('# If it fails, we still have the global-routed DEF.'); buf.writeln('detailed_route'); buf.writeln(); } void _writeReports(StringBuffer buf) { - buf.writeln( - '# ================================================================', - ); - buf.writeln('# Reports'); - buf.writeln( - '# ================================================================', - ); - buf.writeln(); + buf.writeln(_section('Reports')); + buf.writeln('report_checks -path_delay min_max > timing.rpt'); buf.writeln('report_design_area > area.rpt'); buf.writeln('report_power > power.rpt'); @@ -311,17 +656,40 @@ class OpenroadTclEmitter { } void _writeOutputs(StringBuffer buf) { + buf.writeln(_section('Write outputs')); + + buf.writeln('write_def \${DEVICE_NAME}_final.def'); + buf.writeln('write_verilog \${DEVICE_NAME}_final.v'); + buf.writeln(); + } + + void _writeLayerDetection(StringBuffer buf) { + buf.writeln('set hor_layer ""'); + buf.writeln('set ver_layer ""'); + buf.writeln('foreach layer [get_routing_layers] {'); buf.writeln( - '# ================================================================', + ' if {![catch {set tl ' + '[[[ord::get_db] getTech] findLayer \$layer]}]} {', ); - buf.writeln('# Write outputs'); + buf.writeln(' if {\$tl ne "NULL"} {'); + buf.writeln(' set dir [\$tl getDirection]'); buf.writeln( - '# ================================================================', + ' if {\$dir eq "HORIZONTAL" && \$hor_layer eq ""} {', ); - buf.writeln(); - buf.writeln('write_def \${DEVICE_NAME}_final.def'); - buf.writeln('write_verilog \${DEVICE_NAME}_final.v'); - buf.writeln(); - buf.writeln(); + buf.writeln(' set hor_layer \$layer'); + buf.writeln(' }'); + buf.writeln(' if {\$dir eq "VERTICAL" && \$ver_layer eq ""} {'); + buf.writeln(' set ver_layer \$layer'); + buf.writeln(' }'); + buf.writeln(' }'); + buf.writeln(' }'); + buf.writeln('}'); + buf.writeln('if {\$hor_layer eq ""} { set hor_layer Metal1 }'); + buf.writeln('if {\$ver_layer eq ""} { set ver_layer Metal2 }'); } + + String _section(String title) => + '# ================================================================\n' + '# $title\n' + '# ================================================================\n'; } diff --git a/ip/lib/src/openroad/tile_tcl_emitter.dart b/ip/lib/src/openroad/tile_tcl_emitter.dart new file mode 100644 index 0000000..a75a934 --- /dev/null +++ b/ip/lib/src/openroad/tile_tcl_emitter.dart @@ -0,0 +1,187 @@ +/// Emits an OpenROAD TCL script to PnR a single tile type as a hard macro. +/// +/// The resulting macro has deterministic pin positions on all 4 edges +/// so adjacent tiles connect correctly when placed in a grid. +class OpenroadTileTclEmitter { + final String deviceName; + final int tracks; + + const OpenroadTileTclEmitter({ + required this.deviceName, + required this.tracks, + }); + + /// Generate a PnR script for a single tile module. + String generateTilePnr(String tileModule) { + final buf = StringBuffer(); + + buf.writeln('# OpenROAD PnR script for $tileModule macro'); + buf.writeln('# Auto-generated - produces a hard macro for tile-based PnR'); + buf.writeln(); + + // Helper: discover routing layers from the PDK tech LEF + buf.writeln('proc get_routing_layers {} {'); + buf.writeln(' set layers {}'); + buf.writeln(' set tech [[ord::get_db] getTech]'); + buf.writeln(' foreach layer [\$tech getLayers] {'); + buf.writeln(' if {[\$layer getType] eq "ROUTING"} {'); + buf.writeln(' lappend layers [\$layer getName]'); + buf.writeln(' }'); + buf.writeln(' }'); + buf.writeln(' return \$layers'); + buf.writeln('}'); + buf.writeln(); + + // Read inputs + buf.writeln('read_liberty \$LIB_FILE'); + buf.writeln('read_lef \$TECH_LEF'); + buf.writeln(); + buf.writeln('foreach lef [glob -directory \$CELL_LEF_DIR *.lef] {'); + buf.writeln(' if {![string match "*tech*" \$lef]} {'); + buf.writeln(' read_lef \$lef'); + buf.writeln(' }'); + buf.writeln('}'); + buf.writeln(); + buf.writeln('read_verilog \${DEVICE_NAME}_${tileModule}_synth.v'); + buf.writeln('link_design $tileModule'); + buf.writeln(); + + buf.writeln('create_clock [get_ports clk] -name clk -period \$CLK_PERIOD'); + buf.writeln(); + + buf.writeln('if {[info exists TILE_DIE_W] && [info exists TILE_DIE_H]} {'); + buf.writeln(' initialize_floorplan \\'); + buf.writeln(' -die_area "0 0 \$TILE_DIE_W \$TILE_DIE_H" \\'); + buf.writeln( + ' -core_area "1 1 [expr {\$TILE_DIE_W - 1}] [expr {\$TILE_DIE_H - 1}]" \\', + ); + buf.writeln(' -site \$SITE_NAME'); + buf.writeln('} else {'); + buf.writeln(' initialize_floorplan \\'); + buf.writeln(' -utilization \$TILE_UTIL \\'); + buf.writeln(' -core_space 1 \\'); + buf.writeln(' -site \$SITE_NAME'); + buf.writeln('}'); + buf.writeln(); + + // Routing tracks + buf.writeln('foreach layer [get_routing_layers] {'); + buf.writeln( + ' if {![catch {set tech_layer ' + '[[[ord::get_db] getTech] findLayer \$layer]}]} {', + ); + buf.writeln(' if {\$tech_layer ne "NULL"} {'); + buf.writeln( + ' set pitch_x ' + '[ord::dbu_to_microns [\$tech_layer getPitchX]]', + ); + buf.writeln( + ' set pitch_y ' + '[ord::dbu_to_microns [\$tech_layer getPitchY]]', + ); + buf.writeln(' if {\$pitch_x > 0 && \$pitch_y > 0} {'); + buf.writeln(' make_tracks \$layer \\'); + buf.writeln(' -x_offset 0 -x_pitch \$pitch_x \\'); + buf.writeln(' -y_offset 0 -y_pitch \$pitch_y'); + buf.writeln(' }'); + buf.writeln(' }'); + buf.writeln(' }'); + buf.writeln('}'); + buf.writeln(); + + buf.writeln('# Pin placement on edges for inter-tile connectivity'); + _writeLayerDetection(buf); + buf.writeln('place_pins -hor_layers \$hor_layer -ver_layers \$ver_layer'); + buf.writeln(); + + // Power + buf.writeln('add_global_connection -net VDD -pin_pattern VDD -power'); + buf.writeln('add_global_connection -net VSS -pin_pattern VSS -ground'); + buf.writeln('global_connect'); + buf.writeln(); + + // Placement + buf.writeln('global_placement -density \$TILE_UTIL'); + buf.writeln('detailed_placement'); + buf.writeln(); + + // CTS + buf.writeln('estimate_parasitics -placement'); + buf.writeln('set cts_bufs {}'); + buf.writeln('foreach sz {2 4 8} {'); + buf.writeln(' set cell_name \${CELL_LIB}__clkbuf_\$sz'); + buf.writeln(' lappend cts_bufs \$cell_name'); + buf.writeln('}'); + buf.writeln('clock_tree_synthesis -buf_list \$cts_bufs'); + buf.writeln('detailed_placement'); + buf.writeln(); + + // Route + buf.writeln('set top_route_layer ""'); + buf.writeln('foreach layer [lreverse [get_routing_layers]] {'); + buf.writeln( + ' if {![catch {set tl ' + '[[[ord::get_db] getTech] findLayer \$layer]}]} {', + ); + buf.writeln(' if {\$tl ne "NULL" && \$top_route_layer eq ""} {'); + buf.writeln(' set top_route_layer \$layer'); + buf.writeln(' }'); + buf.writeln(' }'); + buf.writeln('}'); + buf.writeln('if {\$top_route_layer eq ""} { set top_route_layer Metal2 }'); + buf.writeln('set_routing_layers -signal Metal1-\$top_route_layer'); + buf.writeln('global_route -allow_congestion'); + buf.writeln('detailed_route'); + buf.writeln(); + + // Reports + buf.writeln( + 'report_checks -path_delay min_max ' + '> ${tileModule}_timing.rpt', + ); + buf.writeln('report_design_area > ${tileModule}_area.rpt'); + buf.writeln(); + + // Write outputs + buf.writeln('write_def \${DEVICE_NAME}_${tileModule}_final.def'); + buf.writeln('write_verilog \${DEVICE_NAME}_${tileModule}_final.v'); + buf.writeln(); + + // Generate LEF abstract for top-level PnR + buf.writeln('# Generate LEF abstract for use as a macro'); + buf.writeln('write_abstract_lef \${DEVICE_NAME}_${tileModule}.lef'); + buf.writeln(); + + // Generate liberty timing model for top-level STA + buf.writeln('# Generate liberty timing model for timing analysis'); + buf.writeln('write_timing_model \${DEVICE_NAME}_${tileModule}.lib'); + buf.writeln(); + + return buf.toString(); + } + + void _writeLayerDetection(StringBuffer buf) { + buf.writeln('set hor_layer ""'); + buf.writeln('set ver_layer ""'); + buf.writeln('foreach layer [get_routing_layers] {'); + buf.writeln( + ' if {![catch {set tl ' + '[[[ord::get_db] getTech] findLayer \$layer]}]} {', + ); + buf.writeln(' if {\$tl ne "NULL"} {'); + buf.writeln(' set dir [\$tl getDirection]'); + buf.writeln( + ' if {\$dir eq "HORIZONTAL" && \$hor_layer eq ""} {', + ); + buf.writeln(' set hor_layer \$layer'); + buf.writeln(' }'); + buf.writeln(' if {\$dir eq "VERTICAL" && \$ver_layer eq ""} {'); + buf.writeln(' set ver_layer \$layer'); + buf.writeln(' }'); + buf.writeln(' }'); + buf.writeln(' }'); + buf.writeln('}'); + buf.writeln('if {\$hor_layer eq ""} { set hor_layer Metal1 }'); + buf.writeln('if {\$ver_layer eq ""} { set ver_layer Metal2 }'); + } +} diff --git a/ip/lib/src/yosys/tcl_emitter.dart b/ip/lib/src/yosys/tcl_emitter.dart index c472d6a..ace1fda 100644 --- a/ip/lib/src/yosys/tcl_emitter.dart +++ b/ip/lib/src/yosys/tcl_emitter.dart @@ -13,70 +13,96 @@ class YosysTclEmitter { required this.clockTileCount, }); - /// Generates the complete Yosys TCL synthesis script. - String generate() { + /// All modules that get pre-synthesized as hard macros. + /// Includes tile types and helper modules with behavioral code. + static const macroModules = [ + 'Tile', + 'BramTile', + 'DspBasicTile', + 'ClockTile', + 'IOTile', + 'SerDesTile', + 'FabricConfigLoader', + ]; + + /// Backward compat alias + static const tileModules = macroModules; + + /// Generates a per-module synthesis script. + String generateTileSynth(String tileModule) { final buf = StringBuffer(); - _writeHeader(buf); - _writeRead(buf); - _writeSynth(buf); - _writeMapping(buf); - _writeOutput(buf); + buf.writeln('# Synthesize $tileModule as a hard macro'); + buf.writeln('# Auto-generated - run once per module type'); + buf.writeln(); + buf.writeln('yosys read_verilog -sv \$SV_FILE'); + buf.writeln('yosys hierarchy -top $tileModule'); + buf.writeln('yosys synth -top $tileModule -flatten'); + buf.writeln('yosys dfflibmap -liberty \$LIB_FILE'); + buf.writeln('yosys abc -liberty \$LIB_FILE'); + buf.writeln( + 'yosys hilomap -hicell \${CELL_LIB}__tieh Z ' + '-locell \${CELL_LIB}__tiel ZN', + ); + buf.writeln('yosys opt_clean -purge'); + buf.writeln( + 'yosys write_verilog -noattr \${DEVICE_NAME}_${tileModule}_synth.v', + ); + buf.writeln('yosys stat -liberty \$LIB_FILE'); return buf.toString(); } - void _writeHeader(StringBuffer buf) { - buf.writeln('# Auto-generated Yosys synthesis script for $moduleName'); + /// Generates the top-level assembly script. + String generate() { + final buf = StringBuffer(); + + buf.writeln('# Auto-generated Yosys top-level assembly for $moduleName'); buf.writeln( '# Fabric: ${width}x$height, $serdesCount SerDes, ' '$clockTileCount clock tiles', ); buf.writeln('#'); - buf.writeln('# TCL variables (SV_FILE, LIB_FILE, CELL_LIB, DEVICE_NAME)'); - buf.writeln('# must be set before sourcing this script.'); + buf.writeln('# All behavioral modules are pre-synthesized.'); + buf.writeln('# This script only assembles them - no synth/abc passes.'); + buf.writeln('#'); + buf.writeln('# TCL variables: SV_FILE, LIB_FILE, CELL_LIB, DEVICE_NAME,'); + buf.writeln('# STUBS_V'); buf.writeln(); - } - void _writeRead(StringBuffer buf) { - buf.writeln('# Read design'); + // Read the full SV with behavioral module definitions buf.writeln('yosys read_verilog -sv \$SV_FILE'); buf.writeln(); - } - void _writeSynth(StringBuffer buf) { - buf.writeln('# Hierarchical synthesis (no -flatten)'); - buf.writeln('#'); - buf.writeln('# Each unique tile module (Tile, BramTile, etc.) is'); - buf.writeln('# synthesized once and reused across all instances.'); - buf.writeln('# Flattening a ${width}x$height fabric would require'); - buf.writeln('# hundreds of GB of RAM.'); - buf.writeln('yosys synth -top $moduleName'); + buf.writeln('# Replace behavioral modules with blackbox stubs'); + for (final mod in macroModules) { + buf.writeln('yosys delete $mod'); + } + buf.writeln('yosys read_verilog -sv \$STUBS_V'); buf.writeln(); - } - void _writeMapping(StringBuffer buf) { - buf.writeln('# Map flip-flops to PDK cells'); - buf.writeln('yosys dfflibmap -liberty \$LIB_FILE'); + buf.writeln('yosys read_liberty -lib \$LIB_FILE'); + buf.writeln('yosys hierarchy -top $moduleName'); buf.writeln(); - buf.writeln('# Map combinational logic to PDK cells'); + buf.writeln('# Lower remaining structural wrappers to gate-level'); + buf.writeln('# (instant - tiles are blackboxes, only glue logic remains)'); + buf.writeln('yosys proc'); + buf.writeln('yosys techmap'); + buf.writeln('yosys dfflibmap -liberty \$LIB_FILE'); buf.writeln('yosys abc -liberty \$LIB_FILE'); - buf.writeln(); - buf.writeln('# Map tie-high/tie-low to PDK cells'); buf.writeln( 'yosys hilomap -hicell \${CELL_LIB}__tieh Z ' '-locell \${CELL_LIB}__tiel ZN', ); - buf.writeln(); - buf.writeln('# Clean up'); buf.writeln('yosys opt_clean -purge'); + buf.writeln('yosys check'); buf.writeln(); - } - void _writeOutput(StringBuffer buf) { - buf.writeln('# Write outputs'); - buf.writeln('yosys write_verilog -noattr \${DEVICE_NAME}_synth.v'); + buf.writeln('# Write final netlist'); + buf.writeln('yosys write_verilog -noattr -noexpr \${DEVICE_NAME}_synth.v'); buf.writeln('yosys stat -liberty \$LIB_FILE'); buf.writeln(); + + return buf.toString(); } } diff --git a/pkgs/aegis-tapeout/default.nix b/pkgs/aegis-tapeout/default.nix index 4f33bf5..e02bee4 100644 --- a/pkgs/aegis-tapeout/default.nix +++ b/pkgs/aegis-tapeout/default.nix @@ -22,6 +22,10 @@ lib.extendMkDerivation { "dieWidthUm" "dieHeightUm" "coreUtilization" + "macroHaloUm" + "gridMarginUm" + "tileUtilization" + "tileDieSizes" ]; extendDrvArgs = @@ -34,6 +38,10 @@ lib.extendMkDerivation { dieWidthUm ? null, dieHeightUm ? null, coreUtilization ? 0.5, + macroHaloUm ? 20, + gridMarginUm ? 20, + tileUtilization ? 0.85, + tileDieSizes ? { }, ... }@args: @@ -45,9 +53,293 @@ lib.extendMkDerivation { ) "aegis-tapeout: coreUtilization must be in (0, 1], got ${toString coreUtilization}"; let - deviceName = aegis-ip.deviceName; + inherit (aegis-ip) deviceName; pdkPath = "${pdk}/${pdk.pdkPath}"; libsRef = "${pdkPath}/libs.ref/${cellLib}"; + + # Default tile die sizes per PDK + defaultTileDieSizes = + { + gf180mcu = { + Tile = { + w = 155; + h = 95; + }; + IOTile = { + w = 105; + h = 30; + }; + ClockTile = { + w = 230; + h = 145; + }; + BramTile = { + w = 27; + h = 45; + }; + DspBasicTile = { + w = 71; + h = 42; + }; + FabricConfigLoader = { + w = 30; + h = 12; + }; + SerDesTile = { + w = 853; + h = 132; + }; + }; + sky130 = { + # TODO: characterize tile sizes on sky130 + Tile = { + w = 100; + h = 60; + }; + IOTile = { + w = 70; + h = 20; + }; + ClockTile = { + w = 150; + h = 90; + }; + BramTile = { + w = 18; + h = 30; + }; + DspBasicTile = { + w = 45; + h = 27; + }; + FabricConfigLoader = { + w = 20; + h = 8; + }; + SerDesTile = { + w = 550; + h = 85; + }; + }; + } + .${pdk.pdkName} or { }; + + # Merge user overrides on top of defaults + effectiveTileDieSizes = defaultTileDieSizes // tileDieSizes; + + # Find PDK files at eval time + libFile = "${libsRef}/lib"; + techLefDir = "${libsRef}/lef"; + + mkTileMacro = + tileModule: + stdenv.mkDerivation { + name = "aegis-tile-${lib.toLower tileModule}-${deviceName}"; + + dontUnpack = true; + dontConfigure = true; + + nativeBuildInputs = [ + yosys + openroad + klayout + ]; + + buildPhase = '' + runHook preBuild + + # Find PDK files + LIB_FILE=$(find ${libFile} -name '*tt*' -name '*.lib' -print -quit) + if [ -z "$LIB_FILE" ]; then + LIB_FILE=$(find ${libFile} -name '*.lib' -print -quit) + fi + TECH_LEF=$(find ${techLefDir} -name '*tech*.lef' -print -quit) + + # Skip if this tile type doesn't exist in the device + if [ ! -f "${aegis-ip}/${deviceName}-yosys-${tileModule}.tcl" ]; then + echo "Skipping ${tileModule} (not present in device)" + mkdir -p $out + exit 0 + fi + + echo "=== Synthesizing ${tileModule} ===" + cat > synth.tcl << EOF + set SV_FILE "${aegis-ip}/${deviceName}.sv" + set LIB_FILE "$LIB_FILE" + set CELL_LIB "${cellLib}" + set DEVICE_NAME "${deviceName}" + source ${aegis-ip}/${deviceName}-yosys-${tileModule}.tcl + EOF + yosys -c synth.tcl 2>&1 | tee yosys.log + + echo "=== PnR ${tileModule} macro ===" + cat > pnr.tcl << EOF + set LIB_FILE "$LIB_FILE" + set TECH_LEF "$TECH_LEF" + set CELL_LEF_DIR "${techLefDir}" + set DEVICE_NAME "${deviceName}" + set SITE_NAME "${pdk.siteName}" + set CELL_LIB "${cellLib}" + set CLK_PERIOD ${toString clockPeriodNs} + set TILE_UTIL ${toString tileUtilization} + ${lib.optionalString (builtins.hasAttr tileModule effectiveTileDieSizes) '' + set TILE_DIE_W ${toString effectiveTileDieSizes.${tileModule}.w} + set TILE_DIE_H ${toString effectiveTileDieSizes.${tileModule}.h} + ''} + source ${aegis-ip}/${deviceName}-openroad-${tileModule}.tcl + EOF + openroad -threads $NIX_BUILD_CORES -exit pnr.tcl 2>&1 | tee openroad.log + + echo "=== GDS for ${tileModule} ===" + if [ -f "${deviceName}_${tileModule}_final.def" ]; then + CELL_GDS_DIR="${libsRef}/gds" \ + LEF_DIR="${techLefDir}" \ + TECH_LEF="$TECH_LEF" \ + DEF_FILE="${deviceName}_${tileModule}_final.def" \ + OUT_GDS="${deviceName}_${tileModule}_final.gds" \ + QT_QPA_PLATFORM=offscreen \ + klayout -b -r ${./scripts/def2gds.py} 2>&1 | tee klayout.log || true + fi + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + mkdir -p $out + cp ${deviceName}_${tileModule}_synth.v $out/ 2>/dev/null || true + cp ${deviceName}_${tileModule}_final.def $out/ 2>/dev/null || true + cp ${deviceName}_${tileModule}_final.v $out/ 2>/dev/null || true + cp ${deviceName}_${tileModule}_final.gds $out/ 2>/dev/null || true + cp ${deviceName}_${tileModule}.lef $out/ 2>/dev/null || true + cp ${deviceName}_${tileModule}.lib $out/ 2>/dev/null || true + cp ${tileModule}_timing.rpt $out/ 2>/dev/null || true + cp ${tileModule}_area.rpt $out/ 2>/dev/null || true + cp yosys.log $out/ 2>/dev/null || true + cp openroad.log $out/ 2>/dev/null || true + runHook postInstall + ''; + }; + + # Build all tile macros + tileMacros = builtins.listToAttrs ( + map + (mod: { + name = mod; + value = mkTileMacro mod; + }) + [ + "Tile" + "BramTile" + "DspBasicTile" + "ClockTile" + "IOTile" + "SerDesTile" + "FabricConfigLoader" + ] + ); + + topSynth = stdenv.mkDerivation { + name = "aegis-top-synth-${deviceName}"; + + dontUnpack = true; + dontConfigure = true; + + nativeBuildInputs = [ yosys ]; + + buildPhase = '' + runHook preBuild + + LIB_FILE=$(find ${libFile} -name '*tt*' -name '*.lib' -print -quit) + if [ -z "$LIB_FILE" ]; then + LIB_FILE=$(find ${libFile} -name '*.lib' -print -quit) + fi + + echo "=== Top-level assembly ===" + cat > synth.tcl << EOF + set SV_FILE "${aegis-ip}/${deviceName}.sv" + set LIB_FILE "$LIB_FILE" + set CELL_LIB "${cellLib}" + set DEVICE_NAME "${deviceName}" + set STUBS_V "${aegis-ip}/${deviceName}_tile_stubs.v" + source ${aegis-ip}/${deviceName}-yosys.tcl + EOF + yosys -c synth.tcl 2>&1 | tee yosys.log + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + mkdir -p $out + cp ${deviceName}_synth.v $out/ 2>/dev/null || true + cp yosys.log $out/ 2>/dev/null || true + runHook postInstall + ''; + }; + + topPnr = stdenv.mkDerivation { + name = "aegis-top-pnr-${deviceName}"; + + dontUnpack = true; + dontConfigure = true; + + nativeBuildInputs = [ openroad ]; + + buildPhase = '' + runHook preBuild + + LIB_FILE=$(find ${libFile} -name '*tt*' -name '*.lib' -print -quit) + if [ -z "$LIB_FILE" ]; then + LIB_FILE=$(find ${libFile} -name '*.lib' -print -quit) + fi + TECH_LEF=$(find ${techLefDir} -name '*tech*.lef' -print -quit) + + # Copy tile macro LEFs and liberty timing models into working directory + ${lib.concatMapStringsSep "\n" (mod: '' + cp ${tileMacros.${mod}}/${deviceName}_${mod}.lef . 2>/dev/null || true + cp ${tileMacros.${mod}}/${deviceName}_${mod}.lib . 2>/dev/null || true + '') (builtins.attrNames tileMacros)} + + cat > constraints.sdc << EOF + create_clock [get_ports clk] -name clk -period ${toString clockPeriodNs} + EOF + + echo "=== Top-level PnR (macro-based) ===" + cat > pnr.tcl << OPENROAD_EOF + set LIB_FILE "$LIB_FILE" + set TECH_LEF "$TECH_LEF" + set CELL_LEF_DIR "${techLefDir}" + set SYNTH_V "${topSynth}/${deviceName}_synth.v" + set SDC_FILE "constraints.sdc" + set DEVICE_NAME "${deviceName}" + set SITE_NAME "${pdk.siteName}" + set UTILIZATION ${toString coreUtilization} + set CELL_LIB "${cellLib}" + set MACRO_HALO ${toString macroHaloUm} + set GRID_MARGIN ${toString gridMarginUm} + ${lib.optionalString (dieWidthUm != null && dieHeightUm != null) '' + set DIE_AREA "0 0 ${toString dieWidthUm} ${toString dieHeightUm}" + ''} + source ${aegis-ip}/${deviceName}-openroad.tcl + OPENROAD_EOF + openroad -threads $NIX_BUILD_CORES -exit pnr.tcl 2>&1 | tee openroad.log + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + mkdir -p $out + cp ${deviceName}_final.def $out/ 2>/dev/null || true + cp ${deviceName}_final.v $out/ 2>/dev/null || true + cp timing.rpt $out/ 2>/dev/null || true + cp area.rpt $out/ 2>/dev/null || true + cp power.rpt $out/ 2>/dev/null || true + cp openroad.log $out/ 2>/dev/null || true + runHook postInstall + ''; + }; in builtins.removeAttrs args [ "pdk" @@ -56,6 +348,10 @@ lib.extendMkDerivation { "dieWidthUm" "dieHeightUm" "coreUtilization" + "macroHaloUm" + "gridMarginUm" + "tileUtilization" + "tileDieSizes" ] // { inherit name; @@ -64,106 +360,56 @@ lib.extendMkDerivation { dontConfigure = true; nativeBuildInputs = (args.nativeBuildInputs or [ ]) ++ [ - yosys - openroad - xschem klayout ]; buildPhase = '' runHook preBuild - # Find PDK files (use -print -quit to avoid SIGPIPE) - LIB_FILE=$(find ${libsRef}/lib -name '*tt*' -name '*.lib' -print -quit) - if [ -z "$LIB_FILE" ]; then - LIB_FILE=$(find ${libsRef}/lib -name '*.lib' -print -quit) - fi - echo "Using liberty: $LIB_FILE" - - TECH_LEF=$(find ${libsRef}/lef -name '*tech*.lef' -print -quit) - echo "Using tech LEF: $TECH_LEF" - - # ================================================================ - # Stage 1: Yosys synthesis - # ================================================================ - echo "=== Stage 1: Yosys synthesis ===" - - # Write TCL prologue with variables, then source generated script - cat > synth.tcl << YOSYS_EOF - set SV_FILE "${aegis-ip}/${deviceName}.sv" - set LIB_FILE "$LIB_FILE" - set CELL_LIB "${cellLib}" - set DEVICE_NAME "${deviceName}" - source ${aegis-ip}/${deviceName}-yosys.tcl - YOSYS_EOF - yosys -c synth.tcl 2>&1 | tee yosys.log - - # ================================================================ - # Stage 2: SDC constraints - # ================================================================ - echo "=== Stage 2: Generating constraints ===" - - cat > constraints.sdc << EOF - create_clock [get_ports clk] -name clk -period ${toString clockPeriodNs} - EOF - - # ================================================================ - # Stage 3: OpenROAD place and route - # ================================================================ - echo "=== Stage 3: OpenROAD place and route ===" - - # Write TCL prologue with variables, then source generated script - cat > pnr.tcl << OPENROAD_EOF - set LIB_FILE "$LIB_FILE" - set TECH_LEF "$TECH_LEF" - set CELL_LEF_DIR "${libsRef}/lef" - set SYNTH_V "${deviceName}_synth.v" - set SDC_FILE "constraints.sdc" - set DEVICE_NAME "${deviceName}" - set SITE_NAME "${pdk.siteName}" - set UTILIZATION ${toString coreUtilization} - set CELL_LIB "${cellLib}" - ${lib.optionalString (dieWidthUm != null && dieHeightUm != null) '' - set DIE_AREA "0 0 ${toString dieWidthUm} ${toString dieHeightUm}" - ''} - source ${aegis-ip}/${deviceName}-openroad.tcl - OPENROAD_EOF - openroad -exit pnr.tcl 2>&1 | tee openroad.log - - # ================================================================ - # Stage 4: GDS generation via KLayout - # ================================================================ - echo "=== Stage 4: GDS generation ===" - - if [ -f "${deviceName}_final.def" ]; then - CELL_GDS=$(find ${libsRef}/gds -name '*.gds' -print -quit) - - if [ -n "$CELL_GDS" ]; then - CELL_GDS="$CELL_GDS" \ - DEF_FILE="${deviceName}_final.def" \ - OUT_GDS="${deviceName}.gds" \ - QT_QPA_PLATFORM=offscreen \ - klayout -b -r ${./scripts/def2gds.py} \ - > klayout.log 2>&1 || true - - # ================================================================ - # Stage 5: Render layout image - # ================================================================ - if [ -f "${deviceName}.gds" ]; then - echo "=== Stage 5: Render layout image ===" - - GDS_FILE="${deviceName}.gds" \ - OUT_PNG="${deviceName}_layout.png" \ - TOP_CELL_NAME="$TOP_MODULE" \ - QT_QPA_PLATFORM=offscreen \ - klayout -b -r ${./scripts/render_layout.py} \ - >> klayout.log 2>&1 || true + echo "=== GDS generation ===" + + if [ -f "${topPnr}/${deviceName}_final.def" ]; then + # Collect tile macro GDS files into one directory + mkdir -p macro_gds + ${lib.concatMapStringsSep "\n" (mod: '' + if [ -f "${tileMacros.${mod}}/${deviceName}_${mod}_final.gds" ]; then + cp ${tileMacros.${mod}}/${deviceName}_${mod}_final.gds macro_gds/ fi - else - echo "Warning: No cell GDS found, skipping GDS generation" + '') (builtins.attrNames tileMacros)} + + TECH_LEF=$(find ${libsRef}/lef -name '*tech*.lef' -print -quit) + + CELL_GDS_DIR="${libsRef}/gds" \ + MACRO_GDS_DIR="macro_gds" \ + LEF_DIR="${libsRef}/lef" \ + TECH_LEF="$TECH_LEF" \ + DEF_FILE="${topPnr}/${deviceName}_final.def" \ + OUT_GDS="${deviceName}.gds" \ + QT_QPA_PLATFORM=offscreen \ + klayout -b -r ${./scripts/def2gds.py} \ + 2>&1 | tee klayout.log || true + + if [ -f "${deviceName}.gds" ]; then + echo "=== Stamp Nix store path ===" + + GDS_FILE="${deviceName}.gds" \ + STAMP_TEXT="$out" \ + LAYER="${toString pdk.commentLayer.layer}" \ + DATATYPE="${toString pdk.commentLayer.datatype}" \ + QT_QPA_PLATFORM=offscreen \ + klayout -b -r ${./scripts/stamp_text.py} \ + 2>&1 | tee -a klayout.log + + echo "=== Render layout image ===" + + GDS_FILE="${deviceName}.gds" \ + OUT_PNG="${deviceName}_layout.png" \ + QT_QPA_PLATFORM=offscreen \ + klayout -b -r ${./scripts/render_layout.py} \ + >> klayout.log 2>&1 || true fi else - echo "Warning: No DEF output from OpenROAD, skipping GDS generation" + echo "Warning: No DEF output from top-level PnR, skipping GDS generation" fi runHook postBuild @@ -174,30 +420,33 @@ lib.extendMkDerivation { mkdir -p $out - # Synthesis artifacts - cp ${deviceName}_synth.v $out/ 2>/dev/null || true - cp synth.ys $out/${deviceName}-yosys.tcl 2>/dev/null || true - cp yosys.log $out/ 2>/dev/null || true - cp constraints.sdc $out/ 2>/dev/null || true - - # PnR artifacts - cp ${deviceName}_final.def $out/ 2>/dev/null || true - cp ${deviceName}_final.v $out/ 2>/dev/null || true - cp pnr.tcl $out/${deviceName}-openroad.tcl 2>/dev/null || true - cp openroad.log $out/ 2>/dev/null || true - cp timing.rpt $out/ 2>/dev/null || true - cp area.rpt $out/ 2>/dev/null || true - cp power.rpt $out/ 2>/dev/null || true - - # GDS for fab submission + # Tile macro artifacts + mkdir -p $out/macros + ${lib.concatMapStringsSep "\n" (mod: '' + if [ -d "${tileMacros.${mod}}" ]; then + cp -r ${tileMacros.${mod}}/* $out/macros/ 2>/dev/null || true + fi + '') (builtins.attrNames tileMacros)} + + # Top-level synthesis + cp ${topSynth}/${deviceName}_synth.v $out/ 2>/dev/null || true + cp ${topSynth}/yosys.log $out/yosys_top.log 2>/dev/null || true + + # Top-level PnR + cp ${topPnr}/${deviceName}_final.def $out/ 2>/dev/null || true + cp ${topPnr}/${deviceName}_final.v $out/ 2>/dev/null || true + cp ${topPnr}/openroad.log $out/openroad_top.log 2>/dev/null || true + cp ${topPnr}/timing.rpt $out/ 2>/dev/null || true + cp ${topPnr}/area.rpt $out/ 2>/dev/null || true + cp ${topPnr}/power.rpt $out/ 2>/dev/null || true + + # GDS cp ${deviceName}.gds $out/ 2>/dev/null || true cp ${deviceName}_layout.png $out/ 2>/dev/null || true cp klayout.log $out/ 2>/dev/null || true - # Source IP artifacts for reference + # Source IP for reference cp ${aegis-ip}/${deviceName}.json $out/ 2>/dev/null || true - cp ${aegis-ip}/${deviceName}-xschem.tcl $out/ 2>/dev/null || true - cp ${aegis-ip}/${deviceName}-xschem.sch $out/ 2>/dev/null || true runHook postInstall ''; @@ -208,6 +457,13 @@ lib.extendMkDerivation { cellLib clockPeriodNs coreUtilization + macroHaloUm + gridMarginUm + tileUtilization + tileDieSizes + tileMacros + topSynth + topPnr ; inherit (aegis-ip) deviceName diff --git a/pkgs/aegis-tapeout/scripts/def2gds.py b/pkgs/aegis-tapeout/scripts/def2gds.py index 20ea65d..78f9659 100644 --- a/pkgs/aegis-tapeout/scripts/def2gds.py +++ b/pkgs/aegis-tapeout/scripts/def2gds.py @@ -1,21 +1,57 @@ -"""Convert a DEF file to GDS, merging with cell GDS library. +"""Convert a DEF file to GDS, merging with cell and macro GDS libraries. Environment variables: - CELL_GDS - path to cell GDS library - DEF_FILE - path to routed DEF file - OUT_GDS - output GDS path + CELL_GDS_DIR - directory containing PDK standard cell GDS files + MACRO_GDS_DIR - directory containing tile macro GDS files (optional) + LEF_DIR - directory containing LEF files for cell/macro definitions + TECH_LEF - path to tech LEF file + DEF_FILE - path to routed DEF file + OUT_GDS - output GDS path """ +import glob import os + import pya -cell_gds = os.environ["CELL_GDS"] +cell_gds_dir = os.environ["CELL_GDS_DIR"] +macro_gds_dir = os.environ.get("MACRO_GDS_DIR", "") +lef_dir = os.environ.get("LEF_DIR", "") +tech_lef = os.environ.get("TECH_LEF", "") def_file = os.environ["DEF_FILE"] out_gds = os.environ["OUT_GDS"] layout = pya.Layout() -layout.read(cell_gds) + +# Read tech LEF first (layer definitions) +if tech_lef and os.path.exists(tech_lef): + print(f"Reading tech LEF: {tech_lef}") + layout.read(tech_lef) + +# Read all cell LEF files for geometry definitions +if lef_dir and os.path.isdir(lef_dir): + lef_files = sorted(glob.glob(os.path.join(lef_dir, "*.lef"))) + print(f"Reading {len(lef_files)} cell LEF files from {lef_dir}") + for lef in lef_files: + if "tech" not in os.path.basename(lef).lower(): + layout.read(lef) + +# Read all standard cell GDS files from the PDK +gds_files = sorted(glob.glob(os.path.join(cell_gds_dir, "*.gds"))) +print(f"Reading {len(gds_files)} cell GDS files from {cell_gds_dir}") +for gds in gds_files: + layout.read(gds) + +# Read tile macro GDS files +if macro_gds_dir and os.path.isdir(macro_gds_dir): + macro_files = sorted(glob.glob(os.path.join(macro_gds_dir, "*.gds"))) + print(f"Reading {len(macro_files)} macro GDS files from {macro_gds_dir}") + for gds in macro_files: + layout.read(gds) + +# Read the routed DEF (references cells and macros by name) +print(f"Reading DEF: {def_file}") layout.read(def_file) -layout.write(out_gds) +layout.write(out_gds) print(f"Wrote {out_gds}") diff --git a/pkgs/aegis-tapeout/scripts/render_layout.py b/pkgs/aegis-tapeout/scripts/render_layout.py index 5f5b419..be9441d 100644 --- a/pkgs/aegis-tapeout/scripts/render_layout.py +++ b/pkgs/aegis-tapeout/scripts/render_layout.py @@ -51,19 +51,11 @@ view.set_current_cell_path(cv, [top_cell.cell_index()]) view.set_config("background-color", "#000000") - # Find the bounding box of child cell instances (placed standard cells) - # rather than the top cell bbox (which includes the die boundary) - content_bbox = pya.Box() - for inst in top_cell.each_inst(): - content_bbox += inst.bbox() - - if not content_bbox.empty(): - dbox = pya.DBox(content_bbox) * layout.dbu - margin = max(dbox.width(), dbox.height()) * 0.1 - dbox = dbox.enlarged(margin, margin) - view.zoom_box(dbox) - else: - view.zoom_fit() + # Zoom to the full top cell bounding box (die boundary) + dbox = pya.DBox(top_cell.bbox()) * layout.dbu + margin = max(dbox.width(), dbox.height()) * 0.05 + dbox = dbox.enlarged(margin, margin) + view.zoom_box(dbox) view.save_image(out_png, img_w, img_h) print(f"Rendered {top_cell.name} ({img_w}x{img_h}) to {out_png}") diff --git a/pkgs/aegis-tapeout/scripts/stamp_text.py b/pkgs/aegis-tapeout/scripts/stamp_text.py new file mode 100644 index 0000000..287a1d5 --- /dev/null +++ b/pkgs/aegis-tapeout/scripts/stamp_text.py @@ -0,0 +1,91 @@ +"""Stamp text into a GDS file on a comment/documentation layer. + +Places text (e.g. a Nix store path) as polygon outlines on a +non-routing layer so it's visible in the die but doesn't affect +DRC or signal integrity. + +Environment variables: + GDS_FILE - path to input GDS file (modified in-place) + STAMP_TEXT - text string to stamp + LAYER - (optional) GDS layer number, default 236 + DATATYPE - (optional) GDS datatype, default 0 + FONT_SIZE - (optional) text height in microns, default 50 + X_OFFSET - (optional) X position in microns from left edge, default 50 + Y_OFFSET - (optional) Y position in microns from top edge, default 50 +""" + +import os + +import pya + +gds_file = os.environ["GDS_FILE"] +stamp_text = os.environ["STAMP_TEXT"] +layer_num = int(os.environ.get("LAYER", "236")) +datatype = int(os.environ.get("DATATYPE", "0")) +font_height_um = float(os.environ.get("FONT_SIZE", "80")) +x_offset = float(os.environ.get("X_OFFSET", "50")) +y_offset = float(os.environ.get("Y_OFFSET", "50")) + +layout = pya.Layout() +layout.read(gds_file) + +# Find top cell (largest bounding box) +top_cell = None +best_area = 0 +for ci in range(layout.cells()): + c = layout.cell(ci) + bbox = c.bbox() + area = bbox.width() * bbox.height() + if area > best_area: + best_area = area + top_cell = c + +if top_cell is None: + print("Warning: No cells found, skipping text stamp") +else: + li = layout.layer(layer_num, datatype) + dbu = layout.dbu + die_bbox = top_cell.bbox() + available_w_um = (die_bbox.width() * dbu) - 2 * x_offset + + gen = pya.TextGenerator.default_generator() + + # text() args: string, target_dbu, height_in_microns + # Probe a single character to measure width + probe = gen.text("X", dbu, font_height_um) + char_w_um = probe.bbox().width() * dbu + + # Split text into lines that fit within the available die width + if char_w_um > 0: + chars_per_line = max(1, int(available_w_um / (char_w_um * 1.1))) + else: + chars_per_line = len(stamp_text) + + lines = [] + for i in range(0, len(stamp_text), chars_per_line): + lines.append(stamp_text[i : i + chars_per_line]) + + # Render each line and stack top-down from the top of the die + line_spacing_um = font_height_um * 1.4 + anchor_x_dbu = die_bbox.left + int(x_offset / dbu) + anchor_y_dbu = die_bbox.top - int(y_offset / dbu) + + for i, line in enumerate(lines): + text_region = gen.text(line, dbu, font_height_um) + text_bbox = text_region.bbox() + + dx = anchor_x_dbu - text_bbox.left + dy = anchor_y_dbu - text_bbox.top - int(i * line_spacing_um / dbu) + text_region.move(dx, dy) + + top_cell.shapes(li).insert(text_region) + + tmp_file = gds_file + ".tmp.gds" + layout.write(tmp_file) + os.replace(tmp_file, gds_file) + + total_h = len(lines) * line_spacing_um + print( + f"Stamped {len(lines)} line(s) on layer {layer_num}/{datatype}, " + f"height {font_height_um:.0f} um/line, total {total_h:.0f} um tall" + ) diff --git a/pkgs/gf180mcu-pdk/default.nix b/pkgs/gf180mcu-pdk/default.nix index c08cc31..47aa61e 100644 --- a/pkgs/gf180mcu-pdk/default.nix +++ b/pkgs/gf180mcu-pdk/default.nix @@ -96,6 +96,10 @@ stdenvNoCC.mkDerivation { siteName = "GF018hv5v_mcu_sc7"; pdkName = "gf180mcu"; pdkPath = "share/pdk/gf180mcu"; + commentLayer = { + layer = 236; + datatype = 0; + }; }; meta = { diff --git a/pkgs/nextpnr-aegis/default.nix b/pkgs/nextpnr-aegis/default.nix index c0c9901..29a7578 100644 --- a/pkgs/nextpnr-aegis/default.nix +++ b/pkgs/nextpnr-aegis/default.nix @@ -1,6 +1,5 @@ { nextpnr, - aegisSrc ? ../.., }: nextpnr.overrideAttrs (old: { @@ -24,8 +23,8 @@ nextpnr.overrideAttrs (old: { postPatch = (old.postPatch or "") + '' # Add Aegis viaduct uarch mkdir -p generic/viaduct/aegis - cp ${aegisSrc}/nextpnr-aegis/aegis.cc generic/viaduct/aegis/aegis.cc - cp ${aegisSrc}/nextpnr-aegis/aegis_test.cc generic/viaduct/aegis/aegis_test.cc + cp ${../../nextpnr-aegis/aegis.cc} generic/viaduct/aegis/aegis.cc + cp ${../../nextpnr-aegis/aegis_test.cc} generic/viaduct/aegis/aegis_test.cc # Register uarch source in CMakeLists.txt sed -i '/viaduct\/example\/example.cc/a\ viaduct/aegis/aegis.cc' generic/CMakeLists.txt diff --git a/pkgs/sky130-pdk/default.nix b/pkgs/sky130-pdk/default.nix index bf7dc5f..867497d 100644 --- a/pkgs/sky130-pdk/default.nix +++ b/pkgs/sky130-pdk/default.nix @@ -55,6 +55,10 @@ stdenvNoCC.mkDerivation { siteName = "unithd"; pdkName = "sky130"; pdkPath = "share/pdk/sky130"; + commentLayer = { + layer = 236; + datatype = 0; + }; }; meta = {