The capstone is a paper signoff of a small block. A constraint file has been handed over with a timing report that looks nearly clean. Your job is to find out what the report does not tell you, fix what is really wrong, and write a signoff statement a reviewer can check. Three defects are planted, and the numbers are small enough that a runnable engine of about thirty lines computes every slack. Everything here is illustrative.
A block runs on a 1.0 ns clock clk. A divider produces clk_div2 (period 2.0 ns) for a slow register bank. A second clock clk_b (period 1.3 ns) is truly asynchronous and enters through synchronisers. Eight endpoints matter. Setup time is 0.06 ns and hold time 0.05 ns everywhere; sk is the capture clock latency minus the launch clock latency.
Endpoint | Launch to capture | Max delay (ns) | Min delay (ns) | Skew sk (ns) |
|---|---|---|---|---|
E1 | clk to clk | 0.82 | 0.20 | +0.02 |
E2 | clk to clk | 0.91 | 0.16 | 0.00 |
E3 | clk to clk | 0.90 | 0.12 | -0.03 |
E4 | clk to clk_div2 | 0.75 | 0.30 | 0.00 |
E5 | clk_div2 to clk | 1.02 | 0.30 | 0.00 |
E6 | clk to clk_b (synchroniser input) | 0.40 | 0.10 | 0.00 |
E7 | clk_b to clk (synchroniser input) | 0.55 | 0.10 | 0.00 |
E8 | clk to clk | 0.90 | 0.06 | +0.05 |
# 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]
set_false_path -from [get_clocks clk] -to [get_clocks clk_b]
set_false_path -from [get_clocks clk_b] -to [get_clocks clk]
set_false_path -from [get_clocks clk_div2] -to [get_clocks clk]
set_false_path -from [get_clocks clk] -to [get_clocks clk_div2]The engine computes setup slack as the capture-edge budget plus skew minus setup time minus the maximum delay, and hold slack as the minimum delay minus hold time minus skew. A pair of unrelated clocks gets an arbitrary pessimistic placeholder budget of 0.3 ns, not the real closest-edge gap; an endpoint on a clock that is not defined is unconstrained; a path covered by a false path is excepted.
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())}
res = analyse(PATHS, {"clk", "clk_b"}, [FP_ASYNC, FP_DIV]) # as received: clk_div2 is not defined
s = summary(res)
assert res["E1"] == (0.14, 0.13) and res["E3"] == (0.01, 0.1) and res["E8"] == (0.09, -0.04)
assert res["E4"] == "unconstrained" and res["E5"] == "unconstrained"
assert res["E6"] == "excepted" and res["E7"] == "excepted"
assert s == {"checked": 4, "excepted": 2, "unconstrained": 2, "wns_setup": 0.01, "wns_hold": -0.04}
print("PASS: as received", s)Measure | As received |
|---|---|
Endpoints checked | 4 of 8 |
Excepted by false paths | 2 (E6, E7) |
Unconstrained | 2 (E4, E5) |
Setup WNS | +0.010 ns (E3) |
Hold WNS | -0.040 ns (E8) |
Setup looks clean at +0.010 ns and the only visible violation is a 40 ps hold failure at E8. Before reading on, predict what is wrong: how many of the eight endpoints are really being checked, what do the four false-path lines cover, and is the hold fix at E8 as simple as it looks?
Audit the constraints: coverage and exceptions (next lesson).
Classify what the audit finds and fix it with the right method (lesson three).
Assemble the signoff: final table, checklist, statement (lesson four).
Two endpoints have no check at all and two are excepted, so the number describes only four of the eight.