Most paths play by one rule: launch on one edge, capture on the next. Exceptions mark the paths that play by another rule: ones that cannot switch, ones sampled late, ones live in one mode, and ones budgeted in nanoseconds. Each exception is a claim the tool trusts silently, so a wrong exception is worse than none: a false path on a real path hides its failure.
This lesson proves the datapath structure with a recorded run, then works the exception semantics in hand-derived arithmetic, labelled so throughout. The SDC lines are illustrative, since the open tools have no SDC reader.
The design below is an enable-gated datapath with a quasi-static configuration input: cfg selects between two four-bit functions, captured only when the enable agrees. It pairs the classic multicycle candidate with a static strap. The script synthesises it and asserts the enable flops; the second half is the break-it preview, the enable removed, sampling every cycle.
read_verilog <<EOV
module gated_dp(input clk, input en, input cfg, input [3:0] a, b, c, output reg [3:0] y);
wire [3:0] w = cfg ? ((a & b) | (c & (a ^ b))) : (a ^ b ^ c);
always @(posedge clk) if (en) y <= w;
endmodule
EOV
synth -top gated_dp
tee -o stat_gated.txt stat
write_json net_gated.json
select -assert-count 4 t:$_DFFE_PP_
check
design -reset
read_verilog <<EOV
module free_dp(input clk, input cfg, input [3:0] a, b, c, output reg [3:0] y);
wire [3:0] w = cfg ? ((a & b) | (c & (a ^ b))) : (a ^ b ^ c);
always @(posedge clk) y <= w;
endmodule
EOV
synth -top free_dp
tee -o stat_free.txt stat
write_json net_free.json
select -assert-count 4 t:$_DFF_P_
select -assert-none t:$_DFFE_PP_
check
log PASS: the enable lives in the flop and dropping it changes the flopNone of the exceptions rewrites this logic: they change which edges check the same netlist. The run proves the netlist; the arithmetic moves the edges.
Barely. Both hold 36 cells of the same logic; the gated run keeps 4 enable flops and the free run keeps 4 plain flops. The enable lives inside the flop cell, and no exception has touched either netlist yet.
Version (recorded) | Cells (recorded) | Flop type (recorded) |
|---|---|---|
Gated | 36 | 4 enable flops |
Free | 36 | 4 plain flops |
The gated netlist holds 36 cells with 4 enable flops, drawn below: cfg-selected bit logic into a row of enabled flops. Dropping the enable keeps the count and swaps the flop type to plain flops. Same logic, different contract: the free version is single-cycle on every edge, while the gated version may ask for more cycles.
A false-path exception tells the tool to stop timing a path because it cannot switch in the real design. The headline example is the quasi-static configuration register on this page: cfg is tied off in mission mode and changes only while the clocks are stopped, so timing it would optimise a fiction. An async crossing into its synchronizer is the second honest case: cut, with its own maximum-delay bound. Scan enable is not the headline: it is timed and high-fanout in test mode, including at-speed tests.
# not-run: no SDC reader in the open tools
set_false_path -from [get_ports cfg]What does not belong here is a reset posed as a false path. A reset fans out to live flops and genuinely switches during operation, so cutting it tells the tool to ignore timing on paths that really toggle. A false path on a switching path does not fix timing; it hides the violation. State the reason alongside every false path: tied off, stopped clocks, or synchronized elsewhere.
A multicycle exception gives a path more than one clock period because the design samples it late: the datapath above, valid every second cycle. The condition is strict: the source must hold each value for two full cycles, here the sender holding a steady, or the extra cycle is a lie. The setup part moves the capture edge N cycles later, so setup 2 at an EXAMPLE 10.0 ns period captures at 20 ns.
# not-run: no SDC reader in the open tools
set_multicycle_path -setup 2 -from [get_ports a] -to [get_ports y]
set_multicycle_path -hold 1 -from [get_ports a] -to [get_ports y]The hold half is where designs silently break: unless a -hold value is also given, the hold check moves with the setup. By default the hold edge sits N-1 cycles before the new setup edge, so with setup 2 it moves a full cycle past launch. The second line pins it back: a -hold value of N-1 restores the launch-edge hold. Always state both lines.
import sys
# Multicycle edge timeline in integer picoseconds. EXAMPLE period, hand-derived.
# Standard SDC semantics: -setup N moves the capture edge to launch + N
# cycles. The hold edge is launch + (N - M - 1) cycles, where M is the -hold
# value (0 when no -hold is given), so the default hold edge sits N-1 cycles
# before the new setup edge.
PERIOD_PS = 10000
def edges(setup_mult, hold_mult=None):
setup_edge = setup_mult * PERIOD_PS
m = 0 if hold_mult is None else hold_mult
hold_edge = (setup_mult - m - 1) * PERIOD_PS
return setup_edge, hold_edge
single = edges(1)
default_pair = edges(2)
explicit_pair = edges(2, 1)
if single != (10000, 0):
print("FAIL: single-cycle edges got " + str(single))
sys.exit(1)
if default_pair != (20000, 10000):
print("FAIL: default multicycle pair got " + str(default_pair))
sys.exit(1)
if explicit_pair != (20000, 0):
print("FAIL: explicit multicycle pair got " + str(explicit_pair))
sys.exit(1)
if default_pair[1] != default_pair[0] - PERIOD_PS:
print("FAIL: default hold edge is not one cycle before the setup edge")
sys.exit(1)
if explicit_pair[1] != single[1]:
print("FAIL: explicit hold pairing did not restore the hold edge")
sys.exit(1)
print("PASS: setup 2 moves capture to 20 ns and the hold edge follows unless held")The program models the three edge pairs in integer picoseconds and asserts all of them: single-cycle, setup-2 with default hold, and setup-2 with hold-1 restoring launch-edge hold. The table and figures below read those same hand-derived edges.
Case (hand-derived) | Setup edge (hand-derived) | Hold edge (hand-derived) |
|---|---|---|
Single cycle | 10 ns | launch edge |
Setup 2, default hold | 20 ns | 10 ns after launch |
Setup 2, hold 1 | 20 ns | launch edge |
Case analysis pins a pin or port to a constant value so the tool analyses only the paths live under that value, modelling a mode tie without changing the RTL. With cfg tied to zero the tool explores the fallback function only; mission-mode STA ties scan enable the same way. A maximum-delay exception bounds named paths directly in EXAMPLE nanoseconds, independent of the clock period: the same a-to-y path with a fixed 4.0 ns budget instead of two cycles.
# not-run: no SDC reader in the open tools
set_case_analysis 0 [get_ports cfg]
set_max_delay 4.0 -from [get_ports a] -to [get_ports y]Both need re-checking after every RTL change. A renamed mode signal silently narrows case analysis to paths that no longer exist. A maximum delay detached from the clock drifts whenever the period changes, and nothing in the file says so. Exceptions are dated claims; re-sign them with the RTL.
Exception | Effect | Risk if wrong |
|---|---|---|
False path | Path is not timed against the clock | A real path escapes timing entirely. |
Multicycle path | Setup check moves out N cycles; hold follows unless held | A one-cycle path is given time it does not have; a setup-only pair tightens hold. |
Case analysis | Only the tied value is analysed | Paths live under the other value are ignored. |
Maximum delay | Path is bounded in EXAMPLE ns | A budget detached from the clock drifts when the period changes. |
The break-it is the same logic with the enable always agreeing. The flops change from enabled to plain, and every path is now single-cycle by construction, no exception needed and none allowed. Compare that with the multicycle route, which keeps the enable flops and relaxes the check instead: one way spends samples, the other spends a constraint that must be re-checked forever.
The tool stops timing it, so its failure is hidden, not fixed. A false path must be truly impossible: tied off, stopped clocks, or synchronized elsewhere.
One cycle before the new setup edge, a full cycle later than launch: the hold check moved with the setup. The hold line with value 1 is missing.
Cell counts, flop types, and the netlist picture 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): an enable-gated datapath and its ungated variant are synthesised, with write_json netlists and flop-type assertions. Edge pairs, the hold table, and timing bars come from the stdlib-only Python program shown, which asserts every figure it prints, and are labelled hand-derived. The SDC listings are illustrative: the open tools have no SDC reader.