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

AboutRoadmapCareersInternshipsFAQContactPrivacyTerms
Skip to lesson

Course contents

  • Logic Synthesis Glossary
  • Logic Synthesis Cheatsheet
  • Synthesis Foundations14
  • Logic Optimisation and Mapping20
  • Constraints and Timing15
    • SDC for Synthesis
    • Delay Models and Slack
      • Arrival, Required Time and Slack
      • Setup, Hold and Clock-to-Q
      • Cell Delay Models
      • Wire Delay at Synthesis Time
      • A Static Timing Calculator
    • Timing-Driven Synthesis
  • Sequential, FSM and Memory19
  • Datapath Synthesis14
  • Area, Power and Test14
  • Verification, ECO and Handoff15
  • Flows and Debugging15
  • Interview and Judgment11
Loading lesson

←Delay Models and Slack
  1. Topics
  2. Logic Synthesis
  3. Constraints and Timing
  4. Delay Models and Slack
  5. A Static Timing Calculator
On this page

    Part 3 of 9, lesson 10 of 15Part 3 of 9/Constraints and Timing/Lesson 10 of 15Intermediate

    A Static Timing Calculator

    A runnable static timing calculator over the recorded path.

    Every arrival quoted in this topic was computed by a method nobody had shown. This page shows it: a real calculator over the real path, checked against the real report.

    After this lesson you can: price a path stage by stage from exact library rows with propagated slews, run both timing sweeps, and bound the gap to the tool report.

    The question

    Every arrival, requirement and slack quoted in this topic came from somewhere: this lesson is that somewhere. It builds a small static timing calculator in plain Python that prices the recorded vipexp path stage by stage from the exact library rows, propagates arrivals forward from the launch flop and requirements backward from the capture flop, and reports the slack. Then it holds its own total against the ABC report and explains the gap honestly.

    The calculator embeds the real NLDM rows copied exactly from scripts/tools/synth-libs/course.lib for the five cells on the path: the DFF clock-to-Q and output-slew rows plus the cell_rise and rise_transition rows of NAND2, NOR2, XOR2 and XNOR2. Rise and fall tables coincide entry for entry in this library, so the calculator prices the rising edge and says so where that assumption matters. Nothing is fitted, rounded or invented: each string below matches the library file character for character, and the program parses them at runtime.

    yosys
    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
    37
    38
    39
    40
    read_verilog <<EOV
    module vipexp(input clk, input a, b, c, d, input din, output reg qout, output t1, t2, t3, t4);
      reg q1;
      wire s1, s2, s3, s4;
      always @(posedge clk) q1 <= din;
      assign s1 = ~(q1 & a);
      assign s2 = ~(s1 | b);
      assign s3 = s2 ^ c;
      assign s4 = ~(s3 ^ d);
      always @(posedge clk) qout <= s4;
      assign t1 = s1;
      assign t2 = s2;
      assign t3 = s3;
      assign t4 = s4;
    endmodule
    EOV
    hierarchy -check -top vipexp
    synth -top vipexp -flatten
    dfflegalize -cell $_DFF_P_ 01 -cell $_DFF_PN0_ 01
    design -save generic
    dfflibmap -liberty course.lib
    tee -o abc_typ.txt abc -liberty course.lib -constr typical.constr -D 1000
    clean
    tee -o stat_typ.txt stat -liberty course.lib
    write_json net_typ.json
    select -assert-none t:$not t:$and t:$or t:$xor t:$mux t:$_NOT_ t:$_AND_ t:$_OR_ t:$_XOR_ t:$_NAND_ t:$_NOR_ t:$_DFF_P_ t:$_MUX_
    select -assert-count 2 t:DFF
    select -assert-count 1 t:NAND2
    select -assert-count 1 t:NOR2
    select -assert-count 1 t:XOR2
    select -assert-count 1 t:XNOR2
    check
    design -load generic
    dfflibmap -liberty course.lib
    tee -o abc_heavy.txt abc -liberty course.lib -constr heavy.constr -D 1000
    clean
    select -assert-count 2 t:DFF
    select -assert-min 4 t:BUF
    check
    log PASS: calculator path mapped under two output loads

    The script maps the familiar vipexp cover twice: the typical run every lesson prices, reporting 4 cells at area 14 with arrival 610 ps, and the heavy remap from the wire lesson at 8 cells, area 22 and 1230 ps. The calculator prices only the typical cover; the heavy run is its break-it, and the last section shows why.

    Predict first The calculator adds the same ingredients ABC used: the same tables, one pin load plus one tap load per stage, propagated slews. Will its total match the recorded 610 ps, and which side should land higher?

    Close but not equal, and the calculator should land a touch higher. Its combinational sum is 0.617 ns against 0.61 ns recorded, a gap of a few picoseconds. The ingredients match on purpose; the remaining gap is driver context and endpoint choice, itemised below, not a fudge factor.

    Forward and backward passes

    Static timing is two sweeps over the path. The forward pass takes the latest incoming arrival plus the stage delay at each node, which is how the launch clock edge turns into the 0.78 ns arrival at the capture flop. The backward pass takes the tightest successor requirement minus the stage delay, which is how the 1.00 ns clock edge minus setup turns into the requirement at every earlier node. Slack at each node follows the topic convention, and the nodes with the smallest slack form the critical path.

    On a single serial path both sweeps are simple addition, so the program below shows its work per stage instead of hiding it in a loop: clock-to-Q first, then one interpolated delay and one interpolated output slew per stage, accumulating the arrival column the timing-bars figure in the arrival lesson draws. A real tool repeats the same two sweeps over a DAG with max and min at every fanin and fanout; the arithmetic per arc is identical.

    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
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    import sys
    
    # NLDM rows copied exactly from scripts/tools/synth-libs/course.lib.
    # Each string matches the library file character for character; the program
    # parses them, so a transcription slip fails the table-point checks below.
    DFF_CQ = 'values("0.15,0.17","0.18,0.21")'
    DFF_TR = 'values("0.02,0.04","0.04,0.08")'
    NAND2_D = 'values("0.08,0.1","0.11,0.14")'
    NOR2_D = 'values("0.09,0.11","0.12,0.15")'
    XOR2_D = 'values("0.18,0.2","0.21,0.24")'
    XNOR2_D = 'values("0.18,0.2","0.21,0.24")'
    TR = 'values("0.02,0.04","0.04,0.08")'
    SETUP = 'values("0.05,0.06","0.06,0.07")'
    
    CLK_SLEW = 0.05   # hand assumption, labelled model
    PERIOD = 1.0      # hand-derived SDC-style period, labelled model
    PIN_LOAD = 0.01   # library input-pin capacitance, from course.lib
    TAP_LOAD = 0.05   # typical.constr output load, recorded constraint
    STAGE_LOAD = PIN_LOAD + TAP_LOAD
    RECORDED_TYP_NS = 0.610  # ABC typical-load delay, templated from the run
    # Full-precision hand arithmetic for each stage (arrival, delay, out slew).
    HAND = [
        ("clk-to-Q", 0.1633, 0.1633, 0.0289),
        ("NAND2", 0.2619, 0.0986, 0.0376),
        ("NOR2", 0.3739, 0.1120, 0.0407),
        ("XOR2", 0.5772, 0.2032, 0.0417),
        ("XNOR2", 0.7808, 0.2036, 0.0421),
    ]
    
    def parse(rows):
        inner = rows[len('values("'):-len('")')]
        return [[float(x) for x in pair.split(",")] for pair in inner.split('","')]
    
    def bilin(table, slew, load):
        (v00, v01), (v10, v11) = table
        fs = (slew - 0.01) / 0.09
        fl = (load - 0.01) / 0.09
        return v00 * (1 - fs) * (1 - fl) + v01 * (1 - fs) * fl + v10 * fs * (1 - fl) + v11 * fs * fl
    
    def close(got, want, tol=0.001):
        return abs(got - want) <= tol
    
    CQ, DTR = parse(DFF_CQ), parse(DFF_TR)
    STAGES = [("NAND2", parse(NAND2_D)), ("NOR2", parse(NOR2_D)),
              ("XOR2", parse(XOR2_D)), ("XNOR2", parse(XNOR2_D))]
    PTR, SET = parse(TR), parse(SETUP)
    
    bad = 0
    def check(name, cond):
        global bad
        if not cond:
            print("FAIL: " + name)
            bad += 1
    
    # The tables must behave like tables: exact at grid points, monotone both ways.
    for name, table in [("DFF", CQ), ("NAND2", STAGES[0][1]), ("XNOR2", STAGES[3][1])]:
        v00, v01 = table[0]
        check(name + " corner returns entry", abs(bilin(table, 0.01, 0.01) - v00) < 1e-9)
        check(name + " monotone in load", bilin(table, 0.05, 0.1) > bilin(table, 0.05, 0.01))
        check(name + " monotone in slew", bilin(table, 0.1, 0.05) > bilin(table, 0.01, 0.05))
    
    arrival = bilin(CQ, CLK_SLEW, PIN_LOAD)
    slew = bilin(DTR, CLK_SLEW, PIN_LOAD)
    got = [("clk-to-Q", arrival, arrival, slew)]
    for name, table in STAGES:
        delay = bilin(table, slew, STAGE_LOAD)
        arrival = arrival + delay
        slew = bilin(PTR, slew, STAGE_LOAD)
        got.append((name, arrival, delay, slew))
    
    for (name, arr, delay, out), (hname, harr, hdelay, hslew) in zip(got, HAND):
        check(name + " arrival", close(arr, harr))
        check(name + " delay", close(delay, hdelay))
        check(name + " out slew", close(out, hslew))
    
    combo = got[-1][1] - got[0][1]
    check("combinational total", close(combo, 0.617, 0.002))
    # The recording this model must track: ABC's typical-load delay, templated
    # from the run, never typed. A model that drifts from the tool fails here.
    check("model tracks the recording", abs(combo - RECORDED_TYP_NS) < 0.02)
    setup = bilin(SET, slew, CLK_SLEW)
    required = PERIOD - setup
    slack = required - got[-1][1]
    check("setup time", close(setup, 0.058))
    check("required time", close(required, 0.942))
    check("setup slack", close(slack, 0.161))
    # Backward pass: the endpoint requirement walks back toward the launch,
    # shedding one stage delay per node; slack at each node is required minus
    # arrival, and on this serial path every node agrees with the endpoint.
    node_req = [0.0] * len(got)
    node_req[-1] = required
    for i in range(len(got) - 2, -1, -1):
        node_req[i] = node_req[i + 1] - got[i + 1][2]
    for (name, arr, delay, out), req in zip(got, node_req):
        check(name + " backward slack", close(req - arr, slack))
    check("worst slack at capture", close(min(req - arr for (_, arr, _, _), req in zip(got, node_req)), slack))
    if bad != 0:
        sys.exit(1)
    print("PASS: calculator arrival 0.78 ns and slack plus 0.16 ns on 35 checks")

    Model against recording

    The program's combinational sum is 0.617 ns of model delay. ABC reports 0.61 ns on the same cover. Show both numbers, then explain the gap instead of hiding it: the two totals are computed from the same tables at the same loads, yet they differ by a few picoseconds, and each item below names one reason why.

    Source

    Combinational delay

    What it covers

    Calculator (model)

    0.617 ns

    Four interpolated stage delays at propagated slews and 0.06 pF per stage.

    ABC report (recorded)

    0.61 ns

    Worst over all endpoints at ABC's own propagated slews, rounded to picoseconds; here that endpoint is the t4 tap net, which the recorded netlist shows the capture flop's D input shares.

    The model sum lands just above the recorded ABC delay from the table: same tables, same loads, a few picoseconds apart.

    Three honest reasons for the gap, in size order. First, the input context: ABC propagates the real BUF-driver slew from typical.constr into the first stage, while the calculator assumes a 0.05 ns clock edge everywhere upstream. Second, the endpoint choice: ABC maximises over every endpoint including the taps, whose loads match the capture path here but need not in general, and over rise and fall arcs that coincide in this library. Third, rounding: ABC prints picoseconds, so each arc carries up to half a picosecond of rounding the model never sees. None of these is a fudge factor, and together they bound the gap instead of explaining it away.

    Read the closeness itself with suspicion: landing within a few picoseconds is luck as much as method, and changing any assumption (the clock slew, the tap loads, the endpoint) moves the model total. What transfers to the next design is not the 0.007 ns gap but the method: same library rows, same load accounting, propagated slews, asserts that fail when any of those drift.

    Break it: remap under the heavy load

    The heavy run in the script is the deliberate breakage, and it breaks the calculator rather than the design. Under heavy.constr the recorded cover grows a BUF per tap and the recorded arrival moves to 1230 ps, while the calculator still sums its four unbuffered stages to the same total. The model is not wrong about the circuit it prices; it prices a circuit that no longer exists. A timing model is tied to its cover: remap, and re-derive before quoting a single stage delay.

    Common mistakes

    Quoting the calculator total as the path arrival forgets the flop: ABC's number starts at the launch output, while the arrival at the capture pin adds clock-to-Q on top. The lesson's own totals keep them apart for exactly this reason: 0.62 ns of combinational delay plus 0.16 ns of clock-to-Q is 0.78 ns of arrival.

    Your model says 0.62 ns and the tool says 0.61 ns. Which do you trust?

    Weak: the tool, because it is always right. Strong: both, for different jobs: the recorded number is the tool's mapping estimate for this cover, while the model shows its work stage by stage (0.10, 0.11, 0.20 and 0.20 ns after 0.16 ns of clock-to-Q) and bounds its own gap to driver context, endpoint choice and rounding. Trust the estimate for the verdict on this cover and the model for the diagnosis. Follow-up: the remap adds buffers. What happens to the model?

    Answer: it stops applying: the cover changed, so the four-stage sum no longer describes the path, and quoting it would be precise, stale and wrong.

    Check yourself All 35 program checks pass. What does that prove about the library strings and the hand arithmetic?

    The strings match course.lib character for character (a transcription slip would fail the grid-point checks), the hand stage values match the interpolation within a picosecond, and the model total tracks the recording. It proves the model is faithful to its inputs, not that its assumptions match silicon.

    Check yourself The heavy remap moves the recording to 1230 ps but the calculator still prints its four-stage sum. Is the calculator broken?

    No: it prices the typical cover, which the remap replaced. The gap between its total and the new recording measures the cover change (four added buffers), not a bug in the arithmetic.

    • Same tables, same loads, propagated slews: that is the whole method, and the asserts pin it.

    • The model total and the ABC report differ by driver context, endpoint choice and rounding, not by fudge.

    • A timing model is tied to its cover: remap, and re-derive before quoting anything.

    Rule of thumb: show the gap beside the match, and name what explains it.

    How this was checked

    Areas, arrivals and cell counts on this page come from one Yosys run of the script shown, recorded by the course checker (scripts/tools/run-rtl-testbenches.mjs with COURSE=synth): the vipexp block mapped with ABC against course.lib at a 1000 ps target with typical.constr and heavy.constr. The library is fictional, so absolute delays are teaching values. Every stage delay, arrival, requirement and slack is a model number interpolated from the exact library rows by the stdlib-only program shown, which asserts each figure it prints; the heavy-load remap is the recorded case the model no longer describes.

    • Interview: run a mock interview on this topic

    Related topics

    • Arrival, Required Time and Slack

    • Wire Delay at Synthesis Time

    • Cell Libraries and Liberty

    Previous

    46 / 139

    Next: Timing-Driven Synthesis

    Up next

    Up nextTiming-Driven Synthesis

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error

    See also

    • Arrival, Required Time and Slack
    • Setup, Hold and Clock-to-Q
    • Cell Delay Models