Every design has a slowest path, even when nothing violates. This lesson builds a design where the slowest path is obvious: a chain of AND and XOR stages feeding one output, next to a single AND gate feeding another. The chain is the critical path by construction; the side gate is done in one stage. If timing-driven synthesis works as advertised, a tighter target should spend cells on the chain and leave the side gate alone. The script below tests exactly that: it synthesises the design once, saves the generic gates, then maps the same gates three times against sized.lib, where each cell exists in three sizes, plain, X2 and X4, with a bigger cell faster and larger.
read_verilog <<EOV
module critpath(input [7:0] a, input b, output y_long, output y_short);
wire c0 = a[0] & a[1];
wire c1 = c0 & a[2];
wire c2 = c1 & a[3];
wire c3 = c2 & a[4];
wire c4 = c3 & a[5];
wire c5 = c4 ^ b;
assign y_long = c5 ^ a[6];
assign y_short = a[6] & a[7];
endmodule
EOV
synth -top critpath -flatten
design -save generic
tee -o abc_1000.txt abc -liberty sized.lib -constr typical.constr -D 1000
tee -o stat_1000.txt stat -liberty sized.lib
write_json net_1000.json
select -assert-none t:*_X2 t:*_X4
design -load generic
tee -o abc_300.txt abc -liberty sized.lib -constr typical.constr -D 300
tee -o stat_300.txt stat -liberty sized.lib
write_json net_300.json
select -assert-min 1 t:*_X2 t:*_X4 %u
design -load generic
tee -o abc_heavy.txt abc -liberty sized.lib -constr heavy.constr -D 300
tee -o stat_heavy.txt stat -liberty sized.lib
write_json net_heavy.json
select -assert-min 1 t:BUF*
log PASS: a tighter target upsizes only the critical coneIn a commercial flow the target comes from the clock: asking for a 0.3 ns period is the same idea as the -D 300 run here. Both hand the mapper one number to meet. ABC also needs the load every output drives, which typical.constr gives it as a BUF driver and 0.05 pF on every output; the third run swaps in heavy.constr at 0.2 pF to change only the load. The select lines are the script's assertions: the loose run uses no upsized cell, the tight run uses at least one, and the heavy run inserts at least one buffer.
Against a 1000 ps target the mapper is already comfortable: the recorded delay is 402 ps, so ABC reports that the current delay does not exceed the target and performs no upsizing. The register of that comfort is the cell histogram: 6 cells at area 21, every one of them the smallest size. A loose constraint is a permission to stay small, and the tool takes it. There is no reason to pay for speed nobody asked for.
Run (recorded) | Cells | Area | Delay reached | Upsized cells |
|---|---|---|---|---|
-D 1000, typical load | 6 | 21 | 402 ps | 0 |
-D 300, typical load | 6 | 33.2 | 302 ps | 3 |
-D 300, heavy load | 8 | 40.4 | 386 ps | 4 |
Only the long chain feeding y_long changes: 3 cells there grow to X2 or X4 while the single AND2 feeding y_short stays exactly as it is. The side path already arrives early, so spending area on it cannot improve the worst delay.
At -D 300 the same gates map to the same 6 cells, but the area rises from 21 to 33.2 and the delay falls from 402 ps to 302 ps. The structure barely moved; the drive strength did. Splitting the tight netlist by output cone shows where every upsized cell went: all 3 upsized cells sit in the cone behind y_long, and the cone behind y_short keeps its plain AND2. That split is hand-derived from the recorded netlist file, while every total in the table is a recording.
Output cone (recorded netlist, hand-derived split) | Cells in cone | Upsized at -D 300 |
|---|---|---|
y_long (the chain) | 5 | 3 |
y_short (the side gate) | 1 | 0 |
Read the two pictures as effort maps. Orange marks where the tool spent area, and every orange cell lies on the path that was worst. The side output never turns orange because its slack was never the binding one. This is the whole lesson in one image: this ABC script does not improve the design, it improves the worst path, and the area bill names that path. That single-target behaviour is ABC's, not synthesis in general: commercial tools work on every violating path at once, TNS-driven within a critical range around the worst.
Slack is required time minus arrival time on one path, and the -D target plays the required time here. The program below recomputes the slack of the worst path before and after sizing from the recorded arrivals, rounded to whole picoseconds. Its checks pin both slacks and the direction of the move: negative slack means the path misses its target, and a worse slack after sizing would fail the run.
import sys
# Slack arithmetic (ps): required time is the -D target used above; arrivals
# are rounded from the recorded ABC reports in the table above, so a new
# recording moves every check with it.
REQUIRED = 300
ARR_LOOSE = 402
ARR_TIGHT = 302
slack_loose = REQUIRED - ARR_LOOSE
slack_tight = REQUIRED - ARR_TIGHT
if slack_loose != -102:
print("FAIL: loose slack " + str(slack_loose) + " want -102")
sys.exit(1)
if slack_tight != -2:
print("FAIL: tight slack " + str(slack_tight) + " want -2")
sys.exit(1)
if not (slack_tight > slack_loose):
print("FAIL: tightening did not improve slack")
sys.exit(1)
print("PASS: slack at a 300 ps target rises from -102 ps to -2 ps after sizing")The worst slack rises from -102 ps to -2 ps: almost closed, paid for with 12.2 extra area units. Note what the number does not say: it says nothing about y_short, whose slack was positive all along. One number per path; effort follows the smallest.
The third run in the script keeps -D 300 but swaps typical.constr for heavy.constr, quadrupling every output load to 0.2 pF. The recorded result is a different netlist: 8 cells at area 40.4 with a delay of 386 ps. The mapper answers load with buffering, inserting BUF and BUF_X4 where the lighter runs needed no buffer, with all 4 upsized cells in the y_long cone, yet the delay still rises from 302 ps to 386 ps. The load changed the problem faster than sizing could answer it: driving four times the capacitance costs area and still arrives later. A target is never the whole constraint; the load beside it decides how far sizing can reach.
y_long was critical: every upsized cell sits in its cone. Next, re-rank to confirm y_long is still worst at -2 ps: sizing narrowed its slack but left it binding, so the next spend lands on the same cone.
No. The problem changed: a 0.2 pF load needs buffering first, and the inserted buffer costs area while the heavier capacitance still arrives later. Judge sizing against a fixed load.
Every number and picture on this page comes from one Yosys 0.70 run of the script shown, recorded by the course checker (scripts/tools/run-rtl-testbenches.mjs with COURSE=synth): ABC maps a two-output AND-chain-plus-side-gate design against the teaching library sized.lib at -D 1000 and -D 300 with a 0.05 pF load, plus -D 300 with a 0.2 pF load. Cone splits are hand-derived from the recorded netlist files; the slack program recomputes required-minus-arrival from rounded recorded arrivals. The library is fictional, so absolute areas and delays are teaching units.