Skip to content
VLSI Mentor

Tcl · Module 23

Clock Tree Reports

Clock tree synthesis builds the network that delivers the clock. Two numbers say whether it worked, and one of them is derived from the other.

Intermediate16 min readtcledapart-iiphysical-designctsclock-skew

Part II · Module 23 · Chapter 23.4 · Clock Tree Reports

Until clock tree synthesis runs, the clock is an idea. create_clock declared a period and a waveform; nothing yet delivers that waveform to the thousands of flip-flops that need it.

CTS builds the delivery network. Afterwards, two numbers say whether it did what the script asked for.

1. Latency: how long the clock takes to arrive

Every clock sink — every flip-flop clock pin — has an insertion delay: the time from the clock source to that pin, through the buffers CTS inserted.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
foreach {sink latency} [clock_latencies] {
    puts [format "    %-24s %.3f" $sink $latency]
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    u_core/u_ctrl/CLK        0.412
    u_core/u_pipe_reg/CLK    0.398
    u_mem/u_bank0/CLK        0.455
    u_mem/u_bank1/CLK        0.461
    u_io/u_sync/CLK          0.389

Five sinks in a teaching mock; a real design has tens of thousands. The loop is the same either way — foreach over pairs, format to align the columns.

Latency on its own is mostly not the thing you care about. A clock that takes 400 ps to arrive everywhere is fine. What matters is the spread.

2. Skew: the spread across sinks

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc clock_skew {} {
    set lats {}
    foreach {sink l} [clock_latencies] { lappend lats $l }
    set max [lindex [lsort -real -decreasing $lats] 0]
    set min [lindex [lsort -real -increasing $lats] 0]
    return [expr {$max - $min}]
}
puts [format "    skew = max - min = %.3f" [clock_skew]]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    skew = max - min = 0.072

72 ps between the earliest and latest sink. That is the number quoted when someone asks how CTS went.

Notice what the procedure is made of: lappend into a list, lsort -real twice, a subtraction. Nothing in it is EDA-specific. The tool supplies the latencies; the arithmetic is Part I.

3. Skew is computed, not measured

This is the point of the chapter.

This is why the procedure above returns a number and why a script that uses it should say what it computed over:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc skew_report {label pairs} {
    set lats {}
    foreach {s l} $pairs { lappend lats $l }
    set sorted [lsort -real $lats]
    set min [lindex $sorted 0]
    set max [lindex $sorted end]
    puts [format "  %-12s sinks %-5d min %.3f  max %.3f  skew %.3f" \
              $label [llength $lats] $min $max [expr {$max - $min}]]
}

Now the denominator travels with the number — sinks 5 — which is Chapter 22.7 §4's rule applied to a different report.

[lindex $sorted end] rather than a second sort: lsort -real is ascending by default, so one sort gives both ends. The two-sort version in §2 was written to be read, not to be fast.

4. One tree, four skew numbers

§3 is worth running rather than asserting, because the result is more striking than the argument.

Here is the same clock tree — the same five latencies from §1, unchanged — reported first across the whole design and then per block:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc sinks_under {prefix pairs} {
    set out {}
    foreach {s l} $pairs {
        if {[string match $prefix* $s]} { lappend out $s $l }
    }
    return $out
}

set all [clock_latencies]
skew_report whole-design $all
foreach blk {u_core/ u_mem/ u_io/} {
    skew_report $blk [sinks_under $blk $all]
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== one tree, three different skew numbers ===
  whole-design sinks 5   min 0.389  max 0.461  skew 0.072
  u_core/    sinks 2   min 0.398  max 0.412  skew 0.014
  u_mem/     sinks 2   min 0.455  max 0.461  skew 0.006
  u_io/      sinks 1   min 0.389  max 0.389  skew 0.000

72 ps, 14 ps, 6 ps and 0 ps, all from one tree, with nothing rebuilt between the lines.

Which is correct? All of them. Which is useful depends entirely on where the paths are:

  • A path inside u_mem sees 6 ps of skew. The 72 ps headline is irrelevant to it.
  • A path from u_io/u_sync to u_mem/u_bank1 sees the full 72 ps, because those are the two extremes of the tree.
  • u_io/ reports 0 ps — not because that branch is balanced, but because it has one sink. Skew over a single element is zero by construction, and a per-block skew table will quietly contain such entries.

That last point is why skew_report prints sinks 1. A zero with a denominator of one is not an achievement, and without the denominator it looks like the best number on the page.

5. Why skew costs timing on both sides

Worth being precise, because it is a standard interview question and the two sides behave oppositely.

Take a path from launch flop A to capture flop B.

  • Setup gets harder when B's clock arrives earlier than A's. The data has less time to travel, because the capture edge came sooner than nominal.
  • Hold gets harder when B's clock arrives later than A's. The data has longer to travel before B samples, so new data can arrive before B has captured the old.

So skew does not simply "cost margin" — it costs setup margin in one direction and hold margin in the other, which is why a CTS tool cannot fix timing by minimising one number. It is the same two-sided structure as Chapter 22.8's corners, where improving setup at the fast corner worsened hold.

6. Structure: the other half of the report

Skew says what the tree achieved. Structure says how, and that is what you read when the skew number is bad.

A clock tree report typically carries:

  • Levels of buffering from source to sink. Deeper trees are harder to balance and burn more power.
  • Buffer and inverter count, which is where a surprising fraction of a design's dynamic power goes.
  • Maximum transition on clock nets, because a slow clock edge degrades every flop it reaches.
  • Which sinks ended up on which branch, which is how you find out that two flops on the same critical path were put on opposite sides of the tree.

That last item is the one that explains a bad skew number, and it is the reason the per-sink list in §1 is worth keeping rather than collapsing immediately to one number.

7. The gate after CTS

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
clock_tree_synthesis

set skew [clock_skew]
puts [format "  clock skew: %.3f ns (budget %.3f)" $skew $::SKEW_BUDGET]
if {$skew > $::SKEW_BUDGET} {
    puts stderr "  CTS skew exceeds budget — do not route on this tree"
    exit 1
}

update_timing
report_timing -max_paths 20 > reports/post_cts_timing.rpt

Two things in that shape are worth naming.

The skew check comes before timing. If the tree is wrong, the post-CTS timing report describes a tree you are going to rebuild, and reading it is wasted effort.

update_timing appears explicitly. CTS changed every clock arrival in the design, so the timing graph is stale — and Chapter 22.5 is the chapter about exactly how quietly a stale graph produces a plausible report. After CTS is the single most important place in the flow to remember it.

8. Exercises

Exercise 1. What is clock insertion delay, and why is it usually not the number that matters?

Exercise 2. How is skew obtained from the latencies?

Exercise 3. Why is "skew 72 ps" an incomplete statement?

Exercise 4. The same tree reports 72 ps, 14 ps, 6 ps and 0 ps. Which is correct?

Exercise 5. u_io/ reports skew 0.000. Why is that not good news?

Exercise 6. Which skew does timing analysis actually use?

Exercise 7. Does an earlier capture clock help or hurt setup? And hold?

Exercise 8. Why does skew_report print sinks 5?

Exercise 9. Why check skew before reading the post-CTS timing report?

Exercise 10. Why must update_timing be called after CTS?

Where this fits

Part of the Tcl curriculum.