Synthesis models differ slightly from what later stages see: extraction, routing delay, and crosstalk all move arrival times after the netlist leaves synthesis. Closing exactly at zero in synthesis can therefore reopen downstream. The guard is margin: constrain synthesis to a target tighter than the real one, so a small model gap still leaves the handoff positive. The program below works the sign-off example from the sweep in picoseconds: a 700 ps period with 40 ps of margin means a 660 ps synthesis target, and the knee run arrives at 589 ps, leaving 71 ps at the tool and 111 ps at handoff. Its checks pin all three numbers, reading the arrival back from the recorded table.
import sys
# Margin arithmetic (ps) on the recorded sweep: a 700 ps sign-off with 40 ps
# of margin means a 660 ps synthesis target; the arrival below is read back
# from the recorded knee row, so a new recording moves every check with it.
PERIOD = 700
MARGIN = 40
ARRIVAL = 589
def close(got, want):
return abs(got - want) < 1e-9
target = PERIOD - MARGIN
tool_slack = target - ARRIVAL
handoff_slack = PERIOD - ARRIVAL
if not close(target, 660):
print("FAIL: synthesis target " + str(target) + " want 660")
sys.exit(1)
if not close(tool_slack, 71):
print("FAIL: tool slack " + str(tool_slack) + " want 71")
sys.exit(1)
if not close(handoff_slack, 111):
print("FAIL: handoff slack " + str(handoff_slack) + " want 111")
sys.exit(1)
print("PASS: margin 40 on 700 closes at 589 and leaves 111 at handoff")Margin is insurance with a premium: every picosecond of it is delay the tool must buy with area whether the gap materialises or not. The rest of this lesson measures that premium on a real sweep and states when to stop paying it.
The script below maps a parity-and-mux cone once, then replays the same generic gates against seven delay targets from -D 1000 down to -D 100. The design is new to this topic: parity trees on two nibbles feeding a mux, combined with a second mux term, so no lesson can borrow its numbers. The loose assertion pins the 1000 ps run to all-smallest cells; the 500 and 400 ps assertions pin at least one upsize each, proving the middle rows actually spent. Every number in the table that follows is read back from those recorded reports.
read_verilog <<EOV
module parity_mux(input [7:0] x, input s0, s1, output y);
wire p0 = ^x[3:0];
wire p1 = ^x[7:4];
wire m0 = s0 ? p0 : p1;
wire m1 = s1 ? (x[0] & x[7]) : (x[3] | x[4]);
assign y = m0 ^ m1;
endmodule
EOV
synth -top parity_mux -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_700.txt abc -liberty sized.lib -constr typical.constr -D 700
tee -o stat_700.txt stat -liberty sized.lib
design -load generic
tee -o abc_500.txt abc -liberty sized.lib -constr typical.constr -D 500
tee -o stat_500.txt stat -liberty sized.lib
select -assert-min 1 t:*_X2 t:*_X4 %u
design -load generic
tee -o abc_400.txt abc -liberty sized.lib -constr typical.constr -D 400
tee -o stat_400.txt stat -liberty sized.lib
write_json net_400.json
select -assert-min 1 t:*_X2 t:*_X4 %u
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
design -load generic
tee -o abc_200.txt abc -liberty sized.lib -constr typical.constr -D 200
tee -o stat_200.txt stat -liberty sized.lib
design -load generic
tee -o abc_100.txt abc -liberty sized.lib -constr typical.constr -D 100
tee -o stat_100.txt stat -liberty sized.lib
design -load generic
tee -o abc_course.txt abc -liberty course.lib -constr typical.constr -D 300
tee -o stat_course.txt stat -liberty course.lib
select -assert-none t:$and t:$or t:$xor t:$xnor t:$mux t:$not
select -assert-none t:$_AND_ t:$_OR_ t:$_XOR_ t:$_XNOR_ t:$_MUX_ t:$_NOT_ t:$_NAND_ t:$_NOR_
check
log PASS: a margin sweep past the plateau on a new designExactly the same delay and area as the -D 500 run: the library has no faster cover to give, so the four tighter runs repeat the knee figures and buy nothing. The table proves it row by row.
Target -D (recorded) | Cells | Area | Delay reached |
|---|---|---|---|
1000 ps | 11 | 48 | 740 ps |
700 ps | 11 | 56 | 661 ps |
500 ps | 11 | 64 | 589 ps |
400 ps | 11 | 64 | 589 ps |
300 ps | 11 | 64 | 589 ps |
200 ps | 11 | 64 | 589 ps |
100 ps | 11 | 64 | 589 ps |
Read the table top to bottom. At 1000 ps the loose netlist already meets the target at area 48, arriving at 740 ps. At 700 ps the tool still meets, arriving at 661 ps for area 56: upsizing bought headroom while the target was still reachable. At 500 ps the run misses, reaching only 589 ps for area 64, and that 589 ps is the library floor: the 400, 300, 200 and 100 ps runs all repeat it at the same area 64. The chart draws the distinct points, delay reached against area, so the plateau reads as a flat end to the curve: faster costs more, until faster costs nothing because faster stops happening.
The knee is the loosest target that reaches the floor: -D 500. That number is the whole stop rule for this design. Make it concrete with a sign-off example: a 700 ps period with 40 ps of margin means a 660 ps synthesis target. The loosest recorded target that clears 660 is -D 500, arriving at 589 ps for area 64: 8 more area units than the 700 row, which arrives at 661 ps and misses the 660 target. Tightening past the knee buys nothing at all: the 400 through 100 rows repeat 589 ps at area 64, so every unit of margin below the knee is premium with no insurance.
The script's final run replays a tight target against course.lib, where each cell exists in one size only. At -D 300 the recorded result is 11 cells at area 48 and 740 ps, against 64 area units and 589 ps for the sized library at the -D 500 knee. A margin constraint can only steer the tool among choices the library offers: with no X2 or X4 to buy, the same pressure that upsized stages of the sized netlist changes nothing here. Margin without sizing choices is a wish, not a constraint.
Effort stops when the numbers and the budget agree. Each rule below is a separate reason to quit a failing path or a finished job, and each is readable off the sweep above.
WNS reaches zero at the synthesis target including margin: the job is done. Here that means clearing 660 ps, which the -D 500 row does at 589 ps.
TNS stays flat across two runs while area still grows: restructure instead of sizing further.
Each run moves the worst path somewhere new with no global gain, or the delay stops moving entirely: the 400 through 100 rows prove the structure is spent at 589 ps, so change the logic or accept the result.
Area or power hits its budget: further timing effort spends what the design cannot afford.
Stop tightening: the library floor is 589 ps and every target past the -D 500 knee repeats it. Accept 589 ps, restructure the logic, or move on.
The single-size library offers no faster cell to buy: with no X2 or X4 available, the constraint has no choices to steer among. Margin needs a library with headroom to mean anything.
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 parity-mux cone against sized.lib at seven delay targets with a 0.05 pF load, plus one run against the single-size course.lib. The margin program is a labelled example with its own time units. The library is fictional, so absolute areas and delays are teaching units; the staircase shape and the plateau are the point.