Skip to main content

logicaffeine_compile/codegen_sva/
pdk_target.rs

1//! `PdkTarget` — the PROCESS a design is built out of, as data.
2//!
3//! [`super::fabric_target::FabricTarget`] describes an ARCHITECTURE (the grid you place onto).
4//! This describes an IMPLEMENTATION (what the grid is made of): the standard-cell library, the
5//! metal stack, the shuttle vehicle's fixed geometry, the SRAM macros actually available, and the
6//! golden DRC deck. They are deliberately separate types — the same fabric is placeable on several
7//! processes, and the same process carries many fabrics.
8//!
9//! WHY THIS EXISTS. Every PDK fact in this project used to be a literal in a shell script:
10//! `sky130A_mr.drc` in one, `"0 0 2920 3520"` in another, `"0.99,$X,0,0,0,0"` (a six-column
11//! `li1,met1..met5` vector) in a third, and `contains("sky130")` inside four Rust locks that would
12//! have actively REJECTED a GF180 ledger. Bringing up a second process meant editing eight files
13//! and hoping. The acceptance test for this module is blunt: **adding a PDK must be adding one
14//! value here, and nothing else.** If a script or a lock has to change, the abstraction leaked.
15//!
16//! The shape follows [`crate::codegen_ana::target::AnalogTarget`]: every quantity carries its
17//! `provenance`, and asking for something the process cannot do returns a typed refusal rather
18//! than a silently-wrong number.
19
20use std::fmt;
21
22/// A standard-cell SRAM macro the PDK actually ships, with the facts that decide whether a design
23/// can use it.
24///
25/// `corners` is not decoration. Of sky130's four macros, three ship **only** `TT_1p8V_25C`, and
26/// the signoff recorder gates on `multi_corner` — so a design built from those three cannot
27/// honestly claim a multi-corner signoff no matter how clean its logic is. That fact belongs next
28/// to the macro, not in someone's memory.
29// No `Eq`: these carry LEF dimensions as `f64`, and `f64` is `PartialEq` but not `Eq`. The values
30// are exact decimals in the LEF (683.10 x 416.54), so integer nanometres would be the more honest
31// representation and would restore `Eq` — worth doing if these ever need to be a map key or
32// deduplicated. Nothing needs that today, and float equality on a spec sheet is not a comparison
33// anyone should be making.
34#[derive(Debug, Clone, PartialEq)]
35pub struct SramMacro {
36    pub name: &'static str,
37    /// Addressable words.
38    pub words: u32,
39    /// Bits per word — the macro's data width. A design whose arrays are this wide needs no
40    /// width-banking, which is most of what makes a memory plan cheap or expensive.
41    pub bits: u32,
42    /// Physical size in micrometres, from the macro's own LEF `SIZE` line.
43    pub width_um: f64,
44    pub height_um: f64,
45    /// Read/write ports, e.g. "1rw1r". An array read every cycle against writes in the same block
46    /// needs a dual-port macro; a single-port one cannot express it at any speed.
47    pub ports: &'static str,
48    /// Liberty corners shipped. Fewer than a full set caps what the design can claim.
49    pub corners: &'static [&'static str],
50}
51
52impl SramMacro {
53    pub fn area_mm2(&self) -> f64 {
54        self.width_um * self.height_um / 1.0e6
55    }
56    pub fn capacity_bits(&self) -> u64 {
57        u64::from(self.words) * u64::from(self.bits)
58    }
59    /// Whether this macro can carry a multi-corner signoff claim. A single shipped corner cannot:
60    /// slow and fast silicon are simply not characterized.
61    pub fn is_multi_corner(&self) -> bool {
62        self.corners.len() > 1
63    }
64}
65
66/// A memory the design needs, expressed the way RTL expresses it.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub struct MemoryRequest {
69    pub words: u32,
70    pub bits: u32,
71    /// Set when the array is read and written in the same cycle (the common case for an engine
72    /// that reads unconditionally at the top of its always block).
73    pub dual_port: bool,
74    /// Set when the design intends to claim multi-corner signoff over this memory.
75    pub needs_multi_corner: bool,
76}
77
78/// Measured standard-cell density on this flow: 173,895 cells/mm^2, the median across the twelve
79/// sky130 blocks in `benchmarks/silicon/sky130_signoff*.json` (range 121k-224k).
80pub const MEASURED_CELLS_PER_MM2: f64 = 173_895.0;
81
82/// Silicon area per stored bit when an array is built from FLIP-FLOPS, in um^2.
83///
84/// Measured from the sky130 library's own LEF, not estimated: `dfxtp_1` is 20.019 um^2, plus the
85/// access logic every addressed array needs — a `mux2_1` read path at 11.261 and roughly a
86/// `nand2_1` of write decode at 3.754 — is 35.03 um^2 of CELL per bit. Standard cells do not fill
87/// their area: these blocks place at FP_CORE_UTIL 30-40, so the die cost is that over ~0.40.
88///
89/// The comparison that matters: this is ~87 um^2/bit against a macro's 17.4-20.5 um^2/bit, where
90/// the macro figure is its true LEF footprint with its own decode already inside. **SRAM is about
91/// four times denser than flops.** An earlier version of this constant was expressed in generic
92/// "cells" and made flops look CHEAPER than macros, which put a 512 kbit heap in flip-flops at
93/// 36 mm^2 and called it the smaller option.
94pub const FLOP_BIT_UM2: f64 = (20.019 + 11.261 + 3.754) / 0.40;
95
96/// How a [`MemoryRequest`] is best built. NOT always a macro — and getting this wrong is
97/// expensive in both directions.
98///
99/// An SRAM macro has a MINIMUM depth (256 words on sky130's smallest), so a shallow array pays for
100/// a whole macro to store a fraction of one. The WPE's `blk_*` arrays are 13 x 160 words: as
101/// macros that is 13 instances and 2.18 mm^2 to hold 66 kbit, which is worse than flops AND drags
102/// in port constraints the flops would not have. Conversely the heap at 2.1 Mbit would be ~36 mm^2
103/// of flops and has to be a macro. The planner must pick, not assume.
104#[derive(Debug, Clone, PartialEq)]
105pub enum Implementation {
106    /// Flip-flops and their access logic — no macro, no port limit, no corner problem.
107    Flops { cells: u64 },
108    /// One or more SRAM macro instances.
109    Macros {
110        macro_name: &'static str,
111        depth_banks: u32,
112        width_banks: u32,
113        instances: u32,
114    },
115}
116
117/// How a [`MemoryRequest`] maps onto real macros.
118#[derive(Debug, Clone, PartialEq)]
119pub struct MemoryPlan {
120    /// Flops or macros, whichever is smaller on this process.
121    pub implementation: Implementation,
122    pub macro_name: &'static str,
123    /// Instances along the address axis.
124    pub depth_banks: u32,
125    /// Instances along the data axis. More than 1 means the array is wider than the macro and must
126    /// be split — every access then drives several macros.
127    pub width_banks: u32,
128    pub instances: u32,
129    pub area_mm2: f64,
130}
131
132/// Why a process cannot build what was asked for. Typed, so a caller cannot mistake a refusal for
133/// a plan — the mistake that produces a design nobody can fabricate.
134// No `Eq` — `ExceedsUserArea` carries `f64` areas. See the note on `SramMacro`.
135#[derive(Debug, Clone, PartialEq)]
136pub enum PdkUnsupported {
137    /// The process ships no SRAM macro at all; the memory would have to be flip-flops.
138    NoSramMacros { pdk: &'static str },
139    /// No macro offers the required port count.
140    NoDualPortMacro { pdk: &'static str },
141    /// A multi-corner claim was requested but every usable macro ships a single corner.
142    NoMultiCornerMacro {
143        pdk: &'static str,
144        best: &'static str,
145    },
146    /// The plan does not fit the vehicle's user area.
147    ExceedsUserArea { needed_mm2: f64, available_mm2: f64 },
148    /// A user macro for this process's vehicle was requested, but the vehicle's power nets and the
149    /// layer ceiling a macro must stay under have not been read here.
150    VehicleUncharacterized { pdk: &'static str },
151}
152
153impl fmt::Display for PdkUnsupported {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        match self {
156            Self::NoSramMacros { pdk } => {
157                write!(
158                    f,
159                    "{pdk} ships no SRAM macro — this memory can only be flip-flops"
160                )
161            }
162            Self::NoDualPortMacro { pdk } => write!(
163                f,
164                "{pdk} ships no dual-port SRAM macro, but the array is read and written in the \
165                 same cycle"
166            ),
167            Self::NoMultiCornerMacro { pdk, best } => write!(
168                f,
169                "{pdk}: a multi-corner signoff was requested, but the widest usable macro ({best}) \
170                 ships a single Liberty corner — slow and fast silicon are not characterized"
171            ),
172            Self::ExceedsUserArea {
173                needed_mm2,
174                available_mm2,
175            } => write!(
176                f,
177                "the memory plan needs {needed_mm2:.2} mm² of macro against a {available_mm2:.2} \
178                 mm² user area, before any logic"
179            ),
180            Self::VehicleUncharacterized { pdk } => write!(
181                f,
182                "{pdk}: the shuttle vehicle's user-macro power nets and routing ceiling have not \
183                 been read — refusing to guess them"
184            ),
185        }
186    }
187}
188
189/// The fixed geometry of a shuttle vehicle. `None` for a standalone die, where the user chooses.
190#[derive(Debug, Clone, Copy, PartialEq)]
191pub struct UserArea {
192    pub width_um: f64,
193    pub height_um: f64,
194    /// The power and ground nets the vehicle's harness provides to a user macro, when read.
195    pub macro_power: Option<(&'static str, &'static str)>,
196    /// The highest layer a user macro with its own grid may route on, so the wrapper's power grid can pass over it.
197    pub macro_max_layer: Option<&'static str>,
198    /// The highest layer a user macro fed by the wrapper's grid may route on: its own strap layer.
199    pub fed_macro_max_layer: Option<&'static str>,
200    /// How the harness times a user macro, when read.
201    pub harness: Option<HarnessTiming>,
202}
203
204/// The vehicle's timing as efabless characterizes it: what a user macro's ports see from the harness, and the clock
205/// model the whole chip is signed off with.
206#[derive(Debug, Clone, Copy, PartialEq)]
207pub struct HarnessTiming {
208    /// The earliest the harness presents each synchronous user-area input after the clock edge, in ns: the
209    /// `set_input_delay -min` a user macro's own ports are held to, so its hold repair closes the port-to-flop
210    /// paths the wrapper would otherwise see violated.
211    pub input_min_ns: &'static [(&'static str, f64)],
212    /// The clock uncertainty the chip is signed off with, in ns.
213    pub clock_uncertainty_ns: f64,
214    /// The on-chip-variation derate the chip is signed off with: early paths at `1 - d`, late paths at `1 + d`.
215    pub timing_derate: f64,
216    /// The period the chip signs its core clock off at, in ns: [`HarnessClock::Core`], which also runs the
217    /// management SoC and the bus.
218    pub core_clock_period_ns: f64,
219    /// The user-area ports the harness times against its core clock, inputs and outputs: the bus, its reset and the
220    /// logic analyzer. A macro whose logic runs on another clock still times these against [`HarnessClock::Core`].
221    pub core_clock_inputs: &'static [&'static str],
222    pub core_clock_outputs: &'static [&'static str],
223}
224
225impl HarnessTiming {
226    /// The chip's signoff clock as `NAME=value` lines for the flow scripts to `eval`.
227    pub fn shell(&self) -> String {
228        format!(
229            "CV_CLOCK_UNCERTAINTY_NS={}\nCV_TIMING_DERATE={}\nCV_CORE_CLOCK_PERIOD_NS={}\n",
230            self.clock_uncertainty_ns, self.timing_derate, self.core_clock_period_ns
231        )
232    }
233}
234
235/// The clocks the vehicle's harness gives a user macro.
236#[derive(Debug, Clone, Copy, PartialEq, Eq)]
237pub enum HarnessClock {
238    /// `wb_clk_i`: the core clock, which also runs the management SoC and the bus, signed off with the chip at
239    /// [`HarnessTiming::core_clock_period_ns`].
240    Core,
241    /// `user_clock2`: Caravel's PLL 90-degree phase through a divider of its own (`caravel_clocking.v`, housekeeping's
242    /// `pll90_sel`), set apart from the core clock's (`pll_sel`), so a macro on it runs at a period of its own.
243    User2,
244}
245
246impl HarnessClock {
247    /// The user-area port the clock enters on.
248    pub fn port(self) -> &'static str {
249        match self {
250            HarnessClock::Core => "wb_clk_i",
251            HarnessClock::User2 => "user_clock2",
252        }
253    }
254
255    pub fn by_port(port: &str) -> Option<HarnessClock> {
256        [HarnessClock::Core, HarnessClock::User2]
257            .into_iter()
258            .find(|c| c.port() == port)
259    }
260}
261
262impl UserArea {
263    pub fn mm2(&self) -> f64 {
264        self.width_um * self.height_um / 1.0e6
265    }
266    /// The OpenLane `DIE_AREA` string for this vehicle.
267    pub fn die_area(&self) -> String {
268        format!("0 0 {} {}", self.width_um, self.height_um)
269    }
270}
271
272/// One process, as data.
273#[derive(Debug, Clone, PartialEq)]
274pub struct PdkTarget {
275    /// The PDK variant exactly as `ciel`/open_pdks names it — this is the string that must appear
276    /// in a run's `config.tcl` and therefore in the signoff ledger.
277    pub name: &'static str,
278    /// Drawn feature size in nanometres. Useful for first-order area scaling and nothing else.
279    pub node_nm: u32,
280    pub std_cell_library: &'static str,
281    /// Routing layers bottom-to-top, in the names the tools use. sky130 says `li1,met1..met5`;
282    /// GF180 says `Metal1..Metal5` and has no `li1`. A six-column layer-adjustment vector written
283    /// for one is silently wrong on the other, which is exactly the bug this field removes.
284    pub metal_stack: &'static [&'static str],
285    /// The LOCAL-INTERCONNECT layer, when the process has one. sky130 has `li1` below met1; GF180
286    /// does not. This is a ROLE, not a name, and conflating the two is a live bug: a layer vector
287    /// that hard-penalises "the lowest layer" is right on sky130 (li1 is not for routing) and
288    /// wrong on GF180, where the lowest layer IS the first real metal and hard-penalising it
289    /// strangles the router. Caught by emitting for both processes and reading the result.
290    pub local_interconnect: Option<&'static str>,
291    /// Highest layer signal routing may use on this vehicle.
292    pub top_routing_layer: &'static str,
293    /// This process's clock-root buffer and its output pin — the cell that should be declared as
294    /// the CLOCK input port's driver, when the process has been characterized here.
295    ///
296    /// OpenLane models the clock port's drive with `SYNTH_CLK_DRIVING_CELL`, which DEFAULTS TO
297    /// `SYNTH_DRIVING_CELL` — the minimum data-input inverter (`sky130_fd_sc_hd__inv_2`). A clock
298    /// port is not driven by a minimum logic inverter on any real die; it arrives from a pad, a
299    /// PLL, or (on Caravel) the management SoC's clock distribution. The pessimism lands on the
300    /// ONE net a design cannot repair, because an input port's transition is an ASSUMPTION rather
301    /// than something a resizer can buffer. Measured: `systolic_os_pipe8_realbist` closed every
302    /// other gate and signed off with exactly three max-slew violations, and all three were that
303    /// single net — `clk`, `clkbuf_0_clk/A`, and a diode sitting on it, each 0.97 against a 0.75
304    /// limit. `systolic_pipe16_realbist` shows the identical triple at 1.25.
305    ///
306    /// The value is not invented: it is the process's OWN `ROOT_CLK_BUFFER`, read from its
307    /// `libs.tech/openlane/<scl>/config.tcl`. `None` means this process has not been installed and
308    /// read here, and the flow then emits nothing rather than guessing a cell name that may not
309    /// exist in the library — a wrong name fails the flow, and a plausible wrong one is worse.
310    ///
311    /// (This cannot be expressed as `ref::$ROOT_CLK_BUFFER` in config.json: design-config
312    /// references resolve BEFORE the PDK config is sourced, so the variable is not yet defined and
313    /// OpenLane exits with `Referenced variable 'ROOT_CLK_BUFFER' not found`.)
314    pub clk_driving_cell: Option<(&'static str, &'static str)>,
315    /// The efabless/foundry maximal-rule deck `mpw_precheck` accepts a submission against.
316    pub golden_drc_deck: &'static str,
317    /// The shuttle vehicle's fixed user area, when there is one.
318    pub user_area: Option<UserArea>,
319    /// SRAM macros the PDK ships, widest-capacity first.
320    pub sram_macros: &'static [SramMacro],
321    /// How many of `mpw_precheck`'s checks are scheduled for this PDK. It is NOT the same number
322    /// for every process — gf180 drops six that sky130 runs (magic_drc, zeroarea, consistency,
323    /// illegal_cellname, pin_label, metalcheck) and adds pdnmulti. "19/19" and "13/13" are not the
324    /// same assurance, and a claim built on one must not be read as the other.
325    pub precheck_checks: u32,
326    /// Per-layer wire RC for the PRE-ROUTE parasitic estimate (`LAYERS_RC`), calibrated against
327    /// this project's own extraction — or `None` where no process measurement exists, in which
328    /// case nothing is emitted rather than another process's numbers borrowed.
329    ///
330    /// Every resizer and pre-route STA step sizes drivers against this estimate, so its error
331    /// becomes the gap signoff later finds. See [`SKY130A_LAYER_RC`] for the measurement.
332    pub estimator_layer_rc: Option<&'static [LayerRc]>,
333    /// Where these numbers come from, so a reader can check them rather than trust them.
334    pub provenance: &'static str,
335}
336
337/// One routing layer's wire RC per unit length, as `set_layer_rc` takes it.
338#[derive(Debug, Clone, Copy, PartialEq)]
339pub struct LayerRc {
340    pub layer: &'static str,
341    pub cap_pf_per_um: f64,
342    pub res_kohm_per_um: f64,
343}
344
345/// sky130A signal-layer RC for the pre-route estimate.
346///
347/// CAPACITANCE is FITTED: per-net routed length on each layer (the detailed route of
348/// `systolic_pipe16_realbist`, RUN_2026.09.23_03.24.05) regressed against that net's OpenRCX wire
349/// capacitance (nominal corner, `rules.openrcx.sky130A.nom.calibre`), over 23,557 nets of 20 um
350/// or more. The fit reproduces extraction with a 7.3% median error and 0.8% in total. OpenLane
351/// ships no `LAYERS_RC`, and the uncalibrated estimate it fell back to was optimistic by a median
352/// 1.39x (2.02x at p90) on the 30-80 fF nets that fail slew; with this table those estimate at
353/// 0.95x (p10 0.86, p90 1.07).
354///
355/// RESISTANCE is the tech LEF's own `RESISTANCE RPERSQ` over the layer's minimum `WIDTH`
356/// (`sky130_fd_sc_hd__nom.tlef`), e.g. met1 0.125 ohm/sq / 0.14 um.
357pub const SKY130A_LAYER_RC: &[LayerRc] = &[
358    LayerRc {
359        layer: "met1",
360        cap_pf_per_um: 1.6116e-4,
361        res_kohm_per_um: 8.9286e-4,
362    },
363    LayerRc {
364        layer: "met2",
365        cap_pf_per_um: 1.4460e-4,
366        res_kohm_per_um: 8.9286e-4,
367    },
368    LayerRc {
369        layer: "met3",
370        cap_pf_per_um: 1.8581e-4,
371        res_kohm_per_um: 1.5667e-4,
372    },
373    LayerRc {
374        layer: "met4",
375        cap_pf_per_um: 1.6682e-4,
376        res_kohm_per_um: 1.5667e-4,
377    },
378    LayerRc {
379        layer: "met5",
380        cap_pf_per_um: 2.4831e-4,
381        res_kohm_per_um: 1.7812e-5,
382    },
383];
384
385impl PdkTarget {
386    /// Every process this project can currently build against.
387    pub fn all() -> &'static [PdkTarget] {
388        &[SKY130A, GF180MCUC]
389    }
390
391    /// Look a process up by the name a run's `config.tcl` records.
392    pub fn by_name(name: &str) -> Option<&'static PdkTarget> {
393        Self::all().iter().find(|p| p.name == name)
394    }
395
396    /// The first layer signal routing should actually use: the one above local interconnect where
397    /// the process has it, otherwise the bottom of the stack.
398    pub fn first_signal_layer(&self) -> &'static str {
399        match self.local_interconnect {
400            Some(li) => self
401                .metal_stack
402                .iter()
403                .find(|l| **l != li)
404                .copied()
405                .unwrap_or(li),
406            None => self.metal_stack[0],
407        }
408    }
409
410    /// The `GRT_LAYER_ADJUSTMENTS` vector — one column per routing layer, so the string can never
411    /// have the wrong count for the process, and assigned by ROLE so it can never have the right
412    /// count with the wrong meaning.
413    ///
414    /// `local_penalty` applies to local interconnect (which is not for routing at all), and
415    /// `first_metal_penalty` to the lowest real signal layer (the antenna lever). On a process
416    /// with no local interconnect only the second applies — the bug this replaced put the
417    /// local-interconnect penalty on GF180's `Metal1` and silently dropped the antenna one.
418    pub fn layer_adjustments(&self, local_penalty: f64, first_metal_penalty: f64) -> String {
419        let first = self.first_signal_layer();
420        self.metal_stack
421            .iter()
422            .map(|l| {
423                let v = if Some(*l) == self.local_interconnect {
424                    local_penalty
425                } else if *l == first {
426                    first_metal_penalty
427                } else {
428                    0.0
429                };
430                format!("{v}")
431            })
432            .collect::<Vec<_>>()
433            .join(",")
434    }
435
436    /// Plan a memory onto this process's macros, or refuse with a reason.
437    ///
438    /// Refusing is the point. A design that silently "fits" because nobody checked the port count
439    /// or the corner set is a design that fails at signoff, or worse, on silicon.
440    pub fn plan_memory(&self, req: MemoryRequest) -> Result<MemoryPlan, PdkUnsupported> {
441        // Flops are always available: no macro, no port limit, no corner gap. For a shallow array
442        // they are also SMALLER, because a macro cannot be shorter than its minimum depth.
443        let bits = u64::from(req.words) * u64::from(req.bits);
444        let flop_area = bits as f64 * FLOP_BIT_UM2 / 1.0e6;
445        let flop_cells = (flop_area * MEASURED_CELLS_PER_MM2) as u64;
446        let flop_plan = MemoryPlan {
447            implementation: Implementation::Flops { cells: flop_cells },
448            macro_name: "(flops)",
449            depth_banks: 0,
450            width_banks: 0,
451            instances: 0,
452            area_mm2: flop_area,
453        };
454
455        if self.sram_macros.is_empty() {
456            // Not a refusal: an array with no macro available is simply built from flops, and
457            // only the VEHICLE can refuse it.
458            return self.fits_vehicle(flop_plan);
459        }
460        let dual = |m: &SramMacro| m.ports.contains("1rw1r") || m.ports.contains("2rw");
461        let usable: Vec<&SramMacro> = self
462            .sram_macros
463            .iter()
464            .filter(|m| !req.dual_port || dual(m))
465            .collect();
466        if usable.is_empty() {
467            return Err(PdkUnsupported::NoDualPortMacro { pdk: self.name });
468        }
469        let corner_ok: Vec<&&SramMacro> = usable
470            .iter()
471            .filter(|m| !req.needs_multi_corner || m.is_multi_corner())
472            .collect();
473        if corner_ok.is_empty() {
474            // Flops have no Liberty corner problem — they are characterized with the standard
475            // cells — so an uncharacterized macro library is a reason to use flops, not to refuse.
476            return self.fits_vehicle(flop_plan);
477        }
478
479        // Choose by smallest resulting area, not by largest macro: a wide-but-shallow array can be
480        // cheaper on a narrower part, and width-banking multiplies instance count.
481        let best = corner_ok
482            .iter()
483            .map(|m| {
484                let width_banks = req.bits.div_ceil(m.bits);
485                let depth_banks = req.words.div_ceil(m.words);
486                let instances = width_banks * depth_banks;
487                MemoryPlan {
488                    implementation: Implementation::Macros {
489                        macro_name: m.name,
490                        depth_banks,
491                        width_banks,
492                        instances,
493                    },
494                    macro_name: m.name,
495                    depth_banks,
496                    width_banks,
497                    instances,
498                    area_mm2: f64::from(instances) * m.area_mm2(),
499                }
500            })
501            .min_by(|a, b| a.area_mm2.total_cmp(&b.area_mm2))
502            .expect("corner_ok is non-empty");
503
504        // Whichever is actually smaller. A shallow array that "fits" a macro is still the wrong
505        // build if flops cost less silicon.
506        let best = if flop_plan.area_mm2 < best.area_mm2 {
507            flop_plan
508        } else {
509            best
510        };
511        self.fits_vehicle(best)
512    }
513
514    /// A plan is only real if the vehicle can carry it.
515    fn fits_vehicle(&self, p: MemoryPlan) -> Result<MemoryPlan, PdkUnsupported> {
516        if let Some(ua) = self.user_area {
517            if p.area_mm2 > ua.mm2() {
518                return Err(PdkUnsupported::ExceedsUserArea {
519                    needed_mm2: p.area_mm2,
520                    available_mm2: ua.mm2(),
521                });
522            }
523        }
524        Ok(p)
525    }
526}
527
528/// SkyWater 130 nm, the process every block in this repo has been hardened on.
529pub const SKY130A: PdkTarget = PdkTarget {
530    name: "sky130A",
531    node_nm: 130,
532    std_cell_library: "sky130_fd_sc_hd",
533    metal_stack: &["li1", "met1", "met2", "met3", "met4", "met5"],
534    local_interconnect: Some("li1"),
535    top_routing_layer: "met5",
536    clk_driving_cell: Some(("sky130_fd_sc_hd__clkbuf_16", "X")),
537    golden_drc_deck: "sky130A_mr.drc",
538    // Caravel: user macros take vccd1/vssd1 from the harness. One with its own grid stays on met3 and below, under
539    // the wrapper's met4/met5 power straps — how the shipped submission was hardened and precheck-passed (19/19,
540    // 2026-09-17). One fed by the wrapper is efabless's reference user macro (caravel_user_project,
541    // openlane/user_proj_example/config.json: `DESIGN_IS_CORE` 0, `RT_MAX_LAYER` met4): met4 straps only, crossed
542    // by the wrapper's met5.
543    user_area: Some(UserArea {
544        width_um: 2920.0,
545        height_um: 3520.0,
546        macro_power: Some(("vccd1", "vssd1")),
547        macro_max_layer: Some("met3"),
548        fed_macro_max_layer: Some("met4"),
549        harness: Some(HarnessTiming {
550            // efabless's characterization of the harness in the scaffold's `base_user_project_wrapper.sdc`
551            // (generated from Caravel's own STA by get_cup_sdc.py): the `-min` input delays of the logic
552            // analyzer and the Wishbone bus. The GPIOs carry none — `IO_SYNC` 0 declares them asynchronous.
553            input_min_ns: &[
554                ("la_data_in[*]", 0.18),
555                ("la_oenb[*]", 0.3),
556                ("wbs_adr_i[*]", 0.79),
557                ("wbs_dat_i[*]", 1.04),
558                ("wbs_sel_i[*]", 1.19),
559                ("wbs_we_i", 1.65),
560                ("wbs_cyc_i", 1.69),
561                ("wbs_stb_i", 1.86),
562            ],
563            // Caravel's chip-level signoff SDC (caravel/signoff/caravel/caravel.sdc, Rev 1, 2023-12-02):
564            // `set_clock_uncertainty 0.1000` on `clk`, `set derate 0.0375`, and `set clk_period 25` on the clock pad,
565            // which `caravel_clocking.v` hands the user area as `wb_clk_i`.
566            clock_uncertainty_ns: 0.1,
567            timing_derate: 0.0375,
568            core_clock_period_ns: 25.0,
569            // Every port the scaffold's wrapper SDC delays against `clk` outside its `IO_SYNC` budget.
570            core_clock_inputs: &[
571                "wb_rst_i",
572                "la_data_in[*]",
573                "la_oenb[*]",
574                "wbs_sel_i[*]",
575                "wbs_we_i",
576                "wbs_adr_i[*]",
577                "wbs_stb_i",
578                "wbs_dat_i[*]",
579                "wbs_cyc_i",
580            ],
581            core_clock_outputs: &["user_irq[*]", "la_data_out[*]", "wbs_dat_o[*]", "wbs_ack_o"],
582        }),
583    }),
584    sram_macros: &[
585        SramMacro {
586            name: "sky130_sram_2kbyte_1rw1r_32x512_8",
587            words: 512,
588            bits: 32,
589            width_um: 683.10,
590            height_um: 416.54,
591            ports: "1rw1r",
592            corners: &["TT_1p8V_25C"],
593        },
594        SramMacro {
595            name: "sram_1rw1r_32_256_8_sky130",
596            words: 256,
597            bits: 32,
598            width_um: 376.48,
599            height_um: 446.235,
600            ports: "1rw1r",
601            corners: &[
602                "FF_1p8V_25C",
603                "SS_1p8V_25C",
604                "TT_1p7V_25C",
605                "TT_1p8V_0C",
606                "TT_1p8V_25C",
607                "TT_1p8V_100C",
608                "TT_1p9V_25C",
609            ],
610        },
611        SramMacro {
612            name: "sky130_sram_1kbyte_1rw1r_32x256_8",
613            words: 256,
614            bits: 32,
615            width_um: 479.78,
616            height_um: 397.50,
617            ports: "1rw1r",
618            corners: &["TT_1p8V_25C"],
619        },
620        SramMacro {
621            name: "sky130_sram_1kbyte_1rw1r_8x1024_8",
622            words: 1024,
623            bits: 8,
624            width_um: 455.30,
625            height_um: 446.46,
626            ports: "1rw1r",
627            corners: &["TT_1p8V_25C"],
628        },
629    ],
630    precheck_checks: 19,
631    estimator_layer_rc: Some(SKY130A_LAYER_RC),
632    provenance:
633        "open_pdks 0fe599b2afb6708d281543108caf8310912f54af via ciel; macro sizes read from \
634                 each macro's own LEF SIZE line under ~/.ciel/sky130A/libs.ref/sky130_sram_macros; \
635                 user area is the Caravel fixed 2920x3520 um; precheck count measured from a real \
636                 mpw_precheck run (2026-09-17, 19/19 passed)",
637};
638
639/// GlobalFoundries 180 nm MCU. The variant this project's vendored OpenLane supports is C.
640///
641/// Note the two asymmetries against sky130 that matter for planning: the SRAM IP is far smaller
642/// and only 8 bits wide, and `mpw_precheck` schedules SIX FEWER checks — so a passing gf180
643/// submission is a weaker statement than a passing sky130 one, and should never be reported as
644/// the same thing.
645pub const GF180MCUC: PdkTarget = PdkTarget {
646    name: "gf180mcuC",
647    node_nm: 180,
648    std_cell_library: "gf180mcu_fd_sc_mcu7t5v0",
649    metal_stack: &["Metal1", "Metal2", "Metal3", "Metal4", "Metal5"],
650    local_interconnect: None,
651    top_routing_layer: "Metal4",
652    clk_driving_cell: None,
653    golden_drc_deck: "gf180mcuC_mr.drc",
654    user_area: Some(UserArea {
655        width_um: 3000.0,
656        height_um: 3000.0,
657        macro_power: None,
658        macro_max_layer: None,
659        fed_macro_max_layer: None,
660        harness: None,
661    }),
662    sram_macros: &[
663        SramMacro {
664            name: "gf180mcu_fd_ip_sram__sram512x8m8wm1",
665            words: 512,
666            bits: 8,
667            width_um: 0.0,
668            height_um: 0.0,
669            ports: "1rw",
670            corners: &[],
671        },
672        SramMacro {
673            name: "gf180mcu_fd_ip_sram__sram256x8m8wm1",
674            words: 256,
675            bits: 8,
676            width_um: 0.0,
677            height_um: 0.0,
678            ports: "1rw",
679            corners: &[],
680        },
681    ],
682    precheck_checks: 13,
683    estimator_layer_rc: None,
684    provenance: "PDK NOT YET INSTALLED — `ciel ls --pdk-family gf180mcu` is empty; install with \
685                 `cd tools/OpenLane && make pdk PDK=gf180mcuC`. Library/stack/die/deck names are \
686                 read from the vendored tools (tools/caravel_user_project/openlane/\
687                 user_project_wrapper/config.json pdk::gf180mcuC block; \
688                 tools/mpw_precheck/checks/tech-files/gf180mcuC_mr.drc; the caravel-gf180mcu \
689                 harness is a DIFFERENT repo from tools/caravel). SRAM macro names come from \
690                 tools/mpw_precheck lvs_config files; their SIZE and corners are UNMEASURED (zeroed \
691                 here on purpose) and must be filled from the installed PDK's LEF before any \
692                 gf180 memory plan is trusted.",
693};