diff --git a/README.md b/README.md index a435d53..1315a6a 100644 --- a/README.md +++ b/README.md @@ -23,17 +23,17 @@ foundry via open PDKs and shuttle services like ### Aegis Luna 1 A compact Aegis device targeting GF180MCU via [wafer.space](https://wafer.space) -(1x1 Full slot, 3.93 x 5.12mm die). -| Resource | Count | -|-------------------|---------------| -| LUT4 | ~760 | -| BRAM (128x8) | 40 tiles | -| DSP18 (18x18 MAC) | 40 tiles | -| I/O pads | 118 | -| SerDes | 1 | -| Clock tiles | 1 (4 outputs) | -| Routing tracks | 1 per edge | +| Resource | Count | +|-------------------|---------------------------| +| Fabric | 23 x 23 tiles (529 total) | +| LUT4 (CLB) | 437 | +| BRAM (128x8) | 46 tiles | +| DSP18 (18x18 MAC) | 46 tiles | +| I/O tiles | 92 (25 bonded bidir) | +| SerDes | 0 | +| Clock tiles | 1 (4 outputs) | +| Routing tracks | 1 per edge | ```bash nix build .#luna-1 # Generate IP (SV, JSON, chipdb, techmap) @@ -84,12 +84,12 @@ The tapeout pipeline synthesizes the FPGA fabric itself to PDK standard cells: ```bash nix build .#terra-1-tapeout ls result/ -# terra_1_synth.v — gate-level netlist (Yosys) -# terra_1_final.def — placed & routed layout (OpenROAD) -# terra_1.gds — GDS2 for fab submission -# terra_1_layout.png — layout render -# timing.rpt — timing analysis -# power.rpt — power report +# terra_1_synth.v - gate-level netlist (Yosys) +# terra_1_final.def - placed & routed layout (OpenROAD) +# terra_1.gds - GDS2 for fab submission +# terra_1_layout.png - layout render +# timing.rpt - timing analysis +# power.rpt - power report ``` Supports GF180MCU (wafer.space) and Sky130 PDKs. @@ -175,20 +175,20 @@ clock tiles -> IO tiles -> SerDes tiles -> fabric tiles (row-major). ## Related Projects -- **[OpenFPGA](https://github.com/lnis-uofu/OpenFPGA)** — An open-source FPGA +- **[OpenFPGA](https://github.com/lnis-uofu/OpenFPGA)** - An open-source FPGA IP generator from the University of Utah. Given an XML architecture description, it generates synthesizable Verilog for a complete FPGA fabric along with bitstream tooling and self-testing infrastructure. Silicon-proven through DARPA's POSH program. -- **[FABulous](https://github.com/FPGA-Research-Manchester/FABulous)** — An +- **[FABulous](https://github.com/FPGA-Research-Manchester/FABulous)** - An open-source embedded FPGA (eFPGA) framework from the University of Manchester. Generates custom FPGA fabric from CSV-based configuration and integrates Yosys and nextpnr. Silicon-proven with 12+ tapeouts across nodes from TSMC 180nm down to 28nm CMOS. - **[Cologne Chip GateMate](https://colognechip.com/programmable-logic/gatemate/)** - — A commercial FPGA on GlobalFoundries 28nm with a fully open-source, + - A commercial FPGA on GlobalFoundries 28nm with a fully open-source, license-free toolchain built on Yosys, nextpnr, and openFPGALoader. The silicon itself is proprietary, but it is notable as one of the few commercial FPGAs to embrace open-source EDA tools end-to-end. diff --git a/crates/aegis-ip/src/tile_bits.rs b/crates/aegis-ip/src/tile_bits.rs index 8c18b95..4bb12a5 100644 --- a/crates/aegis-ip/src/tile_bits.rs +++ b/crates/aegis-ip/src/tile_bits.rs @@ -9,7 +9,7 @@ //! [18..18+4*ISW-1] input mux sel0..sel3 (ISW = input_sel_width(T)) //! [18+4*ISW..] per-track output: 4 dirs × T tracks × (1 en + 3 sel) //! -//! For T=1: 46 bits (backward compatible with original layout) +//! For T=1: 50 bits //! For T=4: 102 bits // --- Fixed offsets (track-independent) --- @@ -33,9 +33,10 @@ pub const OUTPUT_SEL_WIDTH: usize = 3; // --- Parametric layout functions --- /// Width of input select field for T tracks. -/// Encodes: N0..N(T-1), E0..E(T-1), S0..S(T-1), W0..W(T-1), CLB_OUT, const0, const1 +/// Encodes: N0..N(T-1), E0..E(T-1), S0..S(T-1), W0..W(T-1), +/// CLB_OUT, const0, const1, NB_N, NB_E, NB_S, NB_W pub fn input_sel_width(tracks: usize) -> usize { - let n = 4 * tracks + 3; + let n = 4 * tracks + 7; (usize::BITS - (n - 1).leading_zeros()) as usize } @@ -84,6 +85,11 @@ pub fn mux_const1(tracks: usize) -> u64 { (4 * tracks + 2) as u64 } +/// Input mux select value for neighbor CLB output (direction 0=N, 1=E, 2=S, 3=W). +pub fn mux_neighbor(dir: usize, tracks: usize) -> u64 { + (4 * tracks + 3 + dir) as u64 +} + // --- Bitstream read/write helpers --- /// Set a single bit in a bitstream buffer. diff --git a/crates/aegis-ip/src/tile_bits_tests.rs b/crates/aegis-ip/src/tile_bits_tests.rs index bfc7545..853a39e 100644 --- a/crates/aegis-ip/src/tile_bits_tests.rs +++ b/crates/aegis-ip/src/tile_bits_tests.rs @@ -3,8 +3,9 @@ use super::*; // === Layout formula tests === #[test] -fn t1_backward_compatible_width() { - assert_eq!(tile_config_width(1), 46); +fn t1_width() { + // 18 + 4*4 + 4*1*4 = 18 + 16 + 16 = 50 + assert_eq!(tile_config_width(1), 50); } #[test] @@ -21,36 +22,34 @@ fn t4_width() { #[test] fn input_sel_width_values() { - assert_eq!(input_sel_width(1), 3); // 7 values -> 3 bits - assert_eq!(input_sel_width(2), 4); // 11 values -> 4 bits - assert_eq!(input_sel_width(4), 5); // 19 values -> 5 bits + assert_eq!(input_sel_width(1), 4); // 11 values -> 4 bits + assert_eq!(input_sel_width(2), 4); // 15 values -> 4 bits + assert_eq!(input_sel_width(4), 5); // 23 values -> 5 bits } #[test] fn input_sel_offsets_t1() { assert_eq!(input_sel_offset(0, 1), 18); - assert_eq!(input_sel_offset(1, 1), 21); - assert_eq!(input_sel_offset(2, 1), 24); - assert_eq!(input_sel_offset(3, 1), 27); + assert_eq!(input_sel_offset(1, 1), 22); + assert_eq!(input_sel_offset(2, 1), 26); + assert_eq!(input_sel_offset(3, 1), 30); } #[test] fn output_base_t1() { - // 18 + 4*3 = 30 - assert_eq!(output_base(1), 30); + // 18 + 4*4 = 34 + assert_eq!(output_base(1), 34); } #[test] -fn output_offsets_t1_match_original_layout() { - // Original layout: EN_NORTH=30, EN_EAST=31, EN_SOUTH=32, EN_WEST=33 - // SEL_NORTH=34, SEL_EAST=37, SEL_SOUTH=40, SEL_WEST=43 - // New layout: output_en(dir, 0, 1) = 30 + dir*4, output_sel(dir, 0, 1) = 30 + dir*4 + 1 - assert_eq!(output_en(0, 0, 1), 30); // EN_NORTH - assert_eq!(output_sel(0, 0, 1), 31); // SEL_NORTH at 31 (was 34) - // Note: the new layout packs (en, sel[2:0]) as 4 contiguous bits per track, - // which differs from the original layout where enables were grouped together. - // For T=1 the total width is still 46, but the bit positions within the - // output section differ. The Dart tile_config.dart uses the new layout. +fn output_offsets_t1() { + // output_base(1) = 34 + assert_eq!(output_en(0, 0, 1), 34); // EN_NORTH + assert_eq!(output_sel(0, 0, 1), 35); // SEL_NORTH + assert_eq!(output_en(1, 0, 1), 38); // EN_EAST + assert_eq!(output_en(2, 0, 1), 42); // EN_SOUTH + assert_eq!(output_en(3, 0, 1), 46); // EN_WEST + // Last bit: 46 + 3 = 49, total width = 50 } #[test] @@ -73,7 +72,7 @@ fn output_offsets_t4() { // === Mux select value tests === #[test] -fn mux_values_t1_backward_compatible() { +fn mux_values_t1() { assert_eq!(mux_dir_track(0, 0, 1), 0); // N0 assert_eq!(mux_dir_track(1, 0, 1), 1); // E0 assert_eq!(mux_dir_track(2, 0, 1), 2); // S0 @@ -81,6 +80,10 @@ fn mux_values_t1_backward_compatible() { assert_eq!(mux_clb_out(1), 4); assert_eq!(mux_const0(1), 5); assert_eq!(mux_const1(1), 6); + assert_eq!(mux_neighbor(0, 1), 7); // NB_N + assert_eq!(mux_neighbor(1, 1), 8); // NB_E + assert_eq!(mux_neighbor(2, 1), 9); // NB_S + assert_eq!(mux_neighbor(3, 1), 10); // NB_W } #[test] @@ -100,7 +103,7 @@ fn mux_values_t4() { fn all_mux_values_fit_in_input_sel_width() { for tracks in [1, 2, 4, 8] { let isw = input_sel_width(tracks); - let max_val = mux_const1(tracks); + let max_val = mux_neighbor(3, tracks); assert!( max_val < (1 << isw), "max mux value {} doesn't fit in {} bits for T={}", @@ -395,7 +398,7 @@ fn max_lut_init_roundtrips() { #[test] fn max_sel_value_roundtrips() { for tracks in [1, 2, 4] { - let max_sel = mux_const1(tracks) as u8; + let max_sel = mux_neighbor(3, tracks) as u8; let mut cfg = TileConfig::default_for(tracks); cfg.sel = [max_sel; 4]; let mut bits = vec![0u8; (tile_config_width(tracks) + 7) / 8]; diff --git a/crates/aegis-pack/src/lib.rs b/crates/aegis-pack/src/lib.rs index 375c4c4..3efd871 100644 --- a/crates/aegis-pack/src/lib.rs +++ b/crates/aegis-pack/src/lib.rs @@ -363,7 +363,39 @@ fn pack_routing_pip( } } + // Neighbor direct connections: adjacent CLB output -> this tile's CLB input if src_gx != dst_gx || src_gy != dst_gy { + if let Some(rest) = dst_wire.strip_prefix("CLB_I") { + if let Ok(idx) = rest.parse::() { + if idx < 4 && (src_wire == "CLB_O" || src_wire == "CLB_Q") { + let nb_dir = if dy == 1 { + 0 + } + // src is north + else if dx == -1 { + 1 + } + // src is east + else if dy == -1 { + 2 + } + // src is south + else { + 3 + }; // src is west + if let Some(&(tile_offset, config_width)) = tile_offsets.get(&(dst_x, dst_y)) { + let min_width = tile_bits::tile_config_width(tracks); + if config_width >= min_width { + let base = fabric_base + tile_offset; + let isw = tile_bits::input_sel_width(tracks); + let sel_val = tile_bits::mux_neighbor(nb_dir, tracks); + let sel_offset = base + tile_bits::input_sel_offset(idx, tracks); + write_bits(bits, sel_offset, sel_val, isw); + } + } + } + } + } return; } @@ -428,9 +460,11 @@ fn parse_track_wire(wire: &str) -> Option<(usize, usize)> { Some((dir, track)) } -/// Extract the track number from a wire name (e.g., "S1" -> 1, "N0" -> 0). +/// Extract the track number from a wire name (e.g., "S1" -> 1, "N0" -> 0, "OUT_N0" -> 0). fn parse_track(wire: &str) -> Option { - parse_track_wire(wire).map(|(_, t)| t) + parse_track_wire(wire) + .or_else(|| parse_output_mux_wire(wire)) + .map(|(_, t)| t) } /// Parse a per-track output mux wire like "OUT_N0", "OUT_E3". @@ -529,7 +563,7 @@ mod tests { "device": "test", "fabric": { "width": 2, "height": 2, "tracks": 1, - "tile_config_width": 46, + "tile_config_width": 50, "bram": { "column_interval": 0, "columns": [], "data_width": null, "addr_width": null, "depth": null, "tile_config_width": 8 }, @@ -543,19 +577,19 @@ mod tests { "clock": { "tile_count": 0, "tile_config_width": 49, "outputs_per_tile": 4, "total_outputs": 0 }, "config": { - "total_bits": 248, + "total_bits": 264, "chain_order": [ { "section": "io_tiles", "count": 8, "bits_per_tile": 8, "total_bits": 64 }, { "section": "fabric_tiles", "count": 4, - "total_bits": 184 } + "total_bits": 200 } ] }, "tiles": [ - { "x": 0, "y": 0, "type": "lut", "config_width": 46, "config_offset": 0 }, - { "x": 1, "y": 0, "type": "lut", "config_width": 46, "config_offset": 46 }, - { "x": 0, "y": 1, "type": "lut", "config_width": 46, "config_offset": 92 }, - { "x": 1, "y": 1, "type": "lut", "config_width": 46, "config_offset": 138 } + { "x": 0, "y": 0, "type": "lut", "config_width": 50, "config_offset": 0 }, + { "x": 1, "y": 0, "type": "lut", "config_width": 50, "config_offset": 50 }, + { "x": 0, "y": 1, "type": "lut", "config_width": 50, "config_offset": 100 }, + { "x": 1, "y": 1, "type": "lut", "config_width": 50, "config_offset": 150 } ] }"#, ) @@ -656,7 +690,7 @@ mod tests { let pnr = test_pnr_with_lut(1, 0, "16'h1234"); let bits = pack(&desc, &pnr); - let init = read_bits(&bits, 64 + 46, 16); // tile (1,0) offset=46 + let init = read_bits(&bits, 64 + 50, 16); // tile (1,0) offset=50 assert_eq!(init, 0x1234); } @@ -713,8 +747,8 @@ mod tests { ); let bits = pack(&desc, &PnrOutput { modules }); - // tile (1,1) offset=138 - assert_ne!(read_bits(&bits, 64 + 138 + tile_bits::CARRY_MODE, 1), 0); + // tile (1,1) offset=150 + assert_ne!(read_bits(&bits, 64 + 150 + tile_bits::CARRY_MODE, 1), 0); } #[test] @@ -737,7 +771,7 @@ mod tests { let bits = pack(&desc, &pnr); let isw = tile_bits::input_sel_width(tracks); - let sel = read_bits(&bits, 64 + 46 + tile_bits::input_sel_offset(2, tracks), isw); + let sel = read_bits(&bits, 64 + 50 + tile_bits::input_sel_offset(2, tracks), isw); assert_eq!(sel, tile_bits::mux_dir_track(1, 0, tracks)); // E0 } @@ -779,14 +813,14 @@ mod tests { let pnr = test_pnr_with_routing(&["X2/Y2/OUT_W0/X2/Y2/CLB_Q"]); let bits = pack(&desc, &pnr); - // tile (1,1) offset=138 + // tile (1,1) offset=150 assert_ne!( - read_bits(&bits, 64 + 138 + tile_bits::output_en(3, 0, tracks), 1), + read_bits(&bits, 64 + 150 + tile_bits::output_en(3, 0, tracks), 1), 0 ); let sel = read_bits( &bits, - 64 + 138 + tile_bits::output_sel(3, 0, tracks), + 64 + 150 + tile_bits::output_sel(3, 0, tracks), tile_bits::OUTPUT_SEL_WIDTH, ); assert_eq!(sel, tile_bits::OUT_MUX_CLB); diff --git a/crates/aegis-sim/src/lib.rs b/crates/aegis-sim/src/lib.rs index d17521c..a7c7443 100644 --- a/crates/aegis-sim/src/lib.rs +++ b/crates/aegis-sim/src/lib.rs @@ -198,19 +198,19 @@ impl Simulator { false }; + // CLB output matches Dart: mux(carryMode, sum, mux(useFF, ffQ, lutOut)) let clb_out = if cfg.carry_mode { lut_out ^ carry_in + } else if cfg.ff_enable { + self.state[x][y].ff_q } else { lut_out }; self.next_state[x][y].lut_out = clb_out; self.next_state[x][y].carry_out = carry_out; - self.next_state[x][y].ff_q = if cfg.ff_enable { - clb_out - } else { - self.state[x][y].ff_q - }; + // FF always captures LUT output (Dart: Sequential(clk, [ffQ < lutOut])) + self.next_state[x][y].ff_q = lut_out; // Per-track output routing for dir in 0..4usize { @@ -287,13 +287,15 @@ impl Simulator { } /// Input mux: decode select value to get input signal. - /// Encoding: dir*T + track for directional, 4*T for CLB_OUT, 4*T+1 for const0, 4*T+2 for const1. + /// Encoding: dir*T + track for directional, 4*T for CLB_OUT, + /// 4*T+1 for const0, 4*T+2 for const1, 4*T+3..4*T+6 for neighbor N/E/S/W. fn select_input(&self, x: usize, y: usize, sel: u8) -> bool { let sel = sel as usize; let t = self.tracks; let clb_out_val = 4 * t; let const0_val = 4 * t + 1; let const1_val = 4 * t + 2; + let nb_base = 4 * t + 3; if sel < clb_out_val { let dir = sel / t; @@ -305,6 +307,20 @@ impl Simulator { false } else if sel == const1_val { true + } else if sel >= nb_base && sel < nb_base + 4 { + // Neighbor CLB output: N=0, E=1, S=2, W=3 + let nb_dir = sel - nb_base; + let (nx, ny) = match nb_dir { + 0 => (x, y.wrapping_sub(1)), // north + 1 => (x + 1, y), // east + 2 => (x, y + 1), // south + _ => (x.wrapping_sub(1), y), // west + }; + if nx < self.gw && ny < self.gh { + self.state[nx][ny].lut_out + } else { + false + } } else { false } diff --git a/crates/aegis-sim/src/main.rs b/crates/aegis-sim/src/main.rs index 0309ac9..0d4c061 100644 --- a/crates/aegis-sim/src/main.rs +++ b/crates/aegis-sim/src/main.rs @@ -41,6 +41,11 @@ struct Args { /// Clock pad by edge and position (e.g., w0) #[arg(long)] clock_pin: Option, + + /// Set a pin high for a cycle range: "w1:0-9" sets west pad 1 high + /// during cycles 0 through 9. Multiple allowed. + #[arg(long, value_delimiter = ',')] + set_pin: Vec, } fn main() { @@ -122,11 +127,47 @@ fn main() { eprintln!("Clock pad: {cp}"); } + // Parse --set-pin entries: "w1:0-9" -> (pad_idx, start_cycle, end_cycle) + let mut stimuli: Vec<(usize, u64, u64)> = Vec::new(); + for spec in &args.set_pin { + let parts: Vec<&str> = spec.split(':').collect(); + if parts.len() != 2 { + eprintln!("Invalid --set-pin format '{spec}', expected 'pin:start-end'"); + continue; + } + let pin = parts[0]; + let (edge, pos) = pin.split_at(1); + let p: usize = pos.parse().expect("Invalid pin position"); + let pad_idx = match edge { + "n" | "N" => p, + "e" | "E" => fw + p, + "s" | "S" => fw + fh + p, + "w" | "W" => 2 * fw + fh + p, + _ => { + eprintln!("Unknown edge '{edge}' in set-pin '{spec}'"); + continue; + } + }; + let range: Vec<&str> = parts[1].split('-').collect(); + let start: u64 = range[0].parse().expect("Invalid start cycle"); + let end: u64 = if range.len() > 1 { + range[1].parse().expect("Invalid end cycle") + } else { + args.cycles + }; + eprintln!("Set pin {pin} (pad {pad_idx}) high for cycles {start}-{end}"); + stimuli.push((pad_idx, start, end)); + } + for cycle in 0..args.cycles { // Toggle clock pad each cycle if let Some(cp) = clock_pad { sim.set_io(cp, cycle % 2 == 0); } + // Apply stimuli + for &(pad, start, end) in &stimuli { + sim.set_io(pad, cycle >= start && cycle <= end); + } sim.step(); if let Some(ref mut w) = vcd { diff --git a/crates/aegis-sim/src/tests.rs b/crates/aegis-sim/src/tests.rs index 250f039..dfe6e28 100644 --- a/crates/aegis-sim/src/tests.rs +++ b/crates/aegis-sim/src/tests.rs @@ -99,15 +99,37 @@ fn ff_captures_lut_output() { } #[test] -fn ff_disabled_holds_zero() { +fn ff_always_latches_regardless_of_enable() { + // FF always captures LUT output (Dart: Sequential(clk, [ffQ < lutOut])). + // ff_enable only controls the output mux, not the FF clock. let mut cfg = make_cfg(1); cfg.lut_init = 0xFFFF; cfg.ff_enable = false; let mut sim = make_sim(cfg); sim.step(); + // FF captures lut_out=1 even though ff_enable is false + assert!(sim.state[1][1].ff_q); + // Output uses raw lut_out (not ff_q) when ff_enable=false + assert!(sim.state[1][1].lut_out); +} + +#[test] +fn ff_enable_selects_ff_output() { + // When ff_enable=true, CLB output is ff_q (one cycle delayed) + let mut cfg = make_cfg(1); + cfg.lut_init = 0xFFFF; // constant 1 + cfg.ff_enable = true; + + let mut sim = make_sim(cfg); + // Cycle 1: lut_out=1, clb_out=ff_q_prev=0 (initial), ff_q_next=1 sim.step(); - assert!(!sim.state[1][1].ff_q); + assert!(sim.state[1][1].ff_q); // FF captured lut_out=1 + assert!(!sim.state[1][1].lut_out); // clb_out = ff_q_prev = 0 + + // Cycle 2: lut_out=1, clb_out=ff_q_prev=1, ff_q_next=1 + sim.step(); + assert!(sim.state[1][1].lut_out); // clb_out = ff_q_prev = 1 } #[test] diff --git a/devices.nix b/devices.nix index 0c9f8de..aff1908 100644 --- a/devices.nix +++ b/devices.nix @@ -23,20 +23,22 @@ luna-1 = { ip = aegis-ip-tools.mkIp { deviceName = "luna_1"; - width = 19; - height = 40; + width = 23; + height = 23; tracks = 1; - serdesCount = 1; - bramColumnInterval = 9; - dspColumnInterval = 10; + serdesCount = 0; + bramColumnInterval = 7; + dspColumnInterval = 8; clockTileCount = 1; }; tapeout = { pdk = gf180mcu-pdk; clockPeriodNs = 20; - dieWidthUm = 3930; - dieHeightUm = 5120; - tilePlacementDensities.SerDesTile = 0.5; + fabSlot = "1x1"; + tilePlacementDensities = { + Tile = 0.6; + ClockTile = 0.6; + }; }; }; } diff --git a/flake.lock b/flake.lock index 78e5189..a12290f 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,26 @@ { "nodes": { + "ciel": { + "inputs": { + "nix-eda": [ + "librelane", + "nix-eda" + ] + }, + "locked": { + "lastModified": 1764091696, + "narHash": "sha256-AWbkHL0zO3tD0mE3dZIcj8mVND7o3imTxOpEfOtlRDI=", + "owner": "fossi-foundation", + "repo": "ciel", + "rev": "afcb23d368614ffa1e7e96584ed33f839c71c576", + "type": "github" + }, + "original": { + "owner": "fossi-foundation", + "repo": "ciel", + "type": "github" + } + }, "crane": { "locked": { "lastModified": 1774313767, @@ -15,6 +36,42 @@ "type": "github" } }, + "devshell": { + "inputs": { + "nixpkgs": [ + "librelane", + "nix-eda", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1768818222, + "narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=", + "owner": "numtide", + "repo": "devshell", + "rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "devshell", + "type": "github" + } + }, + "flake-compat": { + "locked": { + "lastModified": 1733328505, + "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", + "revCount": 69, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz" + } + }, "flake-parts": { "inputs": { "nixpkgs-lib": "nixpkgs-lib" @@ -48,17 +105,59 @@ "type": "github" } }, + "librelane": { + "inputs": { + "ciel": "ciel", + "devshell": "devshell", + "flake-compat": "flake-compat", + "nix-eda": "nix-eda" + }, + "locked": { + "lastModified": 1774453904, + "narHash": "sha256-BZmoneeMpnnQ2wUb5sorLFsFZsLaKclhAtCIiMsW8Qc=", + "owner": "librelane", + "repo": "librelane", + "rev": "69b2067bd2b5eb89b84649b76e9edaa9e51e6735", + "type": "github" + }, + "original": { + "owner": "librelane", + "ref": "3.0.0", + "repo": "librelane", + "type": "github" + } + }, + "nix-eda": { + "inputs": { + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1773918136, + "narHash": "sha256-nSKBMGP8/ZC7qB3Lzd+FwM8REqOxlh8wpYDf2hlK6Gg=", + "owner": "fossi-foundation", + "repo": "nix-eda", + "rev": "8f990fb77529c09e540e453cd836af9930ec58db", + "type": "github" + }, + "original": { + "owner": "fossi-foundation", + "ref": "6.11.0", + "repo": "nix-eda", + "type": "github" + } + }, "nixpkgs": { "locked": { - "lastModified": 1774926773, - "narHash": "sha256-1qXnJJVSSlZ5UXOI0bn+OyTCRV1uzOIi5KWlr5tr/4A=", - "owner": "NixOS", + "lastModified": 1766201043, + "narHash": "sha256-eplAP+rorKKd0gNjV3rA6+0WMzb1X1i16F5m5pASnjA=", + "owner": "nixos", "repo": "nixpkgs", - "rev": "8cbb749977bb1b1776654c3c6631b358feee2187", + "rev": "b3aad468604d3e488d627c0b43984eb60e75e782", "type": "github" }, "original": { - "owner": "NixOS", + "owner": "nixos", + "ref": "nixos-25.11", "repo": "nixpkgs", "type": "github" } @@ -79,6 +178,21 @@ } }, "nixpkgs_2": { + "locked": { + "lastModified": 1774926773, + "narHash": "sha256-1qXnJJVSSlZ5UXOI0bn+OyTCRV1uzOIi5KWlr5tr/4A=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "8cbb749977bb1b1776654c3c6631b358feee2187", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { "locked": { "lastModified": 1770107345, "narHash": "sha256-tbS0Ebx2PiA1FRW8mt8oejR0qMXmziJmPaU1d4kYY9g=", @@ -99,13 +213,14 @@ "crane": "crane", "flake-parts": "flake-parts", "flakever": "flakever", - "nixpkgs": "nixpkgs", + "librelane": "librelane", + "nixpkgs": "nixpkgs_2", "treefmt-nix": "treefmt-nix" } }, "treefmt-nix": { "inputs": { - "nixpkgs": "nixpkgs_2" + "nixpkgs": "nixpkgs_3" }, "locked": { "lastModified": 1773297127, diff --git a/flake.nix b/flake.nix index 72153b5..26315cc 100644 --- a/flake.nix +++ b/flake.nix @@ -1,10 +1,20 @@ { + nixConfig = { + extra-substituters = [ + "https://nix-cache.fossi-foundation.org" + ]; + extra-trusted-public-keys = [ + "nix-cache.fossi-foundation.org:3+K59iFwXqKsL7BNu6Guy0v+uTlwsxYQxjspXzqLYQs=" + ]; + }; + inputs = { nixpkgs.url = "github:NixOS/nixpkgs"; flake-parts.url = "github:hercules-ci/flake-parts"; flakever.url = "github:numinit/flakever"; treefmt-nix.url = "github:numtide/treefmt-nix"; crane.url = "github:ipetkov/crane"; + librelane.url = "github:librelane/librelane/3.0.0"; }; outputs = @@ -15,6 +25,7 @@ flakever, treefmt-nix, crane, + librelane, ... }@inputs: let @@ -50,12 +61,18 @@ let inherit (pkgs) lib; craneLib = crane.mkLib pkgs; + devices = import ./devices.nix { + inherit (pkgs) aegis-ip-tools gf180mcu-pdk sky130-pdk; + }; in { _module.args.pkgs = import inputs.nixpkgs { inherit system; overlays = [ self.overlays.default + (final: _: { + librelane = inputs.librelane.packages.${system}.librelane or null; + }) ]; }; @@ -82,16 +99,19 @@ packages = let - devices = import ./devices.nix { - inherit (pkgs) aegis-ip-tools gf180mcu-pdk sky130-pdk; - }; - - mkDevicePackages = name: cfg: { - "${name}" = cfg.ip; - "${name}-tapeout" = cfg.ip.mkTapeout cfg.tapeout; - "${name}-deb" = cfg.ip.deb; - "${name}-docker" = cfg.ip.docker; - }; + mkDevicePackages = + name: cfg: + { + "${name}" = cfg.ip; + "${name}-deb" = cfg.ip.deb; + "${name}-docker" = cfg.ip.docker; + } + // lib.optionalAttrs (cfg.tapeout.pdk ? ioLib) { + "${name}-tapeout" = cfg.ip.mkTapeout cfg.tapeout; + } + // lib.optionalAttrs (cfg.tapeout.pdk ? librelane) { + "${name}-tapeout-lr" = cfg.ip.mkTapeoutLr cfg.tapeout; + }; in { default = pkgs.aegis-ip-tools; @@ -103,18 +123,11 @@ checks = let - devices = builtins.filter ( - name: - let - pkg = self.packages.${system}.${name}; - in - pkg ? deviceName && !(pkg ? tileMacros) - ) (builtins.attrNames self.packages.${system}); mkDeviceChecks = name: let + cfg = devices.${name}; ip = self.packages.${system}.${name}; - tapeout = self.packages.${system}."${name}-tapeout"; in { "${name}-blinky" = pkgs.callPackage ./examples/blinky { @@ -146,33 +159,35 @@ "${name}-formal-ip" = pkgs.callPackage ./tests/formal-ip { aegis-ip = ip; }; + } + // lib.optionalAttrs (cfg.tapeout.pdk ? ioLib) { "${name}-gds-verify" = pkgs.callPackage ./tests/gds-verify { - aegis-tapeout = tapeout; + aegis-tapeout = self.packages.${system}."${name}-tapeout"; }; }; in - lib.foldl' (acc: name: acc // mkDeviceChecks name) { } devices; + lib.foldl' (acc: name: acc // mkDeviceChecks name) { } (builtins.attrNames devices); devShells = let - devices = builtins.filter ( + mkDeviceShells = name: let - pkg = self.packages.${system}.${name}; + cfg = devices.${name}; in - pkg ? deviceName && !(pkg ? tileMacros) - ) (builtins.attrNames self.packages.${system}); - mkDeviceShells = name: { - "${name}" = self.packages.${system}.${name}.shell; - "${name}-tapeout" = self.packages.${system}."${name}-tapeout".shell; - "${name}-blinky" = self.checks.${system}."${name}-blinky".shell; - }; + { + "${name}" = self.packages.${system}.${name}.shell; + "${name}-blinky" = self.checks.${system}."${name}-blinky".shell; + } + // lib.optionalAttrs (cfg.tapeout.pdk ? ioLib) { + "${name}-tapeout" = self.packages.${system}."${name}-tapeout".shell; + }; in { default = pkgs.aegis-ip-tools.shell; ip-tools = pkgs.aegis-ip-tools.shell; } - // lib.foldl' (acc: name: acc // mkDeviceShells name) { } devices; + // lib.foldl' (acc: name: acc // mkDeviceShells name) { } (builtins.attrNames devices); }; }; } diff --git a/ip/bin/aegis_genip.dart b/ip/bin/aegis_genip.dart index 024ce14..85fe927 100644 --- a/ip/bin/aegis_genip.dart +++ b/ip/bin/aegis_genip.dart @@ -134,7 +134,12 @@ Future main(List arguments) async { dir.createSync(recursive: true); } - File('$outputDir/${fpga.name}.sv').writeAsStringSync(fpga.generateSynth()); + // Tag fabric/tile modules with `(* keep_hierarchy *)` so flat + // synthesis flows (LibreLane, OpenLane) cannot constant-fold + // through the LUT config registers and dead-code-eliminate the + // entire fabric. + final svContent = KeepHierarchy.inject(fpga.generateSynth()); + File('$outputDir/${fpga.name}.sv').writeAsStringSync(svContent); const encoder = JsonEncoder.withIndent(' '); File( @@ -185,7 +190,6 @@ Future main(List arguments) async { '$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(); diff --git a/ip/lib/src/components/digital/fabric.dart b/ip/lib/src/components/digital/fabric.dart index 4db5ec9..3c061d7 100644 --- a/ip/lib/src/components/digital/fabric.dart +++ b/ip/lib/src/components/digital/fabric.dart @@ -211,6 +211,8 @@ class LutFabric extends Module { final tileCfgLoad = Logic(); final tileCarryIn = Logic(name: 'carryIn_${x}_$y'); + final tileNbClb = TileInterface(); + final Module tile; if (bram.contains(x)) { tile = BramTile( @@ -243,17 +245,36 @@ class LutFabric extends Module { tileIn, tileOut, carryIn: tileCarryIn, + neighborClbOut: tileNbClb, tracks: tracks, ); } - return (tile, tileIn, tileOut, tileCfgIn, tileCfgLoad, tileCarryIn); + return ( + tile, + tileIn, + tileOut, + tileCfgIn, + tileCfgLoad, + tileCarryIn, + tileNbClb, + ); }), ); // Config chain: row-major order final flat = - <(Module, TileInterface, TileInterface, Logic, Logic, Logic)>[]; + < + ( + Module, + TileInterface, + TileInterface, + Logic, + Logic, + Logic, + TileInterface, + ) + >[]; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { @@ -282,6 +303,20 @@ class LutFabric extends Module { } } + // Neighbor CLB output wiring + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + final nbClb = tiles[x][y].$7; + Logic clbOutOf(int nx, int ny) => + tiles[nx][ny].$1.tryOutput('clbOut') ?? Const(0); + + nbClb.north <= ((y > 0) ? clbOutOf(x, y - 1) : Const(0)); + nbClb.east <= ((x < width - 1) ? clbOutOf(x + 1, y) : Const(0)); + nbClb.south <= ((y < height - 1) ? clbOutOf(x, y + 1) : Const(0)); + nbClb.west <= ((x > 0) ? clbOutOf(x - 1, y) : Const(0)); + } + } + // Routing connections for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { diff --git a/ip/lib/src/components/digital/io_fabric.dart b/ip/lib/src/components/digital/io_fabric.dart index 2ac85ba..b761c22 100644 --- a/ip/lib/src/components/digital/io_fabric.dart +++ b/ip/lib/src/components/digital/io_fabric.dart @@ -95,6 +95,8 @@ class IOFabric extends Module { for (int i = 0; i < pads; i++) { ioTiles[i].$2 <= padIn[i]; + } + for (int i = 0; i < pads; i++) { ioTiles[i].$5 <= cfgLoad; } @@ -142,6 +144,8 @@ class IOFabric extends Module { if (serdesCount > 0) { for (int i = 0; i < serdesCount; i++) { serdesTiles[i].$2 <= serialIn![i]; + } + for (int i = 0; i < serdesCount; i++) { serdesTiles[i].$5 <= cfgLoad; } diff --git a/ip/lib/src/components/digital/tile.dart b/ip/lib/src/components/digital/tile.dart index 8d1e6b1..b369989 100644 --- a/ip/lib/src/components/digital/tile.dart +++ b/ip/lib/src/components/digital/tile.dart @@ -39,6 +39,8 @@ class Tile extends Module { Logic get carryIn => input('carryIn'); Logic get carryOut => output('carryOut'); + Logic get clbOut => output('clbOut'); + final int tracks; int get configWidth => tileConfigWidth(tracks); @@ -51,6 +53,7 @@ class Tile extends Module { TileInterface input, TileInterface output, { required Logic carryIn, + required TileInterface neighborClbOut, this.tracks = 1, }) : super(name: 'tile') { clk = addInput('clk', clk); @@ -62,6 +65,16 @@ class Tile extends Module { carryIn = addInput('carryIn', carryIn); addOutput('carryOut'); + addOutput('clbOut'); + + neighborClbOut = neighborClbOut.clone() + ..connectIO( + this, + neighborClbOut, + inputTags: {TilePortGroup.routing}, + outputTags: {}, + uniquify: (orig) => 'nb_$orig', + ); input = input.clone() ..connectIO( @@ -118,16 +131,31 @@ class Tile extends Module { final outBase = 18 + 4 * isw; - final clbOut = Logic(); + // Neighbor CLB output ports (N, E, S, W) + final nbPorts = [ + neighborClbOut.north, + neighborClbOut.east, + neighborClbOut.south, + neighborClbOut.west, + ]; - // Input mux: select from direction*T+track for directional, 4*T+{0,1,2} for internal + // Input mux: select from direction*T+track for directional, + // 4*T+{0,1,2} for internal, 4*T+{3,4,5,6} for neighbor CLB outputs Logic selectInput(Logic selBits) { final result = Logic(); - final nValues = 4 * tracks + 3; // Build mux chain from highest value down Logic chain = Const(0, width: 1); + // Neighbor CLB outputs: W, S, E, N (highest values down) + for (var d = 3; d >= 0; d--) { + chain = mux( + selBits.eq(Const(inputSelNeighbor(d, tracks), width: isw)), + nbPorts[d], + chain, + ); + } + // const1 chain = mux( selBits.eq(Const(inputSelConst1(tracks), width: isw)), @@ -211,7 +239,4 @@ class Tile extends Module { output.south <= dirOutputs[2].reversed.toList().swizzle(); output.west <= dirOutputs[3].reversed.toList().swizzle(); } - - // For backward compatibility (T=1) - static const int CONFIG_WIDTH = 46; } diff --git a/ip/lib/src/config/tile_config.dart b/ip/lib/src/config/tile_config.dart index f15c3e3..70ce129 100644 --- a/ip/lib/src/config/tile_config.dart +++ b/ip/lib/src/config/tile_config.dart @@ -2,8 +2,9 @@ import 'dart:math'; import 'clb_config.dart'; /// Compute the input select width for a given number of tracks. -/// Values: N0..N(T-1), E0..E(T-1), S0..S(T-1), W0..W(T-1), CLB_OUT, const0, const1 -int inputSelWidth(int tracks) => (4 * tracks + 3 - 1).bitLength; +/// Values: N0..N(T-1), E0..E(T-1), S0..S(T-1), W0..W(T-1), +/// CLB_OUT, const0, const1, NB_N, NB_E, NB_S, NB_W +int inputSelWidth(int tracks) => (4 * tracks + 7 - 1).bitLength; /// Compute the total tile config width for a given number of tracks. /// @@ -14,7 +15,7 @@ int inputSelWidth(int tracks) => (4 * tracks + 3 - 1).bitLength; /// for each direction (N,E,S,W) and track (0..T-1): /// 1 enable bit + 3 select bits = 4 bits /// -/// For T=1: 18 + 4*3 + 4*1*4 = 46 (backward compatible) +/// For T=1: 18 + 4*4 + 4*1*4 = 50 /// For T=4: 18 + 4*5 + 4*4*4 = 102 int tileConfigWidth(int tracks) => 18 + 4 * inputSelWidth(tracks) + 4 * tracks * 4; @@ -32,6 +33,9 @@ int inputSelConst0(int tracks) => 4 * tracks + 1; /// Input mux select value for constant 1. int inputSelConst1(int tracks) => 4 * tracks + 2; +/// Input mux select value for neighbor CLB output (0=N, 1=E, 2=S, 3=W). +int inputSelNeighbor(int direction, int tracks) => 4 * tracks + 3 + direction; + /// Per-track output configuration. class TrackOutputConfig { final bool enable; diff --git a/ip/lib/src/openroad/tcl_emitter.dart b/ip/lib/src/openroad/tcl_emitter.dart index a917c88..05b8fb5 100644 --- a/ip/lib/src/openroad/tcl_emitter.dart +++ b/ip/lib/src/openroad/tcl_emitter.dart @@ -46,9 +46,11 @@ class OpenroadTclEmitter { _writeHeader(buf); _writeReadInputs(buf); _writeFloorplan(buf); + _writePadring(buf); _writePinPlacement(buf); _writePowerGrid(buf); _writePlacement(buf); + _writePdnGen(buf); _writeCts(buf); _writeRouting(buf); _writeReports(buf); @@ -100,6 +102,19 @@ class OpenroadTclEmitter { buf.writeln(' }'); buf.writeln('}'); buf.writeln(); + // Read I/O pad library LEFs (gf180mcu_fd_io: bidir/input/output pads, + // power pads, padring fillers, corner cell). Only relevant when the + // chip-level wrapper is being placed; tile-level builds don't supply + // IO_LEF_DIR. + buf.writeln('if {[info exists IO_LEF_DIR]} {'); + buf.writeln(' foreach lef [glob -directory \$IO_LEF_DIR *.lef] {'); + buf.writeln(' read_lef \$lef'); + buf.writeln(' }'); + buf.writeln('}'); + buf.writeln('if {[info exists IO_LIB_FILE]} {'); + buf.writeln(' read_liberty \$IO_LIB_FILE'); + buf.writeln('}'); + buf.writeln(); // Read tile macro LEFs and liberty timing models buf.writeln('# Read tile macro LEF abstracts and timing models'); @@ -114,9 +129,20 @@ class OpenroadTclEmitter { buf.writeln(); buf.writeln('read_verilog \$SYNTH_V'); - buf.writeln('link_design $moduleName'); + buf.writeln( + 'if {![info exists TOP_MODULE]} { set TOP_MODULE $moduleName }', + ); + buf.writeln('link_design \$TOP_MODULE'); buf.writeln('read_sdc \$SDC_FILE'); buf.writeln(); + // Wire RC values are required by CTS, repair_design, and the global + // router's parasitic estimator. Use explicit numeric values rather + // than -layer so every routing/clock net has a defined R+C the + // estimator can fall back on, avoiding segfaults inside layerRC + // when the router asks about a layer we did not parameterize. + buf.writeln('set_wire_rc -signal -resistance 0.0001 -capacitance 0.0001'); + buf.writeln('set_wire_rc -clock -resistance 0.0001 -capacitance 0.0001'); + buf.writeln(); } void _writeFloorplan(StringBuffer buf) { @@ -124,12 +150,23 @@ class OpenroadTclEmitter { // 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('# tile macro dimensions × grid size.'); + buf.writeln( + '# When PAD_HEIGHT is set we are doing chip-level integration:', + ); + buf.writeln('# the core area is inset from the die boundary by PAD_HEIGHT'); + buf.writeln( + '# on every side so the perimeter is reserved for the padring.', + ); buf.writeln('if {[info exists DIE_AREA]} {'); + buf.writeln( + ' set _core_inset ' + '[expr {[info exists PAD_HEIGHT] ? \$PAD_HEIGHT : 1}]', + ); buf.writeln(' initialize_floorplan \\'); buf.writeln(' -die_area \$DIE_AREA \\'); 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}]" \\', + ' -core_area "[expr {[lindex \$DIE_AREA 0] + \$_core_inset}] [expr {[lindex \$DIE_AREA 1] + \$_core_inset}] [expr {[lindex \$DIE_AREA 2] - \$_core_inset}] [expr {[lindex \$DIE_AREA 3] - \$_core_inset}]" \\', ); buf.writeln(' -site \$SITE_NAME'); buf.writeln('} else {'); @@ -226,6 +263,148 @@ class OpenroadTclEmitter { buf.writeln(); } + /// Place I/O pads around the die perimeter and instantiate corner cells. + /// + /// The chip wrapper Verilog already contains the signal-bearing pad + /// instances. Their physical placement is computed here: pads are + /// distributed evenly across the four die sides, walking clockwise + /// starting at the south-west corner. Corner cells are physical-only + /// instances inserted via odb so they don't need to live in the netlist. + void _writePadring(StringBuffer buf) { + buf.writeln(_section('Padring placement')); + + buf.writeln('if {![info exists PAD_HEIGHT]} {'); + buf.writeln(' puts "Skipping padring (PAD_HEIGHT not set)"'); + buf.writeln('} else {'); + 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(' set pad_h \$PAD_HEIGHT'); + buf.writeln(' set corner \$CORNER_SIZE'); + buf.writeln(); + + // Collect every instance whose master is a PAD-class cell. Bidir, + // input, output, and power pads all have CLASS PAD in the LEF; the + // db reports their type as PAD or PAD_INPUT/PAD_OUTPUT/PAD_POWER. + buf.writeln(' set pads {}'); + buf.writeln(' foreach inst [[ord::get_db_block] getInsts] {'); + buf.writeln(' set m [\$inst getMaster]'); + buf.writeln(' set t [\$m getType]'); + buf.writeln(' if {[string match "PAD*" \$t]} {'); + buf.writeln(' lappend pads \$inst'); + buf.writeln(' }'); + buf.writeln(' }'); + buf.writeln(' set npads [llength \$pads]'); + buf.writeln(' puts "Found \$npads pad instances to place"'); + buf.writeln(); + + // Distribute round-robin across the 4 sides so power and signal + // pads end up evenly spread (the wrapper interleaves them in + // declaration order: bidir IO, then per-port pads, then power pads). + buf.writeln(' set per_side [expr {(\$npads + 3) / 4}]'); + buf.writeln( + ' set pad_pitch_x [expr {(\$die_w - 2 * \$corner) / \$per_side}]', + ); + buf.writeln( + ' set pad_pitch_y [expr {(\$die_h - 2 * \$corner) / \$per_side}]', + ); + buf.writeln(); + + buf.writeln(' set mfg 0.005'); + buf.writeln(' proc snap_um {v mfg} {'); + buf.writeln(' return [expr {int(\$v / \$mfg) * \$mfg}]'); + buf.writeln(' }'); + buf.writeln(); + + buf.writeln(' for {set i 0} {\$i < \$npads} {incr i} {'); + buf.writeln(' set inst [lindex \$pads \$i]'); + buf.writeln(' set side [expr {\$i / \$per_side}]'); + buf.writeln(' if {\$side > 3} { set side 3 }'); + buf.writeln(' set idx [expr {\$i % \$per_side}]'); + buf.writeln(' if {\$side == 0} {'); + buf.writeln(' # South (bottom). Pad in default orientation R0,'); + buf.writeln(' # bond pad faces -Y (towards die edge).'); + buf.writeln( + ' set x [snap_um [expr {\$corner + \$idx * \$pad_pitch_x}] \$mfg]', + ); + buf.writeln(' set y 0'); + buf.writeln(' set ori R0'); + buf.writeln(' } elseif {\$side == 1} {'); + buf.writeln(' # East (right). Pad rotated R270 (clockwise 90).'); + buf.writeln(' set x [snap_um [expr {\$die_w - \$pad_h}] \$mfg]'); + buf.writeln( + ' set y [snap_um [expr {\$corner + \$idx * \$pad_pitch_y}] \$mfg]', + ); + buf.writeln(' set ori R270'); + buf.writeln(' } elseif {\$side == 2} {'); + buf.writeln(' # North (top). Pad rotated R180.'); + buf.writeln( + ' set x [snap_um [expr {\$die_w - \$corner - (\$idx + 1) * \$pad_pitch_x}] \$mfg]', + ); + buf.writeln(' set y [snap_um [expr {\$die_h - \$pad_h}] \$mfg]'); + buf.writeln(' set ori R180'); + buf.writeln(' } else {'); + buf.writeln(' # West (left). Pad rotated R90.'); + buf.writeln(' set x 0'); + buf.writeln( + ' set y [snap_um [expr {\$die_h - \$corner - (\$idx + 1) * \$pad_pitch_y}] \$mfg]', + ); + buf.writeln(' set ori R90'); + buf.writeln(' }'); + buf.writeln( + ' \$inst setLocation [ord::microns_to_dbu \$x] [ord::microns_to_dbu \$y]', + ); + buf.writeln(' \$inst setLocationOrient \$ori'); + buf.writeln(' \$inst setPlacementStatus FIRM'); + buf.writeln(' }'); + buf.writeln( + ' puts "Placed \$npads pads (\$per_side per side, pitch x=\$pad_pitch_x y=\$pad_pitch_y)"', + ); + buf.writeln(); + + // Corner cells. These are physical-only instances created via odb so + // they don't need to appear in the wrapper netlist. + buf.writeln( + ' set corner_master [[ord::get_db] findMaster \$PAD_CORNER]', + ); + buf.writeln(' if {\$corner_master ne "NULL"} {'); + buf.writeln(' set block [ord::get_db_block]'); + buf.writeln( + ' proc place_corner {block master name x_um y_um ori} {', + ); + buf.writeln( + ' set inst [odb::dbInst_create \$block \$master \$name]', + ); + buf.writeln( + ' \$inst setLocation [ord::microns_to_dbu \$x_um] ' + '[ord::microns_to_dbu \$y_um]', + ); + buf.writeln(' \$inst setLocationOrient \$ori'); + buf.writeln(' \$inst setPlacementStatus FIRM'); + buf.writeln(' }'); + buf.writeln( + ' place_corner \$block \$corner_master "corner_bl" 0 0 R0', + ); + buf.writeln( + ' place_corner \$block \$corner_master "corner_br" ' + '[expr {\$die_w - \$corner}] 0 R270', + ); + buf.writeln( + ' place_corner \$block \$corner_master "corner_tr" ' + '[expr {\$die_w - \$corner}] [expr {\$die_h - \$corner}] R180', + ); + buf.writeln( + ' place_corner \$block \$corner_master "corner_tl" ' + '0 [expr {\$die_h - \$corner}] R90', + ); + buf.writeln(' puts "Placed 4 corner cells"'); + buf.writeln(' } else {'); + buf.writeln(' puts "WARNING: corner cell \$PAD_CORNER not loaded"'); + buf.writeln(' }'); + buf.writeln('}'); + buf.writeln(); + } + void _writePinPlacement(StringBuffer buf) { buf.writeln(_section('Pin placement')); @@ -253,8 +432,21 @@ class OpenroadTclEmitter { eastPins.addAll(['clkOut\\[*\\]', 'clkLocked\\[*\\]']); } + // place_pins assigns physical locations to top-level ports. The + // chip-level wrapper has only VDD/VSS as ports (bond pads are the + // signal boundary), and those are power nets that OpenROAD's + // global_connect handles separately, so place_pins isn't needed + // when PAD_HEIGHT is set. + buf.writeln('if {![info exists PAD_HEIGHT]} {'); _writeLayerDetection(buf); - buf.writeln('place_pins -hor_layers \$hor_layer -ver_layers \$ver_layer'); + buf.writeln( + ' place_pins -hor_layers \$hor_layer -ver_layers \$ver_layer', + ); + buf.writeln('} else {'); + buf.writeln( + ' puts "Skipping place_pins (chip wrapper has no signal ports)"', + ); + buf.writeln('}'); buf.writeln(); } @@ -267,6 +459,62 @@ class OpenroadTclEmitter { buf.writeln(); } + /// Generate PDN after macros are placed and fixed. + void _writePdnGen(StringBuffer buf) { + buf.writeln(_section('Power delivery network')); + + buf.writeln('if {[info exists PDN_RAIL_LAYER]} {'); + buf.writeln(' set_voltage_domain -name CORE -power VDD -ground VSS'); + buf.writeln(); + buf.writeln( + ' # Standard cell grid: M1 followpins + M4 vertical + M5 horizontal', + ); + buf.writeln(' define_pdn_grid -name stdcell_grid \\'); + buf.writeln(' -starts_with POWER -voltage_domain CORE'); + buf.writeln(); + buf.writeln(' add_pdn_stripe -grid stdcell_grid \\'); + buf.writeln( + ' -layer \$PDN_RAIL_LAYER -width \$PDN_RAIL_WIDTH -followpins', + ); + buf.writeln(); + buf.writeln(' add_pdn_stripe -grid stdcell_grid \\'); + buf.writeln(' -layer \$PDN_VERTICAL_LAYER -width \$PDN_VWIDTH \\'); + buf.writeln(' -pitch \$PDN_VPITCH -offset \$PDN_VOFFSET \\'); + buf.writeln(' -spacing \$PDN_VSPACING -starts_with POWER'); + buf.writeln(); + buf.writeln(' add_pdn_stripe -grid stdcell_grid \\'); + buf.writeln(' -layer \$PDN_HORIZONTAL_LAYER -width \$PDN_HWIDTH \\'); + buf.writeln(' -pitch \$PDN_HPITCH -offset \$PDN_HOFFSET \\'); + buf.writeln(' -spacing \$PDN_HSPACING -starts_with POWER'); + buf.writeln(); + buf.writeln(' # Stitch the three strap layers together'); + buf.writeln(' add_pdn_connect -grid stdcell_grid \\'); + buf.writeln(' -layers "\$PDN_RAIL_LAYER \$PDN_VERTICAL_LAYER"'); + buf.writeln(' add_pdn_connect -grid stdcell_grid \\'); + buf.writeln( + ' -layers "\$PDN_VERTICAL_LAYER \$PDN_HORIZONTAL_LAYER"', + ); + buf.writeln(); + buf.writeln(' # Macro grid: bind to each tile macro\'s exposed M1'); + buf.writeln(' # power pins. -grid_over_pg_pins tells pdngen to use'); + buf.writeln(' # the macro\'s existing PG pin locations as via drops,'); + buf.writeln(' # so the M4/M5 straps that pass over the macro can land'); + buf.writeln(' # vias to power the cells inside.'); + buf.writeln(' define_pdn_grid -name macro_grid -macro -default \\'); + buf.writeln(' -starts_with POWER -voltage_domain CORE \\'); + buf.writeln(' -grid_over_pg_pins'); + buf.writeln(' add_pdn_connect -grid macro_grid \\'); + buf.writeln(' -layers "\$PDN_RAIL_LAYER \$PDN_VERTICAL_LAYER"'); + buf.writeln(' add_pdn_connect -grid macro_grid \\'); + buf.writeln( + ' -layers "\$PDN_VERTICAL_LAYER \$PDN_HORIZONTAL_LAYER"', + ); + buf.writeln(); + buf.writeln(' pdngen'); + buf.writeln('}'); + buf.writeln(); + } + void _writePlacement(StringBuffer buf) { buf.writeln(_section('Placement')); @@ -311,10 +559,31 @@ class OpenroadTclEmitter { buf.writeln(' }'); buf.writeln(); - // 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]]'); + // Use the *core* area for placement: with chip-level integration + // the padring occupies a PAD_HEIGHT strip between core and die, so + // tile macros must be shifted inwards by the core origin and sized + // against the core dimensions. + buf.writeln(' set core_rect [[ord::get_db_block] getCoreArea]'); + buf.writeln(' set core_xmin [ord::dbu_to_microns [\$core_rect xMin]]'); + buf.writeln(' set core_ymin [ord::dbu_to_microns [\$core_rect yMin]]'); + buf.writeln(' set core_xmax [ord::dbu_to_microns [\$core_rect xMax]]'); + buf.writeln(' set core_ymax [ord::dbu_to_microns [\$core_rect yMax]]'); + buf.writeln(' set die_w [expr {\$core_xmax - \$core_xmin}]'); + buf.writeln(' set die_h [expr {\$core_ymax - \$core_ymin}]'); + buf.writeln(); + // place_inst translates a core-relative (x_um, y_um) location to the + // absolute die coordinate that OpenROAD wants. Using this helper + // means the rest of the placement logic can keep speaking in + // core-relative terms even when the core has been inset. + buf.writeln(' proc place_inst {inst x_um y_um} {'); + buf.writeln(' upvar core_xmin xoff core_ymin yoff'); + buf.writeln( + ' \$inst setLocation ' + '[ord::microns_to_dbu [expr {\$x_um + \$xoff}]] ' + '[ord::microns_to_dbu [expr {\$y_um + \$yoff}]]', + ); + buf.writeln(' \$inst setPlacementStatus FIRM'); + buf.writeln(' }'); buf.writeln(); // Place fabric tiles (Tile, BramTile, DspBasicTile) in main grid @@ -435,10 +704,7 @@ class OpenroadTclEmitter { 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(' place_inst \$inst \$cx \$cy'); buf.writeln(' incr placed'); buf.writeln(' }'); buf.writeln(' }'); @@ -446,10 +712,7 @@ class OpenroadTclEmitter { 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(' place_inst \$inst \$cx \$cy'); buf.writeln(' incr placed'); buf.writeln(' }'); buf.writeln(' }'); @@ -457,10 +720,7 @@ class OpenroadTclEmitter { 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(' place_inst \$inst \$cx \$cy'); buf.writeln(' incr placed'); buf.writeln(' }'); buf.writeln(' }'); @@ -495,10 +755,8 @@ class OpenroadTclEmitter { ); 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', + ' place_inst [lindex \$macro_groups(IOTile) \$io_idx] ' + '\$io_x \$io_y', ); buf.writeln(' incr io_idx'); buf.writeln(' }'); @@ -509,10 +767,8 @@ class OpenroadTclEmitter { ); 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', + ' place_inst [lindex \$macro_groups(IOTile) \$io_idx] ' + '\$io_x \$io_y', ); buf.writeln(' incr io_idx'); buf.writeln(' }'); @@ -526,10 +782,8 @@ class OpenroadTclEmitter { ' 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', + ' place_inst [lindex \$macro_groups(IOTile) \$io_idx] ' + '\$io_x \$io_y', ); buf.writeln(' incr io_idx'); buf.writeln(' incr io_row'); @@ -551,10 +805,7 @@ class OpenroadTclEmitter { 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(' place_inst \$inst \$serdes_x \$serdes_y'); buf.writeln( ' puts "Placed SerDesTile at \${serdes_x}um x \${serdes_y}um"', ); @@ -577,10 +828,7 @@ class OpenroadTclEmitter { ' 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(' place_inst \$inst \$x \$edge_y'); 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"', @@ -619,41 +867,89 @@ class OpenroadTclEmitter { buf.writeln(); buf.writeln('detailed_placement'); buf.writeln(); + + // Filler cell insertion: fillcap first (decoupling capacitance), + // then plain fill for the smaller leftover gaps. Largest first so + // big gaps consume one big cell. + buf.writeln('set fillers {}'); + buf.writeln('foreach sz {64 32 16 8 4} {'); + buf.writeln(' lappend fillers \${CELL_LIB}__fillcap_\$sz'); + buf.writeln('}'); + buf.writeln('foreach sz {64 32 16 8 4 2 1} {'); + buf.writeln(' lappend fillers \${CELL_LIB}__fill_\$sz'); + buf.writeln('}'); + buf.writeln('filler_placement \$fillers'); + 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]] {'); + // Top-level signals route on Metal2 through Metal4. The chip-level + // pad cells expose their internal A/Y/OE pins on Metal2 - that is + // where signals enter and leave the pad. The Metal5 PAD pin is the + // physical bond-wire attachment point and intentionally has no + // internal access (it's reached by the bond wire externally), so + // capping signal routing at Metal4 keeps detailed_route from + // failing pin-access checks on those PAD pins. + buf.writeln('set_routing_layers -signal Metal2-Metal4'); + buf.writeln('# Apply per-layer routing capacity adjustments'); + buf.writeln('if {[array exists LAYER_ADJ]} {'); + buf.writeln(' foreach layer [array names LAYER_ADJ] {'); buf.writeln( - ' if {![catch {set tl ' - '[[[ord::get_db] getTech] findLayer \$layer]}]} {', + ' set_global_routing_layer_adjustment \$layer \$LAYER_ADJ(\$layer)', ); - 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'); + // -congestion_iterations 0 skips the rip-up-and-reroute pass that + // calls parasitic estimation (which is segfaulting in + // MakeWireParasitics::layerRC for this chip-level design). The + // initial route still runs; detailed_route handles the cleanup. + buf.writeln('global_route -allow_congestion -congestion_iterations 0'); 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.', + 'if {![info exists DROUTE_END_ITER]} { set DROUTE_END_ITER 32 }', ); - buf.writeln('# If it fails, we still have the global-routed DEF.'); + // -top_routing_layer caps detailed_route at Metal4 so it never + // looks at Metal5 (where the bond-pad PAD pins live, and where + // we deliberately do not have internal access). Without this + // restriction the router fails with DRT-0073 on PAD pins. buf.writeln( - 'if {![info exists DROUTE_END_ITER]} { set DROUTE_END_ITER 8 }', + 'detailed_route -droute_end_iter \$DROUTE_END_ITER ' + '-or_seed 42 -or_k 3 ' + '-bottom_routing_layer Metal2 -top_routing_layer Metal4 ' + '-output_drc \${DEVICE_NAME}_drc.rpt', ); - buf.writeln('detailed_route -droute_end_iter \$DROUTE_END_ITER'); + buf.writeln(); + + // Antenna check + repair via layer-bumping only. We reclassify any + // ANTENNACELL master to plain CORE so repair_antennas cannot insert + // diode cells (the gf180mcu __antenna cell has class ANTENNACELL but + // we don't want it instantiated). + buf.writeln('foreach lib [[ord::get_db] getLibs] {'); + buf.writeln(' foreach mast [\$lib getMasters] {'); + buf.writeln(' if {[\$mast getType] eq "CORE_ANTENNACELL"} {'); + buf.writeln(' \$mast setType "CORE"'); + buf.writeln(' }'); + buf.writeln(' }'); + buf.writeln('}'); + buf.writeln('check_antennas -report_file antenna_pre.rpt'); + // repair_antennas does its own incremental routing for layer-bumping; + // a follow-up detailed_route would conflict with M4 pin access on the + // tile macros (DRT-1231) so we trust repair_antennas to leave the + // design routed. + buf.writeln('repair_antennas'); + buf.writeln('check_antennas -report_file antenna.rpt'); + buf.writeln(); + } + + void _writeDensityFill(StringBuffer buf) { + buf.writeln(_section('Density fill')); + + buf.writeln(r'density_fill -rules $FILL_CONFIG'); buf.writeln(); } @@ -675,8 +971,13 @@ class OpenroadTclEmitter { } void _writeLayerDetection(StringBuffer buf) { + // Push top-level pins up to Metal3 (hor) / Metal4 (ver). Tile macros + // also expose pins on these layers, so inter-tile routing stays on + // higher layers and avoids saturating M2/M3. buf.writeln('set hor_layer ""'); buf.writeln('set ver_layer ""'); + buf.writeln('set skip_first_hor 1'); + buf.writeln('set skip_first_ver 1'); buf.writeln('foreach layer [get_routing_layers] {'); buf.writeln( ' if {![catch {set tl ' @@ -684,19 +985,26 @@ class OpenroadTclEmitter { ); buf.writeln(' if {\$tl ne "NULL"} {'); buf.writeln(' set dir [\$tl getDirection]'); + buf.writeln(' if {\$dir eq "HORIZONTAL" && \$skip_first_hor} {'); + buf.writeln(' set skip_first_hor 0'); buf.writeln( - ' if {\$dir eq "HORIZONTAL" && \$hor_layer eq ""} {', + ' } elseif {\$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(' if {\$dir eq "VERTICAL" && \$skip_first_ver} {'); + buf.writeln(' set skip_first_ver 0'); + buf.writeln( + ' } elseif {\$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 }'); + buf.writeln('if {\$hor_layer eq ""} { set hor_layer Metal3 }'); + buf.writeln('if {\$ver_layer eq ""} { set ver_layer Metal4 }'); + buf.writeln('puts "Pin layers: hor=\$hor_layer ver=\$ver_layer"'); } String _section(String title) => diff --git a/ip/lib/src/openroad/tile_tcl_emitter.dart b/ip/lib/src/openroad/tile_tcl_emitter.dart index 30dea61..67544f6 100644 --- a/ip/lib/src/openroad/tile_tcl_emitter.dart +++ b/ip/lib/src/openroad/tile_tcl_emitter.dart @@ -100,6 +100,55 @@ class OpenroadTileTclEmitter { buf.writeln('global_connect'); buf.writeln(); + // Power delivery network for tile. + // + // The tile owns its own M1 + M4 power grid. The abstract LEF written + // at the end of this script exposes the M4 power straps as VDD/VSS + // pins on the tile boundary, so the top-level PDN can land vias from + // its M5 horizontal straps onto these M4 rails. + // + // Top metal stays at M4: M5 is reserved for the top-level horizontal + // power straps so we never compete with the top PDN inside macros. + buf.writeln('if {[info exists PDN_RAIL_LAYER]} {'); + buf.writeln(' set_voltage_domain -name CORE -power VDD -ground VSS'); + buf.writeln(' define_pdn_grid -name stdcell_grid \\'); + buf.writeln(' -starts_with POWER -voltage_domain CORE'); + buf.writeln(' add_pdn_stripe -grid stdcell_grid \\'); + buf.writeln( + ' -layer \$PDN_RAIL_LAYER -width \$PDN_RAIL_WIDTH -followpins', + ); + buf.writeln(' add_pdn_stripe -grid stdcell_grid \\'); + buf.writeln(' -layer \$PDN_VERTICAL_LAYER -width \$PDN_VWIDTH \\'); + buf.writeln(' -pitch \$PDN_VPITCH -offset \$PDN_VOFFSET \\'); + buf.writeln(' -spacing \$PDN_VSPACING -starts_with POWER'); + buf.writeln(' add_pdn_connect -grid stdcell_grid \\'); + buf.writeln(' -layers "\$PDN_RAIL_LAYER \$PDN_VERTICAL_LAYER"'); + buf.writeln(' pdngen'); + buf.writeln('}'); + buf.writeln(); + + // Tap cell and endcap insertion (required for well taps - DF.13). + // Only apply to tiles large enough to absorb the tap cells without + // breaking placement legalization. + buf.writeln('set die_area [ord::get_die_area]'); + buf.writeln( + 'set die_w [expr {[lindex \$die_area 2] - [lindex \$die_area 0]}]', + ); + buf.writeln( + 'set die_h [expr {[lindex \$die_area 3] - [lindex \$die_area 1]}]', + ); + buf.writeln('if {min(\$die_w, \$die_h) > 60} {'); + buf.writeln(' tapcell -tapcell_master \${CELL_LIB}__filltie \\'); + buf.writeln(' -endcap_master \${CELL_LIB}__endcap -distance 15'); + buf.writeln('}'); + buf.writeln(); + + // Add cell padding to prevent M1.2a violations at abutment. + // Only apply when utilization is low enough to absorb the padding. + buf.writeln('if {min(\$die_w, \$die_h) > 200} {'); + buf.writeln(' set_placement_padding -global -left 1 -right 1'); + buf.writeln('}'); + // Placement buf.writeln( 'if {![info exists TILE_PLACEMENT_DENSITY]} ' @@ -120,22 +169,57 @@ class OpenroadTileTclEmitter { buf.writeln('detailed_placement'); buf.writeln(); - // Route - buf.writeln('set top_route_layer ""'); - buf.writeln('foreach layer [lreverse [get_routing_layers]] {'); + // Filler cell insertion: closes gaps between standard cells so + // VDD/VSS rails are continuous. Fillcap cells are tried first so big + // gaps get decoupling capacitance, then plain fill cells handle the + // small leftover gaps where fillcap doesn't go below 4 sites wide. + buf.writeln('set fillers {}'); + buf.writeln('foreach sz {64 32 16 8 4} {'); + buf.writeln(' lappend fillers \${CELL_LIB}__fillcap_\$sz'); + buf.writeln('}'); + buf.writeln('foreach sz {64 32 16 8 4 2 1} {'); + buf.writeln(' lappend fillers \${CELL_LIB}__fill_\$sz'); + buf.writeln('}'); + buf.writeln('filler_placement \$fillers'); + buf.writeln(); + + // Route. Tile signals stay between Metal2 and Metal4. Metal5 is + // reserved for the top-level horizontal power straps so the tile + // never collides with the top PDN inside its own footprint. + buf.writeln('set_routing_layers -signal Metal2-Metal4'); + buf.writeln('# Apply per-layer routing capacity adjustments'); + buf.writeln('if {[array exists LAYER_ADJ]} {'); + buf.writeln(' foreach layer [array names LAYER_ADJ] {'); buf.writeln( - ' if {![catch {set tl ' - '[[[ord::get_db] getTech] findLayer \$layer]}]} {', + ' set_global_routing_layer_adjustment \$layer \$LAYER_ADJ(\$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( + 'detailed_route -droute_end_iter 32 ' + '-output_drc \${DEVICE_NAME}_${tileModule}_drc.rpt', + ); + buf.writeln(); + + // Antenna check + repair via layer-bumping only. Reclassify any + // ANTENNACELL master to plain CORE so repair_antennas will not + // instantiate diode cells. + buf.writeln('foreach lib [[ord::get_db] getLibs] {'); + buf.writeln(' foreach mast [\$lib getMasters] {'); + buf.writeln(' if {[\$mast getType] eq "CORE_ANTENNACELL"} {'); + buf.writeln(' \$mast setType "CORE"'); + buf.writeln(' }'); + buf.writeln(' }'); + buf.writeln('}'); + buf.writeln( + 'check_antennas -report_file ' + '${tileModule}_antenna_pre.rpt', + ); + // repair_antennas does its own incremental routing; skip a follow-up + // detailed_route to avoid pin-access conflicts (DRT-1231). + buf.writeln('repair_antennas'); + buf.writeln('check_antennas -report_file ${tileModule}_antenna.rpt'); buf.writeln(); // Reports @@ -165,8 +249,13 @@ class OpenroadTileTclEmitter { } void _writeLayerDetection(StringBuffer buf) { + // Push pin exit layers up. Skip Metal1 (cell-internal) and the first + // vertical layer (Metal2). Pins on Metal3/Metal4 keep top-level + // inter-tile routing on the higher, less-congested layers. buf.writeln('set hor_layer ""'); buf.writeln('set ver_layer ""'); + buf.writeln('set skip_first_hor 1'); + buf.writeln('set skip_first_ver 1'); buf.writeln('foreach layer [get_routing_layers] {'); buf.writeln( ' if {![catch {set tl ' @@ -174,18 +263,24 @@ class OpenroadTileTclEmitter { ); buf.writeln(' if {\$tl ne "NULL"} {'); buf.writeln(' set dir [\$tl getDirection]'); + buf.writeln(' if {\$dir eq "HORIZONTAL" && \$skip_first_hor} {'); + buf.writeln(' set skip_first_hor 0'); buf.writeln( - ' if {\$dir eq "HORIZONTAL" && \$hor_layer eq ""} {', + ' } elseif {\$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(' if {\$dir eq "VERTICAL" && \$skip_first_ver} {'); + buf.writeln(' set skip_first_ver 0'); + buf.writeln( + ' } elseif {\$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 }'); + buf.writeln('if {\$hor_layer eq ""} { set hor_layer Metal3 }'); + buf.writeln('if {\$ver_layer eq ""} { set ver_layer Metal4 }'); } } diff --git a/ip/lib/src/yosys.dart b/ip/lib/src/yosys.dart index 2ed4c60..e8faf40 100644 --- a/ip/lib/src/yosys.dart +++ b/ip/lib/src/yosys.dart @@ -1,2 +1,3 @@ +export 'yosys/keep_hierarchy.dart'; export 'yosys/tcl_emitter.dart'; export 'yosys/techmap_emitter.dart'; diff --git a/ip/lib/src/yosys/keep_hierarchy.dart b/ip/lib/src/yosys/keep_hierarchy.dart new file mode 100644 index 0000000..6a696f8 --- /dev/null +++ b/ip/lib/src/yosys/keep_hierarchy.dart @@ -0,0 +1,33 @@ +class KeepHierarchy { + static const modules = [ + 'AegisFPGA', + 'IOFabric', + 'LutFabric', + 'ClockTile', + 'FabricConfigLoader', + 'JtagTap', + 'IOTile', + 'SerDesTile', + 'Tile', + 'BramTile', + 'DspBasicTile', + 'Clb', + 'Lut4', + ]; + + static String inject(String sv, {Iterable? names}) { + final tagged = (names ?? modules).toSet(); + final lines = sv.split('\n'); + for (var i = 0; i < lines.length; i++) { + final line = lines[i]; + if (!line.startsWith('module ')) continue; + final rest = line.substring('module '.length); + final endIdx = rest.indexOf(RegExp(r'[\s(]')); + if (endIdx <= 0) continue; + final name = rest.substring(0, endIdx); + if (!tagged.contains(name)) continue; + lines[i] = '(* keep_hierarchy *) $line'; + } + return lines.join('\n'); + } +} diff --git a/ip/test/components/tile_test.dart b/ip/test/components/tile_test.dart index 2102ef7..d996e78 100644 --- a/ip/test/components/tile_test.dart +++ b/ip/test/components/tile_test.dart @@ -26,6 +26,7 @@ void main() { tileIn, tileOut, carryIn: carryIn, + neighborClbOut: TileInterface(), tracks: 1, ); await tile.build(); @@ -74,6 +75,7 @@ void main() { tileIn, tileOut, carryIn: carryIn, + neighborClbOut: TileInterface(), tracks: 4, ); await tile.build(); @@ -122,6 +124,7 @@ void main() { tileIn, tileOut, carryIn: carryIn, + neighborClbOut: TileInterface(), tracks: 1, ); await tile.build(); diff --git a/ip/test/config/config_test.dart b/ip/test/config/config_test.dart index 0bc3d5a..118dae6 100644 --- a/ip/test/config/config_test.dart +++ b/ip/test/config/config_test.dart @@ -115,8 +115,8 @@ void main() { expect(decoded.outputs[3][0].select, 0); }); - test('T=1 fits in 46 bits', () { - expect(tileConfigWidth(1), 46); + test('T=1 fits in 50 bits', () { + expect(tileConfigWidth(1), 50); final cfg = TileConfig( clb: const ClbConfig( lut: Lut4Config(truthTable: 0xFFFF), @@ -124,7 +124,7 @@ void main() { carryMode: true, ), tracks: 1, - inputSel: [6, 6, 6, 6], // max value for T=1 + inputSel: [10, 10, 10, 10], // max value for T=1 (NB_W) outputs: [ [const TrackOutputConfig(enable: true, select: 7)], [const TrackOutputConfig(enable: true, select: 7)], @@ -133,7 +133,7 @@ void main() { ], ); final bits = cfg.encode(); - expect(bits < (BigInt.one << 46), true); + expect(bits < (BigInt.one << 50), true); }); test('T=4 width is 102', () { diff --git a/ip/test/yosys/keep_hierarchy_test.dart b/ip/test/yosys/keep_hierarchy_test.dart new file mode 100644 index 0000000..8fb2d2a --- /dev/null +++ b/ip/test/yosys/keep_hierarchy_test.dart @@ -0,0 +1,175 @@ +import 'package:aegis_ip/aegis_ip.dart'; +import 'package:test/test.dart'; + +void main() { + group('KeepHierarchy.inject', () { + test('tags every default-list module that appears', () { + const sv = ''' +module Lut4 ( + input wire a +); +endmodule + +module Clb ( + input wire a +); +endmodule + +module SomeOtherModule ( + input wire a +); +endmodule +'''; + final out = KeepHierarchy.inject(sv); + expect(out, contains('(* keep_hierarchy *) module Lut4 (')); + expect(out, contains('(* keep_hierarchy *) module Clb (')); + expect(out, contains('module SomeOtherModule (')); + expect( + out, + isNot(contains('(* keep_hierarchy *) module SomeOtherModule')), + ); + }); + + test('tags every module in the default list', () { + final sv = KeepHierarchy.modules + .map((m) => 'module $m (\n input wire a\n);\nendmodule\n') + .join('\n'); + final out = KeepHierarchy.inject(sv); + for (final mod in KeepHierarchy.modules) { + expect( + out, + contains('(* keep_hierarchy *) module $mod ('), + reason: '$mod should be tagged', + ); + } + }); + + test('is idempotent', () { + const sv = 'module Tile (\n input wire a\n);\nendmodule\n'; + final once = KeepHierarchy.inject(sv); + final twice = KeepHierarchy.inject(once); + expect(twice, equals(once)); + // The `(* keep_hierarchy *)` prefix should appear exactly once. + expect('(* keep_hierarchy *)'.allMatches(twice).length, equals(1)); + }); + + test('leaves missing modules silently', () { + const sv = 'module Lut4 (\n input wire a\n);\nendmodule\n'; + // SerDesTile is in the default list but not in [sv]. + final out = KeepHierarchy.inject(sv); + expect(out, contains('(* keep_hierarchy *) module Lut4 (')); + expect(out, isNot(contains('SerDesTile'))); + }); + + test('respects a custom names list', () { + const sv = ''' +module Lut4 ( +); +endmodule + +module Clb ( +); +endmodule +'''; + final out = KeepHierarchy.inject(sv, names: ['Lut4']); + expect(out, contains('(* keep_hierarchy *) module Lut4 (')); + expect(out, contains('module Clb (')); + expect(out, isNot(contains('(* keep_hierarchy *) module Clb'))); + }); + + test('does not tag modules whose name is a prefix of a listed name', () { + const sv = 'module Lut (\n);\nendmodule\n'; + final out = KeepHierarchy.inject(sv); + // 'Lut' is not in the default list; only 'Lut4' is. + expect(out, isNot(contains('(* keep_hierarchy *) module Lut '))); + expect(out, contains('module Lut (')); + }); + + test( + 'does not touch lines that look like module declarations inside strings or comments', + () { + // Comment / string lines never start at column 0 with `module ` in + // ROHD-emitted SV; we still check that an inline occurrence is + // safe. + const sv = ''' +// module Lut4 fake comment + module Lut4 ( +); +endmodule +module Clb ( +); +endmodule +'''; + final out = KeepHierarchy.inject(sv); + // The genuine top-level Clb declaration is tagged. + expect(out, contains('(* keep_hierarchy *) module Clb (')); + // The indented `module Lut4` is not at column 0, so untouched. + expect(out, contains(' module Lut4 (')); + expect(out, isNot(contains('(* keep_hierarchy *) module Lut4'))); + // The comment line is untouched. + expect(out, contains('// module Lut4 fake comment')); + }, + ); + + test('preserves non-module content verbatim', () { + const sv = ''' +// header comment +`default_nettype none + +module Tile ( + input wire clk +); + wire foo; + assign foo = 1'b0; +endmodule + +`default_nettype wire +'''; + final out = KeepHierarchy.inject(sv); + expect(out, contains('// header comment')); + expect(out, contains('`default_nettype none')); + expect(out, contains(' wire foo;')); + expect(out, contains(" assign foo = 1'b0;")); + expect(out, contains('endmodule')); + expect(out, contains('`default_nettype wire')); + }); + + test('handles empty input', () { + expect(KeepHierarchy.inject(''), equals('')); + }); + + test('handles input with no module declarations', () { + const sv = '// just a comment\n`default_nettype none\n'; + expect(KeepHierarchy.inject(sv), equals(sv)); + }); + }); + + group('KeepHierarchy.modules', () { + test('covers all macro modules from YosysTclEmitter', () { + // KeepHierarchy is a superset of YosysTclEmitter.macroModules so + // anything pre-synthesized as a hard macro is also flat-synth-safe. + for (final mod in YosysTclEmitter.macroModules) { + expect( + KeepHierarchy.modules, + contains(mod), + reason: '$mod is a macro module but is not in KeepHierarchy.modules', + ); + } + }); + + test('includes the structural modules above the tile macros', () { + // These wrap the tile macros and would otherwise let yosys + // dead-code-eliminate the fabric. + expect(KeepHierarchy.modules, contains('AegisFPGA')); + expect(KeepHierarchy.modules, contains('IOFabric')); + expect(KeepHierarchy.modules, contains('LutFabric')); + }); + + test('includes the per-LUT structural modules', () { + // Lut4 holds the actual config register bits; if yosys flattens + // through Clb -> Lut4 it constant-folds them to 0. + expect(KeepHierarchy.modules, contains('Clb')); + expect(KeepHierarchy.modules, contains('Lut4')); + }); + }); +} diff --git a/nextpnr-aegis/aegis.cc b/nextpnr-aegis/aegis.cc index 92ea141..87ab232 100644 --- a/nextpnr-aegis/aegis.cc +++ b/nextpnr-aegis/aegis.cc @@ -47,7 +47,7 @@ struct AegisImpl : ViaductAPI { WireId ff_d, ff_q; // DFF wires WireId carry_in, carry_out; std::vector track_n, track_e, track_s, track_w; // T tracks per dir - // Per-track output mux wires — one per track per direction + // Per-track output mux wires - one per track per direction std::vector out_n, out_e, out_s, out_w; // IO wires (only for IO tiles) std::vector pad; @@ -117,6 +117,56 @@ struct AegisImpl : ViaductAPI { h.constrain_cell_pairs(pool{{id_lut, id_Y}}, pool{{id_dff_p, id_D}}, 1); log_info("Constrained %d LUTFF pairs.\n", lutffs); + + // Insert identity LUTs for unpaired DFFs. The DFF BEL's D input is + // only reachable via lut_out, so every DFF needs a paired LUT. + int inserted = 0; + std::vector dff_ids; + for (auto &cell : ctx->cells) + if (cell.second->type == id_dff_p) + dff_ids.push_back(cell.first); + + for (auto &id : dff_ids) { + CellInfo *dff = ctx->cells.at(id).get(); + if (dff->cluster != ClusterId()) + continue; // already paired + + // Create an identity LUT: Y = A[0] (init = 0xAAAA) + std::string name = dff->name.str(ctx) + std::string("_pass_lut"); + CellInfo *lut = ctx->createCell(ctx->id(name), id_lut); + lut->params[ctx->id("LUT")] = Property(0xAAAA, 16); + lut->params[ctx->id("WIDTH")] = Property(4, 32); + lut->addInput(ctx->id("A[0]")); + lut->addInput(ctx->id("A[1]")); + lut->addInput(ctx->id("A[2]")); + lut->addInput(ctx->id("A[3]")); + lut->addOutput(id_Y); + + // Rewire: DFF.D source → LUT.A[0], LUT.Y → DFF.D + NetInfo *d_net = dff->getPort(id_D); + dff->disconnectPort(id_D); + + NetInfo *pass_net = ctx->createNet(ctx->id(name + "_y")); + lut->connectPort(id_Y, pass_net); + dff->connectPort(id_D, pass_net); + + if (d_net) + lut->connectPort(ctx->id("A[0]"), d_net); + + // Constrain LUT+DFF as a cluster + lut->cluster = lut->name; + lut->constr_abs_z = false; + lut->constr_children.push_back(dff); + dff->cluster = lut->name; + dff->constr_x = 0; + dff->constr_y = 0; + dff->constr_z = 1; + dff->constr_abs_z = false; + + inserted++; + } + if (inserted > 0) + log_info("Inserted %d identity LUTs for unpaired DFFs.\n", inserted); } void prePlace() override { @@ -216,7 +266,7 @@ struct AegisImpl : ViaductAPI { tw.carry_out = ctx->addWire(h.xy_id(x, y, ctx->id("CARRY_OUT")), ctx->id("CARRY"), x, y); - // Per-track output mux wires — each track has its own independent mux + // Per-track output mux wires - each track has its own independent mux for (int t = 0; t < T; t++) { tw.out_n.push_back( ctx->addWire(h.xy_id(x, y, ctx->idf("OUT_N%d", t)), @@ -335,7 +385,7 @@ struct AegisImpl : ViaductAPI { void add_logic_bels(int x, int y) { auto &tw = tile_wires[y][x]; - // LUT4 BEL — pins match $lut cell ports: A[0]-A[3], Y + // LUT4 BEL - pins match $lut cell ports: A[0]-A[3], Y BelId lut = ctx->addBel(h.xy_id(x, y, ctx->id("SLICE0_LUT")), id_LUT4, Loc(x, y, 0), false, false); for (int k = 0; k < K; k++) @@ -346,7 +396,7 @@ struct AegisImpl : ViaductAPI { // LUT output -> FF D pip add_pip(Loc(x, y, 0), tw.lut_out, tw.ff_d); - // DFF BEL — pins match $_DFF_P_ cell ports: C, D, Q + // DFF BEL - pins match $_DFF_P_ cell ports: C, D, Q BelId dff = ctx->addBel(h.xy_id(x, y, ctx->id("SLICE0_FF")), id_DFF, Loc(x, y, 1), false, false); ctx->addBelInput(dff, ctx->id("C"), tw.clk); @@ -415,6 +465,22 @@ struct AegisImpl : ViaductAPI { add_pip(loc, tw.ff_q, dst, 0.05); // FF output } + // Neighbor direct connections: adjacent CLB outputs drive this tile's + // inputs without consuming routing tracks. + const int nb_dx[] = {0, 1, 0, -1}; // N, E, S, W + const int nb_dy[] = {-1, 0, 1, 0}; + for (int d = 0; d < 4; d++) { + int nx = x + nb_dx[d]; + int ny = y + nb_dy[d]; + if (nx > 0 && nx < W - 1 && ny > 0 && ny < H - 1) { + auto &ntw = tile_wires[ny][nx]; + for (int i = 0; i < K; i++) { + add_pip(loc, ntw.lut_out, tw.lut_in[i], 0.03); + add_pip(loc, ntw.ff_q, tw.lut_in[i], 0.03); + } + } + } + // Clock: any track from any direction can drive clock for (int t = 0; t < T; t++) { add_pip(loc, tw.track_n[t], tw.clk, 0.05); @@ -424,8 +490,10 @@ struct AegisImpl : ViaductAPI { } // Per-track output routing. Each track in each direction has its own - // independent output mux, selecting from CLB_O, CLB_Q, or the same - // track index from any other direction (pass-through). + // independent output mux, selecting from CLB_O, CLB_Q, or any + // incoming track (pass-through). The output mux wires (out_X) drive + // inter-tile pips directly, keeping input tracks (track_X) and output + // mux wires as independent resources. std::array *, 4> out_vecs = {&tw.out_n, &tw.out_e, &tw.out_s, &tw.out_w}; std::array *, 4> trk_vecs = {&tw.track_n, &tw.track_e, @@ -436,13 +504,10 @@ struct AegisImpl : ViaductAPI { // CLB sources add_pip(loc, tw.lut_out, out_wire, 0.05); add_pip(loc, tw.ff_q, out_wire, 0.05); - // Pass-through from same track of other directions + // Pass-through from any incoming direction (including same direction) for (int s = 0; s < 4; s++) { - if (s != d) - add_pip(loc, (*trk_vecs[s])[t], out_wire, 0.05); + add_pip(loc, (*trk_vecs[s])[t], out_wire, 0.05); } - // Output mux wire → track (1:1, not configurable) - add_pip(loc, out_wire, (*trk_vecs[d])[t], 0.01); } } } @@ -492,9 +557,18 @@ struct AegisImpl : ViaductAPI { if (tw.track_n.empty()) return; + // Logic tiles drive inter-tile pips from their output mux wires (out_X), + // keeping input tracks (track_X) as receive-only. IO tiles use their + // combined track wires directly (they have no output mux). + bool logic = !is_io(x, y); + auto &src_n = logic ? tw.out_n : tw.track_n; + auto &src_s = logic ? tw.out_s : tw.track_s; + auto &src_e = logic ? tw.out_e : tw.track_e; + auto &src_w = logic ? tw.out_w : tw.track_w; + // IO ring tiles only get span-1 connections (no multi-span routing - // through the IO ring — the sim models IO tiles as simple pass-through) - int max_span = is_io(x, y) ? 1 : 4; + // through the IO ring - the sim models IO tiles as simple pass-through) + int max_span = logic ? 4 : 1; int spans[] = {1, 2, 4}; for (int span : spans) { if (span > max_span) @@ -503,20 +577,16 @@ struct AegisImpl : ViaductAPI { for (int t = 0; t < T; t++) { // North if (y - span >= 0 && !tile_wires[y - span][x].track_s.empty()) - add_pip(loc, tw.track_n[t], tile_wires[y - span][x].track_s[t], - delay); + add_pip(loc, src_n[t], tile_wires[y - span][x].track_s[t], delay); // South if (y + span < H && !tile_wires[y + span][x].track_n.empty()) - add_pip(loc, tw.track_s[t], tile_wires[y + span][x].track_n[t], - delay); + add_pip(loc, src_s[t], tile_wires[y + span][x].track_n[t], delay); // East if (x + span < W && !tile_wires[y][x + span].track_w.empty()) - add_pip(loc, tw.track_e[t], tile_wires[y][x + span].track_w[t], - delay); + add_pip(loc, src_e[t], tile_wires[y][x + span].track_w[t], delay); // West if (x - span >= 0 && !tile_wires[y][x - span].track_e.empty()) - add_pip(loc, tw.track_w[t], tile_wires[y][x - span].track_e[t], - delay); + add_pip(loc, src_w[t], tile_wires[y][x - span].track_e[t], delay); } } } diff --git a/nextpnr-aegis/aegis_test.cc b/nextpnr-aegis/aegis_test.cc index c0e210d..b1f3633 100644 --- a/nextpnr-aegis/aegis_test.cc +++ b/nextpnr-aegis/aegis_test.cc @@ -192,18 +192,15 @@ TEST_F(AegisTest, CLBOutputDrivesAllPerTrackMuxes) { } TEST_F(AegisTest, PassThroughPipsUseSameTrackIndex) { - // OUT_N{t} should have pips from E{t}, S{t}, W{t} (same track index) + // OUT_N{t} should have pips from all 4 directions at same track index for (int t = 0; t < TEST_T; t++) { auto dst = "X2/Y2/OUT_N" + std::to_string(t); - // Should have pip from E{t}, S{t}, W{t} - EXPECT_NE(find_pip(dst, "X2/Y2/E" + std::to_string(t)), PipId()) - << "Missing pass-through pip: E" << t << " -> OUT_N" << t; - EXPECT_NE(find_pip(dst, "X2/Y2/S" + std::to_string(t)), PipId()); - EXPECT_NE(find_pip(dst, "X2/Y2/W" + std::to_string(t)), PipId()); - // Should NOT have pip from N{t} (same direction) - EXPECT_EQ(find_pip(dst, "X2/Y2/N" + std::to_string(t)), PipId()) - << "Should not have self-direction pass-through: N" << t << " -> OUT_N" - << t; + const char *dirs[] = {"N", "E", "S", "W"}; + for (auto dir : dirs) { + EXPECT_NE(find_pip(dst, "X2/Y2/" + std::string(dir) + std::to_string(t)), + PipId()) + << "Missing pass-through pip: " << dir << t << " -> OUT_N" << t; + } } } @@ -215,26 +212,29 @@ TEST_F(AegisTest, PassThroughDoesNotCrossTrackIndices) { << "Cross-track pass-through should not exist"; } -TEST_F(AegisTest, FanOutPipFromOutputMuxToTrack) { - // Each OUT_N{t} should drive N{t} (1:1 fan-out) +TEST_F(AegisTest, OutputMuxDrivesInterTileDirectly) { + // OUT_N{t} should drive the neighboring tile's S{t} via inter-tile pip, + // not the local N{t} track (which is input-only). for (int t = 0; t < TEST_T; t++) { - auto src = "X1/Y1/OUT_N" + std::to_string(t); - auto dst = "X1/Y1/N" + std::to_string(t); - EXPECT_NE(find_pip(dst, src), PipId()) - << "Missing fan-out pip: OUT_N" << t << " -> N" << t; + auto src = "X2/Y2/OUT_N" + std::to_string(t); + // Should NOT drive local track (input/output are separated) + auto local_dst = "X2/Y2/N" + std::to_string(t); + EXPECT_EQ(find_pip(local_dst, src), PipId()) + << "OUT_N" << t << " should not drive local N" << t; + // Should drive neighbor's track via inter-tile + auto nb_dst = "X2/Y1/S" + std::to_string(t); + EXPECT_NE(find_pip(nb_dst, src), PipId()) + << "OUT_N" << t << " should drive neighbor's S" << t; } - // OUT_N0 should NOT drive N1 (fan-out is 1:1) - EXPECT_EQ(find_pip("X1/Y1/N1", "X1/Y1/OUT_N0"), PipId()) - << "Fan-out should be 1:1, not cross-track"; } TEST_F(AegisTest, OutputMuxSourceCount) { - // Each per-track output mux wire should have exactly 5 uphill pips: - // CLB_O, CLB_Q, and 3 pass-through from other directions + // Each per-track output mux wire should have exactly 6 uphill pips: + // CLB_O, CLB_Q, and 4 pass-through from all directions for (int t = 0; t < TEST_T; t++) { auto wire = "X2/Y2/OUT_N" + std::to_string(t); - EXPECT_EQ(count_uphill(wire), 5) - << "OUT_N" << t << " should have 5 sources (CLB_O, CLB_Q, E, S, W)"; + EXPECT_EQ(count_uphill(wire), 6) + << "OUT_N" << t << " should have 6 sources (CLB_O, CLB_Q, N, E, S, W)"; } } @@ -266,9 +266,9 @@ TEST_F(AegisTest, InputMuxHasCLBFeedback) { } TEST_F(AegisTest, InputMuxTotalSources) { - // Each CLB_I should have 4*T + 2 uphill pips (4 dirs * T tracks + CLB_O + - // CLB_Q) - int expected = 4 * TEST_T + 2; + // Each CLB_I should have 4*T + 2 + 8 uphill pips (4 dirs * T tracks + + // CLB_O + CLB_Q + 4 neighbor lut_out + 4 neighbor ff_q) + int expected = 4 * TEST_T + 2 + 8; for (int i = 0; i < 4; i++) { auto wire = "X2/Y2/CLB_I" + std::to_string(i); EXPECT_EQ(count_uphill(wire), expected) @@ -288,32 +288,32 @@ TEST_F(AegisTest, ClockWireDrivenByAllTracks) { // === Inter-tile pip tests === TEST_F(AegisTest, Span1InterTilePips) { - // N0 at (2,2) should drive S0 at (2,1) (span-1 northward) - EXPECT_NE(find_pip("X2/Y1/S0", "X2/Y2/N0"), PipId()) + // OUT_N0 at (2,2) should drive S0 at (2,1) (span-1 northward) + EXPECT_NE(find_pip("X2/Y1/S0", "X2/Y2/OUT_N0"), PipId()) << "Missing span-1 inter-tile pip northward"; - // E0 at (2,2) should drive W0 at (3,2) (span-1 eastward) - EXPECT_NE(find_pip("X3/Y2/W0", "X2/Y2/E0"), PipId()) + // OUT_E0 at (2,2) should drive W0 at (3,2) (span-1 eastward) + EXPECT_NE(find_pip("X3/Y2/W0", "X2/Y2/OUT_E0"), PipId()) << "Missing span-1 inter-tile pip eastward"; } TEST_F(AegisTest, Span2InterTilePips) { - // N0 at (2,3) should drive S0 at (2,1) (span-2 northward) - EXPECT_NE(find_pip("X2/Y1/S0", "X2/Y3/N0"), PipId()) + // OUT_N0 at (2,3) should drive S0 at (2,1) (span-2 northward) + EXPECT_NE(find_pip("X2/Y1/S0", "X2/Y3/OUT_N0"), PipId()) << "Missing span-2 inter-tile pip northward"; } TEST_F(AegisTest, Span4InterTilePips) { - // S0 at (2,1) should drive N0 at (2,5) (span-4 southward) + // OUT_S0 at (2,1) should drive N0 at (2,5) (span-4 southward) // y=1 + 4 = 5, which is within the grid (gh=6) - EXPECT_NE(find_pip("X2/Y5/N0", "X2/Y1/S0"), PipId()) + EXPECT_NE(find_pip("X2/Y5/N0", "X2/Y1/OUT_S0"), PipId()) << "Missing span-4 inter-tile pip southward"; } TEST_F(AegisTest, InterTilePipsPreserveTrackIndex) { - // N2 at (2,2) should drive S2 at (2,1), not S0 - EXPECT_NE(find_pip("X2/Y1/S2", "X2/Y2/N2"), PipId()) + // OUT_N2 at (2,2) should drive S2 at (2,1), not S0 + EXPECT_NE(find_pip("X2/Y1/S2", "X2/Y2/OUT_N2"), PipId()) << "Inter-tile pip should preserve track index"; - EXPECT_EQ(find_pip("X2/Y1/S0", "X2/Y2/N2"), PipId()) + EXPECT_EQ(find_pip("X2/Y1/S0", "X2/Y2/OUT_N2"), PipId()) << "Inter-tile pip should not cross track indices"; } @@ -321,6 +321,7 @@ TEST_F(AegisTest, InterTilePipsPreserveTrackIndex) { TEST_F(AegisTest, IOTileSpan1Only) { // IO tile at (0,1): should have span-1 inter-tile pips + // IO tiles use track wires directly (no output mux) EXPECT_NE(find_pip("X1/Y1/W0", "X0/Y1/E0"), PipId()) << "IO tile should have span-1 eastward pip"; diff --git a/pkgs/aegis-ip/default.nix b/pkgs/aegis-ip/default.nix index 87cdb84..17f31e2 100644 --- a/pkgs/aegis-ip/default.nix +++ b/pkgs/aegis-ip/default.nix @@ -168,6 +168,9 @@ lib.extendMkDerivation { enableJtag ; mkTapeout = callPackage ../aegis-tapeout { aegis-ip = finalAttrs.finalPackage; }; + mkTapeoutLr = callPackage ../aegis-tapeout-lr { + aegis-ip = finalAttrs.finalPackage; + }; shell = mkShell { name = "aegis-${deviceName}-shell"; packages = [ diff --git a/pkgs/aegis-tapeout-lr/default.nix b/pkgs/aegis-tapeout-lr/default.nix new file mode 100644 index 0000000..fb6a071 --- /dev/null +++ b/pkgs/aegis-tapeout-lr/default.nix @@ -0,0 +1,187 @@ +{ + lib, + callPackage, + stdenv, + writeText, + librelane, + klayout, + yosys, + python3, + aegis-ip, +}: + +{ + pdk, + cellLib ? pdk.cellLib, + clockPeriodNs ? 20, + fabSlot ? "1x1", + designName ? "chip_top", + clockPort ? "clk_PAD", + clockNet ? "clk_pad/Y", + placementDensityPct ? 75, + fabMacros ? { + gf180mcu_ws_ip__id = { + gds = [ "dir::../ip/gf180mcu_ws_ip__id/gds/gf180mcu_ws_ip__id.gds" ]; + lef = [ "dir::../ip/gf180mcu_ws_ip__id/lef/gf180mcu_ws_ip__id.lef" ]; + vh = [ "dir::../ip/gf180mcu_ws_ip__id/vh/gf180mcu_ws_ip__id.v" ]; + lib."*" = [ "dir::../ip/gf180mcu_ws_ip__id/lib/gf180mcu_ws_ip__id.lib" ]; + instances.chip_id = { + location = [ + 26 + 26 + ]; + orientation = "N"; + }; + }; + gf180mcu_ws_ip__logo = { + gds = [ "dir::../ip/gf180mcu_ws_ip__logo/gds/gf180mcu_ws_ip__logo.gds" ]; + lef = [ "dir::../ip/gf180mcu_ws_ip__logo/lef/gf180mcu_ws_ip__logo.lef" ]; + vh = [ "dir::../ip/gf180mcu_ws_ip__logo/vh/gf180mcu_ws_ip__logo.v" ]; + lib."*" = [ "dir::../ip/gf180mcu_ws_ip__logo/lib/gf180mcu_ws_ip__logo.lib" ]; + instances.wafer_space_logo = { + location = [ + "expr::$DIE_AREA[2] + -169.25" + "expr::$DIE_AREA[3] + -169.25" + ]; + orientation = "N"; + }; + }; + }, + fabIgnoreDisconnectedModules ? [ + "gf180mcu_ws_ip__id" + "gf180mcu_ws_ip__logo" + ], + fabRequiredCells ? [ + { + module = "gf180mcu_ws_ip__id"; + instance = "chip_id"; + comment = "Chip ID - required for tapeout"; + } + { + module = "gf180mcu_ws_ip__logo"; + instance = "wafer_space_logo"; + comment = "wafer.space logo - optional"; + } + ], + ... +}@args: + +let + inherit (aegis-ip) deviceName; + + config = callPackage ./lib/mk-librelane-config.nix { } { + inherit + pdk + designName + clockPort + clockNet + clockPeriodNs + placementDensityPct + fabMacros + fabIgnoreDisconnectedModules + ; + verilogFiles = [ + "dir::../templates/chip_top.sv" + "dir::../templates/chip_core.sv" + "dir::../templates/aegis_fpga.sv" + ]; + sdcFile = "dir::${designName}.sdc"; + pdnConfigFile = "dir::pdn_cfg.tcl"; + }; + + padDefines = callPackage ./lib/mk-pad-defines.nix { } { inherit pdk; }; + fabRequiredCellsHeader = callPackage ./lib/mk-fab-required-cells.nix { } { + cells = fabRequiredCells; + }; + + slot = + pdk.librelane.slots.${fabSlot} + or (throw "aegis-tapeout-lr: pdk.librelane.slots has no slot named '${fabSlot}'"); + slotConfig = callPackage ./lib/mk-slot-config.nix { } { + slotName = fabSlot; + inherit slot; + }; +in +stdenv.mkDerivation { + name = "aegis-tapeout-lr-${deviceName}"; + + dontUnpack = true; + dontConfigure = true; + + nativeBuildInputs = [ + librelane + klayout + yosys + python3 + ]; + + AEGIS_IP = "${aegis-ip}"; + PDK_ROOT = "${pdk.librelane.pdkRoot}"; + PDK = pdk.librelane.pdkName; + + buildPhase = '' + runHook preBuild + + echo "=== Setting up LibreLane working directory ===" + cp -r ${./templates} templates + cp -r ${./librelane} librelane + cp -r ${pdk.librelane.fabRequiredIp} ip + + chmod -R u+w templates librelane ip + install -m 0644 ${config} librelane/config.yaml + + cp ${aegis-ip}/${deviceName}.sv templates/aegis_fpga.sv + chmod u+w templates/aegis_fpga.sv + + install -m 0644 ${padDefines} templates/pad_defines.svh + install -m 0644 ${fabRequiredCellsHeader} templates/fab_required_cells.svh + + mkdir -p librelane/slots + install -m 0644 ${slotConfig} librelane/slots/slot_${fabSlot}.yaml + + echo "=== Running LibreLane Chip flow ===" + mkdir -p out + + librelane \ + librelane/slots/slot_${fabSlot}.yaml \ + librelane/config.yaml \ + --save-views-to out/final \ + --pdk "$PDK" \ + --pdk-root "$PDK_ROOT" \ + --manual-pdk \ + --skip OpenROAD.STAPostPNR \ + --skip OpenROAD.IRDropReport \ + --skip Checker.SetupViolations \ + --skip Checker.HoldViolations \ + --skip Checker.MaxSlewViolations \ + --skip Checker.MaxCapViolations \ + --skip KLayout.Render \ + 2>&1 | tee librelane.log + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + mkdir -p $out + cp -r out/final $out/ 2>/dev/null || true + cp librelane.log $out/ 2>/dev/null || true + cp -r librelane/runs $out/runs 2>/dev/null || true + + runHook postInstall + ''; + + passthru = { + inherit + pdk + cellLib + clockPeriodNs + fabSlot + placementDensityPct + ; + inherit (aegis-ip) deviceName; + topCellName = designName; + librelaneConfig = config; + }; +} diff --git a/pkgs/aegis-tapeout-lr/lib/mk-fab-required-cells.nix b/pkgs/aegis-tapeout-lr/lib/mk-fab-required-cells.nix new file mode 100644 index 0000000..0b4f9e5 --- /dev/null +++ b/pkgs/aegis-tapeout-lr/lib/mk-fab-required-cells.nix @@ -0,0 +1,31 @@ +{ lib, writeText }: +{ + cells ? [ ], +}: + +let + emit = + cell: + let + commentLine = if cell ? comment then " // ${cell.comment}\n" else ""; + in + '' + ${commentLine} (* keep *) + ${cell.module} ${cell.instance} (); + ''; + + body = '' + // SPDX-FileCopyrightText: (C) 2026 Midstall Inc. + // SPDX-License-Identifier: Apache-2.0 + // + // GENERATED by pkgs/aegis-tapeout-lr/lib/mk-fab-required-cells.nix. + // Do not edit; regenerate by rebuilding the tapeout-lr derivation. + // + // This file is `\`included inside the chip_top module body, so each + // entry below is a module-scope instantiation, not a separate + // module declaration. + + '' + + lib.concatMapStrings emit cells; +in +writeText "fab_required_cells.svh" body diff --git a/pkgs/aegis-tapeout-lr/lib/mk-librelane-config.nix b/pkgs/aegis-tapeout-lr/lib/mk-librelane-config.nix new file mode 100644 index 0000000..60d794c --- /dev/null +++ b/pkgs/aegis-tapeout-lr/lib/mk-librelane-config.nix @@ -0,0 +1,85 @@ +{ lib, writeText }: +{ + pdk, + designName, + verilogFiles, + sdcFile, + pdnConfigFile, + clockPort, + clockNet, + clockPeriodNs, + placementDensityPct ? 75, + fabMacros ? { }, + fabIgnoreDisconnectedModules ? [ ], + extraConfig ? { }, +}: + +let + lr = pdk.librelane; + pdn = lr.pdn; + + config = { + meta = { + version = 3; + flow = "Chip"; + }; + + DESIGN_NAME = designName; + VERILOG_FILES = verilogFiles; + + USE_SLANG = false; + + PRIMARY_GDSII_STREAMOUT_TOOL = "klayout"; + + PNR_SDC_FILE = sdcFile; + SIGNOFF_SDC_FILE = sdcFile; + FALLBACK_SDC = sdcFile; + + VDD_NETS = [ "VDD" ]; + GND_NETS = [ "VSS" ]; + + IGNORE_DISCONNECTED_MODULES = lr.ignoreDisconnectedModules ++ fabIgnoreDisconnectedModules; + + CLOCK_PORT = clockPort; + CLOCK_NET = clockNet; + CLOCK_PERIOD = clockPeriodNs; + + PL_RESIZER_HOLD_SLACK_MARGIN = lr.resizer.plHoldSlackMargin; + GRT_RESIZER_HOLD_SLACK_MARGIN = lr.resizer.grtHoldSlackMargin; + + PL_TARGET_DENSITY_PCT = placementDensityPct; + GRT_ALLOW_CONGESTION = true; + + DRT_ANTENNA_REPAIR_ITERS = lr.antennaRepair.iters; + DRT_ANTENNA_REPAIR_MARGIN = lr.antennaRepair.margin; + + PDN_VWIDTH = pdn.vWidth; + PDN_HWIDTH = pdn.hWidth; + PDN_VSPACING = pdn.vSpacing; + PDN_HSPACING = pdn.hSpacing; + PDN_VPITCH = pdn.vPitch; + PDN_HPITCH = pdn.hPitch; + PDN_CORE_RING = pdn.coreRing.enable; + PDN_CORE_RING_VWIDTH = pdn.coreRing.vWidth; + PDN_CORE_RING_HWIDTH = pdn.coreRing.hWidth; + PDN_CORE_RING_CONNECT_TO_PADS = pdn.coreRing.connectToPads; + PDN_ENABLE_PINS = pdn.coreRing.enablePins; + PDN_CORE_VERTICAL_LAYER = pdn.coreVerticalLayer; + PDN_CORE_HORIZONTAL_LAYER = pdn.coreHorizontalLayer; + + FP_MACRO_HORIZONTAL_HALO = pdn.macroHorizontalHalo; + FP_MACRO_VERTICAL_HALO = pdn.macroVerticalHalo; + PDN_HORIZONTAL_HALO = pdn.horizontalHalo; + PDN_VERTICAL_HALO = pdn.verticalHalo; + + ERROR_ON_MAGIC_DRC = lr.errorOnMagicDrc; + MAGIC_GDS_FLATGLOB = lr.magicGdsFlatglob; + KLAYOUT_FILLER_OPTIONS = lr.klayoutFillerOptions; + MAGIC_EXT_UNIQUE = lr.magicExtUnique; + + MACROS = fabMacros; + PDN_CFG = pdnConfigFile; + } + // extraConfig; +in +writeText "${designName}-librelane-config.yaml" (lib.generators.toJSON { } config) diff --git a/pkgs/aegis-tapeout-lr/lib/mk-pad-defines.nix b/pkgs/aegis-tapeout-lr/lib/mk-pad-defines.nix new file mode 100644 index 0000000..e2483b1 --- /dev/null +++ b/pkgs/aegis-tapeout-lr/lib/mk-pad-defines.nix @@ -0,0 +1,27 @@ +{ lib, writeText }: +{ pdk }: + +let + c = pdk.librelane.padCells; + body = '' + // SPDX-FileCopyrightText: (C) 2026 Midstall Inc. + // SPDX-License-Identifier: Apache-2.0 + // + // GENERATED by pkgs/aegis-tapeout-lr/lib/mk-pad-defines.nix from + // pdk.librelane.padCells. Do not edit; regenerate by rebuilding + // the tapeout-lr derivation. + + `ifndef AEGIS_PAD_DEFINES_SVH + `define AEGIS_PAD_DEFINES_SVH + + `define BIDIR_PAD_CELL ${c.bidirCell} + `define INPUT_CMOS_PAD_CELL ${c.inputCmosCell} + `define INPUT_SCHMITT_PAD_CELL ${c.inputSchmittCell} + `define ANALOG_PAD_CELL ${c.analogCell} + `define POWER_PAD_CELL ${c.powerPadCell} + `define GROUND_PAD_CELL ${c.groundPadCell} + + `endif // AEGIS_PAD_DEFINES_SVH + ''; +in +writeText "pad_defines.svh" body diff --git a/pkgs/aegis-tapeout-lr/lib/mk-slot-config.nix b/pkgs/aegis-tapeout-lr/lib/mk-slot-config.nix new file mode 100644 index 0000000..3c24000 --- /dev/null +++ b/pkgs/aegis-tapeout-lr/lib/mk-slot-config.nix @@ -0,0 +1,19 @@ +{ lib, writeText }: +{ + slotName, + slot, +}: + +let + config = { + FP_SIZING = "absolute"; + DIE_AREA = slot.dieArea; + CORE_AREA = slot.coreArea; + VERILOG_DEFINES = slot.verilogDefines; + PAD_SOUTH = slot.pads.south; + PAD_EAST = slot.pads.east; + PAD_NORTH = slot.pads.north; + PAD_WEST = slot.pads.west; + }; +in +writeText "slot_${slotName}.yaml" (lib.generators.toJSON { } config) diff --git a/pkgs/aegis-tapeout-lr/librelane/chip_top.sdc b/pkgs/aegis-tapeout-lr/librelane/chip_top.sdc new file mode 100644 index 0000000..b2e71e7 --- /dev/null +++ b/pkgs/aegis-tapeout-lr/librelane/chip_top.sdc @@ -0,0 +1,82 @@ +current_design $::env(DESIGN_NAME) +set_units -time ns + +set clock_port __VIRTUAL_CLK__ +if { [info exists ::env(CLOCK_PORT)] } { + set port_count [llength $::env(CLOCK_PORT)] + + if { $port_count == "0" } { + puts "\[WARNING] No CLOCK_PORT found. A dummy clock will be used." + } elseif { $port_count != "1" } { + puts "\[WARNING] Multi-clock files are not currently supported by the base SDC file. Only the first clock will be constrained." + } + + if { $port_count > "0" } { + set ::clock_port [lindex $::env(CLOCK_PORT) 0] + } +} + +if { $::env(CLOCK_PORT) == $::env(CLOCK_NET) } { + set port_args [get_ports $clock_port] +} else { + # This should actually use CLOCK_PIN? + set port_args [get_pins [lindex $::env(CLOCK_NET) 0]] +} + +puts "\[INFO] Using clock $clock_port…" +create_clock {*}$port_args -name $clock_port -period $::env(CLOCK_PERIOD) + +set input_delay_value [expr $::env(CLOCK_PERIOD) * $::env(IO_DELAY_CONSTRAINT) / 100] +set output_delay_value [expr $::env(CLOCK_PERIOD) * $::env(IO_DELAY_CONSTRAINT) / 100] +puts "\[INFO] Setting output delay to: $output_delay_value" +puts "\[INFO] Setting input delay to: $input_delay_value" + +set_max_fanout $::env(MAX_FANOUT_CONSTRAINT) [current_design] +if { [info exists ::env(MAX_TRANSITION_CONSTRAINT)] } { + set_max_transition $::env(MAX_TRANSITION_CONSTRAINT) [current_design] +} +if { [info exists ::env(MAX_CAPACITANCE_CONSTRAINT)] } { + set_max_capacitance $::env(MAX_CAPACITANCE_CONSTRAINT) [current_design] +} + +set clocks [get_clocks $clock_port] + +# Bidirectional pads +set clk_core_inout_ports [get_ports { + bidir_PAD[*] +}] + +set_input_delay -min 0 -clock $clocks $clk_core_inout_ports +set_input_delay -max $input_delay_value -clock $clocks $clk_core_inout_ports +set_output_delay $output_delay_value -clock $clocks $clk_core_inout_ports + +# Input-only pads +set clk_core_input_ports [get_ports { + rst_n_PAD + input_PAD[*] +}] + +set_input_delay -min 0 -clock $clocks $clk_core_input_ports +set_input_delay -max $input_delay_value -clock $clocks $clk_core_input_ports + +# Output load +set cap_load [expr $::env(OUTPUT_CAP_LOAD) / 1000.0] +puts "\[INFO] Setting load to: $cap_load" +set_load $cap_load [all_outputs] + +puts "\[INFO] Setting clock uncertainty to: $::env(CLOCK_UNCERTAINTY_CONSTRAINT)" +set_clock_uncertainty $::env(CLOCK_UNCERTAINTY_CONSTRAINT) $clocks + +puts "\[INFO] Setting clock transition to: $::env(CLOCK_TRANSITION_CONSTRAINT)" +set_clock_transition $::env(CLOCK_TRANSITION_CONSTRAINT) $clocks + +puts "\[INFO] Setting timing derate to: $::env(TIME_DERATING_CONSTRAINT)%" +set_timing_derate -early [expr 1-[expr $::env(TIME_DERATING_CONSTRAINT) / 100]] +set_timing_derate -late [expr 1+[expr $::env(TIME_DERATING_CONSTRAINT) / 100]] + +if { [info exists ::env(OPENLANE_SDC_IDEAL_CLOCKS)] && $::env(OPENLANE_SDC_IDEAL_CLOCKS) } { + unset_propagated_clock [all_clocks] +} else { + set_propagated_clock [all_clocks] +} + diff --git a/pkgs/aegis-tapeout-lr/librelane/pdn_cfg.tcl b/pkgs/aegis-tapeout-lr/librelane/pdn_cfg.tcl new file mode 100644 index 0000000..572c5bd --- /dev/null +++ b/pkgs/aegis-tapeout-lr/librelane/pdn_cfg.tcl @@ -0,0 +1,195 @@ +# Copyright 2025 LibreLane Contributors +# +# Adapted from OpenLane +# +# Copyright 2020-2022 Efabless Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +source $::env(SCRIPTS_DIR)/openroad/common/io.tcl +source $::env(SCRIPTS_DIR)/openroad/common/set_global_connections.tcl +set_global_connections + +set secondary [] +foreach vdd $::env(VDD_NETS) gnd $::env(GND_NETS) { + if { $vdd != $::env(VDD_NET)} { + lappend secondary $vdd + + set db_net [[ord::get_db_block] findNet $vdd] + if {$db_net == "NULL"} { + set net [odb::dbNet_create [ord::get_db_block] $vdd] + $net setSpecial + $net setSigType "POWER" + } + } + + if { $gnd != $::env(GND_NET)} { + lappend secondary $gnd + + set db_net [[ord::get_db_block] findNet $gnd] + if {$db_net == "NULL"} { + set net [odb::dbNet_create [ord::get_db_block] $gnd] + $net setSpecial + $net setSigType "GROUND" + } + } +} + +set_voltage_domain -name CORE -power $::env(VDD_NET) -ground $::env(GND_NET) \ + -secondary_power $secondary + + + +if { $::env(PDN_MULTILAYER) == 1 } { + + set arg_list [list] + if { $::env(PDN_ENABLE_PINS) } { + lappend arg_list -pins "$::env(PDN_VERTICAL_LAYER) $::env(PDN_HORIZONTAL_LAYER)" + } + + define_pdn_grid \ + -name stdcell_grid \ + -starts_with POWER \ + -voltage_domain CORE \ + {*}$arg_list + + set arg_list [list] + append_if_equals arg_list PDN_EXTEND_TO "core_ring" -extend_to_core_ring + append_if_equals arg_list PDN_EXTEND_TO "boundary" -extend_to_boundary + + add_pdn_stripe \ + -grid stdcell_grid \ + -layer $::env(PDN_VERTICAL_LAYER) \ + -width $::env(PDN_VWIDTH) \ + -pitch $::env(PDN_VPITCH) \ + -offset $::env(PDN_VOFFSET) \ + -spacing $::env(PDN_VSPACING) \ + -starts_with POWER \ + {*}$arg_list + + add_pdn_stripe \ + -grid stdcell_grid \ + -layer $::env(PDN_HORIZONTAL_LAYER) \ + -width $::env(PDN_HWIDTH) \ + -pitch $::env(PDN_HPITCH) \ + -offset $::env(PDN_HOFFSET) \ + -spacing $::env(PDN_HSPACING) \ + -starts_with POWER \ + {*}$arg_list + + add_pdn_connect \ + -grid stdcell_grid \ + -layers "$::env(PDN_VERTICAL_LAYER) $::env(PDN_HORIZONTAL_LAYER)" +} else { + + set arg_list [list] + if { $::env(PDN_ENABLE_PINS) } { + lappend arg_list -pins "$::env(PDN_VERTICAL_LAYER)" + } + + define_pdn_grid \ + -name stdcell_grid \ + -starts_with POWER \ + -voltage_domain CORE \ + {*}$arg_list + + set arg_list [list] + append_if_equals arg_list PDN_EXTEND_TO "core_ring" -extend_to_core_ring + append_if_equals arg_list PDN_EXTEND_TO "boundary" -extend_to_boundary + + add_pdn_stripe \ + -grid stdcell_grid \ + -layer $::env(PDN_VERTICAL_LAYER) \ + -width $::env(PDN_VWIDTH) \ + -pitch $::env(PDN_VPITCH) \ + -offset $::env(PDN_VOFFSET) \ + -spacing $::env(PDN_VSPACING) \ + -starts_with POWER \ + {*}$arg_list +} + +# Adds the standard cell rails if enabled. +if { $::env(PDN_ENABLE_RAILS) == 1 } { + add_pdn_stripe \ + -grid stdcell_grid \ + -layer $::env(PDN_RAIL_LAYER) \ + -width $::env(PDN_RAIL_WIDTH) \ + -followpins + + add_pdn_connect \ + -grid stdcell_grid \ + -layers "$::env(PDN_RAIL_LAYER) $::env(PDN_VERTICAL_LAYER)" +} + + +# Adds the core ring if enabled. +if { $::env(PDN_CORE_RING) == 1 } { + if { $::env(PDN_MULTILAYER) == 1 } { + set arg_list [list] + append_if_flag arg_list PDN_CORE_RING_ALLOW_OUT_OF_DIE -allow_out_of_die + append_if_flag arg_list PDN_CORE_RING_CONNECT_TO_PADS -connect_to_pads + append_if_equals arg_list PDN_EXTEND_TO "boundary" -extend_to_boundary + + set pdn_core_vertical_layer $::env(PDN_VERTICAL_LAYER) + set pdn_core_horizontal_layer $::env(PDN_HORIZONTAL_LAYER) + + if { [info exists ::env(PDN_CORE_VERTICAL_LAYER)] } { + set pdn_core_vertical_layer $::env(PDN_CORE_VERTICAL_LAYER) + } + + if { [info exists ::env(PDN_CORE_HORIZONTAL_LAYER)] } { + set pdn_core_horizontal_layer $::env(PDN_CORE_HORIZONTAL_LAYER) + } + + add_pdn_ring \ + -grid stdcell_grid \ + -layers "$pdn_core_vertical_layer $pdn_core_horizontal_layer" \ + -widths "$::env(PDN_CORE_RING_VWIDTH) $::env(PDN_CORE_RING_HWIDTH)" \ + -spacings "$::env(PDN_CORE_RING_VSPACING) $::env(PDN_CORE_RING_HSPACING)" \ + -core_offset "$::env(PDN_CORE_RING_VOFFSET) $::env(PDN_CORE_RING_HOFFSET)" \ + {*}$arg_list + + if { [info exists ::env(PDN_CORE_VERTICAL_LAYER)] } { + add_pdn_connect \ + -grid stdcell_grid \ + -layers "$::env(PDN_CORE_VERTICAL_LAYER) $::env(PDN_HORIZONTAL_LAYER)" + } + + if { [info exists ::env(PDN_CORE_HORIZONTAL_LAYER)] } { + add_pdn_connect \ + -grid stdcell_grid \ + -layers "$::env(PDN_CORE_HORIZONTAL_LAYER) $::env(PDN_VERTICAL_LAYER)" + } + + if { [info exists ::env(PDN_CORE_VERTICAL_LAYER)] && [info exists ::env(PDN_CORE_HORIZONTAL_LAYER)] } { + add_pdn_connect \ + -grid stdcell_grid \ + -layers "$::env(PDN_CORE_VERTICAL_LAYER) $::env(PDN_CORE_HORIZONTAL_LAYER)" + } + + } else { + throw APPLICATION "PDN_CORE_RING cannot be used when PDN_MULTILAYER is set to false." + } +} + +define_pdn_grid \ + -macro \ + -default \ + -name macro \ + -starts_with POWER \ + -halo "$::env(PDN_HORIZONTAL_HALO) $::env(PDN_VERTICAL_HALO)" + +add_pdn_connect \ + -grid macro \ + -layers "$::env(PDN_VERTICAL_LAYER) $::env(PDN_HORIZONTAL_LAYER)" + diff --git a/pkgs/aegis-tapeout-lr/templates/chip_core.sv b/pkgs/aegis-tapeout-lr/templates/chip_core.sv new file mode 100644 index 0000000..0f51164 --- /dev/null +++ b/pkgs/aegis-tapeout-lr/templates/chip_core.sv @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: (C) 2026 Midstall Inc. +// SPDX-License-Identifier: Apache-2.0 + +`default_nettype none + +// Chip core wrapper that adapts AegisFPGA's ports to the wafer.space +// gf180mcu-project-template chip_core interface. The pad cells live +// in chip_top, this module is what gets placed inside the padring. +// +// Pad budget on the 1x1 slot (per slot_1x1.yaml + slot_defines.svh): +// - 12 CMOS input pads +// - 40 bidirectional pads (24mA) +// - 2 analog pads +// +// Mapping for Luna-1 (23x23 fabric, 92 user-IO pads, no SerDes): +// inputs[0..7] <- AegisFPGA.configRead_data[7:0] +// inputs[8..11] <- unused (tied off in core) +// bidir[0..24] <-> AegisFPGA.padIn / padOut / padOutputEnable[24:0] +// (the first 25 of 92 perimeter IO pads get bond pads) +// bidir[25..28] <- AegisFPGA.clkOut[3:0] (driven, OE=1) +// bidir[29] <- AegisFPGA.clkLocked (driven, OE=1) +// bidir[30..37] <- AegisFPGA.configRead_addr[7:0] (driven, OE=1) +// bidir[38] <- AegisFPGA.configRead_en (driven, OE=1) +// bidir[39] <- AegisFPGA.configDone (driven, OE=1) +// AegisFPGA.padIn[91:25] = 0 (67 IO pads with no bond pad - tied low) +// analog[*] <- unused +// +// rst_n is the chip-level active-low reset, AegisFPGA expects active-high +// reset, so we invert. + +module chip_core #( + parameter NUM_INPUT_PADS = 12, + parameter NUM_BIDIR_PADS = 40, + parameter NUM_ANALOG_PADS = 2 +) ( +`ifdef USE_POWER_PINS + inout wire VDD, + inout wire VSS, +`endif + input wire clk, + input wire rst_n, + + input wire [NUM_INPUT_PADS-1:0] input_in, + output wire [NUM_INPUT_PADS-1:0] input_pu, + output wire [NUM_INPUT_PADS-1:0] input_pd, + + input wire [NUM_BIDIR_PADS-1:0] bidir_in, + output wire [NUM_BIDIR_PADS-1:0] bidir_out, + output wire [NUM_BIDIR_PADS-1:0] bidir_oe, + output wire [NUM_BIDIR_PADS-1:0] bidir_cs, + output wire [NUM_BIDIR_PADS-1:0] bidir_sl, + output wire [NUM_BIDIR_PADS-1:0] bidir_ie, + output wire [NUM_BIDIR_PADS-1:0] bidir_pu, + output wire [NUM_BIDIR_PADS-1:0] bidir_pd, + + inout wire [NUM_ANALOG_PADS-1:0] analog +); + + localparam int FABRIC_USER_IOS = 92; // 2*W + 2*H for 23x23 fabric + localparam int USER_IO_PADS = 25; // bidir slots reserved for user IOs + + (* keep = "true" *) wire [FABRIC_USER_IOS-1:0] core_padIn; + (* keep = "true" *) wire [FABRIC_USER_IOS-1:0] core_padOut; + (* keep = "true" *) wire [FABRIC_USER_IOS-1:0] core_padOutputEnable; + (* keep = "true" *) wire [3:0] core_clkOut; + (* keep = "true" *) wire core_clkLocked; + (* keep = "true" *) wire [7:0] core_configRead_data; + (* keep = "true" *) wire [7:0] core_configRead_addr; + (* keep = "true" *) wire core_configRead_en; + (* keep = "true" *) wire core_configDone; + + assign core_configRead_data = input_in[7:0]; + // Pull controls: leave all inputs floating (no pull-up / pull-down). + assign input_pu = {NUM_INPUT_PADS{1'b0}}; + assign input_pd = {NUM_INPUT_PADS{1'b0}}; + + assign core_padIn = { + {(FABRIC_USER_IOS - USER_IO_PADS){1'b0}}, + bidir_in[USER_IO_PADS-1:0] + }; + + // bidir_out: low USER_IO_PADS bits come from AegisFPGA padOut, the + // remaining 15 are control outputs. + assign bidir_out[USER_IO_PADS-1:0] = core_padOut[USER_IO_PADS-1:0]; + assign bidir_out[28:25] = core_clkOut; + assign bidir_out[29] = core_clkLocked; + assign bidir_out[37:30] = core_configRead_addr; + assign bidir_out[38] = core_configRead_en; + assign bidir_out[39] = core_configDone; + + // bidir_oe: user IOs follow padOutputEnable; control outputs always drive. + assign bidir_oe[USER_IO_PADS-1:0] = core_padOutputEnable[USER_IO_PADS-1:0]; + assign bidir_oe[NUM_BIDIR_PADS-1:USER_IO_PADS] = + {(NUM_BIDIR_PADS - USER_IO_PADS){1'b1}}; + + // Bidir control settings: enable input receivers, drive at 24mA, no + // pull-up / pull-down, no current source, fast slew. + assign bidir_cs = {NUM_BIDIR_PADS{1'b0}}; + assign bidir_sl = {NUM_BIDIR_PADS{1'b0}}; + assign bidir_ie = {NUM_BIDIR_PADS{1'b1}}; + assign bidir_pu = {NUM_BIDIR_PADS{1'b0}}; + assign bidir_pd = {NUM_BIDIR_PADS{1'b0}}; + + (* keep *) + AegisFPGA u_core ( + .clk (clk), + .reset (~rst_n), + .padIn (core_padIn), + .configRead_data (core_configRead_data), + .padOut (core_padOut), + .padOutputEnable (core_padOutputEnable), + .clkOut (core_clkOut), + .clkLocked (core_clkLocked), + .configRead_en (core_configRead_en), + .configRead_addr (core_configRead_addr), + .configDone (core_configDone) + ); + + // Analog pads carry no internal connection. They appear on the + // padring only so the slot pad list stays consistent with the + // wafer.space template. + genvar i; + generate + for (i = 0; i < NUM_ANALOG_PADS; i = i + 1) begin : g_analog_unused + // Intentionally empty - analog pad has no driver here. + end + endgenerate + +endmodule + +`default_nettype wire diff --git a/pkgs/aegis-tapeout-lr/templates/chip_top.sv b/pkgs/aegis-tapeout-lr/templates/chip_top.sv new file mode 100644 index 0000000..10fbd45 --- /dev/null +++ b/pkgs/aegis-tapeout-lr/templates/chip_top.sv @@ -0,0 +1,198 @@ +// SPDX-FileCopyrightText: (C) 2026 Midstall Inc. +// SPDX-License-Identifier: Apache-2.0 + +`default_nettype none + +`include "slot_defines.svh" +`include "pad_defines.svh" + +module chip_top #( + parameter NUM_DVDD_PADS = `NUM_DVDD_PADS, + parameter NUM_DVSS_PADS = `NUM_DVSS_PADS, + parameter NUM_INPUT_PADS = `NUM_INPUT_PADS, + parameter NUM_BIDIR_PADS = `NUM_BIDIR_PADS, + parameter NUM_ANALOG_PADS = `NUM_ANALOG_PADS + )( + `ifdef USE_POWER_PINS + inout wire VDD, + inout wire VSS, + `endif + + inout wire clk_PAD, + inout wire rst_n_PAD, + + inout wire [NUM_INPUT_PADS-1:0] input_PAD, + inout wire [NUM_BIDIR_PADS-1:0] bidir_PAD, + + inout wire [NUM_ANALOG_PADS-1:0] analog_PAD +); + + wire clk_PAD2CORE; + wire rst_n_PAD2CORE; + + wire [NUM_INPUT_PADS-1:0] input_PAD2CORE; + wire [NUM_INPUT_PADS-1:0] input_CORE2PAD_PU; + wire [NUM_INPUT_PADS-1:0] input_CORE2PAD_PD; + + wire [NUM_BIDIR_PADS-1:0] bidir_PAD2CORE; + wire [NUM_BIDIR_PADS-1:0] bidir_CORE2PAD; + wire [NUM_BIDIR_PADS-1:0] bidir_CORE2PAD_OE; + wire [NUM_BIDIR_PADS-1:0] bidir_CORE2PAD_CS; + wire [NUM_BIDIR_PADS-1:0] bidir_CORE2PAD_SL; + wire [NUM_BIDIR_PADS-1:0] bidir_CORE2PAD_IE; + wire [NUM_BIDIR_PADS-1:0] bidir_CORE2PAD_PU; + wire [NUM_BIDIR_PADS-1:0] bidir_CORE2PAD_PD; + + generate + for (genvar i=0; iM4->M3->M2->M1 via stack onto its M1 power pins + # without colliding with the macro's internal routing. + w = 100; + h = 80; }; SerDesTile = { - w = 853; - h = 132; + w = 950; + h = 160; }; JtagTap = { - w = 45; - h = 30; + # Same rationale as FabricConfigLoader: needs enough area + # for the top-level PDN via stack to land cleanly. + w = 80; + h = 60; }; }; sky130 = { @@ -167,7 +212,7 @@ lib.extendMkDerivation { if [ -z "$LIB_FILE" ]; then LIB_FILE=$(find ${libFile} -name '*.lib' -print -quit) fi - TECH_LEF=$(find ${techLefDir} -name '*tech*.lef' -print -quit) + TECH_LEF="${techLefDir}/${pdk.techLef}" # Skip if this tile type doesn't exist in the device if [ ! -f "${aegis-ip}/${deviceName}-yosys-${tileModule}.tcl" ]; then @@ -203,6 +248,23 @@ lib.extendMkDerivation { ${lib.optionalString (builtins.hasAttr tileModule tilePlacementDensities) '' set TILE_PLACEMENT_DENSITY ${toString tilePlacementDensities.${tileModule}} ''} + ${lib.concatStringsSep "\n" ( + lib.mapAttrsToList (layer: adj: "set LAYER_ADJ(${layer}) ${toString adj}") tileLayerAdjustments + )} + ${lib.optionalString (pdk ? pdn) ( + let + pdn = pdk.pdn; + in + '' + set PDN_RAIL_LAYER "${pdn.railLayer}" + set PDN_RAIL_WIDTH ${toString pdn.railWidth} + set PDN_VERTICAL_LAYER "${pdn.verticalLayer}" + set PDN_VWIDTH ${toString pdn.verticalWidth} + set PDN_VPITCH ${toString pdn.verticalPitch} + set PDN_VOFFSET ${toString pdn.verticalOffset} + set PDN_VSPACING ${toString pdn.verticalSpacing} + '' + )} source ${aegis-ip}/${deviceName}-openroad-${tileModule}.tcl EOF openroad -threads $NIX_BUILD_CORES -exit pnr.tcl 2>&1 | tee openroad.log @@ -214,6 +276,7 @@ lib.extendMkDerivation { TECH_LEF="$TECH_LEF" \ DEF_FILE="${deviceName}_${tileModule}_final.def" \ OUT_GDS="${deviceName}_${tileModule}_final.gds" \ + LAYER_MAP="${lefGdsMapFile}" \ QT_QPA_PLATFORM=offscreen \ klayout -b -r ${./scripts/def2gds.py} 2>&1 | tee klayout.log || true fi @@ -232,6 +295,8 @@ lib.extendMkDerivation { 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 ${tileModule}_antenna.rpt $out/ 2>/dev/null || true + cp ${tileModule}_antenna_pre.rpt $out/ 2>/dev/null || true cp yosys.log $out/ 2>/dev/null || true cp openroad.log $out/ 2>/dev/null || true runHook postInstall @@ -257,13 +322,23 @@ lib.extendMkDerivation { ] ); + chipModule = "${deviceName}_chip"; + ioLibsRef = "${pdkPath}/libs.ref/${pdk.ioLib}"; + ioVerilog = "${ioLibsRef}/verilog/${pdk.ioLib}__blackbox.v"; + # Typical-corner liberty for the IO pad cells. Picking the 5V + # variant since that matches the 5v0 standard-cell voltage. + ioLibFile = "${ioLibsRef}/lib/${pdk.ioLib}__tt_025C_5v00.lib"; + topSynth = stdenv.mkDerivation { name = "aegis-top-synth-${deviceName}"; dontUnpack = true; dontConfigure = true; - nativeBuildInputs = [ yosys ]; + nativeBuildInputs = [ + yosys + (python3.withPackages (_: [ ])) + ]; buildPhase = '' runHook preBuild @@ -273,14 +348,53 @@ lib.extendMkDerivation { LIB_FILE=$(find ${libFile} -name '*.lib' -print -quit) fi - echo "=== Top-level assembly ===" + echo "=== Generate chip-level wrapper with padring ===" + IN_SV="${aegis-ip}/${deviceName}.sv" \ + OUT_SV="${deviceName}_chip.sv" \ + CORE_MODULE=AegisFPGA \ + CHIP_MODULE=${chipModule} \ + PAD_BIDIR="${pdk.padCells.signalPad}" \ + PAD_INPUT="${pdk.padCells.inputPad}" \ + PAD_VDD="${pdk.padCells.powerPad}" \ + PAD_VSS="${pdk.padCells.groundPad}" \ + POWER_PAD_PER_SIDE=2 \ + python3 ${./scripts/gen_chip_wrapper.py} + + echo "=== Top-level assembly + chip wrapper ===" 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 + # Read core, chip wrapper, tile stubs, and I/O pad blackbox views. + yosys read_verilog -sv ${aegis-ip}/${deviceName}.sv + yosys read_verilog -sv ${deviceName}_chip.sv + # Drop tile bodies and replace with blackbox stubs (same pattern as + # ${aegis-ip}/${deviceName}-yosys.tcl). + yosys delete Tile + yosys delete BramTile + yosys delete DspBasicTile + yosys delete ClockTile + yosys delete IOTile + yosys delete SerDesTile + yosys delete FabricConfigLoader + yosys delete JtagTap + yosys read_verilog -sv ${aegis-ip}/${deviceName}_tile_stubs.v + # Pad-cell blackbox declarations from the I/O library. + yosys read_verilog -sv ${ioVerilog} + yosys read_liberty -lib $LIB_FILE + # I/O pad liberty so the chip wrapper has timing models for pads. + yosys read_liberty -lib ${ioLibFile} + yosys hierarchy -top ${chipModule} + yosys proc + yosys techmap + # Flatten the AegisFPGA wrapper so tile macros become direct + # children of the chip wrapper. The OpenROAD placement code + # walks getInsts at the top block expecting tiles there. + yosys flatten + yosys dfflibmap -liberty $LIB_FILE + yosys abc -liberty $LIB_FILE + yosys hilomap -hicell ${cellLib}__tieh Z -locell ${cellLib}__tiel ZN + yosys opt_clean -purge + yosys check + yosys write_verilog -noattr -noexpr ${deviceName}_synth.v + yosys stat -liberty $LIB_FILE EOF yosys -c synth.tcl 2>&1 | tee yosys.log @@ -291,6 +405,7 @@ lib.extendMkDerivation { runHook preInstall mkdir -p $out cp ${deviceName}_synth.v $out/ 2>/dev/null || true + cp ${deviceName}_chip.sv $out/ 2>/dev/null || true cp yosys.log $out/ 2>/dev/null || true runHook postInstall ''; @@ -311,7 +426,7 @@ lib.extendMkDerivation { if [ -z "$LIB_FILE" ]; then LIB_FILE=$(find ${libFile} -name '*.lib' -print -quit) fi - TECH_LEF=$(find ${techLefDir} -name '*tech*.lef' -print -quit) + TECH_LEF="${techLefDir}/${pdk.techLef}" # Copy tile macro LEFs and liberty timing models into working directory ${lib.concatMapStringsSep "\n" (mod: '' @@ -319,28 +434,65 @@ lib.extendMkDerivation { cp ${tileMacros.${mod}}/${deviceName}_${mod}.lib . 2>/dev/null || true '') (builtins.attrNames tileMacros)} + # Clock comes from the receiver Y pin of the clk input pad. The + # chip wrapper has no signal ports (bond pads are the only + # external boundary for signals) so we cannot reference a + # top-level port here. cat > constraints.sdc << EOF - create_clock [get_ports clk] -name clk -period ${toString clockPeriodNs} + create_clock [get_pins u_pad_clk/Y] -name clk -period ${toString clockPeriodNs} EOF - echo "=== Top-level PnR (macro-based) ===" + echo "=== Top-level PnR (macro-based, with padring) ===" cat > pnr.tcl << OPENROAD_EOF set LIB_FILE "$LIB_FILE" set TECH_LEF "$TECH_LEF" set CELL_LEF_DIR "${techLefDir}" + set IO_LEF_DIR "${ioLibsRef}/lef" + set IO_LIB_FILE "${ioLibFile}" set SYNTH_V "${topSynth}/${deviceName}_synth.v" set SDC_FILE "constraints.sdc" set DEVICE_NAME "${deviceName}" + set TOP_MODULE "${chipModule}" set SITE_NAME "${pdk.siteName}" set UTILIZATION ${toString coreUtilization} set CELL_LIB "${cellLib}" + set IO_LIB "${pdk.ioLib}" + set PAD_BIDIR "${pdk.padCells.signalPad}" + set PAD_VDD_CELL "${pdk.padCells.powerPad}" + set PAD_VSS_CELL "${pdk.padCells.groundPad}" + set PAD_CORNER "${pdk.padCells.cornerCell}" + set PAD_HEIGHT ${toString pdk.padCells.padHeight} + set CORNER_SIZE ${toString pdk.padCells.cornerSize} set MACRO_HALO ${toString macroHaloUm} set GRID_MARGIN ${toString gridMarginUm} set PLACEMENT_DENSITY ${toString topPlacementDensity} set DROUTE_END_ITER ${toString topDetailedRouteIter} - ${lib.optionalString (dieWidthUm != null && dieHeightUm != null) '' - set DIE_AREA "0 0 ${toString dieWidthUm} ${toString dieHeightUm}" + ${lib.optionalString (userWidthUm != null && userHeightUm != null) '' + set DIE_AREA "0 0 ${toString userWidthUm} ${toString userHeightUm}" ''} + ${lib.concatStringsSep "\n" ( + lib.mapAttrsToList (layer: adj: "set LAYER_ADJ(${layer}) ${toString adj}") topLayerAdjustments + )} + ${lib.optionalString (pdk ? pdn) ( + let + pdn = pdk.pdn; + in + '' + set PDN_RAIL_LAYER "${pdn.railLayer}" + set PDN_RAIL_WIDTH ${toString pdn.railWidth} + set PDN_VERTICAL_LAYER "${pdn.verticalLayer}" + set PDN_VWIDTH ${toString pdn.verticalWidth} + set PDN_VPITCH ${toString pdn.verticalPitch} + set PDN_VOFFSET ${toString pdn.verticalOffset} + set PDN_VSPACING ${toString pdn.verticalSpacing} + set PDN_HORIZONTAL_LAYER "${pdn.horizontalLayer}" + set PDN_HWIDTH ${toString pdn.horizontalWidth} + set PDN_HPITCH ${toString pdn.horizontalPitch} + set PDN_HOFFSET ${toString pdn.horizontalOffset} + set PDN_HSPACING ${toString pdn.horizontalSpacing} + set PDN_HALO ${toString pdn.halo} + '' + )} source ${aegis-ip}/${deviceName}-openroad.tcl OPENROAD_EOF openroad -threads $NIX_BUILD_CORES -exit pnr.tcl 2>&1 | tee openroad.log @@ -356,6 +508,8 @@ lib.extendMkDerivation { 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 antenna.rpt $out/ 2>/dev/null || true + cp antenna_pre.rpt $out/ 2>/dev/null || true cp openroad.log $out/ 2>/dev/null || true runHook postInstall ''; @@ -365,6 +519,7 @@ lib.extendMkDerivation { "pdk" "cellLib" "clockPeriodNs" + "fabSlot" "dieWidthUm" "dieHeightUm" "coreUtilization" @@ -392,22 +547,27 @@ lib.extendMkDerivation { echo "=== GDS generation ===" if [ -f "${topPnr}/${deviceName}_final.def" ]; then - # Collect tile macro GDS files into one directory - mkdir -p macro_gds + # Collect tile macro GDS and LEF files into directories + mkdir -p macro_gds macro_lef ${lib.concatMapStringsSep "\n" (mod: '' if [ -f "${tileMacros.${mod}}/${deviceName}_${mod}_final.gds" ]; then cp ${tileMacros.${mod}}/${deviceName}_${mod}_final.gds macro_gds/ fi + if [ -f "${tileMacros.${mod}}/${deviceName}_${mod}.lef" ]; then + cp ${tileMacros.${mod}}/${deviceName}_${mod}.lef macro_lef/ + fi '') (builtins.attrNames tileMacros)} - TECH_LEF=$(find ${libsRef}/lef -name '*tech*.lef' -print -quit) + TECH_LEF="${libsRef}/lef/${pdk.techLef}" CELL_GDS_DIR="${libsRef}/gds" \ MACRO_GDS_DIR="macro_gds" \ + MACRO_LEF_DIR="macro_lef" \ LEF_DIR="${libsRef}/lef" \ TECH_LEF="$TECH_LEF" \ DEF_FILE="${topPnr}/${deviceName}_final.def" \ OUT_GDS="${deviceName}.gds" \ + LAYER_MAP="${lefGdsMapFile}" \ QT_QPA_PLATFORM=offscreen \ klayout -b -r ${./scripts/def2gds.py} \ 2>&1 | tee klayout.log || true @@ -423,6 +583,54 @@ lib.extendMkDerivation { klayout -b -r ${./scripts/stamp_text.py} \ 2>&1 | tee -a klayout.log + echo "=== Fab finalize ===" + + GDS_FILE="${deviceName}.gds" \ + TOP_CELL="${chipModule}" \ + ${ + lib.optionalString (effectiveDieWidthUm != null) ''DIE_W_UM="${toString effectiveDieWidthUm}"'' + } \ + ${ + lib.optionalString (effectiveDieHeightUm != null) ''DIE_H_UM="${toString effectiveDieHeightUm}"'' + } \ + ${lib.optionalString (fab ? sealRing) ''SEAL_LAYER="${toString fab.sealRing.layer}"''} \ + ${lib.optionalString (fab ? sealRing) ''SEAL_DATATYPE="${toString fab.sealRing.datatype}"''} \ + ${lib.optionalString (fab ? sealRing) ''SEAL_WIDTH_UM="${toString fab.sealRing.width}"''} \ + ${lib.optionalString (fab ? idCell) ''ID_CELL="${fab.idCell}"''} \ + QT_QPA_PLATFORM=offscreen \ + klayout -b -r ${./scripts/fab_finalize.py} \ + 2>&1 | tee -a klayout.log + + echo "=== Density fill ===" + + FILL_SCRIPT="${pdkPath}/pv/klayout/drc/filler_generation/fill_all.rb" + if [ -f "$FILL_SCRIPT" ]; then + cp "${deviceName}.gds" "${deviceName}_prefill.gds" + # Set Metal*_ignore_active to allow fill near active metal + # (matches wafer.space's LibreLane filler options). + QT_QPA_PLATFORM=offscreen klayout -b -zz \ + -r "$FILL_SCRIPT" \ + -rd input="${deviceName}_prefill.gds" \ + -rd output="${deviceName}.gds" \ + -rd Metal1_ignore_active=true \ + -rd Metal2_ignore_active=true \ + -rd Metal3_ignore_active=true \ + -rd Metal4_ignore_active=true \ + -rd Metal5_ignore_active=true \ + 2>&1 | tee -a klayout.log + rm "${deviceName}_prefill.gds" + echo "Density fill complete" + + # Clean up orphan fill cells created by the filler + GDS_FILE="${deviceName}.gds" \ + TOP_CELL="${chipModule}" \ + QT_QPA_PLATFORM=offscreen \ + klayout -b -r ${./scripts/fab_finalize.py} \ + 2>&1 | tee -a klayout.log + else + echo "NOTE: Fill script not found, skipping density fill" + fi + echo "=== Render layout image ===" GDS_FILE="${deviceName}.gds" \ @@ -462,6 +670,8 @@ lib.extendMkDerivation { 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 + cp ${topPnr}/antenna.rpt $out/ 2>/dev/null || true + cp ${topPnr}/antenna_pre.rpt $out/ 2>/dev/null || true # GDS cp ${deviceName}.gds $out/ 2>/dev/null || true @@ -490,7 +700,9 @@ lib.extendMkDerivation { tileMacros topSynth topPnr + chipModule ; + topCellName = chipModule; inherit (aegis-ip) deviceName width diff --git a/pkgs/aegis-tapeout/scripts/def2gds.py b/pkgs/aegis-tapeout/scripts/def2gds.py index 78f9659..c816740 100644 --- a/pkgs/aegis-tapeout/scripts/def2gds.py +++ b/pkgs/aegis-tapeout/scripts/def2gds.py @@ -3,10 +3,12 @@ Environment variables: CELL_GDS_DIR - directory containing PDK standard cell GDS files MACRO_GDS_DIR - directory containing tile macro GDS files (optional) + MACRO_LEF_DIR - directory containing tile macro LEF 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 + LAYER_MAP - path to KLayout LEF/DEF layer map file (optional) """ import glob @@ -16,42 +18,84 @@ cell_gds_dir = os.environ["CELL_GDS_DIR"] macro_gds_dir = os.environ.get("MACRO_GDS_DIR", "") +macro_lef_dir = os.environ.get("MACRO_LEF_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"] +layer_map = os.environ.get("LAYER_MAP", "") layout = pya.Layout() +# Use default DBU (0.001um) to match standard cell GDS files. +# The DEF reader handles DATABASE MICRONS conversion internally. -# Read tech LEF first (layer definitions) +# Collect standard cell and tech LEF files for the DEF reader +lef_files = [] 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 + lef_files.append(tech_lef) 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: + for lef in sorted(glob.glob(os.path.join(lef_dir, "*.lef"))): if "tech" not in os.path.basename(lef).lower(): - layout.read(lef) + lef_files.append(lef) + +# Read tile macro LEFs first so their MACRO definitions are in the +# layout database before the DEF reader tries to resolve them. +macro_lef_files = [] +if macro_lef_dir and os.path.isdir(macro_lef_dir): + macro_lef_files = sorted(glob.glob(os.path.join(macro_lef_dir, "*.lef"))) + for lef in macro_lef_files: + lef_files.append(lef) + print(f"Including {len(macro_lef_files)} macro LEF files from {macro_lef_dir}") + +# Read all LEFs first, then read DEF referencing them +opts = pya.LoadLayoutOptions() +lefdef = opts.lefdef_config +lefdef.read_lef_with_def = True +lefdef.lef_files = lef_files +if layer_map and os.path.exists(layer_map): + lefdef.map_file = layer_map + print(f"Using layer map: {layer_map}") +print(f"Reading DEF with {len(lef_files)} LEF files") +print(f" Tech LEF: {tech_lef}") + +# Also read macro LEFs independently into the layout to pre-populate +# macro cell definitions before the DEF reader processes COMPONENTS. +if macro_lef_files: + lef_opts = pya.LoadLayoutOptions() + lef_lefdef = lef_opts.lefdef_config + # Include tech LEF for layer definitions needed by macro LEFs + if tech_lef and os.path.exists(tech_lef): + lef_lefdef.read_lef_with_def = True + lef_lefdef.lef_files = [tech_lef] + if layer_map and os.path.exists(layer_map): + lef_lefdef.map_file = layer_map + for lef in macro_lef_files: + print(f" Pre-reading macro LEF: {os.path.basename(lef)}") + layout.read(lef, lef_opts) + +print(f"Reading DEF: {def_file}") +layout.read(def_file, opts) + +# When merging GDS files, use CellConflictResolution to replace +# LEF abstract cells with full GDS geometry. +gds_opts = pya.LoadLayoutOptions() +gds_opts.cell_conflict_resolution = ( + pya.LoadLayoutOptions.CellConflictResolution.OverwriteCell +) # 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) + layout.read(gds, gds_opts) -# Read tile macro GDS files +# Read tile macro GDS files - these replace the LEF abstract shapes +# with full physical geometry including Metal2/Metal3 routing 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.read(gds, gds_opts) layout.write(out_gds) print(f"Wrote {out_gds}") diff --git a/pkgs/aegis-tapeout/scripts/fab_finalize.py b/pkgs/aegis-tapeout/scripts/fab_finalize.py new file mode 100644 index 0000000..0f5b6e7 --- /dev/null +++ b/pkgs/aegis-tapeout/scripts/fab_finalize.py @@ -0,0 +1,118 @@ +"""Finalize GDS for fab submission. + +Performs fab-specific post-processing on the merged GDS: + - Adds seal ring on the specified layer + - Adds an empty ID cell (fab fills with QR code) + - Removes orphan top-level cells (unused standard cells) + - Offsets layout to center within die (accounting for seal ring) + - Validates origin and dimensions + +Environment variables: + GDS_FILE - input/output GDS path (modified in place) + TOP_CELL - name of the top-level cell + DIE_W_UM - full die width in um (including seal ring) + DIE_H_UM - full die height in um (including seal ring) + SEAL_LAYER - seal ring GDS layer (optional) + SEAL_DATATYPE - seal ring GDS datatype (optional) + SEAL_WIDTH_UM - seal ring width in um (optional) + ID_CELL - name of required ID cell (optional) +""" + +import os +import pya + +gds_file = os.environ["GDS_FILE"] +top_cell_name = os.environ["TOP_CELL"] +die_w = float(os.environ.get("DIE_W_UM", "0")) +die_h = float(os.environ.get("DIE_H_UM", "0")) +seal_layer = int(os.environ.get("SEAL_LAYER", "0")) +seal_datatype = int(os.environ.get("SEAL_DATATYPE", "0")) +seal_width = float(os.environ.get("SEAL_WIDTH_UM", "0")) +id_cell_name = os.environ.get("ID_CELL", "") + +layout = pya.Layout() +layout.read(gds_file) + +top = layout.cell(top_cell_name) +if top is None: + print(f"ERROR: Top cell '{top_cell_name}' not found") + exit(1) + +dbu = layout.dbu + +# Remove orphan top-level cells (standard cells loaded but not instantiated) +orphans = [] +for cell in layout.each_cell(): + if cell.is_top() and cell.name != top_cell_name: + orphans.append(cell.cell_index()) +if orphans: + print(f"Removing {len(orphans)} orphan top-level cells") + for ci in orphans: + layout.delete_cell(ci) + +# Add seal ring if configured +if seal_width > 0 and die_w > 0 and die_h > 0: + li = layout.layer(seal_layer, seal_datatype) + sw = int(seal_width / dbu) + dw = int(die_w / dbu) + dh = int(die_h / dbu) + + # Get current layout bounds + bbox = top.bbox() + # Offset to center user area within die (seal ring on all sides) + ox = sw # offset from die origin to user area + oy = sw + + # Move all existing geometry by the seal ring offset + top.transform(pya.Trans(ox, oy)) + + # Draw seal ring as a frame around the full die + # Outer boundary + outer = pya.Box(0, 0, dw, dh) + # Inner boundary (user area) + inner = pya.Box(sw, sw, dw - sw, dh - sw) + # Create ring as outer minus inner + ring = pya.Region(outer) - pya.Region(inner) + for poly in ring.each(): + top.shapes(li).insert(poly) + + print(f"Added seal ring: {seal_width}um wide on layer {seal_layer}/{seal_datatype}") + print(f"Die: {die_w}x{die_h}um, User area offset: ({seal_width},{seal_width})um") + +# Add ID cell if required (with correct bounding box for precheck QR code) +id_w = float(os.environ.get("ID_CELL_W_UM", "142.8")) +id_h = float(os.environ.get("ID_CELL_H_UM", "142.8")) +if id_cell_name: + id_cell = layout.cell(id_cell_name) + if id_cell is None: + id_cell = layout.create_cell(id_cell_name) + else: + id_cell.clear() + # Create a bounding box matching the precheck's QR code dimensions + # exactly. The precheck asserts id_cell.bbox() == qrcode_cell.bbox(). + # Use Metal1 (34/0) as a placeholder since the QR code uses metal layers. + m1_li = layout.layer(34, 0) + iw = int(round(id_w / dbu)) + ih = int(round(id_h / dbu)) + id_cell.shapes(m1_li).insert(pya.Box(0, 0, iw, ih)) + # Place it in the top cell (fab precheck will replace contents) + if not any( + layout.cell(inst.cell_index).name == id_cell_name for inst in top.each_inst() + ): + top.insert(pya.CellInstArray(id_cell.cell_index(), pya.Trans())) + print(f"Added ID cell: {id_cell_name} ({id_w}x{id_h}um)") + +# Validate +final_bbox = top.bbox() +print(f"Final layout: {final_bbox.width()*dbu:.1f}x{final_bbox.height()*dbu:.1f}um") +print(f"Origin: ({final_bbox.left*dbu:.1f}, {final_bbox.bottom*dbu:.1f})") + +# Count top-level cells +top_count = sum(1 for c in layout.each_cell() if c.is_top()) +if top_count != 1: + print(f"WARNING: {top_count} top-level cells (expected 1)") +else: + print("OK: exactly 1 top-level cell") + +layout.write(gds_file) +print(f"Wrote {gds_file}") diff --git a/pkgs/aegis-tapeout/scripts/gen_chip_wrapper.py b/pkgs/aegis-tapeout/scripts/gen_chip_wrapper.py new file mode 100644 index 0000000..5890b76 --- /dev/null +++ b/pkgs/aegis-tapeout/scripts/gen_chip_wrapper.py @@ -0,0 +1,305 @@ +#!/usr/bin/env python3 +"""Generate a chip-level Verilog wrapper that adds an I/O padring around the +already-elaborated AegisFPGA core. + +Inputs (env vars): + IN_SV - core SV file containing the AegisFPGA module + OUT_SV - output SV file for the chip wrapper + CORE_MODULE - name of the core module being wrapped (default AegisFPGA) + CHIP_MODULE - name of the new top module (default AegisFPGAChip) + PAD_BIDIR - bidirectional pad cell name (e.g. gf180mcu_fd_io__bi_t) + PAD_INPUT - input pad cell name (e.g. gf180mcu_fd_io__in_s) + PAD_VDD - VDD power-pad cell name + PAD_VSS - VSS power-pad cell name + POWER_PAD_PER_SIDE - number of VDD+VSS pad pairs per die side (default 2) + +Each AegisFPGA port becomes a bond-pad-bearing port on the chip wrapper: + - padIn[i] / padOut[i] / padOutputEnable[i] triplets fold into a single + bidirectional pad named pad_io[i]. + - Other inputs (clk, reset, ...) get their own input pad. + - Other outputs (configDone, ...) get their own bidirectional pad with + OE tied high so it acts as a driver. + +Power pads are distributed evenly: every side gets POWER_PAD_PER_SIDE +VDD/VSS pairs, all sharing the chip-level VDD/VSS/DVDD/DVSS rails. + +Pads are emitted only as netlist instances. Their physical placement +around the perimeter is handled later by OpenROAD using -fixed +setLocation calls, and corner / filler cells are added there too (they +have no signal connections so they don't need to live in the netlist). +""" +import os +import re +import sys + + +def parse_ports(content: str, module_name: str): + """Return a list of {name, dir, msb, lsb, width} for the module.""" + m = re.search( + rf"module\s+{re.escape(module_name)}\s*\((.*?)\);", + content, + re.DOTALL, + ) + if not m: + sys.exit(f"could not find module {module_name} in input") + body = m.group(1) + ports = [] + for raw in body.split(","): + line = raw.strip() + if not line: + continue + # input logic [N:M] name / output logic name / inout ... + match = re.match( + r"(input|output|inout)\s+(?:logic\s+)?" + r"(?:\[\s*(\d+)\s*:\s*(\d+)\s*\]\s+)?" + r"(\w+)\s*$", + line, + ) + if not match: + sys.exit(f"could not parse port line: {line!r}") + direction = match.group(1) + msb = int(match.group(2)) if match.group(2) is not None else None + lsb = int(match.group(3)) if match.group(3) is not None else None + width = (msb - lsb + 1) if msb is not None else 1 + ports.append( + { + "name": match.group(4), + "dir": direction, + "msb": msb, + "lsb": lsb, + "width": width, + } + ) + return ports + + +def is_bidir_triplet(ports): + """Detect padIn / padOut / padOutputEnable triplet and return its width. + + Returns the bus width if the triplet is present and consistent, else None. + """ + by_name = {p["name"]: p for p in ports} + needed = ("padIn", "padOut", "padOutputEnable") + if not all(n in by_name for n in needed): + return None + widths = {by_name[n]["width"] for n in needed} + if len(widths) != 1: + sys.exit(f"padIn/padOut/padOutputEnable have inconsistent widths: {widths}") + return widths.pop() + + +def emit_pad_inst(buf, cell, inst_name, pad_pin, pad_net, a_net, y_net, oe_net): + """Emit one I/O pad instance. + + The gf180mcu_fd_io__bi_t pin list: + A input - data driven from the chip onto PAD when OE=1 + Y output - PAD value sampled into the chip when IE=1 + OE input - output enable (drive PAD) + IE input - input enable (sample PAD) + CS input - tied 0 (pad current source disabled) + PD/PU input - tied 0 (no internal pull-up/pull-down) + PDRV0/1 input - tied 1 (max drive strength) + SL input - tied 0 (slow slew, less SI noise) + PAD inout - external bond pad + VDD/VSS, DVDD/DVSS - power rails (shared chip-wide) + """ + buf.append(f" {cell} {inst_name} (") + buf.append(f" .PAD({pad_net}),") + buf.append(f" .A({a_net}),") + buf.append(f" .Y({y_net}),") + buf.append(f" .OE({oe_net}),") + buf.append(f" .IE(1'b1),") + buf.append(f" .CS(1'b0),") + buf.append(f" .PD(1'b0),") + buf.append(f" .PU(1'b0),") + buf.append(f" .PDRV0(1'b1),") + buf.append(f" .PDRV1(1'b1),") + buf.append(f" .SL(1'b0),") + buf.append(f" .DVDD(VDD),") + buf.append(f" .DVSS(VSS),") + buf.append(f" .VDD(VDD),") + buf.append(f" .VSS(VSS)") + buf.append(f" );") + + +def emit_power_pad(buf, cell, inst_name, label): + """Emit a power or ground pad. Power-pad cells only carry the rails + relevant to their domain - the dvdd cell omits VDD (it sources it), + the dvss cell omits VSS - so we need to skip those pins to keep the + netlist legal.""" + # Map every pad power pin onto the chip-level VDD / VSS rails so the + # design has a single power domain. The cell's DVDD pin (pad/IO power) + # is tied to chip VDD, DVSS to chip VSS - we treat IO and core power + # as the same supply for this digital flow. + if cell.endswith("__dvdd"): + pin_map = (("DVDD", "VDD"), ("DVSS", "VSS"), ("VSS", "VSS")) + elif cell.endswith("__dvss"): + pin_map = (("DVDD", "VDD"), ("DVSS", "VSS"), ("VDD", "VDD")) + else: + pin_map = ( + ("DVDD", "VDD"), + ("DVSS", "VSS"), + ("VDD", "VDD"), + ("VSS", "VSS"), + ) + buf.append(f" // {label}") + buf.append(f" {cell} {inst_name} (") + for j, (pin, net) in enumerate(pin_map): + sep = "," if j < len(pin_map) - 1 else "" + buf.append(f" .{pin}({net}){sep}") + buf.append(f" );") + + +def main(): + in_sv = os.environ["IN_SV"] + out_sv = os.environ["OUT_SV"] + core_module = os.environ.get("CORE_MODULE", "AegisFPGA") + chip_module = os.environ.get("CHIP_MODULE", "AegisFPGAChip") + pad_bidir = os.environ.get("PAD_BIDIR", "gf180mcu_fd_io__bi_t") + pad_input = os.environ.get("PAD_INPUT", "gf180mcu_fd_io__in_s") + pad_vdd = os.environ.get("PAD_VDD", "gf180mcu_fd_io__dvdd") + pad_vss = os.environ.get("PAD_VSS", "gf180mcu_fd_io__dvss") + power_pad_per_side = int(os.environ.get("POWER_PAD_PER_SIDE", "2")) + + with open(in_sv) as f: + content = f.read() + + ports = parse_ports(content, core_module) + bidir_w = is_bidir_triplet(ports) + + bidir_names = {"padIn", "padOut", "padOutputEnable"} + other_ports = [p for p in ports if p["name"] not in bidir_names] + + out = [] + out.append(f"// Auto-generated chip-level wrapper for {core_module}.") + out.append(f"// Adds a padring around the core. Do not edit by hand.") + out.append("//") + out.append("// The chip wrapper exposes VDD/VSS as the only top-level ports.") + out.append("// Bond-pad signal connections happen at the pad cells' PAD pins") + out.append("// directly - those pins are physical bond points, not routable") + out.append("// nets, so we keep them as internal wires with no other endpoint.") + out.append("// This avoids OpenROAD's detailed router trying to find an") + out.append("// access point to the PAD pin from inside the die (DRT-0073).") + out.append("") + out.append(f"module {chip_module} (") + # Only VDD/VSS leave the chip as named ports. Bond-wire signals stay + # at pad cells' PAD pins. + out.append(" inout wire VDD,") + out.append(" inout wire VSS") + out.append(");") + out.append("") + + # Internal wires: chip-side nets that attach to pad PAD pins. These + # nets have only the PAD-pin endpoint, which is fine - the bond + # wire physically connects them off-chip. + if bidir_w is not None: + out.append(f" wire [{bidir_w - 1}:0] pad_io;") + for p in other_ports: + sz = "" if p["width"] == 1 else f" [{p['msb']}:{p['lsb']}]" + out.append(f" wire{sz} pad_{p['name']};") + out.append("") + # Internal core-side nets. + if bidir_w is not None: + out.append(f" wire [{bidir_w - 1}:0] core_padIn;") + out.append(f" wire [{bidir_w - 1}:0] core_padOut;") + out.append(f" wire [{bidir_w - 1}:0] core_padOutputEnable;") + for p in other_ports: + sz = "" if p["width"] == 1 else f" [{p['msb']}:{p['lsb']}]" + out.append(f" wire{sz} core_{p['name']};") + out.append("") + + # Bidirectional I/O pads via generate-for so the netlist stays compact. + if bidir_w is not None: + out.append(f" // {bidir_w} bidirectional user-IO pads") + out.append(f" genvar i;") + out.append(f" generate") + out.append(f" for (i = 0; i < {bidir_w}; i = i + 1) begin : g_io_pad") + out.append(f" {pad_bidir} u_pad_io (") + out.append(f" .PAD(pad_io[i]),") + out.append(f" .A(core_padOut[i]),") + out.append(f" .Y(core_padIn[i]),") + out.append(f" .OE(core_padOutputEnable[i]),") + out.append(f" .IE(1'b1),") + out.append(f" .CS(1'b0),") + out.append(f" .PD(1'b0),") + out.append(f" .PU(1'b0),") + out.append(f" .PDRV0(1'b1),") + out.append(f" .PDRV1(1'b1),") + out.append(f" .SL(1'b0),") + out.append(f" .DVDD(VDD),") + out.append(f" .DVSS(VSS),") + out.append(f" .VDD(VDD),") + out.append(f" .VSS(VSS)") + out.append(f" );") + out.append(f" end") + out.append(f" endgenerate") + out.append("") + + # Per-port pads for the non-bus signals. + for p in other_ports: + for bit in range(p["width"]): + if p["width"] == 1: + inst = f"u_pad_{p['name']}" + pad_net = f"pad_{p['name']}" + core_net = f"core_{p['name']}" + else: + inst = f"u_pad_{p['name']}_{bit}" + pad_net = f"pad_{p['name']}[{bit + p['lsb']}]" + core_net = f"core_{p['name']}[{bit + p['lsb']}]" + out.append( + f" // pad for {p['dir']} {p['name']}{'' if p['width']==1 else f'[{bit}]'}" + ) + if p["dir"] == "input": + emit_pad_inst( + out, + pad_bidir, + inst, + "PAD", + pad_net, + a_net="1'b0", + y_net=core_net, + oe_net="1'b0", + ) + else: + emit_pad_inst( + out, + pad_bidir, + inst, + "PAD", + pad_net, + a_net=core_net, + y_net="", + oe_net="1'b1", + ) + out.append("") + + # Power pads, distributed evenly across the four sides. Naming carries + # the side index so the OpenROAD placer can lay them out symmetrically. + out.append(" // Power pads (VDD/VSS) distributed around the perimeter") + side_names = ["n", "s", "e", "w"] + for s in side_names: + for k in range(power_pad_per_side): + emit_power_pad(out, pad_vdd, f"u_pad_vdd_{s}{k}", f"VDD pad {s}{k}") + emit_power_pad(out, pad_vss, f"u_pad_vss_{s}{k}", f"VSS pad {s}{k}") + out.append("") + + # Instantiate the core, hooking up every port to its core-side wire. + out.append(f" // Core instance: {core_module}") + out.append(f" {core_module} u_core (") + conn = [] + for p in ports: + conn.append(f" .{p['name']}(core_{p['name']})") + out.append(",\n".join(conn)) + out.append(" );") + out.append("") + out.append(f"endmodule") + out.append("") + + with open(out_sv, "w") as f: + f.write("\n".join(out)) + + print(f"Wrote {out_sv} ({len(ports)} core ports, {bidir_w or 0} bidir pads)") + + +if __name__ == "__main__": + main() diff --git a/pkgs/gf180mcu-pdk/default.nix b/pkgs/gf180mcu-pdk/default.nix index 47aa61e..3d0cca2 100644 --- a/pkgs/gf180mcu-pdk/default.nix +++ b/pkgs/gf180mcu-pdk/default.nix @@ -5,44 +5,36 @@ }: let - # Standard cell library (7-track, 5V) - fd_sc_mcu7t5v0 = fetchFromGitHub { - owner = "google"; - repo = "globalfoundries-pdk-libs-gf180mcu_fd_sc_mcu7t5v0"; - rev = "43beb45e4d323a76239de436db2df6732e9a689b"; - hash = "sha256-wmXGAUwXbz4TyeIRQZhnspIaNw0G3+tYdIrUIr8XAgw="; + # wafer.space's assembled GF180MCU PDK (gf180mcuD variant) + # Includes standard cells, DRC/LVS rule decks, I/O cells, and tech files + pdk-src = fetchFromGitHub { + owner = "wafer-space"; + repo = "gf180mcu"; + rev = "1.8.0"; + hash = "sha256-+LYKskX0Ym2c9SmZOyiTZblAu1OL0CmM8pBGBVhI7MM="; }; - # Physical verification rule decks (DRC/LVS for KLayout) - fd_pv = fetchFromGitHub { - owner = "efabless"; - repo = "globalfoundries-pdk-libs-gf180mcu_fd_pv"; - rev = "05e7b6adf19edf942969c1c9625f02fd87874f06"; - hash = "sha256-kVR4fk8PnzMGLCWYFR0fjzO+pA1yoWsqlm2Mc9NdKJ8="; + # wafer.space's gf180mcuD project template ships the two physical-only + # macros every shuttle tape-out must instantiate: the chip ID block + # (gf180mcu_ws_ip__id) and the wafer.space logo (gf180mcu_ws_ip__logo). + # We pull them straight from upstream rather than vendoring so we + # track the same revision wafer.space's precheck expects. + projectTemplate-src = fetchFromGitHub { + owner = "wafer-space"; + repo = "gf180mcu-project-template"; + rev = "8bd0f6ff28947bf222c5288343f8f3ee1fc04632"; + hash = "sha256-rU7oKdpOLYQylpZTxCZ8HgfP2/dwMlaOw9Gh0U8XpeM="; }; - # PVT corners to generate merged liberty files for - corners = [ - "ff_125C_1v98" - "ff_125C_3v60" - "ff_125C_5v50" - "ff_n40C_1v98" - "ff_n40C_3v60" - "ff_n40C_5v50" - "ss_125C_1v62" - "ss_125C_3v00" - "ss_125C_4v50" - "ss_n40C_1v62" - "ss_n40C_3v00" - "ss_n40C_4v50" - "tt_025C_1v80" - "tt_025C_3v30" - "tt_025C_5v00" - ]; + cellLib = "gf180mcu_fd_sc_mcu7t5v0"; + ioLib = "gf180mcu_fd_io"; + pdkRoot = "${pdk-src}/gf180mcuD"; + scRoot = "${pdkRoot}/libs.ref/${cellLib}"; + ioRoot = "${pdkRoot}/libs.ref/${ioLib}"; in stdenvNoCC.mkDerivation { pname = "gf180mcu-pdk"; - version = "0-unstable-2025-03-31"; + version = "1.8.0"; dontUnpack = true; dontConfigure = true; @@ -51,60 +43,447 @@ stdenvNoCC.mkDerivation { installPhase = '' runHook preInstall - local sc=$out/share/pdk/gf180mcu/libs.ref/gf180mcu_fd_sc_mcu7t5v0 + local sc=$out/share/pdk/gf180mcu/libs.ref/${cellLib} mkdir -p $sc/{lib,lef,gds,verilog,spice} - # Merge per-cell liberty files into single .lib per PVT corner - ${lib.concatMapStringsSep "\n" (corner: '' - echo "Merging liberty for corner: ${corner}" - local header="${fd_sc_mcu7t5v0}/liberty/gf180mcu_fd_sc_mcu7t5v0__${corner}.lib" - local merged="$sc/lib/gf180mcu_fd_sc_mcu7t5v0__${corner}.lib" + # Pre-merged liberty timing files + cp ${scRoot}/lib/*.lib $sc/lib/ - # Copy header, remove trailing closing brace - sed '$ d' "$header" > "$merged" + # Tech LEF files (copy .tlef as both .tlef and .lef for compatibility) + for f in ${scRoot}/techlef/*.tlef; do + cp "$f" $sc/lef/ + cp "$f" "$sc/lef/$(basename "$f" .tlef).lef" + done - # Append all per-cell liberty fragments for this corner - find ${fd_sc_mcu7t5v0}/cells -name "*__${corner}.lib" -print0 | sort -z | xargs -0 cat >> "$merged" + # Cell LEF, GDS, Verilog, SPICE + for f in ${scRoot}/lef/*.lef; do + if [[ "$(basename "$f")" != *"tech"* ]]; then + cp -n "$f" $sc/lef/ + fi + done + cp ${scRoot}/gds/*.gds $sc/gds/ + cp ${scRoot}/verilog/*.v $sc/verilog/ + cp ${scRoot}/spice/*.spice $sc/spice/ - # Close the library block - echo "}" >> "$merged" - '') corners} + # I/O pad library: bidirectional pads, input pads, power pads, + # padring fillers, and the corner cell. Used to build the chip's + # padring during top-level integration. + local io=$out/share/pdk/gf180mcu/libs.ref/${ioLib} + mkdir -p $io/{lib,lef,gds,verilog,spice} + cp ${ioRoot}/lib/*.lib $io/lib/ 2>/dev/null || true + cp ${ioRoot}/lef/*.lef $io/lef/ 2>/dev/null || true + cp ${ioRoot}/gds/*.gds $io/gds/ 2>/dev/null || true + cp ${ioRoot}/verilog/*.v $io/verilog/ 2>/dev/null || true + cp ${ioRoot}/spice/*.spice $io/spice/ 2>/dev/null || true - # Tech LEF files - cp ${fd_sc_mcu7t5v0}/tech/*.lef $sc/lef/ - - find ${fd_sc_mcu7t5v0}/cells -name '*.lef' -exec cp -n {} $sc/lef/ \; - find ${fd_sc_mcu7t5v0}/cells -name '*.gds' -exec cp -n {} $sc/gds/ \; - find ${fd_sc_mcu7t5v0}/cells -name '*.behavioral.v' -exec cp -n {} $sc/verilog/ \; - find ${fd_sc_mcu7t5v0}/cells -name '*.spice' -exec cp -n {} $sc/spice/ \; - - # Simulation models - if [ -d "${fd_sc_mcu7t5v0}/models" ]; then - mkdir -p $out/share/pdk/gf180mcu/models - cp -r ${fd_sc_mcu7t5v0}/models/* $out/share/pdk/gf180mcu/models/ - fi - - # Physical verification rule decks (DRC/LVS) - mkdir -p $out/share/pdk/gf180mcu/pv - cp -r ${fd_pv}/* $out/share/pdk/gf180mcu/pv/ 2>/dev/null || true + # Physical verification rule decks (DRC/LVS) from wafer-space fork + # Maintain path structure: pv/klayout/drc/, pv/klayout/lvs/ + mkdir -p $out/share/pdk/gf180mcu/pv/klayout + cp -r ${pdkRoot}/libs.tech/klayout/tech/* $out/share/pdk/gf180mcu/pv/klayout/ runHook postInstall ''; passthru = { - cellLib = "gf180mcu_fd_sc_mcu7t5v0"; + inherit cellLib ioLib; siteName = "GF018hv5v_mcu_sc7"; pdkName = "gf180mcu"; pdkPath = "share/pdk/gf180mcu"; + techLef = "${cellLib}__nom.tlef"; + # I/O pad-ring cells used to build the chip perimeter: + # - cornerCell: 355x355 corner block (rotate/mirror at the 4 die corners) + # - signalPad: general-purpose bidirectional 3.3V signal pad (75x350) + # - inputPad: Schmitt-trigger input pad + # - powerPad: VDD pad + # - groundPad: VSS pad + # - fillCells: pad-ring fillers, largest first + padCells = { + cornerCell = "gf180mcu_fd_io__cor"; + signalPad = "gf180mcu_fd_io__bi_t"; + inputPad = "gf180mcu_fd_io__in_s"; + powerPad = "gf180mcu_fd_io__dvdd"; + groundPad = "gf180mcu_fd_io__dvss"; + fillCells = [ + "gf180mcu_fd_io__fill10" + "gf180mcu_fd_io__fill5" + "gf180mcu_fd_io__fill1" + "gf180mcu_fd_io__fillnc" + ]; + padHeight = 350; # um, perpendicular to die edge + padWidth = 75; # um, along die edge for signal pads + cornerSize = 355; # um + }; + # Per-layer routing capacity adjustments (0.0 = full, 1.0 = blocked). + # Penalize the lower routing layers so the global router spreads + # signal nets onto Metal4/Metal5 instead of saturating Metal2/Metal3. + tileLayerAdjustments = { + Metal2 = 0.5; + Metal3 = 0.3; + }; + topLayerAdjustments = { + Metal2 = 0.6; + Metal3 = 0.4; + }; + # Power delivery network configuration + pdn = { + # Standard cell rail + railLayer = "Metal1"; + railWidth = 0.6; + # Vertical Metal4 power straps. Pitch <= half the narrowest fabric + # tile width (Luna-1 Tile is 165um wide) so every macro is crossed + # by at least one VDD and one VSS strap. + verticalLayer = "Metal4"; + verticalWidth = 1.6; + verticalPitch = 80; + verticalOffset = 10; + verticalSpacing = 0.28; + # Horizontal Metal5 power straps. Pitch <= half the shortest tile + # height (102um) so every macro is crossed by at least one VDD + # and one VSS strap pair, per the hierarchical-macro PDN spec. + horizontalLayer = "Metal5"; + horizontalWidth = 1.6; + horizontalPitch = 50; + horizontalOffset = 10; + horizontalSpacing = 0.46; + halo = 5; + }; commentLayer = { layer = 236; datatype = 0; }; + # LibreLane Chip-flow knobs that depend on the PDK technology. + # The custom OpenROAD flow uses the higher metals (M4/M5) per the + # `pdn` block above, but LibreLane's stock Chip flow targets the + # lower metals (M2/M3) for the core PDN, so these settings live + # alongside rather than replacing the custom-flow pdn block. + librelane = { + pdkRoot = pdk-src; + pdkName = "gf180mcuD"; + # Physical-only macros wafer.space requires every shuttle + # tape-out to instantiate (chip ID block + vendor logo). Each + # subdirectory ships its own gds/, lef/, lib/, and vh/ stub. + fabRequiredIp = "${projectTemplate-src}/ip"; + # Pad cells instantiated by the chip_top template. The split + # between fd_io (foundry I/O lib) and ws_io (wafer.space's + # customized power pads) follows wafer.space's project + # template, which uses the foundry signal pads but the + # wafer.space-customized power pads. + padCells = { + # 24mA bidirectional pad with full control set (CS/SL/IE/PU/PD). + bidirCell = "gf180mcu_fd_io__bi_24t"; + # CMOS input receiver, used for general signal pads. + inputCmosCell = "gf180mcu_fd_io__in_c"; + # Schmitt-trigger input receiver, used for the clock pad. + inputSchmittCell = "gf180mcu_fd_io__in_s"; + # 5 V analog passthrough pad. + analogCell = "gf180mcu_fd_io__asig_5p0"; + # wafer.space power / ground pads (different from the fd_io + # equivalents - these route DVDD/DVSS through the seal ring). + powerPadCell = "gf180mcu_ws_io__dvdd"; + groundPadCell = "gf180mcu_ws_io__dvss"; + }; + # Cells whose floating pins must not error LVS / synth. Add + # only cells safe to leave disconnected (bidir control pins, no + # signal-pin pad cells, etc.). Input pad cells (in_c, in_s) are + # NOT here: if added, LibreLane drops their global power-net + # bindings and add_global_connections fails on DVDD/VSS, + # breaking the chip-level PDN connect step. + ignoreDisconnectedModules = [ + "gf180mcu_fd_io__bi_24t" + ]; + # Foundry cells that need flattening for DRC parity with the + # wafer.space precheck. + magicGdsFlatglob = [ + "*_CDNS_*" + "*$$*" + "M1_N*" + "M1_P*" + "M2_M1*" + "M3_M2*" + "nmos_5p0*" + "nmos_1p2*" + "pmos_5p0*" + "pmos_1p2*" + "via1_*" + "ypass_gate*" + "G_ring_*" + "dcap_103*" + "din_*" + "mux821_*" + "rdummy_*" + "pmoscap_*" + "xdec_*" + "ypredec*" + "xpredec*" + "prexdec_*" + "xdec8_*" + "xdec16_*" + "xdec32_*" + "sa_*" + ]; + # gf180mcu's Metal2 cannot meet the minimum density without + # letting the filler walk over active metal. The wafer.space + # precheck moves dummy metal into active metal afterward. + klayoutFillerOptions = { + Metal2_ignore_active = true; + }; + magicExtUnique = "notopports"; + # Magic DRC is informational; KLayout DRC is the gating check. + errorOnMagicDrc = false; + # Core PDN (LibreLane stock Chip flow targets M2/M3). + pdn = { + coreVerticalLayer = "Metal2"; + coreHorizontalLayer = "Metal3"; + vWidth = 5; + hWidth = 5; + vSpacing = 1; + hSpacing = 1; + vPitch = 75; + hPitch = 75; + coreRing = { + enable = true; + vWidth = 25; + hWidth = 25; + connectToPads = true; + enablePins = false; + }; + macroHorizontalHalo = 10; + macroVerticalHalo = 10; + horizontalHalo = 5; + verticalHalo = 5; + }; + # Hold-violation slack margins (template defaults that work for gf180mcu). + resizer = { + plHoldSlackMargin = 0.35; + grtHoldSlackMargin = 0.3; + }; + # Antenna repair iteration budget. + antennaRepair = { + iters = 10; + margin = 10; + }; + # LibreLane Chip-flow slot definitions. Each entry describes the + # die / core box and the padring placement order for that slot. + # Pad instance names match the chip_top template's generate + # blocks (dvdd_pads[i].pad, bidir[i].pad, inputs[i].pad, + # analog[i].pad) plus the standalone clk_pad / rst_n_pad cells. + # The escape in `\[N\]` is what LibreLane's regex matcher needs. + # Sourced from wafer.space's gf180mcu-project-template. + slots."1x1" = { + dieArea = [ + 0 + 0 + 3932 + 5122 + ]; + coreArea = [ + 442 + 442 + 3490 + 4680 + ]; + verilogDefines = [ "SLOT_1X1" ]; + pads = { + south = [ + "clk_pad" + "rst_n_pad" + "bidir\\[0\\].pad" + "bidir\\[1\\].pad" + "bidir\\[2\\].pad" + "bidir\\[3\\].pad" + "bidir\\[4\\].pad" + "bidir\\[5\\].pad" + "dvss_pads\\[0\\].pad" + "bidir\\[6\\].pad" + "bidir\\[7\\].pad" + "bidir\\[8\\].pad" + "bidir\\[9\\].pad" + "bidir\\[10\\].pad" + "bidir\\[11\\].pad" + "bidir\\[12\\].pad" + "bidir\\[13\\].pad" + ]; + east = [ + "dvdd_pads\\[0\\].pad" + "dvss_pads\\[1\\].pad" + "bidir\\[14\\].pad" + "bidir\\[15\\].pad" + "bidir\\[16\\].pad" + "bidir\\[17\\].pad" + "bidir\\[18\\].pad" + "bidir\\[19\\].pad" + "dvdd_pads\\[1\\].pad" + "dvss_pads\\[2\\].pad" + "bidir\\[20\\].pad" + "bidir\\[21\\].pad" + "bidir\\[22\\].pad" + "bidir\\[23\\].pad" + "bidir\\[24\\].pad" + "bidir\\[25\\].pad" + "dvss_pads\\[3\\].pad" + "dvdd_pads\\[2\\].pad" + "dvss_pads\\[4\\].pad" + "dvdd_pads\\[3\\].pad" + ]; + north = [ + "analog\\[1\\].pad" + "analog\\[0\\].pad" + "bidir\\[39\\].pad" + "bidir\\[38\\].pad" + "bidir\\[37\\].pad" + "bidir\\[36\\].pad" + "bidir\\[35\\].pad" + "bidir\\[34\\].pad" + "dvss_pads\\[5\\].pad" + "bidir\\[33\\].pad" + "bidir\\[32\\].pad" + "bidir\\[31\\].pad" + "bidir\\[30\\].pad" + "bidir\\[29\\].pad" + "bidir\\[28\\].pad" + "bidir\\[27\\].pad" + "bidir\\[26\\].pad" + ]; + west = [ + "dvdd_pads\\[7\\].pad" + "dvss_pads\\[9\\].pad" + "dvdd_pads\\[6\\].pad" + "dvss_pads\\[8\\].pad" + "inputs\\[11\\].pad" + "inputs\\[10\\].pad" + "inputs\\[9\\].pad" + "inputs\\[8\\].pad" + "inputs\\[7\\].pad" + "inputs\\[6\\].pad" + "dvdd_pads\\[5\\].pad" + "dvss_pads\\[7\\].pad" + "inputs\\[5\\].pad" + "inputs\\[4\\].pad" + "inputs\\[3\\].pad" + "inputs\\[2\\].pad" + "dvdd_pads\\[4\\].pad" + "dvss_pads\\[6\\].pad" + "inputs\\[1\\].pad" + "inputs\\[0\\].pad" + ]; + }; + }; + }; + # Fab submission requirements (wafer.space gf180mcuD) + fab = { + # Available die slot sizes (um) including seal ring + slots = { + "1x1" = { + w = 3932; + h = 5122; + }; + "0p5x1" = { + w = 1936; + h = 5122; + }; + "1x0p5" = { + w = 3932; + h = 2531; + }; + "0p5x0p5" = { + w = 1936; + h = 2531; + }; + }; + # Seal ring around the die + sealRing = { + layer = 167; + datatype = 5; + width = 26; # um + }; + # Required ID cell for fab tracking + idCell = "gf180mcu_ws_ip__id"; + # Layers that must NOT have shapes (5LM only) + forbiddenLayers = [ + { + layer = 82; + datatype = 0; + name = "Via5"; + } + { + layer = 53; + datatype = 0; + name = "MetalTop"; + } + ]; + # Required DBU for GDS output + dbu = 0.001; + # DRC variant for fab precheck + drcVariant = "D"; + }; + # DRC rule tables relevant to our design (skip analog/specialty decks) + drcTables = [ + "metal1" + "metal2" + "metal3" + "metal4" + "metal5" + "metaltop" + "via1" + "via2" + "via3" + "via4" + "contact" + "geom" + "antenna" + ]; + # LEF layer name -> GDS layer/datatype mapping for KLayout DEF->GDS + lefGdsLayers = { + Poly2 = { + layer = 30; + datatype = 0; + }; + CON = { + layer = 33; + datatype = 0; + }; + Metal1 = { + layer = 34; + datatype = 0; + }; + Via1 = { + layer = 35; + datatype = 0; + }; + Metal2 = { + layer = 36; + datatype = 0; + }; + Via2 = { + layer = 38; + datatype = 0; + }; + Metal3 = { + layer = 42; + datatype = 0; + }; + Via3 = { + layer = 40; + datatype = 0; + }; + Metal4 = { + layer = 46; + datatype = 0; + }; + Via4 = { + layer = 41; + datatype = 0; + }; + Metal5 = { + layer = 81; + datatype = 0; + }; + }; }; meta = { - description = "GlobalFoundries GF180MCU 180nm PDK standard cell library"; - homepage = "https://github.com/google/gf180mcu-pdk"; + description = "GlobalFoundries GF180MCU 180nm PDK (wafer.space gf180mcuD variant)"; + homepage = "https://github.com/wafer-space/gf180mcu"; license = lib.licenses.asl20; platforms = lib.platforms.all; }; diff --git a/pkgs/sky130-pdk/default.nix b/pkgs/sky130-pdk/default.nix index 867497d..c1202d1 100644 --- a/pkgs/sky130-pdk/default.nix +++ b/pkgs/sky130-pdk/default.nix @@ -32,7 +32,11 @@ stdenvNoCC.mkDerivation { local sc=$out/share/pdk/sky130/libs.ref/sky130_fd_sc_hd mkdir -p $sc/{lib,lef,gds,verilog,spice} - # Tech LEF files + for f in ${fd_sc_hd}/tech/*.tlef; do + cp "$f" $sc/lef/ + cp "$f" "$sc/lef/$(basename "$f" .tlef).lef" + done + find ${fd_sc_hd}/tech -name '*.lef' -exec cp -n {} $sc/lef/ \; 2>/dev/null || true # Cell files @@ -55,10 +59,58 @@ stdenvNoCC.mkDerivation { siteName = "unithd"; pdkName = "sky130"; pdkPath = "share/pdk/sky130"; + techLef = "sky130_fd_sc_hd.tlef"; commentLayer = { layer = 236; datatype = 0; }; + # LEF layer name -> GDS layer/datatype mapping for KLayout DEF->GDS + lefGdsLayers = { + li1 = { + layer = 67; + datatype = 20; + }; + mcon = { + layer = 67; + datatype = 44; + }; + met1 = { + layer = 68; + datatype = 20; + }; + via = { + layer = 68; + datatype = 44; + }; + met2 = { + layer = 69; + datatype = 20; + }; + via2 = { + layer = 69; + datatype = 44; + }; + met3 = { + layer = 70; + datatype = 20; + }; + via3 = { + layer = 70; + datatype = 44; + }; + met4 = { + layer = 71; + datatype = 20; + }; + via4 = { + layer = 71; + datatype = 44; + }; + met5 = { + layer = 72; + datatype = 20; + }; + }; }; meta = { diff --git a/tests/formal-ip/default.nix b/tests/formal-ip/default.nix index a20fc18..737333f 100644 --- a/tests/formal-ip/default.nix +++ b/tests/formal-ip/default.nix @@ -83,8 +83,7 @@ stdenvNoCC.mkDerivation { # ---- CLB Formal Proof ---- echo "--- CLB: combinational + carry mode correctness ---" - # Extract CLB with dependencies flattened - yosys -p "read -sv $DEVICE_SV; hierarchy -top Clb; proc; flatten; write_rtlil clb_gate.il" 2>&1 | tail -3 + yosys -p "read -sv $DEVICE_SV; hierarchy -top Clb; setattr -mod -unset keep_hierarchy; proc; flatten; write_rtlil clb_gate.il" 2>&1 | tail -3 # Write reference model cat > clb_ref.v << 'VEOF' diff --git a/tests/gds-verify/default.nix b/tests/gds-verify/default.nix index 5a53b5b..9eab938 100644 --- a/tests/gds-verify/default.nix +++ b/tests/gds-verify/default.nix @@ -7,23 +7,30 @@ stdenvNoCC, python3, klayout, + procps, + yosys, aegis-tapeout, }: let - inherit (aegis-tapeout) deviceName pdk; - inherit (pdk) pdkName pdkPath; + inherit (aegis-tapeout) deviceName pdk topCellName; + inherit (pdk) pdkName pdkPath cellLib; fullPdkPath = "${pdk}/${pdkPath}"; + spicePath = "${fullPdkPath}/libs.ref/${cellLib}/spice"; # PV rule decks live under the PDK's pv/ directory pvPath = "${fullPdkPath}/pv"; - # DRC variant selection per PDK + # DRC variant from PDK fab config, with fallback per PDK name drcVariant = - if pdkName == "gf180mcu" then - "C" # 9K metal_top, 5LM + if pdk ? fab && pdk.fab ? drcVariant then + pdk.fab.drcVariant + else if pdkName == "gf180mcu" then + "D" else if pdkName == "sky130" then "sky130A" else "default"; + # Rule tables to check (from PDK passthru, defaults to all) + drcTables = pdk.drcTables or [ ]; in stdenvNoCC.mkDerivation { name = "aegis-gds-verify-${deviceName}"; @@ -31,13 +38,18 @@ stdenvNoCC.mkDerivation { dontUnpack = true; nativeBuildInputs = [ - python3 + (python3.withPackages (ps: [ ps.docopt ])) klayout + procps + yosys ]; buildPhase = '' runHook preBuild + # Make klayout Python bindings visible to standalone python3 + export PYTHONPATH="${klayout}/lib/pymod''${PYTHONPATH:+:$PYTHONPATH}" + GDS="${aegis-tapeout}/${deviceName}.gds" NETLIST="${aegis-tapeout}/${deviceName}_final.v" @@ -119,58 +131,169 @@ stdenvNoCC.mkDerivation { exit 1 fi - # ---- Step 3: KLayout DRC ---- - echo "--- Step 3: DRC (${pdkName} design rules) ---" + # ---- Step 3: Fab precheck DRC (matches wafer.space precheck) ---- + echo "--- Step 3: Fab precheck DRC (${pdkName}) ---" + FAB_DRC="${pvPath}/klayout/drc/gf180mcu.drc" + + if [ -f "$FAB_DRC" ]; then + mkdir -p drc_output + + # Run the fab's KLayout DRC runset (same as wafer.space precheck) + QT_QPA_PLATFORM=offscreen klayout -b -zz \ + -r "$FAB_DRC" \ + -rd input="$GDS" \ + -rd report=drc_output/fab_drc.xml \ + -rd feol=true \ + -rd beol=true \ + -rd offgrid=true \ + -rd conn_drc=true \ + -rd wedge=true \ + -rd run_mode=deep \ + -rd metal_top=11K \ + -rd metal_level=5LM \ + -rd mim_option=B \ + -rd thr=$NIX_BUILD_CORES \ + 2>&1 | tee drc.log || true + + # Check fab DRC result + if [ -f "drc_output/fab_drc.xml" ]; then + FAB_VIOLATIONS=$(grep -c "" drc_output/fab_drc.xml 2>/dev/null || echo "0") + echo "Fab precheck DRC violations: $FAB_VIOLATIONS" + if [ "$FAB_VIOLATIONS" = "0" ]; then + echo "PASS: Fab precheck DRC clean" + else + echo "FAIL: $FAB_VIOLATIONS fab DRC violations" + exit 1 + fi + else + echo "PASS: Fab DRC produced no report (no violations)" + fi + else + echo "NOTE: Fab DRC runset not found at $FAB_DRC, skipping" + fi + + # ---- Step 3b: Detailed DRC (informational, not gating) ---- + echo "--- Step 3b: Detailed DRC report (informational) ---" DRC_SCRIPT="${pvPath}/klayout/drc/run_drc.py" if [ -f "$DRC_SCRIPT" ]; then - mkdir -p drc_output + # Run with both --antenna and FEOL enabled. The antenna rule deck + # uses connect() against poly2/n_diode/p_diode/nwell to extract nets + # and compute gate-area ratios, so it cannot run with --no_feol. QT_QPA_PLATFORM=offscreen python3 "$DRC_SCRIPT" \ --path="$GDS" \ + --topcell=${topCellName} \ --variant=${drcVariant} \ --run_dir=drc_output \ - --no_feol \ - --thr=1 \ - 2>&1 | tee drc.log || true + --antenna \ + --run_mode=deep \ + --thr=$NIX_BUILD_CORES \ + --mp=$NIX_BUILD_CORES \ + ${lib.concatMapStringsSep " " (t: "--table=${t}") drcTables} \ + 2>&1 | tee -a drc.log || true - # Check for DRC violations - VIOLATION_FILES=$(find drc_output -name "*.lyrdb" 2>/dev/null) + # Report but don't fail on metal/via DRC (informational) + VIOLATION_FILES=$(find drc_output -name "*.lyrdb" -not -name "*antenna*" 2>/dev/null) if [ -n "$VIOLATION_FILES" ]; then VIOLATIONS=$(grep -c "" $VIOLATION_FILES 2>/dev/null || echo "0") - echo "DRC violations found: $VIOLATIONS" - if [ "$VIOLATIONS" = "0" ]; then - echo "PASS: DRC clean" - else - echo "WARNING: $VIOLATIONS DRC violations (review needed)" + echo "Detailed DRC violations (informational): $VIOLATIONS" + fi + + # Antennas are a tape-out blocker: any unrepaired antenna violation + # can cause real-silicon gate damage during fabrication. + ANTENNA_LYRDB=$(find drc_output -name "*antenna*.lyrdb" 2>/dev/null) + if [ -n "$ANTENNA_LYRDB" ]; then + ANTENNA_COUNT=$(grep -c "" $ANTENNA_LYRDB 2>/dev/null || echo "0") + echo "Antenna violations: $ANTENNA_COUNT" + if [ "$ANTENNA_COUNT" != "0" ]; then + echo "FAIL: $ANTENNA_COUNT antenna violations - gate damage risk" + exit 1 fi else - echo "NOTE: DRC output not generated (may need additional setup)" + echo "WARN: No antenna report produced - check failed" fi - else - echo "NOTE: DRC script not found, skipping (PDK: ${pdkName})" fi - # ---- Step 4: KLayout LVS ---- - echo "--- Step 4: LVS (layout vs schematic) ---" - LVS_SCRIPT="${pvPath}/klayout/lvs/run_lvs.py" + # Surface OpenROAD antenna report from the tapeout build for cross- + # checking with KLayout's antenna verification above. + if [ -f "${aegis-tapeout}/antenna.rpt" ]; then + echo "--- OpenROAD top-level antenna report ---" + tail -40 "${aegis-tapeout}/antenna.rpt" || true + fi - if [ -f "$LVS_SCRIPT" ] && [ -f "$NETLIST" ]; then - mkdir -p lvs_output - QT_QPA_PLATFORM=offscreen python3 "$LVS_SCRIPT" \ - --layout="$GDS" \ - --netlist="$NETLIST" \ - --variant=${drcVariant} \ - --run_dir=lvs_output \ - --thr=1 \ - 2>&1 | tee lvs.log || true + # ---- Step 4: Convert Verilog netlists to SPICE ---- + echo "--- Step 4: Verilog to SPICE conversion ---" + if [ -f "$NETLIST" ]; then + CELL_LIB="${fullPdkPath}/libs.ref/${cellLib}/lib/${cellLib}__tt_025C_5v00.lib" + CELL_SPICE="${fullPdkPath}/libs.ref/${cellLib}/spice" + MACRO_DIR="${aegis-tapeout}/macros" + + # Convert each tile macro's gate-level Verilog to SPICE. + # The top-level netlist treats macros as blackboxes, so we need + # the per-macro SPICE subcircuits for a complete LVS netlist. + mkdir -p macro_spice + MACRO_SPICE_OK=1 + for mod in Tile BramTile DspBasicTile ClockTile IOTile SerDesTile FabricConfigLoader; do + MACRO_V="$MACRO_DIR/${deviceName}_''${mod}_final.v" + if [ -f "$MACRO_V" ]; then + echo "Converting $mod to SPICE..." + yosys -p " + read_liberty -lib $CELL_LIB; + read_verilog $MACRO_V; + hierarchy -top $mod; + write_spice -big_endian macro_spice/''${mod}.spice; + " 2>&1 | tee macro_spice/''${mod}_yosys.log || true + if [ ! -f "macro_spice/''${mod}.spice" ]; then + echo "WARNING: Failed to convert $mod to SPICE" + MACRO_SPICE_OK=0 + fi + fi + done + + # Convert top-level netlist to SPICE (macros are blackbox instances) + echo "Converting top-level netlist to SPICE..." + yosys -p " + read_liberty -lib $CELL_LIB; + read_verilog $NETLIST; + hierarchy -top ${topCellName}; + write_spice -big_endian raw_netlist.spice; + " 2>&1 | tee v2spice.log + + if [ -f raw_netlist.spice ]; then + echo "Subcircuits in raw SPICE:" + grep '\.subckt' raw_netlist.spice | head -20 || true + + # Assemble complete SPICE netlist: + # 1. PDK standard cell SPICE models + # 2. Per-macro SPICE subcircuits (from tile hardening) + # 3. Top-level AegisFPGA subcircuit + { + # PDK cell models + for f in $CELL_SPICE/*.spice; do + echo ".include $f" + done + echo "" + + # Macro subcircuit definitions + for f in macro_spice/*.spice; do + if [ -e "$f" ]; then + # Extract subcircuit definitions (skip any top-level test harness) + sed -n '/^\.subckt/,/^\.ends/p' "$f" + echo "" + fi + done + + # Top-level subcircuit + sed -n '/^\.subckt ${topCellName}/,/^\.ends/p' raw_netlist.spice + } > netlist.spice - if grep -q "MATCH" lvs.log 2>/dev/null; then - echo "PASS: LVS matched" + SUBCKT_COUNT=$(grep -c '^\.subckt' netlist.spice || true) + echo "PASS: SPICE netlist generated ($SUBCKT_COUNT subcircuits, $(wc -l < netlist.spice) lines)" else - echo "WARNING: LVS result needs review" + echo "FAIL: SPICE conversion failed" fi else - echo "NOTE: LVS skipped (script or netlist not available, PDK: ${pdkName})" + echo "NOTE: Verilog netlist not found, skipping SPICE conversion" fi echo "=== GDS verification complete ===" @@ -182,8 +305,9 @@ stdenvNoCC.mkDerivation { runHook preInstall mkdir -p $out cp *.log $out/ 2>/dev/null || true + cp netlist.spice $out/ 2>/dev/null || true + cp raw_netlist.spice $out/ 2>/dev/null || true cp -r drc_output $out/ 2>/dev/null || true - cp -r lvs_output $out/ 2>/dev/null || true echo "PASS" > $out/result runHook postInstall ''; diff --git a/tests/shift-register/default.nix b/tests/shift-register/default.nix index ebb18bf..58a8393 100644 --- a/tests/shift-register/default.nix +++ b/tests/shift-register/default.nix @@ -76,16 +76,19 @@ stdenvNoCC.mkDerivation { --output shift.bin echo "=== Simulating ===" + # Drive din (w1) high for the entire simulation. + # After 8+ clock edges (4 FF stages), dout should be high. aegis-sim \ --descriptor ${aegis-ip}/${deviceName}.json \ --bitstream shift.bin \ --clock-pin w0 \ --monitor-pin w2 \ + --set-pin w1:0-39 \ + --vcd shift.vcd \ --cycles 40 \ 2>&1 | tee sim.log else - echo "INFO: Routing failed (known limitation of shared output mux architecture)" - echo " Shift register requires per-track output muxes for full routability" + echo "INFO: Routing not yet supported for this design" fi runHook postBuild @@ -96,8 +99,10 @@ stdenvNoCC.mkDerivation { mkdir -p $out cp sim.log $out/ 2>/dev/null || true cp shift.bin $out/ 2>/dev/null || true + cp shift.vcd $out/ 2>/dev/null || true cp yosys.log $out/ cp nextpnr.log $out/ 2>/dev/null || true + cp shift_routed.json $out/ 2>/dev/null || true echo "PASS" > $out/result runHook postInstall ''; diff --git a/tests/tile-bits-consistency/default.nix b/tests/tile-bits-consistency/default.nix index 76afe83..0618e8b 100644 --- a/tests/tile-bits-consistency/default.nix +++ b/tests/tile-bits-consistency/default.nix @@ -10,6 +10,7 @@ python3, aegis-ip, aegis-sim, + jq, }: let @@ -24,6 +25,7 @@ stdenvNoCC.mkDerivation { python3 aegis-ip.tools aegis-sim + jq ]; buildPhase = '' @@ -36,8 +38,8 @@ stdenvNoCC.mkDerivation { # against the Rust formula. DESCRIPTOR="${aegis-ip}/${deviceName}.json" - TRACKS=$(python3 -c "import json; d=json.load(open('$DESCRIPTOR')); print(d['fabric']['tracks'])") - DART_WIDTH=$(python3 -c "import json; d=json.load(open('$DESCRIPTOR')); print(d['fabric']['tile_config_width'])") + TRACK=$(jq -r '.fabric.tracks' "$DESCRIPTOR") + DART_WIDTH=$(jq -r '.fabric.tile_config_width' "$DESCRIPTOR") echo "Device: ${deviceName}, tracks: $TRACKS, Dart tile_config_width: $DART_WIDTH" @@ -58,9 +60,9 @@ stdenvNoCC.mkDerivation { if errors > 0: sys.exit(1) - # Verify the Dart formula: width = 18 + 4*ceil(log2(4*T+3)) + 4*T*4 + # Verify the Dart formula: width = 18 + 4*ceil(log2(4*T+7)) + 4*T*4 import math - isw = math.ceil(math.log2(4*tracks + 3)) + isw = math.ceil(math.log2(4*tracks + 7)) rust_formula = 18 + 4*isw + 4*tracks*4 if rust_formula != expected: @@ -130,7 +132,6 @@ stdenvNoCC.mkDerivation { installPhase = '' runHook preInstall mkdir -p $out - echo "PASS" > $out/result runHook postInstall ''; }