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
- CMOS Backend for how designs are sized.
- Benchmarks for the benchmark suite and results.
- CLI Reference for all flags.
- Logging to see per-iteration characterization/sizing-sweep detail.
- API Reference for the internal module structure.
Prerequisites
- A Rust toolchain (install via rustup)
ngspice, plus its shared-library development package, sincemacro_gendrives 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) usespkg-configto locatengspice.pc; if it’s missing,cargo buildfails immediately with an explanatory message rather than a cryptic linker error.
- Debian/Ubuntu:
- 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
| Flag | Effect |
|---|---|
--emit-verilog | Also write the sized netlist as structural Verilog. |
--add-buffer | Buffer every output to the optimal number of stages. Same as --add-buffer invertible. |
--add-buffer non-invertible | Buffer 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
- Characterize. ngspice sizes the reference inverter for the target
switching threshold. Its
Wn,Wpandγ = Wp/Wndefine the units: capacitance inC_inv(the inverter’s input capacitance) and drive in multiples of the inverter’s strength. - Import. The
hw+combMLIR becomes a hypergraph netlist.comb.andandcomb.orbecome chains of AND2 and OR2 gates; NOT, which CIRCT writes ascomb.xor %x, %true, becomes an inverter. - Clean up.
dead-logic-eliminationremoves unused gates.gate-simplifyfolds 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. - 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) | cgets an output inverter (AOI21 + INV).
- Size (
logical-effort-sizing). Each stack is sized to drive like the reference inverter, which fixes each input’s logical effortgand the stage’s parasitic delayp(NAND2:g = (2+γ)/(1+γ)). Every stage then runs at one stage effortf: working from the outputs back, a stage with loadC_outgets drives = C_out / f, and each input presentsg · s.fis solved so the heaviest primary input presents exactlycin_cinv. On a single path this is the textbookf = F^(1/N); with fanout, branching is accounted for automatically. - Buffer (optional). For each output, the path effort
F = f^Ngives the optimal stage countround(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 asmacro_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
| Quantity | Unit | Meaning |
|---|---|---|
| Width, length | µm | Physical transistor dimensions. |
| Capacitance | C_inv | Input 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 | × inverter | A drive of 1 conducts like the reference inverter. |
| Delay | τ | Logical effort delay unit. An FO4 inverter is 5 τ. |
| Effort | none | g, 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,
gandp, input and output capacitance, drive, and NMOS and PMOS widths in µm.*marks a width clamped up tomin_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 delayPand delayN·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
trueare 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.andandcomb.orbecome linear chains, and AND-OR logic is not remapped to NAND-NAND. - Transistors narrower than
min_widthare clamped up with a warning, which makes those stages slower than planned. Raisecin_cinvto 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
| Benchmark | What it exercises |
|---|---|
inv | A single inverter driving the full load: the basic buffering case. |
nand2 | AND followed by NOT, folded back into one NAND2 stage. |
c17 | ISCAS-85 c17: six NAND2s, two outputs, reconvergent fanout. |
aoi21 | Inverting complex cell, one stage. |
oai22 | Inverting complex cell with two-high stacks in both networks. |
and_or_chain | Non-inverting complex cell: AOI21 plus an inverter. |
maj3 | Majority (full adder carry) as a complex cell plus an inverter. |
mux2 | A 2:1 mux from AND, OR and NOT, mapped gate by gate. |
dec2to4 | A decoder with four outputs sharing inverted inputs. |
and8 | An 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 τ).
| Benchmark | Cells | Transistors | Logic stages | F (worst) | Added inv | Stage effort | Delay (τ) |
|---|---|---|---|---|---|---|---|
| and8 | 14 → 22 | 42 → 58 | 14 | 2.363e+13 | 8 | 9.02 → 1.55 | 147.3 → 63.2 |
| and_or_chain | 2 → 4 | 8 → 12 | 2 | 128 | 2 | 11.31 → 3.36 | 27.4 → 20.2 |
| aoi21 | 1 → 4 | 6 → 12 | 1 | 128 | 3 | 128.00 → 3.36 | 131.7 → 20.2 |
| c17 | 6 → 12 | 24 → 36 | 3 | 2977 | 6 | 14.39 → 2.98 | 49.2 → 26.9 |
| dec2to4 | 10 → 21 | 28 → 50 | 3 | 2316 | 11 | 13.23 → 3.31 | 43.7 → 26.9 |
| inv | 1 → 3 | 2 → 6 | 1 | 64 | 2 | 64.00 → 4.00 | 65.0 → 15.0 |
| maj3 | 2 → 4 | 14 → 18 | 2 | 442.5 | 2 | 21.04 → 4.59 | 50.5 → 28.8 |
| mux2 | 7 → 7 | 20 → 20 | 5 | 655.2 | 0 | 3.66 → 3.66 | 25.3 → 25.3 |
| nand2 | 1 → 3 | 4 → 8 | 1 | 81.38 | 2 | 81.38 → 4.33 | 83.4 → 17.0 |
| oai22 | 1 → 4 | 8 → 14 | 1 | 128 | 3 | 128.00 → 3.36 | 132.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
| Flag | Short | Default | Description |
|---|---|---|---|
--config <CONFIG> | -c | (required) | Path to the PDK-adapter TOML config file (see examples/130nm.toml) |
--build-dir <BUILD_DIR> | -b | ./build | Output directory. Final decks land under <build-dir>/spice/. Intermediate files (characterization decks, sweep candidates, CSV reports) land under <build-dir>/.macro_gen_project/ |
--emit-verilog | off | CIRCT 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 value | CIRCT 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 | -h | Print 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 (
circtfeature):hw+combMLIR is imported into a hypergraph netlist IR.comb.andandcomb.orbecome AND2 and OR2 chains; NOT (comb.xor %x, %true) becomes an inverter.hw.moduleattributes are kept, andmacro_gen.cell = "complex"marks a module to build as one CMOS stage per output.- A relative
[circt] mlir_pathis resolved against the config file’s directory.
- Pass manager that verifies the IR after every pass, with these passes:
dead-logic-eliminationremoves gates that drive nothing.gate-simplifyfolds an inverter into the AND, OR, NAND or NOR driving it.cmos-maplowers every gate to a static CMOS pull-up and pull-down network.logical-effort-sizingsizes every stage relative to the characterized reference inverter.buffer-insertionbuffers each output toround(log_rho F)stages.
[sizing]config section:cload_cinv,cin_cinvandstage_effort.--add-buffer [invertible|non-invertible]buffers the outputs in place.--emit-verilogalso 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_optand delay, before and after buffering. - Provenance:
--versionprints 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, andbenchmarks/run.pyto 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.mlirfile 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-dirCLI flags.- CI: build, test, and publish to crates.io on merge to
main.