Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

macro_gen is an automated digital IC macro generator. It drives ngspice in process via FFI to characterize a reference inverter against a PDK supplied configuration. With the circt feature it goes further: it reads a combinational CIRCT design, builds each gate as a CMOS pull-up and pull-down network, sizes every transistor by logical effort relative to that inverter, optionally buffers the outputs to the optimal number of stages, and writes SPICE, structural Verilog and a sizing report.

Source code: github.com/JoyenBenitto/macro_gen

New here? Getting Started is the fastest path from clone to a generated macro. CMOS Backend covers sizing a design, and Benchmarks the benchmark suite.

Getting Started

This walks through the fastest path from a clone to a generated macro, using the SKY130 example config shipped in the repo (examples/130nm.toml).

1. Clone and build

$ git clone https://github.com/JoyenBenitto/macro_gen.git
$ cd macro_gen
$ cargo build

See Prerequisites if the build fails looking for ngspice.

2. Run against the example config

$ ./target/debug/macro_gen --config ./examples/130nm.toml

examples/130nm.toml is a PDK-adapter config for the SKY130 process: it points at the NMOS/PMOS device models, the process corner, and a reference inverter’s starting geometry and sizing-sweep parameters.

3. Find the output

By default, output lands under ./build/:

  • ./build/spice/: the final generated SPICE decks.
  • ./build/.macro_gen_project/: intermediate files such as characterization decks, sizing-sweep candidates, and CSV reports.

Use --build-dir to point elsewhere; see CLI Reference.

4. Size a design

Build with the circt feature (see CMOS Backend) and run one of the benchmarks:

$ cargo build --release --features circt
$ ./target/release/macro_gen --config benchmarks/c17/c17.toml --emit-verilog --add-buffer

The log walks through every sizing step, and the netlists land under ./build/spice/ and ./build/verilog/.

Next steps

Prerequisites

  • A Rust toolchain (install via rustup)
  • ngspice, plus its shared-library development package, since macro_gen drives ngspice in-process via FFI rather than shelling out to the CLI:
    • Debian/Ubuntu: sudo apt install ngspice libngspice0-dev
    • The build script (build.rs) uses pkg-config to locate ngspice.pc; if it’s missing, cargo build fails immediately with an explanatory message rather than a cryptic linker error.
  • For the CMOS backend only: a pre-built CIRCT (with its MLIR), pointed to by CIRCT_DIR, and Python 3.11 or newer to run the benchmark suite.

Building

$ git clone https://github.com/JoyenBenitto/macro_gen.git
$ cd macro_gen
$ cargo build

For an optimized binary, build with --release instead (output lands in target/release/macro_gen):

$ cargo build --release

To also build the CMOS backend, point CIRCT_DIR at a pre-built CIRCT and enable the circt feature (see CMOS Backend):

$ export CIRCT_DIR=/path/to/circt
$ cargo build --release --features circt

Run the test suite with:

$ cargo test

and with the CIRCT gated tests included:

$ cargo test --features circt

Running

$ ./target/debug/macro_gen --config ./examples/130nm.toml

Example with a custom build directory:

$ ./target/debug/macro_gen --config ./examples/130nm.toml --build-dir /tmp/my_run

With the circt feature, a config with [circt] and [sizing] sections also sizes a design. It always writes a SPICE netlist and a report, and optionally structural Verilog and a buffered netlist:

$ ./target/release/macro_gen --config benchmarks/c17/c17.toml --emit-verilog --add-buffer

See CMOS Backend for details, Benchmarks for running the whole suite, and CLI Reference for all flags.

CMOS Backend

With the circt feature, macro_gen takes a combinational CIRCT design and produces sized transistor-level CMOS: a SPICE netlist, structural Verilog and a sizing report. Every transistor is sized with the method of logical effort, relative to the reference inverter macro_gen characterizes for your PDK.

Building

The backend needs a pre-built CIRCT, pointed to by CIRCT_DIR (an install prefix, or a CIRCT checkout built with ninja under build/). If MLIR lives in a separate prefix, also set MLIR_DIR.

$ export CIRCT_DIR=/path/to/circt
$ cargo build --release --features circt

Running

A CIRCT run is driven entirely by its config. On top of the usual [environment], [models] and [reference_inverter] sections it has a [sizing] section and a [circt] section naming the design:

# Capacitances are in units of C_inv, the reference inverter's input capacitance.
[sizing]
cload_cinv = 64.0  # load on each module output port
cin_cinv = 1.0     # largest capacitance any input may present (default 1)
stage_effort = 4.0 # buffering targets round(log4 F) stages (default 4)

# mlir_path is relative to this file.
[circt]
mlir_path = "c17.mlir"
top_module = "c17"
$ ./target/release/macro_gen --config benchmarks/c17/c17.toml --emit-verilog --add-buffer
FlagEffect
--emit-verilogAlso write the sized netlist as structural Verilog.
--add-bufferBuffer every output to the optimal number of stages. Same as --add-buffer invertible.
--add-buffer non-invertibleBuffer with inverters in pairs so every output keeps its polarity.

Buffering changes the design in place: the SPICE, Verilog and report describe the buffered design, under the same file and module names as an unbuffered run.

The backend flags need a [circt] section; without one macro_gen stops with an error before characterizing anything.

How a design is sized

  1. Characterize. ngspice sizes the reference inverter for the target switching threshold. Its Wn, Wp and γ = Wp/Wn define the units: capacitance in C_inv (the inverter’s input capacitance) and drive in multiples of the inverter’s strength.
  2. Import. The hw + comb MLIR becomes a hypergraph netlist. comb.and and comb.or become chains of AND2 and OR2 gates; NOT, which CIRCT writes as comb.xor %x, %true, becomes an inverter.
  3. Clean up. dead-logic-elimination removes unused gates. gate-simplify folds an inverter into the AND, OR, NAND or NOR that drives it when it is that gate’s only load, so ~(a & b) is one NAND2.
  4. Map to CMOS (cmos-map). Every gate becomes one static CMOS stage: an NMOS pull-down network and its dual PMOS pull-up network.
    • INV, NAND2 and NOR2 map directly.
    • AND2 and OR2 become NAND2 or NOR2 followed by an inverter.
    • A module tagged macro_gen.cell = "complex" collapses into one stage per output. Its logic is rewritten in negation normal form; the pull-down implements the complement, AND as series and OR as parallel. A non-inverting function such as (a & b) | c gets an output inverter (AOI21 + INV).
  5. Size (logical-effort-sizing). Each stack is sized to drive like the reference inverter, which fixes each input’s logical effort g and the stage’s parasitic delay p (NAND2: g = (2+γ)/(1+γ)). Every stage then runs at one stage effort f: working from the outputs back, a stage with load C_out gets drive s = C_out / f, and each input presents g · s. f is solved so the heaviest primary input presents exactly cin_cinv. On a single path this is the textbook f = F^(1/N); with fanout, branching is accounted for automatically.
  6. Buffer (optional). For each output, the path effort F = f^N gives the optimal stage count round(log_ρ F), with ρ = stage_effort. Inverters make up the difference between the driving stage and the port, then sizing runs again. In invertible mode an odd count inverts the output, which is logged and recorded as macro_gen.inverted.<port>. In non-invertible mode an odd shortfall becomes whichever neighbouring even count is faster.

For and_or_chain with a 64 C_inv load: F = g · H = 2 × 64 = 128. The two logic stages alone run at f = 11.3. log4(128) rounds to 4, so two inverters are added and f drops to 3.36.

Outputs

<build-dir>/
  spice/
    inv.spice        reference inverter
    <top>.spice      sized netlist (always)
  verilog/
    <top>.v          structural netlist (--emit-verilog)
    <top>_cells.v    cell master declarations (--emit-verilog)
  reports/
    <top>.toml       sizing and delay report (always)

SPICE. One .subckt per distinct stage and drive, named <stage>_x<drive> (for example nand2_x4p45), plus a .subckt <top> connecting them. Supplies are explicit VDD and GND pins, and the PDK models are pulled in with .lib, so a testbench can .include the file directly. Widths are relative width × drive × Wn (or Wp), clamped to min_width and snapped to it when w_is_multiple_of_w_min is set.

Verilog. Structural and PD clean: port and wire declarations and cell instances with named connections only. No assign, no behavioural code, no escaped identifiers. Cell masters carry the same names as the SPICE subcircuits so the two line up for LVS, and *_cells.v declares them as (* blackbox *) modules.

Report. reports/<top>.toml holds the generator version, a [units] table, the reference inverter (µm, γ and C_inv in fF), the sizing targets in both C_inv and fF, cell and transistor counts, stage effort and worst delay (τ and FO4) before and after buffering, and each output’s logic stages, path effort, added inverters and delay. Every key carries its unit as a suffix (_um, _ff, _cinv, _tau, _fo4); efforts and γ are dimensionless.

Every generated file is stamped with the macro_gen version, commit and time.

Units

QuantityUnitMeaning
Width, lengthµmPhysical transistor dimensions.
CapacitanceC_invInput capacitance of the reference inverter. Its physical value in fF is estimated from the gate capacitance (cgg) ngspice extracts during characterization, scaled to the inverter’s Wn and Wp.
Drive× inverterA drive of 1 conducts like the reference inverter.
DelayτLogical effort delay unit. An FO4 inverter is 5 τ.
Effortnoneg, h, f, F, G, B, H and γ are ratios.

Delays are not yet converted to picoseconds; that needs a simulated FO4 inverter to calibrate τ.

Sizing log

At the default info level the run log shows the complete calculation:

  • Sizing assumptions: process, reference inverter, γ, units, output load, input limit, ρ, buffering mode, minimum width and delay model.
  • Sized cells: for every cell in signal order, g and p, input and output capacitance, drive, and NMOS and PMOS widths in µm. * marks a width clamped up to min_width.
  • Output paths: for every output, its critical path, N, G, B, H, F = G·B·H, f, N_opt = log_ρ F, inverters added, parasitic delay P and delay N·f + P.
  • Summary: stage effort, cells, transistors and worst delay (in τ and FO4) before and after buffering.

The cell and path tables are printed for the unbuffered and the buffered netlist.

Limitations

  • XOR2, XNOR2 and MUX2 are rejected: they are not unate and have no single stage static CMOS mapping. Constants other than NOT’s true are rejected.
  • Hierarchy (hw.instance) is rejected; only the top module is sized.
  • A complex cell may use only AND, OR, NAND, NOR and NOT, with its inputs all uninverted or all inverted.
  • Gates are mapped one at a time. Wide comb.and and comb.or become linear chains, and AND-OR logic is not remapped to NAND-NAND.
  • Transistors narrower than min_width are clamped up with a warning, which makes those stages slower than planned. Raise cin_cinv to avoid it.
  • Buffering chooses its stage count from the unbuffered path effort; after resizing, the path effort can come out lower than planned.

Benchmarks

The benchmark suite measures the CMOS backend on a set of small combinational designs. Each benchmark is a directory with the design and its own config:

benchmarks/
  run.py
  c17/
    c17.mlir
    c17.toml
  maj3/
    maj3.mlir
    maj3.toml
  ...

Every config is complete, so any benchmark runs on its own:

$ ./target/release/macro_gen --config benchmarks/c17/c17.toml --emit-verilog --add-buffer

All configs target SKY130 (tt, 1.8 V) with a load of 64 C_inv per output and inputs limited to 1 C_inv. Edit include_path to point at your SKY130 install.

Designs

BenchmarkWhat it exercises
invA single inverter driving the full load: the basic buffering case.
nand2AND followed by NOT, folded back into one NAND2 stage.
c17ISCAS-85 c17: six NAND2s, two outputs, reconvergent fanout.
aoi21Inverting complex cell, one stage.
oai22Inverting complex cell with two-high stacks in both networks.
and_or_chainNon-inverting complex cell: AOI21 plus an inverter.
maj3Majority (full adder carry) as a complex cell plus an inverter.
mux2A 2:1 mux from AND, OR and NOT, mapped gate by gate.
dec2to4A decoder with four outputs sharing inverted inputs.
and8An 8 input AND, imported as a chain of AND2 gates.

Running the suite

$ export CIRCT_DIR=/path/to/circt
$ benchmarks/run.py                          # every benchmark
$ benchmarks/run.py c17 maj3                 # a subset
$ benchmarks/run.py --mode non-invertible    # keep output polarity

The runner builds macro_gen (--release --features circt) and runs every config with --emit-verilog --add-buffer <mode>, several at a time (-j). Each run characterizes the reference inverter in ngspice, which takes about 40 seconds. It then reads each run’s report and prints a summary table, which it also writes to benchmarks/build/summary.md. Each benchmark’s netlists, report and full log land in benchmarks/build/<name>/. The runner exits with an error if any benchmark fails.

Results

SKY130 tt, γ = 2.68, --add-buffer invertible. Arrows read unbuffered → buffered; delay is the logical effort estimate of the worst output in τ (an FO4 inverter is 5 τ).

BenchmarkCellsTransistorsLogic stagesF (worst)Added invStage effortDelay (τ)
and814 → 2242 → 58142.363e+1389.02 → 1.55147.3 → 63.2
and_or_chain2 → 48 → 122128211.31 → 3.3627.4 → 20.2
aoi211 → 46 → 1211283128.00 → 3.36131.7 → 20.2
c176 → 1224 → 3632977614.39 → 2.9849.2 → 26.9
dec2to410 → 2128 → 50323161113.23 → 3.3143.7 → 26.9
inv1 → 32 → 6164264.00 → 4.0065.0 → 15.0
maj32 → 414 → 182442.5221.04 → 4.5950.5 → 28.8
mux27 → 720 → 205655.203.66 → 3.6625.3 → 25.3
nand21 → 34 → 8181.38281.38 → 4.3383.4 → 17.0
oai221 → 48 → 1411283128.00 → 3.36132.0 → 20.5

Buffering brings the lone inverter from 65 τ to 15 τ (three stages at f = 4) and c17 from 49 τ to 27 τ. and8 and mux2 show the cost of mapping gate by gate (see Limitations).

Adding a benchmark

Create benchmarks/<name>/ with <name>.mlir, whose top hw.module is <name>, and a <name>.toml config (copy one from another benchmark and change its [circt] section and header comment). The runner picks it up automatically.

CLI Reference

FlagShortDefaultDescription
--config <CONFIG>-c(required)Path to the PDK-adapter TOML config file (see examples/130nm.toml)
--build-dir <BUILD_DIR>-b./buildOutput directory. Final decks land under <build-dir>/spice/. Intermediate files (characterization decks, sweep candidates, CSV reports) land under <build-dir>/.macro_gen_project/
--emit-verilogoffCIRCT flow only. Also writes the sized netlist as structural, PD clean Verilog to <build-dir>/verilog/<top>.v, plus black-box cell declarations in <top>_cells.v
--add-buffer [MODE]off; invertible if given without a valueCIRCT flow only. Buffers every output to the delay optimal round(log_rho F) stages (rho = sizing.stage_effort, default 4). The SPICE and Verilog outputs are then the buffered design, under the same names. invertible allows any number of added inverters, so an output may come out inverted (tagged macro_gen.inverted.<port>). non-invertible adds inverters in pairs (inv + inv), so the logic function is kept
--help-hPrint help
--version-V-V prints the version. --version also prints the git commit, build time, profile, target, rustc, features and ngspice version

Logging

Logging defaults to info level, so you’ll see output without any extra setup. Control the log level with the MACROGEN_LOG environment variable (accepts error, warn, info, debug, trace):

$ MACROGEN_LOG=debug ./target/debug/macro_gen --config ./examples/130nm.toml

debug also surfaces per-iteration characterization/sizing-sweep detail that’s hidden at the default info level.

API Reference

The full rustdoc API reference, generated from the crate’s doc comments, is hosted at /api/.

Changelog

CHANGELOG

All notable changes to this project are documented in this file.

The format is based on Keep a Changelog.

[Unreleased]

[0.2.0] - 2026-09-29

Added

  • CIRCT input (circt feature): hw + comb MLIR is imported into a hypergraph netlist IR.
    • comb.and and comb.or become AND2 and OR2 chains; NOT (comb.xor %x, %true) becomes an inverter.
    • hw.module attributes are kept, and macro_gen.cell = "complex" marks a module to build as one CMOS stage per output.
    • A relative [circt] mlir_path is resolved against the config file’s directory.
  • Pass manager that verifies the IR after every pass, with these passes:
    • dead-logic-elimination removes gates that drive nothing.
    • gate-simplify folds an inverter into the AND, OR, NAND or NOR driving it.
    • cmos-map lowers every gate to a static CMOS pull-up and pull-down network.
    • logical-effort-sizing sizes every stage relative to the characterized reference inverter.
    • buffer-insertion buffers each output to round(log_rho F) stages.
  • [sizing] config section: cload_cinv, cin_cinv and stage_effort.
  • --add-buffer [invertible|non-invertible] buffers the outputs in place.
  • --emit-verilog also writes the sized netlist as structural, PD clean Verilog, with black-box cell declarations.
  • Outputs: a sized SPICE netlist (spice/<top>.spice) and a TOML report (reports/<top>.toml) on every CIRCT run.
  • Units: the report has a [units] table and unit suffixed keys. C_inv is estimated in fF from ngspice gate capacitance, so capacitances appear in both C_inv and fF.
  • Sizing tables in the run log: assumptions, every cell’s logical effort, capacitances, drive and transistor widths, and every output’s critical path with G, B, H, F, f, N_opt and delay, before and after buffering.
  • Provenance: --version prints the commit, build time, toolchain, features and ngspice version; the startup banner adds host, OS, CPU, memory, command line and working directory; generated files are stamped with the generator version and time.
  • Benchmark suite: ten designs in benchmarks/<name>/, each with its own config, and benchmarks/run.py to run them all and tabulate the results.
  • Docs pages: CMOS Backend and Benchmarks.

Changed

  • The CIRCT flow characterizes the reference inverter with ngspice before sizing.
  • Clear errors when backend flags are given without a [circt] section, or when an .mlir file is passed to --config.

[0.1.0]

Added

  • Reference inverter synthesis: sizes the PMOS against a target switching threshold, using device parameters extracted from ngspice.
  • --config / --build-dir CLI flags.
  • CI: build, test, and publish to crates.io on merge to main.