With the audit done, the constraint file is corrected and the real violations are visible. This lesson classifies each one, picks the method, and shows why the obvious hold fix is wrong while the right-sized one works.
# not-run: SDC is not executable here
create_clock -name clk -period 1.0 [get_ports clk]
create_clock -name clk_b -period 1.3 [get_ports clk_b]
create_generated_clock -name clk_div2 -source [get_ports clk] -divide_by 2 [get_pins u_div/q_reg/Q]
set_false_path -from [get_clocks clk] -to [get_clocks clk_b]
set_false_path -from [get_clocks clk_b] -to [get_clocks clk]BUDGET = {("clk", "clk"): 1.0, ("clk", "clk_div2"): 1.0, ("clk_div2", "clk"): 1.0,
("clk", "clk_b"): 0.3, ("clk_b", "clk"): 0.3} # capture-edge budget per clock pair, ns (async pairs: arbitrary pessimistic placeholder, not the closest-edge gap)
def path(l, c, mx, mn, sk=0.0):
return {"l": l, "c": c, "mx": mx, "mn": mn, "su": 0.06, "ho": 0.05, "sk": sk}
PATHS = {"E1": path("clk", "clk", 0.82, 0.20, 0.02), "E2": path("clk", "clk", 0.91, 0.16),
"E3": path("clk", "clk", 0.90, 0.12, -0.03), "E4": path("clk", "clk_div2", 0.75, 0.30),
"E5": path("clk_div2", "clk", 1.02, 0.30), "E6": path("clk", "clk_b", 0.40, 0.10),
"E7": path("clk_b", "clk", 0.55, 0.10), "E8": path("clk", "clk", 0.90, 0.06, 0.05)}
FP_ASYNC = {("clk", "clk_b"), ("clk_b", "clk")} # false paths between the truly asynchronous clocks
FP_DIV = {("clk_div2", "clk"), ("clk", "clk_div2")} # false paths between the divided clock and its source
def analyse(paths, clocks, exceptions):
out = {}
for name, p in paths.items():
if p["l"] not in clocks or p["c"] not in clocks:
out[name] = "unconstrained"
elif any((p["l"], p["c"]) in ex for ex in exceptions):
out[name] = "excepted"
else:
setup = BUDGET[(p["l"], p["c"])] + p["sk"] - p["su"] - p["mx"]
hold = p["mn"] - (p["ho"] + p["sk"])
out[name] = (round(setup, 3), round(hold, 3))
return out
def summary(res):
nums = {k: v for k, v in res.items() if isinstance(v, tuple)}
kinds = [v if isinstance(v, str) else "checked" for v in res.values()]
return {"checked": kinds.count("checked"), "excepted": kinds.count("excepted"), "unconstrained": kinds.count("unconstrained"),
"wns_setup": min(v[0] for v in nums.values()), "wns_hold": min(v[1] for v in nums.values())}
clocks = {"clk", "clk_b", "clk_div2"}
res = analyse(PATHS, clocks, [FP_ASYNC])
s = summary(res)
assert res["E4"] == (0.19, 0.25) and res["E5"] == (-0.08, 0.25) and res["E8"] == (0.09, -0.04)
assert s == {"checked": 6, "excepted": 2, "unconstrained": 0, "wns_setup": -0.08, "wns_hold": -0.04}
print("PASS: corrected constraints", s)Finding | Check | Slack (ns) | Class |
|---|---|---|---|
E5, clk_div2 to clk | Setup | -0.080 | Real violation exposed by the corrected constraints |
E8, clk to clk | Hold | -0.040 | Real violation, present from the start |
E6, E7 | Excepted | n/a | Correct exceptions on truly asynchronous crossings (a CDC check owns them) |
E4 and E5 both cross between the fast clock and the divided clock, so each gets the closest-edge relationship of 1.0 ns rather than the full divided period. The E5 data path takes 1.02 ns against a budget of 1.0 - 0.06 = 0.94 ns. A sizing change on a shared cell that removes 0.10 ns from the path clears it. Check hold at the same endpoint afterwards: the speed-up also shortens the minimum path, here from 0.30 to 0.25 ns.
BUDGET = {("clk", "clk"): 1.0, ("clk", "clk_div2"): 1.0, ("clk_div2", "clk"): 1.0,
("clk", "clk_b"): 0.3, ("clk_b", "clk"): 0.3} # capture-edge budget per clock pair, ns (async pairs: arbitrary pessimistic placeholder, not the closest-edge gap)
def path(l, c, mx, mn, sk=0.0):
return {"l": l, "c": c, "mx": mx, "mn": mn, "su": 0.06, "ho": 0.05, "sk": sk}
PATHS = {"E1": path("clk", "clk", 0.82, 0.20, 0.02), "E2": path("clk", "clk", 0.91, 0.16),
"E3": path("clk", "clk", 0.90, 0.12, -0.03), "E4": path("clk", "clk_div2", 0.75, 0.30),
"E5": path("clk_div2", "clk", 1.02, 0.30), "E6": path("clk", "clk_b", 0.40, 0.10),
"E7": path("clk_b", "clk", 0.55, 0.10), "E8": path("clk", "clk", 0.90, 0.06, 0.05)}
FP_ASYNC = {("clk", "clk_b"), ("clk_b", "clk")} # false paths between the truly asynchronous clocks
FP_DIV = {("clk_div2", "clk"), ("clk", "clk_div2")} # false paths between the divided clock and its source
def analyse(paths, clocks, exceptions):
out = {}
for name, p in paths.items():
if p["l"] not in clocks or p["c"] not in clocks:
out[name] = "unconstrained"
elif any((p["l"], p["c"]) in ex for ex in exceptions):
out[name] = "excepted"
else:
setup = BUDGET[(p["l"], p["c"])] + p["sk"] - p["su"] - p["mx"]
hold = p["mn"] - (p["ho"] + p["sk"])
out[name] = (round(setup, 3), round(hold, 3))
return out
def summary(res):
nums = {k: v for k, v in res.items() if isinstance(v, tuple)}
kinds = [v if isinstance(v, str) else "checked" for v in res.values()]
return {"checked": kinds.count("checked"), "excepted": kinds.count("excepted"), "unconstrained": kinds.count("unconstrained"),
"wns_setup": min(v[0] for v in nums.values()), "wns_hold": min(v[1] for v in nums.values())}
m = 0.005
e8 = PATHS["E8"]
setup0 = BUDGET[("clk", "clk")] + e8["sk"] - e8["su"] - e8["mx"]
hold0 = e8["mn"] - (e8["ho"] + e8["sk"])
assert (round(setup0, 3), round(hold0, 3)) == (0.09, -0.04)
assert round(setup0 + hold0, 3) == 0.05 and setup0 + hold0 >= 2 * m # delay insertion can work in principle
need = m - hold0
assert round(need, 3) == 0.045
def after(delay):
return round(setup0 - delay, 3), round(hold0 + delay, 3)
naive = after(0.10) # one coarse delay cell
sized = after(0.05) # two small cells, sized to the window
assert naive == (-0.01, 0.06) and sized == (0.04, 0.01)
assert naive[0] < 0 and sized[0] >= m and sized[1] >= m
print("PASS: window for the delay is", (round(need, 3), round(setup0 - m, 3)), "; naive 0.10 ns ->", naive, "; sized 0.05 ns ->", sized)Option | Delay added | Setup slack | Hold slack | Verdict |
|---|---|---|---|---|
None | 0 | +0.090 | -0.040 | Hold violation |
One coarse delay cell | 0.10 | -0.010 | +0.060 | Hold fixed, setup now fails |
Sized to the window | 0.05 | +0.040 | +0.010 | Both pass with at least the 5 ps margin |
The setup slack (0.090) plus the hold slack (-0.040) is +0.050 ns, so delay insertion can work. The delay must lie between 0.045 ns (to reach the margin on hold) and 0.085 ns (to keep the margin on setup). The coarse 0.10 ns cell overshoots the window and trades a hold violation for a setup one: the classic fix-breaks-fix. A 0.05 ns delay, built from two small cells, sits inside the window with 5 ps to spare on hold and 35 ps on setup. Insert it at the endpoint so that other paths are untouched, and check it in the slow corner, where the added delay is larger.
Change | Rung of the ECO ladder | Rerun |
|---|---|---|
Delete the false path on clk_div2 | Constraint change | Full constraint audit and timing in all scenarios |
Size a shared cell for E5 | Cell swap or resize | Timing, SI on affected nets, power |
Insert delay at E8 | Buffer insertion | Timing, SI, extraction, DRC, power, and the slow-corner setup check |
The sum is -0.010 ns, below the 0.010 ns needed for margins, so delay insertion cannot work: the endpoint would need a structural fix.