From 4a3a5bf0d8a8f267782ac6b0d56e3548fecfbf77 Mon Sep 17 00:00:00 2001 From: Tristan Ross Date: Mon, 20 Apr 2026 21:16:37 -0700 Subject: [PATCH] fix: verification --- crates/aegis-ip/src/tile_bits.rs | 12 ++- crates/aegis-ip/src/tile_bits_tests.rs | 49 +++++----- crates/aegis-pack/src/lib.rs | 66 +++++++++---- crates/aegis-sim/src/lib.rs | 28 ++++-- crates/aegis-sim/src/main.rs | 41 ++++++++ crates/aegis-sim/src/tests.rs | 26 +++++- ip/lib/src/components/digital/fabric.dart | 39 +++++++- ip/lib/src/components/digital/tile.dart | 37 ++++++-- ip/lib/src/config/tile_config.dart | 10 +- ip/lib/src/openroad/tcl_emitter.dart | 47 ++++++++-- ip/lib/src/openroad/tile_tcl_emitter.dart | 11 ++- ip/test/components/tile_test.dart | 3 + ip/test/config/config_test.dart | 8 +- nextpnr-aegis/aegis.cc | 102 ++++++++++++++++---- nextpnr-aegis/aegis_test.cc | 75 +++++++-------- pkgs/aegis-tapeout/default.nix | 19 +++- pkgs/aegis-tapeout/scripts/def2gds.py | 68 +++++++++++--- pkgs/gf180mcu-pdk/default.nix | 62 ++++++++++++ pkgs/sky130-pdk/default.nix | 47 ++++++++++ tests/gds-verify/default.nix | 109 +++++++++++++++++----- tests/shift-register/default.nix | 9 +- tests/tile-bits-consistency/default.nix | 11 ++- 22 files changed, 706 insertions(+), 173 deletions(-) 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/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/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 0c590ed..df1ff70 100644 --- a/ip/lib/src/openroad/tcl_emitter.dart +++ b/ip/lib/src/openroad/tcl_emitter.dart @@ -619,23 +619,38 @@ class OpenroadTclEmitter { 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 + // Use upper metal layers for top-level routing. + // Metal1 is used internally by tile macros and standard cells; + // routing on it at the top level causes M1.1/M1.2a DRC violations. + // Auto-detect the lowest and highest available routing layers, + // then skip Metal1 by starting from the second layer. + buf.writeln('set all_route_layers [get_routing_layers]'); + buf.writeln('set bot_route ""'); buf.writeln('set top_route ""'); - buf.writeln('foreach layer [lreverse [get_routing_layers]] {'); + buf.writeln('foreach layer \$all_route_layers {'); buf.writeln( ' if {![catch {set tl ' '[[[ord::get_db] getTech] findLayer \$layer]}]} {', ); - buf.writeln(' if {\$tl ne "NULL" && \$top_route eq ""} {'); + buf.writeln(' if {\$tl ne "NULL"} {'); + buf.writeln(' if {\$bot_route eq ""} {'); + buf.writeln( + ' # Skip Metal1: use second routing layer as bottom', + ); + buf.writeln(' } elseif {\$bot_route eq "SKIP"} {'); + buf.writeln(' set bot_route \$layer'); + buf.writeln(' }'); + buf.writeln(' if {\$bot_route eq ""} { set bot_route "SKIP" }'); buf.writeln(' set top_route \$layer'); buf.writeln(' }'); buf.writeln(' }'); buf.writeln('}'); + buf.writeln( + 'if {\$bot_route eq "" || \$bot_route eq "SKIP"} { set bot_route Metal2 }', + ); 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('puts "Routing layers: \$bot_route-\$top_route"'); + buf.writeln('set_routing_layers -signal \$bot_route-\$top_route'); buf.writeln('global_route -allow_congestion'); buf.writeln(); buf.writeln('# Save global-routed DEF (guaranteed output)'); @@ -652,6 +667,13 @@ class OpenroadTclEmitter { buf.writeln(); } + void _writeDensityFill(StringBuffer buf) { + buf.writeln(_section('Density fill')); + + buf.writeln(r'density_fill -rules $FILL_CONFIG'); + buf.writeln(); + } + void _writeReports(StringBuffer buf) { buf.writeln(_section('Reports')); @@ -670,8 +692,11 @@ class OpenroadTclEmitter { } void _writeLayerDetection(StringBuffer buf) { + // Detect pin layers, skipping Metal1 which is reserved for + // internal macro/standard cell routing. buf.writeln('set hor_layer ""'); buf.writeln('set ver_layer ""'); + buf.writeln('set skip_first_hor 1'); buf.writeln('foreach layer [get_routing_layers] {'); buf.writeln( ' if {![catch {set tl ' @@ -679,8 +704,11 @@ 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(' # Skip Metal1 (first horizontal layer)'); + 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(' }'); @@ -690,8 +718,9 @@ class OpenroadTclEmitter { buf.writeln(' }'); buf.writeln(' }'); buf.writeln('}'); - buf.writeln('if {\$hor_layer eq ""} { set hor_layer Metal1 }'); + buf.writeln('if {\$hor_layer eq ""} { set hor_layer Metal3 }'); buf.writeln('if {\$ver_layer eq ""} { set ver_layer Metal2 }'); + 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..5eed510 100644 --- a/ip/lib/src/openroad/tile_tcl_emitter.dart +++ b/ip/lib/src/openroad/tile_tcl_emitter.dart @@ -165,8 +165,12 @@ class OpenroadTileTclEmitter { } void _writeLayerDetection(StringBuffer buf) { + // Skip Metal1 for pin placement. Metal1 is used internally by standard + // cells but macro pins on Metal1 cause M1.1/M1.2a spacing violations + // when tiles are placed adjacent to each other. buf.writeln('set hor_layer ""'); buf.writeln('set ver_layer ""'); + buf.writeln('set skip_first_hor 1'); buf.writeln('foreach layer [get_routing_layers] {'); buf.writeln( ' if {![catch {set tl ' @@ -174,8 +178,11 @@ 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(' # Skip Metal1 (first horizontal layer)'); + 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(' }'); @@ -185,7 +192,7 @@ class OpenroadTileTclEmitter { buf.writeln(' }'); buf.writeln(' }'); buf.writeln('}'); - buf.writeln('if {\$hor_layer eq ""} { set hor_layer Metal1 }'); + buf.writeln('if {\$hor_layer eq ""} { set hor_layer Metal3 }'); buf.writeln('if {\$ver_layer eq ""} { set ver_layer Metal2 }'); } } 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/nextpnr-aegis/aegis.cc b/nextpnr-aegis/aegis.cc index 0fff094..0dd1551 100644 --- a/nextpnr-aegis/aegis.cc +++ b/nextpnr-aegis/aegis.cc @@ -113,6 +113,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 { @@ -352,6 +402,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); @@ -361,8 +427,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, @@ -373,13 +441,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); } } } @@ -429,9 +494,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; + int max_span = logic ? 4 : 1; int spans[] = {1, 2, 4}; for (int span : spans) { if (span > max_span) @@ -440,20 +514,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 1f8ae15..9ef3d8a 100644 --- a/nextpnr-aegis/aegis_test.cc +++ b/nextpnr-aegis/aegis_test.cc @@ -187,18 +187,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; + } } } @@ -210,26 +207,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)"; } } @@ -261,9 +261,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) @@ -283,32 +283,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"; } @@ -316,6 +316,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-tapeout/default.nix b/pkgs/aegis-tapeout/default.nix index afb94d5..51ea21f 100644 --- a/pkgs/aegis-tapeout/default.nix +++ b/pkgs/aegis-tapeout/default.nix @@ -63,6 +63,15 @@ lib.extendMkDerivation { pdkPath = "${pdk}/${pdk.pdkPath}"; libsRef = "${pdkPath}/libs.ref/${cellLib}"; + # Generate KLayout LEF/DEF layer map from PDK layer definitions + lefGdsMapFile = builtins.toFile "lef-gds.map" ( + lib.concatStringsSep "\n" ( + lib.mapAttrsToList (name: val: "${name} ALL ${toString val.layer} ${toString val.datatype}") ( + pdk.lefGdsLayers or { } + ) + ) + ); + # Default tile die sizes per PDK defaultTileDieSizes = { @@ -206,6 +215,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 @@ -383,22 +393,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) 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 diff --git a/pkgs/aegis-tapeout/scripts/def2gds.py b/pkgs/aegis-tapeout/scripts/def2gds.py index 78f9659..10c2d31 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,25 +18,66 @@ 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() +# Match the DBU used by OpenROAD (DATABASE MICRONS 2000 = 0.0005um) +# to avoid geometry scaling warnings from the DEF reader +layout.dbu = 0.0005 -# 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 +lefdef.dbu = layout.dbu +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 + lef_lefdef.dbu = layout.dbu + # 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) # Read all standard cell GDS files from the PDK gds_files = sorted(glob.glob(os.path.join(cell_gds_dir, "*.gds"))) @@ -42,16 +85,13 @@ for gds in gds_files: layout.read(gds) -# 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.write(out_gds) print(f"Wrote {out_gds}") diff --git a/pkgs/gf180mcu-pdk/default.nix b/pkgs/gf180mcu-pdk/default.nix index 47aa61e..caa9963 100644 --- a/pkgs/gf180mcu-pdk/default.nix +++ b/pkgs/gf180mcu-pdk/default.nix @@ -100,6 +100,68 @@ stdenvNoCC.mkDerivation { layer = 236; datatype = 0; }; + # DRC rule tables relevant to our design (skip analog/specialty decks) + drcTables = [ + "metal1" + "metal2" + "metal3" + "metal4" + "metal5" + "metaltop" + "via1" + "via2" + "via3" + "via4" + "contact" + "geom" + ]; + # 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 = { diff --git a/pkgs/sky130-pdk/default.nix b/pkgs/sky130-pdk/default.nix index 867497d..628889c 100644 --- a/pkgs/sky130-pdk/default.nix +++ b/pkgs/sky130-pdk/default.nix @@ -59,6 +59,53 @@ stdenvNoCC.mkDerivation { 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/gds-verify/default.nix b/tests/gds-verify/default.nix index 5a53b5b..778da52 100644 --- a/tests/gds-verify/default.nix +++ b/tests/gds-verify/default.nix @@ -7,23 +7,28 @@ stdenvNoCC, python3, klayout, + procps, + yosys, aegis-tapeout, }: let inherit (aegis-tapeout) deviceName pdk; - inherit (pdk) pdkName pdkPath; + 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 drcVariant = if pdkName == "gf180mcu" then - "C" # 9K metal_top, 5LM + "D" # 11K metal_top, 5LM 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 +36,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" @@ -127,10 +137,14 @@ stdenvNoCC.mkDerivation { mkdir -p drc_output QT_QPA_PLATFORM=offscreen python3 "$DRC_SCRIPT" \ --path="$GDS" \ + --topcell=AegisFPGA \ --variant=${drcVariant} \ --run_dir=drc_output \ --no_feol \ - --thr=1 \ + --run_mode=deep \ + --thr=$NIX_BUILD_CORES \ + --mp=$NIX_BUILD_CORES \ + ${lib.concatMapStringsSep " " (t: "--table=${t}") drcTables} \ 2>&1 | tee drc.log || true # Check for DRC violations @@ -150,27 +164,79 @@ stdenvNoCC.mkDerivation { 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" + # ---- 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" - 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 + # 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 AegisFPGA; + 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 AegisFPGA/,/^\.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 +248,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 ''; }