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
    • Timing-Driven Synthesis
      • Critical-Path Focus
      • Restructuring and Duplication
      • Sizing and Path Groups
      • Reading WNS and TNS
      • Margin and When to Stop
  • Sequential, FSM and Memory19
  • Datapath Synthesis14
  • Area, Power and Test14
  • Verification, ECO and Handoff15
  • Flows and Debugging15
  • Interview and Judgment11
Loading lesson

←Timing-Driven Synthesis
  1. Topics
  2. Logic Synthesis
  3. Constraints and Timing
  4. Timing-Driven Synthesis
  5. Critical-Path Focus
On this page

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

    Critical-Path Focus

    Slack ranks paths so synthesis spends effort on the critical path first.

    A tighter clock that spends cells everywhere is a tool with no sense of priority. Seeing every upsize land on one cone proves the priority is real and tells you where the next bottleneck lives.

    After this lesson you can: split a mapped netlist by output cone, count upsized cells per cone, and recompute worst-path slack from a recorded arrival.

    One long path beside several short ones

    Every design has a slowest path, even when nothing violates. This lesson builds a design where the slowest path is obvious: a chain of AND and XOR stages feeding one output, next to a single AND gate feeding another. The chain is the critical path by construction; the side gate is done in one stage. If timing-driven synthesis works as advertised, a tighter target should spend cells on the chain and leave the side gate alone. The script below tests exactly that: it synthesises the design once, saves the generic gates, then maps the same gates three times against sized.lib, where each cell exists in three sizes, plain, X2 and X4, with a bigger cell faster and larger.

    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
    read_verilog <<EOV
    module critpath(input [7:0] a, input b, output y_long, output y_short);
    wire c0 = a[0] & a[1];
    wire c1 = c0 & a[2];
    wire c2 = c1 & a[3];
    wire c3 = c2 & a[4];
    wire c4 = c3 & a[5];
    wire c5 = c4 ^ b;
    assign y_long = c5 ^ a[6];
    assign y_short = a[6] & a[7];
    endmodule
    EOV
    synth -top critpath -flatten
    design -save generic
    tee -o abc_1000.txt abc -liberty sized.lib -constr typical.constr -D 1000
    tee -o stat_1000.txt stat -liberty sized.lib
    write_json net_1000.json
    select -assert-none t:*_X2 t:*_X4
    design -load generic
    tee -o abc_300.txt abc -liberty sized.lib -constr typical.constr -D 300
    tee -o stat_300.txt stat -liberty sized.lib
    write_json net_300.json
    select -assert-min 1 t:*_X2 t:*_X4 %u
    design -load generic
    tee -o abc_heavy.txt abc -liberty sized.lib -constr heavy.constr -D 300
    tee -o stat_heavy.txt stat -liberty sized.lib
    write_json net_heavy.json
    select -assert-min 1 t:BUF*
    log PASS: a tighter target upsizes only the critical cone

    In a commercial flow the target comes from the clock: asking for a 0.3 ns period is the same idea as the -D 300 run here. Both hand the mapper one number to meet. ABC also needs the load every output drives, which typical.constr gives it as a BUF driver and 0.05 pF on every output; the third run swaps in heavy.constr at 0.2 pF to change only the load. The select lines are the script's assertions: the loose run uses no upsized cell, the tight run uses at least one, and the heavy run inserts at least one buffer.

    A loose target changes nothing

    Against a 1000 ps target the mapper is already comfortable: the recorded delay is 402 ps, so ABC reports that the current delay does not exceed the target and performs no upsizing. The register of that comfort is the cell histogram: 6 cells at area 21, every one of them the smallest size. A loose constraint is a permission to stay small, and the tool takes it. There is no reason to pay for speed nobody asked for.

    Run (recorded)

    Cells

    Area

    Delay reached

    Upsized cells

    -D 1000, typical load

    6

    21

    402 ps

    0

    -D 300, typical load

    6

    33.2

    302 ps

    3

    -D 300, heavy load

    8

    40.4

    386 ps

    4

    Predict first You tighten the target from 1000 ps to 300 ps. Will the mapper upsize cells all over the netlist, or only on one path, and which output benefits?

    Only the long chain feeding y_long changes: 3 cells there grow to X2 or X4 while the single AND2 feeding y_short stays exactly as it is. The side path already arrives early, so spending area on it cannot improve the worst delay.

    A tight target spends only on the critical cone

    At -D 300 the same gates map to the same 6 cells, but the area rises from 21 to 33.2 and the delay falls from 402 ps to 302 ps. The structure barely moved; the drive strength did. Splitting the tight netlist by output cone shows where every upsized cell went: all 3 upsized cells sit in the cone behind y_long, and the cone behind y_short keeps its plain AND2. That split is hand-derived from the recorded netlist file, while every total in the table is a recording.

    Output cone (recorded netlist, hand-derived split)

    Cells in cone

    Upsized at -D 300

    y_long (the chain)

    5

    3

    y_short (the side gate)

    1

    0

    The two-output design mapped for a 1000 ps target: 6 cells, all size 1, area 21.
    The same design for a 300 ps target: 6 cells, area 33.2, with the upsized cells in orange along the chain.

    Read the two pictures as effort maps. Orange marks where the tool spent area, and every orange cell lies on the path that was worst. The side output never turns orange because its slack was never the binding one. This is the whole lesson in one image: this ABC script does not improve the design, it improves the worst path, and the area bill names that path. That single-target behaviour is ABC's, not synthesis in general: commercial tools work on every violating path at once, TNS-driven within a critical range around the worst.

    Slack arithmetic behind the move

    Slack is required time minus arrival time on one path, and the -D target plays the required time here. The program below recomputes the slack of the worst path before and after sizing from the recorded arrivals, rounded to whole picoseconds. Its checks pin both slacks and the direction of the move: negative slack means the path misses its target, and a worse slack after sizing would fail the run.

    example.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import sys
    
    # Slack arithmetic (ps): required time is the -D target used above; arrivals
    # are rounded from the recorded ABC reports in the table above, so a new
    # recording moves every check with it.
    REQUIRED = 300
    ARR_LOOSE = 402
    ARR_TIGHT = 302
    
    slack_loose = REQUIRED - ARR_LOOSE
    slack_tight = REQUIRED - ARR_TIGHT
    if slack_loose != -102:
        print("FAIL: loose slack " + str(slack_loose) + " want -102")
        sys.exit(1)
    if slack_tight != -2:
        print("FAIL: tight slack " + str(slack_tight) + " want -2")
        sys.exit(1)
    if not (slack_tight > slack_loose):
        print("FAIL: tightening did not improve slack")
        sys.exit(1)
    print("PASS: slack at a 300 ps target rises from -102 ps to -2 ps after sizing")

    The worst slack rises from -102 ps to -2 ps: almost closed, paid for with 12.2 extra area units. Note what the number does not say: it says nothing about y_short, whose slack was positive all along. One number per path; effort follows the smallest.

    Break it: the same target under a heavier load

    The third run in the script keeps -D 300 but swaps typical.constr for heavy.constr, quadrupling every output load to 0.2 pF. The recorded result is a different netlist: 8 cells at area 40.4 with a delay of 386 ps. The mapper answers load with buffering, inserting BUF and BUF_X4 where the lighter runs needed no buffer, with all 4 upsized cells in the y_long cone, yet the delay still rises from 302 ps to 386 ps. The load changed the problem faster than sizing could answer it: driving four times the capacitance costs area and still arrives later. A target is never the whole constraint; the load beside it decides how far sizing can reach.

    The same target under a 0.2 pF load: 8 cells, area 40.4, with the inserted buffer and upsized cells highlighted.

    Common mistakes

    Polishing one path far past zero slack while others still fail wastes area. Once the worst path meets its target, the critical path is somewhere else: re-rank before spending again.

    You tighten the clock and area jumps but the worst delay barely moves. Where do you look first?

    • Weak answer: tighten the target further until the delay moves.

    • Strong answer: split the netlist by output cone and count upsized cells per cone, here 3 upsized cells on the y_long chain and none on the side gate, then re-rank, because effort follows the smallest slack and the binding path may have moved.

    • Follow-up: what if both cones had been upsized? Then two paths are near-critical: this ABC script still buys the worst first, while a commercial TNS-driven flow with a critical range would spend on both together.

    Check yourself The tight run reports 6 cells with 3 upsized, all in the y_long cone. Which output was critical, and what would you check next?

    y_long was critical: every upsized cell sits in its cone. Next, re-rank to confirm y_long is still worst at -2 ps: sizing narrowed its slack but left it binding, so the next spend lands on the same cone.

    Check yourself Under the heavy load the delay rises to 386 ps at -D 300 even though area grows. Did sizing fail?

    No. The problem changed: a 0.2 pF load needs buffering first, and the inserted buffer costs area while the heavier capacitance still arrives later. Judge sizing against a fixed load.

    • A tighter target first buys bigger cells on the worst path, not a new structure.

    • A path that already meets its target draws no effort, however tight the rest gets.

    • The load beside the target decides how far sizing can reach; buffering answers load, not depth.

    Rule of thumb: after every target change, count the upsized cells per output cone before judging the result.

    How this was checked

    Every number and picture on this page comes from one Yosys 0.70 run of the script shown, recorded by the course checker (scripts/tools/run-rtl-testbenches.mjs with COURSE=synth): ABC maps a two-output AND-chain-plus-side-gate design against the teaching library sized.lib at -D 1000 and -D 300 with a 0.05 pF load, plus -D 300 with a 0.2 pF load. Cone splits are hand-derived from the recorded netlist files; the slack program recomputes required-minus-arrival from rounded recorded arrivals. The library is fictional, so absolute areas and delays are teaching units.

    • Interview: run a mock interview on this topic

    Related topics

    • Up: Timing-Driven Synthesis

    • Next: Restructuring and Duplication

    • Delay Models and Slack

    Previous

    47 / 139

    Next: Restructuring and Duplication

    Up next

    Up nextRestructuring and Duplication

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error

    See also

    • Sizing and Path Groups
    • Reading WNS and TNS
    • Margin and When to Stop