Synthesis runs before placement, so it cannot know wire lengths. Yet every net has capacitance, and capacitance is delay. This lesson measures what the tool does about that contradiction on the shared vipexp path: it maps the same block under two output loads, watches the cover change, and prices the one wire model small enough to check by hand.
The two loads are the course constraint files. typical.constr puts a 0.05 pF load on every output; heavy.constr puts 0.2 pF there instead. Both files also name a BUF driving cell for the inputs. Those numbers are lumped pin loads, not a wire model, and saying plainly what they are, and are not, is half of this lesson.
read_verilog <<EOV
module vipexp(input clk, input a, b, c, d, input din, output reg qout, output t1, t2, t3, t4);
reg q1;
wire s1, s2, s3, s4;
always @(posedge clk) q1 <= din;
assign s1 = ~(q1 & a);
assign s2 = ~(s1 | b);
assign s3 = s2 ^ c;
assign s4 = ~(s3 ^ d);
always @(posedge clk) qout <= s4;
assign t1 = s1;
assign t2 = s2;
assign t3 = s3;
assign t4 = s4;
endmodule
EOV
hierarchy -check -top vipexp
synth -top vipexp -flatten
dfflegalize -cell $_DFF_P_ 01 -cell $_DFF_PN0_ 01
design -save generic
dfflibmap -liberty course.lib
tee -o abc_typ.txt abc -liberty course.lib -constr typical.constr -D 1000
clean
tee -o stat_typ.txt stat -liberty course.lib
write_json net_typ.json
select -assert-none t:$not t:$and t:$or t:$xor t:$mux t:$_NOT_ t:$_AND_ t:$_OR_ t:$_XOR_ t:$_NAND_ t:$_NOR_ t:$_DFF_P_ t:$_MUX_
select -assert-count 2 t:DFF
select -assert-count 1 t:NAND2
select -assert-count 1 t:NOR2
select -assert-count 1 t:XOR2
select -assert-count 1 t:XNOR2
check
design -load generic
dfflibmap -liberty course.lib
tee -o abc_heavy.txt abc -liberty course.lib -constr heavy.constr -D 1000
clean
tee -o stat_heavy.txt stat -liberty course.lib
write_json net_heavy.json
select -assert-count 2 t:DFF
select -assert-min 4 t:BUF
check
log PASS: one path under a light load and a heavy loadThe script is the familiar vipexp mapping, run twice from the saved generic netlist with only the constraint file changed. The typical run is the cover every other lesson prices: 4 cells at area 14 with arrival 610 ps. The heavy run is the break-it and the result in one: 8 cells at area 22 with arrival 1230 ps. Same RTL, same target, same library; only the load number moved.
It spends cells. The recorded heavy histogram reads BUF x4, DFF x2, NAND2 x1, NOR2 x1, XNOR2 x1, XOR2 x1: the four logic stages survive unchanged and a BUF lands between every stage and the next, each driving its tap port as well as the following stage, isolating the logic from the heavy load at a cost of 8 area units and about 620 ps of arrival. The cover change is the answer, not a side effect.
Load | Cells | Area | Arrival |
|---|---|---|---|
Typical (0.05 pF) | 4 | 14 | 610 ps |
Heavy (0.20 pF) | 8 | 22 | 1230 ps |
Read the table as cause and effect. The heavy cover holds 4 BUF cells the typical cover never needed, one between every stage and the next, and the recorded histogram confirms it: BUF x4, DFF x2, NAND2 x1, NOR2 x1, XNOR2 x1, XOR2 x1 against DFF x2, NAND2 x1, NOR2 x1, XNOR2 x1, XOR2 x1. Each BUF drives its tap load while the logic stage behind it sees only the BUF input pin, so the logic delays barely move; but the same BUF output also feeds the next stage, so the flop-to-flop spine passes through one BUF per stage and pays all four. The arrival roughly doubles, from 610 ps to 1230 ps, and the area rises by 8 units for the four buffers.
Compare this drawing with the typical-load netlist two lessons back: the logic spine is identical, and each new BUF sits between its stage and the next, driving both the tap port and the following stage. That placement is the tool telling you where it thinks the load hurts: the buffers shield the logic from the tap loads, and the spine passes through all four. ABC's delay is the maximum over all its outputs, and the recorded netlist shows the worst path ends on the net the t4 port shares with the capture flop's D input, so the 1230 ps path is the capture path. Against the 1.00 ns period from the earlier slack arithmetic, the heavy arrival plus model clock-to-Q reaches about 1.39 ns against a 0.94 ns requirement: negative slack, failing, on the same clock that met timing under the typical load.
A wire-load model assigns each net a capacitance from its fanout or from the size of the block, before any wire exists. What it adds over the lumped pin load is fanout awareness: a net driving four pins gets more capacitance than a net driving one, so net-heavy paths stop looking free. What it hides is everything placement decides: length, layer, coupling to neighbours, and detours around congestion. It is a statistical guess with the right average shape, wrong in both directions on any particular net.
ABC's constr load is simpler than even that: one fixed capacitor per output port, no fanout term, no length term. Treat it as what it is, a load the mapper must drive, never as a prediction of routed wire. The heavy run shows the honest use: sweep the load number and watch the cover respond. The dishonest use is quoting the resulting slack as signoff: post-layout extraction replaces every one of these guesses with measured nets.
import sys # stdlib only: no files, one PASS line
# Elmore delay of a wire segment, stated as a model (not a recording).
# A 0.05 pF wire is split into two RC lumps: R1, C1 near the driver and R2, C2
# near the load. Elmore: each resistor sees its downstream capacitance.
R1, C1 = 1.0, 0.025 # kohm, pF
R2, C2 = 1.0, 0.025
def elmore(r1, c1, r2, c2):
return r1 * (c1 + c2) + r2 * c2
def close(got, want, tol=1e-9):
return abs(got - want) <= tol
bad = 0
def check(name, cond):
global bad
if not cond:
print("FAIL: " + name)
bad += 1
check("single lump RC", close(elmore(1.0, 0.05, 0.0, 0.0), 0.05))
check("zero resistance is zero", close(elmore(0.0, 0.025, 0.0, 0.025), 0.0))
check("zero capacitance is zero", close(elmore(1.0, 0.0, 1.0, 0.0), 0.0))
check("two lumps add downstream", close(elmore(R1, C1, R2, C2), 0.075))
check("doubling R doubles delay", close(elmore(2 * R1, C1, 2 * R2, C2), 2 * 0.075))
check("wider split, same total", close(elmore(1.0, 0.04, 1.0, 0.01), 0.06))
if bad != 0:
sys.exit(1)
print("PASS: Elmore model of a 0.05 pF wire gives 0.075 ns in two lumps")The program prices the typical tap load as two RC lumps: 0.075 ns of model delay, on the order of one NAND2 stage. That is the scale synthesis guesses at and layout measures. The asserts pin the model's structure, not silicon: zero resistance gives zero delay, doubling resistance doubles the delay, and splitting the same capacitance differently moves the total exactly as the downstream rule says. A model that cannot fail these checks cannot be trusted with the ones that matter.
Four BUF cells appeared, one between every stage and the next: BUF x4, DFF x2, NAND2 x1, NOR2 x1, XNOR2 x1, XOR2 x1. Each buffer shields its logic stage from the 0.2 pF tap load, so the logic delays barely move; the same buffer output feeds the next stage, so the spine pays four buffer delays instead, about 620 ps of extra arrival in total.
The heavy load is still a lumped guess, not extracted wire: it proves the cover responds to load, not that any routed net matches the guess. Failing on a guess means re-examine the assumption and the cover, then close timing after layout.
Areas, arrivals, histograms and both netlists on this page come from one Yosys run of the script shown, recorded by the course checker (scripts/tools/run-rtl-testbenches.mjs with COURSE=synth): the vipexp block mapped with ABC against course.lib at a 1000 ps target with typical.constr (0.05 pF) and heavy.constr (0.2 pF). The library is fictional, so absolute delays are teaching values; the cover change is the point. The Elmore wire delay is a hand-built model computed by the stdlib-only program shown, labelled so on the page.