Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • How to Use This Course
  • Reading Path
  • STA Glossary
  • STA Metrics Reference
  • STA Mastery Cheatsheet
  • Timing Paths and Checks16
  • Constraints & SDC Fundamentals20
  • Delay Graph & Path Analysis16
  • Beyond Setup and Hold16
  • Multi-Corner Multi-Mode Signoff16
  • OCV, AOCV, POCV and Variation16
  • Crosstalk and Noise16
  • ECO and Closure16
  • STA Debug Playbook16
  • STA Interview Prep26
  • Capstone Timing Signoff Lab4
    • Capstone Brief
    • Audit the Constraints
    • Classify and Fix
    • Signoff
Loading lesson

‹Capstone Timing Signoff Lab
  1. Topics
  2. Static Timing Analysis
  3. Capstone Timing Signoff Lab
  4. Audit the Constraints

Part 11 · Capstone Timing Signoff Lab · All levels

Audit the Constraints

Count what is checked, define the missing divided clock, and discover that an exception was waiting to hide the real violation.

On this page

    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.

    Step 1: coverage

    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.

    Diagram
    # 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]

    Step 2: re-run and look again

    example.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    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.

    Step 3: audit the exceptions

    example.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    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.

    Common pitfall

    Declaring the audit complete after the coverage fix because the numbers did not change. Unchanged numbers after a coverage fix mean that something absorbed the new endpoints.

    Interview

    Narrate the order: coverage first, exceptions second, because the first can activate the second.

    Check yourself What would the setup WNS be if the second exception were deleted now?

    E5 would show setup slack of 1.0 - 0.06 - 1.02 = -0.08 ns, so the WNS becomes -0.080 ns.

    Related topics

    • Back: Capstone Brief

    • Next: Classify and Fix

    • Constraint Bug Patterns

    Previous

    181 / 183

    Next: Classify and Fix

    Up next

    Up nextClassify and Fix

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Signoff