Volume 02 Advanced 5 sub-modules ~55 min read

Logic Synthesis with Yosys

"Synthesis" is one word for four quite different transformations: elaborating the language, inferring structure, optimising Boolean logic, and covering that logic with real cells from a real library. Yosys exposes each of them as a separate command, which makes it the best teaching tool in the industry - you can stop after any pass and look at what it did.

2.1 The synthesis pipeline, pass by pass

Commercial tools hide this behind a single compile. Yosys does not, and running the passes by hand once is worth more than any amount of reading.

Yosys pass sequence from reading Verilog through proc, fsm, memory, techmap, dfflibmap and abc to a mapped standard cell netlist RTL → GENERIC CELLS → STANDARD CELLS read_verilog parse only hierarchy -top params, missing modules proc always → mux + dff fsm / memory recognise structure GENERIC $and $mux $dff no library yet portable, un-timed techmap $add → gates dfflibmap flops FIRST - abc cannot abc -liberty AIG + cover with cells write_verilog sky130 cells only opt runs between every pass, always opt; opt; opt; everything left of the purple box is library-independent - you can retarget without redoing it
Figure 2.1 - The pass order. The generic netlist in the middle is the pivot: portable, un-timed, and the last point at which your design is not yet committed to a particular foundry.

# A synthesis script written out by hand. OpenLane runs the equivalent,
# but reading it once removes all the mystery.

read_verilog -sv rtl/counter.sv

# Elaborate. -check errors on a module that was never defined, which is
# the difference between a black box and a typo.
hierarchy -check -top counter

# always blocks become multiplexers and generic flip-flops.
proc
opt

# Recognise higher-level structure BEFORE flattening it into gates.
fsm                   ;# extract and re-encode state machines
opt
memory                ;# arrays become $mem cells
opt

# Generic arithmetic ($add, $mul) into generic gates.
techmap
opt

# Map flip-flops to real library cells. This MUST precede abc, because
# abc is a combinational engine and does not understand state.
dfflibmap -liberty $::env(LIB_TYPICAL)

# Combinational optimisation + technology mapping.
#   -D is a delay target in PICOseconds. Omit it and you get an
#   area-optimised netlist that will not meet timing.
abc -liberty $::env(LIB_TYPICAL) -D 10000

# Housekeeping that matters more than it looks.
setundef -zero        ;# no x in the netlist - x is not a value in silicon
splitnets -ports      ;# one net per bit, so LVS can match names
opt_clean -purge      ;# delete everything nothing drives

# What did we get?
stat -liberty $::env(LIB_TYPICAL)
check -assert         ;# fail the run on a real structural problem

write_verilog -noattr results/counter.synth.v
Two ordering rules that are not negotiable fsm and memory run before techmap. Once a state machine has been flattened into anonymous gates, no pass can recover it, so the chance to re-encode one-hot is gone. dfflibmap runs before abc. ABC is purely combinational; if flops are still generic when it runs, it will either fail or leave them unmapped, and you will find $_DFF_P_ cells in a netlist that was supposed to contain only Sky130 cells.

2.2 Technology mapping with ABC

ABC is where your Boolean logic meets the library. It works in three steps, and the middle one is the interesting part.

  1. Convert to an AIG. Every gate is rewritten as AND gates and inverters - an and-inverter graph. A NAND becomes an AND plus an inverter, an XOR becomes a small AND/inverter subgraph. This canonical form has exactly two node types, which makes the optimisation algorithms tractable.
  2. Restructure the graph. Rewriting, refactoring and balancing move logic around to reduce either depth (for speed) or node count (for area). This is where a deeply nested expression gets flattened into a balanced tree.
  3. Cover it with real cells. Find a set of library cells whose functions, stitched together, implement the graph - at minimum cost, where cost is delay, area or a weighted blend depending on what you asked for.

Step three is why the library matters so much. A library with a rich set of complex cells - AOI, OAI, multiplexers, full adders - gives ABC bigger pieces to cover with, and bigger pieces mean fewer levels:


// Y = !((A & B) | C)
assign Y = ~((A & B) | C);

// The obvious mapping:
//   and2_1  ->  nor2_1
//   two cells, two levels,
//   ~10.0 um2, ~180 ps

// One AND-OR-INVERT cell computes
// exactly this function.
sky130_fd_sc_hd__a21oi_1 u (
  .A1(A), .A2(B), .B1(C), .Y(Y)
);

// one cell, ONE level,
// ~6.3 um2, ~95 ps
//
// Complex cells are not a
// micro-optimisation. Across a
// large design they are 20-30%
// of both area and depth.
The delay target is the steering wheel abc -D 10000 asks for a 10 ns critical path (the unit is picoseconds). ABC will spend area to reach it - bigger drive strengths, more balanced trees, duplicated logic. Ask for 1 ns on the same design and it will spend a great deal more area and still miss. Ask for nothing and it optimises area alone. There is no "just make it good" setting; you are always choosing a point on a curve, and -D is how you choose.

2.3 Drive strength, fanout and buffering

Choosing which cell implements a function is only half of mapping. The other half is choosing how big that cell should be - and this is the decision most directly connected to the Liberty tables from §1.3.

A inv_1 and an inv_8 compute the same function. The inv_8 has roughly eight times the transistor width, so it drives a large load far faster - but it also presents eight times the input capacitance to whatever drives it, and occupies eight times the area. Upsizing a cell moves the problem upstream.

One inverter driving eight loads compared with a buffer tree, showing how splitting the load reduces total delay despite adding a logic level MORE LEVELS CAN BE FASTER inv_1 driving 8 loads inv_1 8 x 0.0025 pF = 0.020 pF load delay ~ 0.42 ns out slew ~ 0.55 ns and that slew slows every load too inv_1 → 2 x buf_4 → 8 loads inv_1 buf_4 buf_4 4 loads each delay ~ 0.11 + 0.14 = 0.25 ns out slew ~ 0.09 ns one more level, 40% less delay, clean slew
Figure 2.3 - Buffering is not padding. Splitting a heavy load across a small tree reduces total delay and restores the output slew, which then speeds up everything downstream. Illustrative Sky130 numbers.

Two Liberty attributes turn this from an aesthetic preference into a hard rule that synthesis must satisfy - the design rule violations, or DRVs:

Limit Meaning What happens if violated
max_capacitance The largest load this pin may drive Delay leaves the characterised table; the timing number is extrapolation
max_transition The slowest edge allowed on this net Same, plus real crowbar current in the receiving gate
max_fanout A count-based proxy for the above Cruder, but catches problems before load is known
A slow edge burns power, not just time While an input sits between the two rails, both the PMOS and NMOS of the receiving gate conduct at once, and current flows straight from VDD to ground doing no useful work. A design full of max_transition violations is not merely slow; it dissipates measurably more power and, in the worst cases, ages faster. Fix DRVs before you look at slack - they distort every timing number in the report anyway.

2.4 Reading the synthesis report honestly

stat is the first real feedback you get about your RTL. It is also routinely misread, because the number everybody looks at is the least informative one.


=== counter ===

   Number of wires:                 1476
   Number of cells:                 1204
     sky130_fd_sc_hd__a21oi_1         92
     sky130_fd_sc_hd__and2_0          61
     sky130_fd_sc_hd__buf_1           74
     sky130_fd_sc_hd__buf_2           38
     sky130_fd_sc_hd__clkbuf_1        16
     sky130_fd_sc_hd__dfrtp_1        312     <-- flip-flops with reset
     sky130_fd_sc_hd__dfxtp_1         48     <-- flip-flops without
     sky130_fd_sc_hd__dlxtp_1          6     <-- LATCHES. investigate.
     sky130_fd_sc_hd__inv_1          143
     sky130_fd_sc_hd__mux2_1          97
     sky130_fd_sc_hd__nand2_1        188
     sky130_fd_sc_hd__nor2_1         112
     sky130_fd_sc_hd__o21ai_1         17

   Chip area for module '\counter': 10847.334400
Line What it really tells you
Chip area The number people quote, and the least actionable. It is core cell area only - no routing, no power grid, no filler. Real die area is roughly this divided by your target utilisation (see Volume 03).
Flip-flop count The honest size metric. Flops come directly from your RTL; combinational cell counts move with every optimisation setting. If flop count surprises you, your RTL is not what you think it is.
Any dlxtp / dlatch Inferred latches. Almost always a bug - an incomplete always @(*). Track every one down. This is the same failure mode as Course 01, §1.5, now visible as real cells.
Buffer and inverter share A design that is 25% buffers is fighting fanout somewhere. Look for a reset or enable driving thousands of loads.
Ratio of complex to simple cells Lots of a21oi, o21ai, a22o means ABC found good coverings. Almost all nand2 and inv can mean your library is being under-used.
Post-synthesis timing is a lower bound, and it always moves No cell has been placed, so there are no wires - synthesis assumes zero interconnect capacitance, or applies a crude fanout-based wire load model. On 130 nm interconnect is already significant; on a modern node it dominates. Expect your critical path to get worse after placement and worse again after routing. A design that closes with 2% margin at synthesis has not closed. Volume 05 quantifies exactly where the extra delay comes from.

Run check alongside stat. It finds structural problems that stat cannot show: multiply-driven wires, combinational loops, cells with unconnected inputs floating. With -assert it fails the run, which is what you want in CI.

2.5 Constraints before synthesis

Synthesis without an SDC file produces a valid netlist that will never meet timing. With no clock defined there is no target, so the optimiser minimises area: minimum-drive cells, deep unbalanced logic, no buffering. It is not wrong - you simply never told it what to want.


# The minimum viable SDC for synthesis. Every line here changes the
# netlist you get out.

# ---- 1. the target ---------------------------------------------------
create_clock -name clk -period 10.0 [get_ports clk]

# Pessimism the tool should assume before CTS exists: jitter, plus the
# skew the clock tree has not been built to have yet. Drop this to a
# realistic number after CTS - see Volume 04.
set_clock_uncertainty 0.25 [get_clocks clk]
set_clock_transition  0.15 [get_clocks clk]

# ---- 2. the boundary -------------------------------------------------
# Without these, synthesis treats every I/O path as having a full cycle,
# which is almost never true and hides real failures.
set_input_delay  -clock clk 3.0 [remove_from_collection [all_inputs] [get_ports clk]]
set_output_delay -clock clk 3.0 [all_outputs]

# ---- 3. the electrical environment -----------------------------------
# What drives our inputs, and what our outputs must drive. Omitting these
# means "an ideal zero-impedance source" and "no load", which is a lie
# that shows up as a timing surprise at chip assembly.
set_driving_cell -lib_cell sky130_fd_sc_hd__inv_2 -pin Y [all_inputs]
set_load 0.05 [all_outputs]

# ---- 4. design rules -------------------------------------------------
set_max_fanout     10  [current_design]
set_max_transition 1.5 [current_design]

# ---- 5. exceptions ---------------------------------------------------
# Same discipline as the FPGA course: an exception is a claim about the
# hardware, not a way to silence a failing path.
set_false_path -from [get_ports rst_n]
Constrain slightly tighter than you need Because post-synthesis timing is optimistic, experienced teams synthesise against a period 10-20% shorter than the real target. The netlist comes out with more buffering and more balanced logic, and it survives the delay that placement and routing will add. Over-tighten - asking for 5 ns when the design can do 9 - and you get the opposite: enormous area, a cell count that will not fit the floorplan, and still no closure. The sweet spot is a target you miss by a little, not one you miss by a lot.
Interview grilling - "Synthesis met timing. Are you done?"

No, and the reasons are worth naming in order, because each one is a different stage catching a different lie:

  • No wires exist yet. Interconnect RC is unmodelled or approximated by a wire load model. Placement will add real delay; routing will add more.
  • The clock is ideal. Synthesis assumes the clock arrives everywhere simultaneously. CTS will build a real tree with real skew and real insertion delay, and it inserts hundreds of buffers that did not exist in this netlist.
  • Only one corner was analysed. Meeting setup at typical says nothing about setup at slow or hold at fast.
  • Hold was probably not checked at all. Hold violations are fixed after CTS, by inserting delay cells - they barely exist as a concept at synthesis.
  • No DRVs were verified against real loads. A net's actual capacitance is not known until it is routed.

The sentence that lands: "Synthesis timing tells me the logic depth is roughly right. It is a feasibility check, not a result. The number I would actually quote is post-route STA at the slow corner."

Volume 02 recap

Concept The one thing to remember
Pass order fsm/memory before techmap; dfflibmap before abc.
Generic netlist The last library-independent form of your design.
ABC AIG → restructure → cover with cells. Combinational only.
Complex cells AOI/OAI collapse two levels into one. 20-30% of area and depth.
Drive strength Upsizing moves the load problem upstream. Buffer trees beat one big cell.
DRVs Fix max_transition first - it distorts every other number.
stat Flop count is the honest metric. Any latch is a bug.
SDC No clock, no target. Constrain 10-20% tight.