Every flop-to-flop path faces two checks. Setup asks whether the slowest data arrives before the next edge minus setup; hold asks whether the fastest data stays past the same edge plus hold. This lesson runs both on the shared vipexp path with numbers from the course.lib DFF tables, deriving both slacks in a checked program.
The setup and hold bounds, and the clock-to-Q that feeds both arrivals, all come from the DFF cell in course.lib: a setup and a hold table on the D pin plus a clock-to-Q table on Q. All three share the teaching template (first index related_pin_transition, the data slew, second index constrained_pin_transition, the clock slew), read at the slews the calculator lesson propagates, so every number below traces to a library row.
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
select -assert-count 2 t:DFF
select -assert-min 4 t:BUF
check
log PASS: setup and hold path mapped under two output loadsThe script maps vipexp twice, once under each load file. The assertions pin the typical cover as the last lesson did, and demand at least four buffers in the heavy cover, where ABC inserts one BUF between every stage and the next to carry the bigger tap loads. The typical stat histogram holds 6 cells including 2 flops, and the two ABC delays below are the whole experiment.
Setup gets harder by about the arrival growth; the hold requirement does not move. The typical-load arrival is the model total from the last lesson, the heavy run adds delay on top, and the setup slack falls by that addition. Hold compares two arrivals from the same edge, so the period never enters it.
Each register-to-register path starts at a launch edge and ends at a capture edge. The launch flop adds its clock-to-Q first, so both the slowest and the fastest arrival carry that term. On the shared path the model clock-to-Q is 0.163 ns, from the DFF row at the assumed 0.05 ns clock slew and the 0.01 pF pin load.
Setup is checked against the next capture edge, one period later; hold against the same edge, zero cycles later. That single difference explains every fix: anything that moves the next edge touches setup only, because the hold comparison never sees a second edge.
Check | Capture edge | Depends on the period | Typical fix |
|---|---|---|---|
Setup | Next edge, one period later | Yes: a slower clock raises the requirement. | Shorten the path or slow the clock. |
Hold | Same edge, zero cycles later | No: both sides share the edge, so the period cancels out. | Add delay on the fast path; never slow the clock. |
The setup check compares the slowest arrival against the period minus setup time. With the hand-derived 1.00 ns period and 0.058 ns of model setup, the requirement is 0.942 ns against a model arrival of 0.781 ns, for a setup slack of plus 0.16 ns. The program below asserts that arithmetic, plus the table-point and monotonicity checks behind the interpolation.
The hold check compares the fastest arrival against the hold time on the same edge. This library's rise and fall tables are identical, so the fastest arrival equals the slowest one; another library would need separate min-delay tables. Against 0.008 ns of model hold the slack is plus 0.77 ns: four stages of logic cannot race a same-edge check.
import sys
# NLDM rows copied exactly from scripts/tools/synth-libs/course.lib, DFF cell.
# Setup (D pin, rise_constraint) and hold share the teaching template: the first
# index is the input slew, the second the clock slew; both span 0.01 to 0.1.
SETUP_ROWS = 'values("0.05,0.06","0.06,0.07")'
HOLD_ROWS = 'values("0.0,0.01","0.01,0.02")'
CLOCK_SLEW = 0.05
PERIOD = 1.0
# Model arrival from the timing-calculator lesson (same path, same operating
# point): rise and fall tables coincide in this library, so min equals max.
ARRIVAL_MAX = 0.781
ARRIVAL_MIN = 0.781
def parse(rows):
inner = rows[len('values("'):-len('")')]
return [[float(x) for x in pair.split(",")] for pair in inner.split('","')]
def bilin(table, slew, load):
(v00, v01), (v10, v11) = table
fs = (slew - 0.01) / 0.09
fl = (load - 0.01) / 0.09
return v00 * (1 - fs) * (1 - fl) + v01 * (1 - fs) * fl + v10 * fs * (1 - fl) + v11 * fs * fl
def close(got, want, tol=0.001):
return abs(got - want) <= tol
SETUP = parse(SETUP_ROWS)
HOLD = parse(HOLD_ROWS)
DATA_SLEW = 0.042
setup = bilin(SETUP, DATA_SLEW, CLOCK_SLEW)
hold = bilin(HOLD, DATA_SLEW, CLOCK_SLEW)
setup_slack = (PERIOD - setup) - ARRIVAL_MAX
hold_slack_1ns = ARRIVAL_MIN - hold
hold_slack_2ns = ARRIVAL_MIN - hold # same edge: recomputed at a 2 ns period
bad = 0
def check(name, cond):
global bad
if not cond:
print("FAIL: " + name)
bad += 1
check("setup at table point", close(bilin(SETUP, 0.01, 0.01), 0.05, 1e-9))
check("hold at table point", close(bilin(HOLD, 0.01, 0.01), 0.0, 1e-9))
check("setup rises with slew", bilin(SETUP, 0.1, 0.01) > bilin(SETUP, 0.01, 0.01))
check("hold rises with slew", bilin(HOLD, 0.1, 0.01) > bilin(HOLD, 0.01, 0.01))
check("setup time", close(setup, 0.058))
check("hold time", close(hold, 0.008))
check("setup slack", close(setup_slack, 0.161))
check("hold slack", close(hold_slack_1ns, 0.773))
check("hold ignores period", close(hold_slack_1ns, hold_slack_2ns, 1e-9))
if bad != 0:
sys.exit(1)
print("PASS: setup plus 0.16 ns and hold plus 0.77 ns derived from DFF tables")The second run is the deliberate change: same RTL and target, but heavy.constr with its 0.2 pF load on every output. ABC answers with one BUF between every stage and the next, each driving its tap port and the following stage, so the recorded arrival grows from 610 ps to 1230 ps while the cell count rises from 4 to 8 and the area from 14 to 22. The checks did not move: setup still uses the 1.00 ns period minus setup, and hold still uses the same edge.
Recomputed against the new arrival, setup slack falls by about the recorded growth: the comparison mixes a model typical arrival with a recorded heavy one (0.612 ns of fall against 0.62 ns of growth), and the setup bound shifts slightly with slew, so read it as about. The model arrival becomes about 1.39 ns (model clock-to-Q plus the recorded heavy combinational delay), so the setup slack is roughly 0.942 minus 1.39, a negative number: the same clock that met timing on the typical load now fails it. ABC's delay is the maximum over all its outputs, and the recorded netlist on the next page shows the worst path ends on the net the t4 port shares with the capture flop's D input, so that recorded delay is the capture path's and the failure claim belongs to it. Hold slack grows instead, since a later arrival sits further past the same-edge bound. One remap, two opposite verdicts, neither requirement moved.
Commercial signoff reports show the same two checks with foundry numbers. The sketch below is hand-written to that shape, and every figure in it is invented: vocabulary transfer, not evidence.
sample, hand-written report sketch (not a recording, not course numbers)
setup: arrival 1.842 ns, required 2.000 - 0.120 = 1.880 ns, slack +0.038 ns MET
hold: earliest arrival 0.214 ns, required 0.050 ns, slack +0.164 ns MET
critical endpoint: MEM_C/D, worst setup slack +0.038 nsSetup slack grows by exactly 1.00 ns, from plus 0.16 ns to plus 1.16 ns. Hold slack is unchanged at plus 0.77 ns: neither its arrival nor its bound mentions the period.
Setup slack goes negative because the arrival outgrows the fixed requirement: the clock that met timing now fails it, while hold slack only grows. That split proves the requirements belong to the clock and the edges, not to the load.
Arrival shifts and cell counts 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 and again with heavy.constr (0.2 pF load). The library is fictional, so absolute delays are teaching values. Setup and hold bounds and the slacks are model numbers interpolated from the exact DFF table rows, labelled so on the page; the hand-written commercial sketch is explicitly not a recording.