The audit asks two questions of any constraint set: is everything that should be checked actually checked, and does every exception cover only what its author meant? Run in that order, because defining a missing clock exposes exceptions that were already in the file.
Two endpoints, E4 and E5, sit on clk_div2, which has no clock definition: they are unconstrained, and no report line says so. The coverage report is the check that finds them; a sane constraint set has zero. The fix defines the divided clock at the divider output.
# not-run: SDC is not executable here
create_generated_clock -name clk_div2 -source [get_ports clk] -divide_by 2 [get_pins u_div/q_reg/Q]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())}
s0 = summary(analyse(PATHS, {"clk", "clk_b"}, [FP_ASYNC, FP_DIV]))
s1 = summary(analyse(PATHS, {"clk", "clk_b", "clk_div2"}, [FP_ASYNC, FP_DIV]))
assert s0["unconstrained"] == 2 and s1["unconstrained"] == 0
assert s1["excepted"] == 4 and s1["checked"] == 4
assert (s1["wns_setup"], s1["wns_hold"]) == (0.01, -0.04)
print("PASS: before", s0, "; after defining the divided clock", s1)Measure | As received | After defining clk_div2 |
|---|---|---|
Checked | 4 | 4 |
Excepted | 2 | 4 |
Unconstrained | 2 | 0 |
Setup WNS | +0.010 ns | +0.010 ns |
Coverage is now complete, but the report did not change: setup WNS is still +0.010 ns. The two newly constrained endpoints moved straight from 'unconstrained' to 'excepted'. A false path on the divided clock was already in the file, merged from an earlier project, and it applies as soon as the clock exists: defining the clock is what exposes it. A reader who looked only at the WNS would think the fix achieved nothing, or worse, that E4 and E5 were fine.
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"}
def covered(pairs):
return sorted(n for n, p in PATHS.items() if (p["l"], p["c"]) in pairs and p["l"] in clocks and p["c"] in clocks)
audit = {"FP_ASYNC": (covered(FP_ASYNC), 2), "FP_DIV": (covered(FP_DIV), 0)} # (paths covered, paths expected)
assert audit == {"FP_ASYNC": (["E6", "E7"], 2), "FP_DIV": (["E4", "E5"], 0)}
related_clocks = {"clk_div2": "clk"} # a generated clock and its source are related
suspicious = [name for name, (paths, expected) in audit.items() if len(paths) > expected]
assert suspicious == ["FP_DIV"]
print("PASS: exception audit", audit, "; suspicious:", suspicious)Exception | Covers | Expected | Verdict |
|---|---|---|---|
Between clk and clk_b (both directions) | E6, E7 | 2 (the two synchroniser inputs) | Correct: the clocks are truly asynchronous |
Between clk_div2 and clk (both directions) | E4, E5 | 0 | Wrong: a divided clock is related to its source, so these are real paths |
The first exception covers exactly the two crossings it was written for. The second covers two related paths and was expected to cover none: it should be deleted, and E4 and E5 timed normally. The audit found two defects in order: a missing generated clock (coverage) and an overbroad false path hiding two real paths (exceptions). The third defect, a hold violation at E8, was visible from the start.
E5 would show setup slack of 1.0 - 0.06 - 1.02 = -0.08 ns, so the WNS becomes -0.080 ns.