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.
foreach {sink latency} [clock_latencies] {
puts [format " %-24s %.3f" $sink $latency]
} 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.389Five 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
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]] skew = max - min = 0.07272 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:
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:
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]
}=== 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.00072 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_memsees 6 ps of skew. The 72 ps headline is irrelevant to it. - A path from
u_io/u_synctou_mem/u_bank1sees 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
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.rptTwo 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.
