An exception is a promise that a path does not need to meet timing, and a wrong promise is silicon that fails at speed after passing signoff. A common integration failure is exactly this: a false path covering a real path, or a multicycle valid for only one clock ratio, both green in the reports and red in the lab. A multicycle written for today's 8-to-1 CPU-to-peripheral ratio silently becomes wrong the day the PLL plan or a divider changes; the exception must name the ratio it holds for and the check that fails when the ratio moves. The review below keeps exceptions honest.
Every exception carries four fields besides the path itself: the reason the path cannot matter functionally, the evidence proving the reason, usually a CDC report, a protocol argument or a constrained-random test, the scope of modes and ratios it holds for, and an expiry no further than the next signoff milestone. The review reads each exception against its evidence and refuses on sight: exceptions without evidence, exceptions that are active in a shipped mode the evidence does not cover (that mode would leave the path untimed), exceptions copied from a block whose paths changed at the top, and false paths written over multi-bit data. On NanoSoC the last case is the APB bridge: the single-bit request and acknowledge toggles entering the first synchroniser flop are a protocol-reviewed crossing and can be false paths, but the held address and write-data buses are multi-bit and need a maximum-delay bound, not a blanket false path.
Exception | Reason | Evidence | Scope | Verdict |
|---|---|---|---|---|
False path: req/ack toggle into the first synchroniser flop | Single-bit crossing, not a timed path | Bridge protocol review | All modes | Keep |
Multicycle 2: status to INTC | Slow-domain sample | None given | All modes | Refuse: no evidence |
False path: test mux select | Static in mission mode | Mode-matrix case | Mission only | Refuse: test scenario has no constraint for this path, so it is untimed there; wait for test-mode timing or a test-mode exception with evidence |
Block copy: DMA status path | Inherited from block SDC | Block report | Block modes | Re-qualify at top |
APB data bus (paddr, pwdata) held until acknowledged | Multi-bit data, stable before the toggle | Bridge protocol review | All modes | Refuse as a false path: needs a maximum-delay bound |
The audit below holds the four fields as a list of dictionaries, applies the rules in the order a reviewer would, and requires exactly one survivor. It then moves the ratio and the expiry to show those rules can refuse too.
PLAN_RATIO = 8 # clk_cpu : clk_per today (NanoSoC teaching plan)
def verdict(e, plan_ratio=PLAN_RATIO):
if e.get("origin") == "block" and not e.get("requalified"):
return "re-qualify at top"
if not e.get("evidence"):
return "refuse: no evidence"
if e["kind"] == "false_path" and e.get("bits", 1) > 1:
return "refuse: multi-bit path needs a maximum-delay bound"
if e["active_modes"] - e["proven_modes"]:
return "refuse: mode without evidence"
if e.get("ratio") not in (None, plan_ratio):
return "refuse: ratio changed"
if e["expiry_days"] <= 0:
return "refuse: expired"
return "keep"
EXC = [
{"name": "req/ack toggle", "kind": "false_path", "bits": 1,
"evidence": "bridge protocol review", "active_modes": {"mission", "test"},
"proven_modes": {"mission", "test"}, "expiry_days": 90},
{"name": "status to INTC", "kind": "multicycle", "evidence": None,
"active_modes": {"mission"}, "proven_modes": {"mission"}, "expiry_days": 90},
{"name": "test mux select", "kind": "false_path", "evidence": "mode-matrix case",
"active_modes": {"mission", "test"}, "proven_modes": {"mission"}, "expiry_days": 90},
{"name": "DMA status path", "kind": "false_path", "origin": "block",
"evidence": "block report", "active_modes": {"mission"},
"proven_modes": {"mission"}, "expiry_days": 90},
{"name": "apb data bus", "kind": "false_path", "bits": 32,
"evidence": "bridge protocol review", "active_modes": {"mission", "test"},
"proven_modes": {"mission", "test"}, "expiry_days": 90},
]
got = [verdict(e) for e in EXC]
assert got == ["keep", "refuse: no evidence", "refuse: mode without evidence",
"re-qualify at top", "refuse: multi-bit path needs a maximum-delay bound"], got
assert got.count("keep") == 1 # exactly one survivor
mc = {"name": "per-ratio multicycle", "kind": "multicycle", "ratio": 8,
"evidence": "ratio-8 timing argument", "active_modes": {"mission"},
"proven_modes": {"mission"}, "expiry_days": 30}
assert verdict(mc) == "keep"
assert verdict(mc, plan_ratio=6) == "refuse: ratio changed" # the plan moved
assert verdict(dict(mc, expiry_days=0)) == "refuse: expired"
print("PASS: 5 exceptions audited, 1 kept; ratio change and expiry are refused")The four fields filled for every exception, with evidence the reviewer can open, not a reference to a hallway conversation.
Scope covering every shipped mode and ratio; a multicycle valid for one ratio names that ratio and the check that enforces it.
Expiry dates in the future with owners; an expired exception fails the gate exactly like an expired waiver.
Re-run the exception audit before every signoff milestone, not just at tapeout: modes added late silently narrow scopes written early, and a changed clock ratio invalidates every multicycle written for the old one. When silicon fails at speed on a path the reports called green, read the exception list first; the wrong promise is usually one row with a plausible reason and no evidence. For clock-gating checks that interact with exceptions see Clock Gating Checks.
Refuse until the test scenario times the path or carries its own evidenced exception; as written the test scenario has no constraint for it, so the path is untimed there. A shipped mode outside the scope is the exact shape of a false path that hides a real path.