A semantic diff of the old and new constraint sets shows one high-risk addition among the harmless reordering: a false path between the two clocks, imported from the earlier project where they were unrelated.
# not-run: SDC is not executable here
# added during the refactor (imported snippet)
set_false_path -from [get_clocks clk_a] -to [get_clocks clk_b]# paths from clk_a to clk_b: (name, truly asynchronous?, slack with the exception removed, ns)
paths = [("p%d" % i, False, 0.2 + 0.0002 * i) for i in range(1, 1118)]
paths += [("q1", False, -0.050), ("q2", False, -0.018), ("q3", False, -0.004)]
covered = len(paths)
assert covered == 1120
violations = sorted((p[0], p[2]) for p in paths if p[2] < 0)
assert violations == [("q1", -0.05), ("q2", -0.018), ("q3", -0.004)]
async_paths = [p for p in paths if p[1]]
assert len(async_paths) == 0
print("PASS: the exception covers", covered, "paths; truly asynchronous:", len(async_paths), "; violating once removed:", violations)Measure | Value |
|---|---|
Paths covered by the exception | 1,120 |
Of which truly asynchronous | 0 (clk_b is generated from clk_a) |
Violations when the exception is removed | 3 (worst -0.050 ns) |
Removing the exception and re-running shows three violations on related paths, worst -0.050 ns, exactly the -0.050 ns that disappeared in the refactor. None of the 1,120 paths is truly asynchronous: the clocks are generated, so every path between them is a related path. The silicon failure is one of the three. It is intermittent because the path is only sensitised by a rare data pattern, and hot is where its delay is worst. The ghost was a constraint that deleted real paths.
generated_from = {"clk_b": "clk_a", "clk_c": "clk_b"} # generated clock -> its source clock
exceptions = [("false_path", "clk_a", "clk_b"), ("false_path", "clk_x", "clk_y"), ("false_path", "clk_a", "clk_c")]
def related(c1, c2):
def root(c):
while c in generated_from:
c = generated_from[c]
return c
return root(c1) == root(c2)
flagged = [e for e in exceptions if related(e[1], e[2])]
assert flagged == [("false_path", "clk_a", "clk_b"), ("false_path", "clk_a", "clk_c")]
print("PASS: exceptions between related clocks:", flagged)A simple lint walks the generated-clock tree and flags any false path between two clocks with the same root. In this example it finds two: the one that caused the failure and a similar one on clk_c, a second divider. Both are suspicious until proven otherwise.
The pre-refactor WNS was the same set of related paths; the refactor simply removed them from the analysis, so removing the exception restores them.