Timing constraints: what exceptions change and what they do not
A setup check asks whether data arrives before its capture deadline; a hold check asks whether it remains stable long enough after the relevant edge. Shortening a data path can improve setup while breaking hold, and an exception changes which timing checks the tool performs. Check the path reports and constraint coverage before treating positive slack as evidence that the design meets its timing contract.
Start with the path that the report actually checks
A useful report identifies the launch register, capture endpoint, clocks, analysis type, arrival time and required time. Read those before the final slack. A large margin on the wrong path says little about the intended transfer. Save both minimum-delay and maximum-delay reports, because improving only one can hide a regression in the other.
The example below holds two alternatives side by side: six buffers between a launch register and capture_slow, and a direct connection to capture_fast. Both carry the same Boolean value to equivalent capture registers. Comparing them isolates a data-path shortening under one constraint set. It does not represent an optimizer repairing a placed design, and its deliberately large hold uncertainty is a teaching condition, not a recommended clock budget.
OpenSTA reads the mapped Verilog, Liberty timing data and SDC-style constraints. Its command reference describes the endpoint and clock checks used here1. The executed source is pinned to the commit listed in the run record, so the article’s observed numbers are tied to that implementation rather than whichever release a reader installs later2.
The complete mapped example
Save this as timing_demo.v. The only external timing input is a three-cell subset of the SkyWater high-density library: a buffer, inverter and flip-flop. The upstream library carries Apache 2.03. Its exact files, commit and hashes are recorded in the library provenance; no library file is redistributed with this example.
The register named divider feeds back its inverted output, providing a divide-by-two structure for the generated-clock case. Static timing uses the declared edge relationship; this netlist supplies neither reset initialization nor a functional proof of clock startup. The independent clock is a separate input. Finally, unconstrained_out intentionally lacks an output-delay constraint so the coverage audit has a concrete omission to find.
// SPDX-License-Identifier: MIT
module timing_demo (
input clk,
input async_clk,
input data_in,
output slow_q,
output fast_q,
output generated_q,
output async_q,
output unconstrained_out
);
wire launched;
wire b1, b2, b3, b4, b5, b6;
wire divided, divided_next;
sky130_fd_sc_hd__dfxtp_1 launch (
.CLK(clk), .D(data_in), .Q(launched)
);
sky130_fd_sc_hd__buf_1 delay1 (.A(launched), .X(b1));
sky130_fd_sc_hd__buf_1 delay2 (.A(b1), .X(b2));
sky130_fd_sc_hd__buf_1 delay3 (.A(b2), .X(b3));
sky130_fd_sc_hd__buf_1 delay4 (.A(b3), .X(b4));
sky130_fd_sc_hd__buf_1 delay5 (.A(b4), .X(b5));
sky130_fd_sc_hd__buf_1 delay6 (.A(b5), .X(b6));
sky130_fd_sc_hd__dfxtp_1 capture_slow (
.CLK(clk), .D(b6), .Q(slow_q)
);
sky130_fd_sc_hd__dfxtp_1 capture_fast (
.CLK(clk), .D(launched), .Q(fast_q)
);
sky130_fd_sc_hd__inv_1 divide_invert (
.A(divided), .Y(divided_next)
);
sky130_fd_sc_hd__dfxtp_1 divider (
.CLK(clk), .D(divided_next), .Q(divided)
);
sky130_fd_sc_hd__dfxtp_1 capture_generated (
.CLK(divided), .D(launched), .Q(generated_q)
);
sky130_fd_sc_hd__dfxtp_1 capture_async (
.CLK(async_clk), .D(launched), .Q(async_q)
);
assign unconstrained_out = launched;
endmodule
Save the following as check.tcl. CELL_LIB identifies the externally prepared Liberty file, and CASE selects one experiment. The reproduction instructions include the exact commands, tool pins, library preparation and expected outputs. Every case starts a fresh OpenSTA process; exceptions from a previous case cannot leak into the next one.
# SPDX-License-Identifier: MIT
read_liberty $::env(CELL_LIB)
read_verilog timing_demo.v
link_design timing_demo
set period [expr {$::env(CASE) eq "coverage_audit" ? 1.10 : 0.90}]
create_clock -name primary -period $period [get_ports clk]
create_clock -name independent -period 1.30 [get_ports async_clk]
create_generated_clock -name divided -source [get_ports clk] \
-divide_by 2 [get_pins divider/Q]
set_clock_transition 0.05 [all_clocks]
set_clock_uncertainty -setup 0.05 [all_clocks]
set_clock_uncertainty -hold 0.45 [all_clocks]
set_input_delay -clock primary -max 0.10 [get_ports data_in]
set_input_delay -clock primary -min 0.10 [get_ports data_in]
set_input_transition 0.05 [get_ports data_in]
set_output_delay -clock primary 0.10 [get_ports {slow_q fast_q}]
set_output_delay -clock divided 0.10 [get_ports generated_q]
set_output_delay -clock independent 0.10 [get_ports async_q]
set_load 0.01 [all_outputs]
set launch_set [get_cells launch]
set slow_set [get_cells capture_slow]
puts "CASE $::env(CASE)"
switch $::env(CASE) {
multicycle_setup {
set_multicycle_path 2 -setup -from $launch_set -to $slow_set
}
multicycle_pair {
set_multicycle_path 2 -setup -from $launch_set -to $slow_set
set_multicycle_path 1 -hold -from $launch_set -to $slow_set
}
false_path {
set_false_path -from $launch_set -to $slow_set
}
asynchronous - coverage_audit {
set_clock_groups -asynchronous -group {primary divided} \
-group {independent}
}
}
report_units
report_clock_properties [all_clocks]
foreach endpoint {capture_slow/D capture_fast/D capture_generated/D capture_async/D} {
puts "ENDPOINT $endpoint"
report_checks -from $launch_set -to [get_pins $endpoint] \
-path_delay min_max -format full_clock_expanded -digits 6
}
puts "CONSTRAINT_AUDIT"
check_setup -verbose
puts "SETUP_SUMMARY"
report_worst_slack -max -digits 6
puts "UNCONSTRAINED_REPORT"
report_checks -unconstrained -to [get_ports unconstrained_out] \
-path_delay max -digits 6
exit
The primary period is 0.90 ns, except for the explicit 1.10 ns coverage audit. Clock transition is 0.05 ns; setup uncertainty is 0.05 ns and hold uncertainty is 0.45 ns. Input delays are 0.10 ns and output loads are 0.01 pF. All numbers below use the typical 25 °C, 1.8 V library tables with ideal clocks and zero wire parasitics. These conditions are part of the result, not representative production constraints.
A setup improvement that costs hold margin
In baseline, the buffered path misses setup by 0.063422 ns. The direct path instead has 0.372329 ns of setup slack. Looking only at those maximum-delay reports, removing the buffers appears to solve the problem. The minimum-delay reports reverse the conclusion: buffered-path hold slack is 0.406981 ns, while direct-path hold slack is −0.033411 ns.
| Baseline path | Setup slack, ns | Hold slack, ns |
|---|---|---|
Six buffers to capture_slow | −0.063422 | 0.406981 |
Direct to capture_fast | 0.372329 | −0.033411 |
The practical check is paired: after a data-path change, compare both setup and hold for the affected endpoint and any other endpoint sharing the changed logic. Here the launch register drives both alternatives, so their loads coexist in the same test netlist. These are controlled alternatives, not a prediction of the delay delta after physically deleting cells from a routed design.
Do not fix this example by quietly reducing the hold uncertainty until its report turns green. That would change the contract instead of demonstrating a repair. A real change needs a justified clock budget and an implementation that satisfies it. The article deliberately preserves the failing hold result so the tradeoff remains visible.
A multicycle setup exception also changes the hold question
The multicycle_setup case applies a two-cycle setup exception only to the launch-to-slow-capture path. Its setup slack rises to 0.836578 ns, but hold becomes −0.493019 ns. In this same-clock example, adding the paired one-cycle hold exception in multicycle_pair restores the previous 0.406981 ns hold margin while retaining the relaxed setup check.
| Slow-capture case | Setup slack, ns | Hold slack, ns |
|---|---|---|
| Baseline | −0.063422 | 0.406981 |
| Two-cycle setup only | 0.836578 | −0.493019 |
| Two-cycle setup, one-cycle hold | 0.836578 | 0.406981 |
This pair is an observed result for the printed clocks and selectors. It is not a universal recipe for unrelated clocks, opposite edges or different -start and -end choices. Inspect the expanded launch and capture edges whenever the relationship changes. The pinned SDC implementation distinguishes setup and hold multipliers and their reference edges2.
There is a second obligation outside the timing command: the receiver must actually be allowed to wait two cycles. This demonstration has no enable protocol establishing that permission. Treat its multicycle exception as an experiment in analysis, not a valid functional waiver for this circuit. The design requirement, control logic and verification evidence must justify any exception before it enters a release.
A false path removes an obligation; a multicycle path retains one
false_path uses the same launch and capture selection, but OpenSTA reports No paths found. for that selected path. The multicycle cases still produce maximum- and minimum-delay reports. That visible difference is why a false path must not be a convenient replacement for a difficult setup target.
A genuinely irrelevant transfer needs an explanation grounded in operating modes or connectivity: what makes the path unavailable to a functional capture, and which modes establish that fact? A delayed but valid transfer needs a timing relationship. Neither conclusion follows from the size of a negative slack number. Keep the rationale with the exact selected objects, then compare the affected path list before and after applying the exception.
An early selector probe for this example found no timing change when a data-output pin was supplied as the launch selector. The delivered version uses the launch register cell and confirms the changed edge reports. This is why a command being accepted is insufficient evidence that its intended path was affected. The final run record retains the literal path results and warnings for all six cases.
Positive setup slack can coexist with missing constraints
coverage_audit uses a 1.10 ns primary period and separates the independent clock group. Its setup summary is positive: worst maximum-delay slack is 0.136578 ns. Nevertheless, check_setup -verbose reports one output missing set_output_delay and one unconstrained endpoint, both naming unconstrained_out. The targeted unconstrained report explicitly labels that path as unconstrained.
The summary therefore answers only a limited question about the checks that exist. It does not certify complete endpoint coverage, and it says nothing about the still-negative hold result. Read coverage warnings and both analysis types before promoting a summary into a release claim. Assign each unreported endpoint a documented interface constraint or a reviewed reason for exclusion; do not simply suppress the warning.
Preserve generated-clock relationships when grouping clocks
create_generated_clock declares divided from clk at divider/Q. With the 0.90 ns primary period, its reported period is 1.80 ns. The generated capture remains related to primary. In contrast, independent has a 1.30 ns period and is declared asynchronous to the group containing both primary and divided.
Before that grouping, the independent capture shows −0.427671 ns setup slack in this artificial edge comparison. In asynchronous, that path disappears from timing checks while the primary-to-divided path remains reported. Check group membership explicitly, including generated descendants. Cutting the related path as well would hide an obligation that the example intends to preserve.
An asynchronous declaration does not build a synchronizer or prove a crossing protocol. The clock-domain crossing guide covers the distinct transfer requirements. The printed direct asynchronous connection is intentionally unsuitable as a crossing implementation; here it exists to expose the timing effect of the grouping command.
Keep physical-stage comparisons separate
OpenROAD documents separate placement-based and global-routing-based parasitic estimates, with routing resistance and capacitance supplied through wire settings. Global-routing estimates use routing topology and layers4. That is a different input basis from this example’s zero-wire model. A later stage can change cell choices, connectivity, loads and clock treatment as well as wire delay; compare those inputs before attributing a slack change to routing alone.
For a useful stage comparison, keep the requirement constant and record the netlist hash, library corner, constraints, clock assumptions and parasitic source beside each report. Name any constraint change explicitly. The design handoff guide provides the surrounding release-record structure. No synthesis, placement or routing stage was executed for this article, so there is no measured stage-to-stage timing delta here.
Not covered on this page
- Physical sign-off, extracted interconnect, variation or multi-corner closure.
- A functional proof authorizing the illustrative timing exceptions.
- Clock startup, CDC correctness or a production synchronizer design.
- Measured synthesis-to-placement-to-routing comparisons.
Sources
-
OpenSTA command reference. Command reference checked 2026-10-24; semantics exercised in OpenSTA 3.1.0. ↩
-
OpenSTA pinned SDC command implementation. OpenSTA commit b3e1b3989809e1c70f265a9e7be33438c3ebf8bf; local source read. ↩ ↩2
-
SkyWater high-density library licence. Apache-2.0; library commit ac7fb61f06e6470b94e8afdf7c25268f62fbd7b1. ↩
-
OpenROAD parasitics estimation. Current documentation checked 2026-10-24; OpenROAD not executed. ↩
How we test: methodology. Found a mistake? Corrections.