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};