Constraints fail in two opposite directions, and both look innocent in the file. A missing constraint leaves its paths without a target: the tool optimises them blind and reports nothing, which is worse than a bad number. An over-tight constraint states a target fiercer than the requirement: the tool spends area chasing it, and past what the library can give, the spending continues while the delay stops moving. This lesson records both on one design: a four-target sweep plus a checker that catches the missing half mechanically.
Port directions hold here: req, data and valid are inputs, ack is an output. The checker enforces exactly that split, so ack is never asked for an input delay.
The design is an eight-input cone of AND, OR and XOR terms feeding one output, big enough to need bigger cells and small enough to draw. The script synthesises it once, saves the generic gates, then maps the same gates four times against sized.lib: a no-pressure run at 100000 ps standing in for a missing target, a middle run, a tight run already past what the library can give, and a past-the-wall run further still. Every run writes its ABC report and stat histogram; the loose and tight runs also save their netlists. The assertions prove the loose run uses no upsized cell and the tight and past-the-wall runs do.
read_verilog <<EOV
module budget8(input [7:0] x, output y);
assign y = ((x[0] & x[1]) | (x[2] & x[3]) | (x[4] ^ x[5])) & (x[6] | x[7]);
endmodule
EOV
synth -top budget8 -flatten
design -save generic
tee -o abc_loose.txt abc -liberty sized.lib -constr typical.constr -D 100000
tee -o stat_loose.txt stat -liberty sized.lib
write_json net_loose.json
select -assert-none t:*_X2 t:*_X4
design -load generic
tee -o abc_mid.txt abc -liberty sized.lib -constr typical.constr -D 300
tee -o stat_mid.txt stat -liberty sized.lib
design -load generic
tee -o abc_tight.txt abc -liberty sized.lib -constr typical.constr -D 200
tee -o stat_tight.txt stat -liberty sized.lib
write_json net_tight.json
select -assert-min 1 t:*_X2 t:*_X4 %u
design -load generic
tee -o abc_past.txt abc -liberty sized.lib -constr typical.constr -D 100
tee -o stat_past.txt stat -liberty sized.lib
select -assert-min 1 t:*_X2 t:*_X4 %u
log PASS: loose meets no pressure and past the wall buys nothingFour targets, one variable: the number after -D. Same RTL, same library, same load throughout, so every difference between the reports is the target talking.
The same one. The 200 ps run already misses at 211 ps against its own target, and the 100 ps run returns exactly those figures: both targets sit past the wall.
Target -D (recorded) | Cells (recorded) | Area (recorded) | Delay reached (recorded) | Upsized cells (recorded) |
|---|---|---|---|---|
100000 ps | 5 | 16 | 342 ps | 0 |
300 ps | 5 | 20.8 | 298 ps | 1 |
200 ps | 5 | 33.4 | 211 ps | 4 |
100 ps | 5 | 33.4 | 211 ps | 4 |
The loose end is the cheapest cover: 5 cells at area 16 and 342 ps, because nothing asked for speed. The middle run pays a little for a little, meeting its target at area 20.8 and 298 ps. The tight run pays 109% more area than the loose end yet still misses at 211 ps. Then the past-the-wall run returns the tight netlist unchanged: pressure past the wall buys no more speed, and the 3 distinct netlists mark each step the tool could take.
A path with no clock or IO constraint covering it is unconstrained, and an unconstrained path is not timed: it appears in no timing report, with no slack, comfortable or otherwise. An empty report looks clean but is not: a path the tool never checked can fail on silicon without ever failing in the flow. The usual causes are a missing clock, a missing input or output delay, or an over-wide exception. In a commercial flow the mechanical way to find them is check_timing, or report_checks -unconstrained; not run here.
Coverage matters as much as values. Every input port needs its input delay or a documented reason it has none; every output port needs its output delay; every clock needs its period. The program below checks exactly that coverage on a small SDC subset, keeping ack an output: it asks for an output delay on ack, never an input delay.
The program parses create_clock lines with their periods and input and output delay lines with their ports, then reports which ports of the input and output lists have no delay. Its fixed EXAMPLE parameters cover three inputs and two outputs against four constraint lines. Eight self-checks pin the parsed clock, both constrained sets, both missing sets, and the recorded wall: both tight targets miss and the past run lands on the same arrival.
import sys
# Tiny SDC-subset coverage checker plus a slack verdict against the recorded
# sweep. Fixed EXAMPLE parameters of the program, hand-derived, except the
# two arrivals, which are interpolated from the recorded ABC delays when the
# page is built. ack is an output everywhere in this topic; req, data and
# valid are inputs.
SDC = [
"create_clock -name CLK -period 10.0 [get_ports clk]",
"set_input_delay -clock CLK 3.0 [get_ports req]",
"set_input_delay -clock CLK 3.0 [get_ports data]",
"set_output_delay -clock CLK 2.5 [get_ports ack]",
]
INPUTS = ["req", "data", "valid"]
OUTPUTS = ["ack", "done"]
def ports_in(line):
start = line.find("[get_ports ")
if start < 0:
return []
inner = line[start + len("[get_ports "):]
end = inner.find("]")
if end < 0:
return []
return inner[:end].split()
clocks = {}
in_hit = set()
out_hit = set()
for line in SDC:
text = line.strip()
if text.startswith("create_clock"):
parts = text.split()
clocks[parts[parts.index("-name") + 1]] = float(parts[parts.index("-period") + 1])
if text.startswith("set_input_delay"):
for p in ports_in(text):
in_hit.add(p)
if text.startswith("set_output_delay"):
for p in ports_in(text):
out_hit.add(p)
missing_in = [p for p in INPUTS if p not in in_hit]
missing_out = [p for p in OUTPUTS if p not in out_hit]
if clocks != {"CLK": 10.0}:
print("FAIL: clocks parsed as " + str(clocks))
sys.exit(1)
if sorted(in_hit) != ["data", "req"]:
print("FAIL: input constrained parsed as " + str(sorted(in_hit)))
sys.exit(1)
if sorted(out_hit) != ["ack"]:
print("FAIL: output constrained parsed as " + str(sorted(out_hit)))
sys.exit(1)
if missing_in != ["valid"]:
print("FAIL: missing inputs parsed as " + str(missing_in))
sys.exit(1)
if missing_out != ["done"]:
print("FAIL: missing outputs parsed as " + str(missing_out))
sys.exit(1)
# Slack verdict against the recorded sweep: slack is required minus arrival,
# in integer picoseconds, and its sign is the verdict. Both tight targets sit
# past the library wall, so both must miss, and the past-the-wall run must
# land on the same arrival as the tight one.
TIGHT_TARGET_PS = 200
PAST_TARGET_PS = 100
TIGHT_ARRIVAL_PS = 211
PAST_ARRIVAL_PS = 211
tight_slack = TIGHT_TARGET_PS - TIGHT_ARRIVAL_PS
past_slack = PAST_TARGET_PS - PAST_ARRIVAL_PS
if tight_slack >= 0:
print("FAIL: tight slack is " + str(tight_slack) + " ps, want a miss")
sys.exit(1)
if past_slack >= 0:
print("FAIL: past-the-wall slack is " + str(past_slack) + " ps, want a miss")
sys.exit(1)
if PAST_ARRIVAL_PS != TIGHT_ARRIVAL_PS:
print("FAIL: plateau broken, past " + str(PAST_ARRIVAL_PS) + " vs tight " + str(TIGHT_ARRIVAL_PS))
sys.exit(1)
print("PASS: 1 of 3 inputs and 1 of 2 outputs lack a delay and the 200 ps target already misses")Two gaps: valid lacks an input delay, done lacks an output delay. Both ports' paths are unconstrained and escape every timing report. A checker this small belongs in every flow: coverage as a verdict with port names in it.
Slack is required minus arrival, and its sign is the verdict. The hand-derived table below fixes an EXAMPLE arrival of 7.0 ns: against 10.0 ns the slack is +3.0 ns, comfortable; against 6.0 ns it is -1.0 ns, a violation. Only the requirement moved, which is precisely how over-constraining manufactures failures out of passing paths.
Requirement (hand-derived) | Arrival (hand-derived) | Slack (hand-derived) |
|---|---|---|
10.0 ns | 7.0 ns | +3.0 ns (meets) |
6.0 ns | 7.0 ns | -1.0 ns (violated) |
Hold that sign flip against the sweep table above. The tight run's extra area bought the last real speed-up, but even its requirement was already past the wall: it reaches 211 ps, a miss the program asserts. The past-the-wall run misses by further and reports the same arrival, because the library had nothing left to sell. Over-constraining is a requirement error wearing a timing report as camouflage.
The last run in the script is the break-it: the same cone past the wall after the tight run already missed its own target. Nothing else changed: the tight netlist back, cell for cell. When delay stops improving while you keep asking for more, change the logic, the library, or the requirement.
Symptom | Cause | Fix |
|---|---|---|
Paths missing from every report | Missing clock, IO delay, or an over-wide exception | Add the missing bound; narrow the exception; run the checker. |
Worse area, same real slack | Target tighter than the requirement | Constrain the real target; move margin to variation. |
Silent stale exceptions | RTL changed under old constraints | Re-check exceptions after each RTL change. |
Unconstrained paths on valid and done: the tool reports nothing for them, neither slack nor violation.
Over-constraining. The 200 ps run already misses at 211 ps and the 100 ps run returns the same netlist, so pressure past the wall buys nothing. Constrain the real target instead.
Cell counts, areas, delays, histograms, and netlist pictures on this page come 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 one cone against the teaching library sized.lib at four delay targets from no-pressure to past-the-wall. The library is fictional, so absolute areas and delays are teaching units; the plateau shape is the point. Coverage gaps and slack signs come from the stdlib-only Python program shown, which asserts every figure it prints, and are labelled hand-derived.