awk Part III · Module 10 · Timing and STA Report Analysis
Grouping Paths by Clock
Per-clock aggregation looks like the easiest thing in the module: the Path Group line holds a clock name, so group on it. The difficulty is that a timing path names up to three clocks — the one that launched the data, the one that captures it, and the group the tool filed the path under — and on a cross-domain path they are not the same. This chapter builds the summary three ways on one report and gets three different answers, including a clock that owns a violation under one grouping and has no paths at all under another, and a fourth clock that launches the report's worst violation while appearing in no path group.
Advanced19 min readawkPart IIISTAClock DomainsGrouping
Part III · Module 10 · Chapter 10.6 · Grouping Paths by Clock
1. Why This Matters
A per-clock timing summary is the table a design review opens with: how many paths in each clock domain, how many violating, and the worst slack in each. It is also the table most likely to be built on an assumption nobody states.
The assumption is that a path has a clock. It does not. A path has a launch clock at its startpoint, a capture clock at its endpoint, and a path group that the tool assigned it to — and on any path that crosses between domains, which is where the hard timing problems live, those are different names. Grouping on one of them is a choice, and this chapter shows that the three choices produce genuinely different reports from the same file.
2. The Easy Case
On the three-path report from Chapter 10.1, where every path stays in one domain, the obvious thing works:
awk '/^ *Path Group:/ { g = $3 } /^ *slack/ { n[g]++; if ($2 == "(VIOLATED)") v[g]++; if (!(g in w) || $3 + 0 < w[g] + 0) w[g] = $3; if (!(g in seen)) { seen[g]=1; o[++k]=g } g = "" } END { for (i=1;i<=k;i++) { gg=o[i]; printf "%-10s %d path(s), %d violating, worst %s\n", gg, n[gg], v[gg]+0, w[gg] } }' timing.rptclk_core 2 path(s), 1 violating, worst -0.018
clk_io 1 path(s), 1 violating, worst -0.023Two domains, one violation each. The !(g in seen) guard records first-seen order — Chapter 10.5 section 9's construction — and !(g in w) seeds each per-group minimum, which is Chapter 10.5 section 3's lesson applied per key rather than once.
That command is correct for this report and it will keep being correct right up to the first report containing a path that crosses domains.
3. The Clock Is Named in Two Other Places
The clock name appears in the continuation lines under the startpoint and the endpoint:
grep "clocked by" timing.rpt (rising edge-triggered flip-flop clocked by clk_core)
(rising edge-triggered flip-flop clocked by clk_core)
(rising edge-triggered flip-flop clocked by clk_core)
(rising edge-triggered flip-flop clocked by clk_core)
(rising edge-triggered flip-flop clocked by clk_io)
(rising edge-triggered flip-flop clocked by clk_io)Six lines for three paths — two per path, one for each end. Extracting the name means taking the last field and dropping the closing parenthesis:
awk '/clocked by/ { c = $NF; gsub(/[()]/, "", c); printf "[%s]", c } END { print "" }' timing.rpt[clk_core][clk_core][clk_core][clk_core][clk_io][clk_io]On this report every pair matches, so the distinction has nothing to show. The next section gives it something.
4. A Cross-Domain Report
Startpoint: u_regfile/dout_reg[3]
(rising edge-triggered flip-flop clocked by clk_core)
Endpoint: u_alu/sum_reg[7]
(rising edge-triggered flip-flop clocked by clk_core)
Path Group: clk_core
Path Type: max
slack (VIOLATED) -0.018
Startpoint: u_periph/req_reg[0]
(rising edge-triggered flip-flop clocked by clk_periph)
Endpoint: u_mem/wdata_reg[0]
(rising edge-triggered flip-flop clocked by clk_ddr)
Path Group: clk_ddr
Path Type: max
slack (VIOLATED) -0.205
Startpoint: u_io/pad_reg[0]
(rising edge-triggered flip-flop clocked by clk_io)
Endpoint: u_io/out_reg[0]
(rising edge-triggered flip-flop clocked by clk_io)
Path Group: clk_io
Path Type: min
slack (VIOLATED) -0.023Pulling all three clock notions out of each path:
awk '/^ *Startpoint:/ { stage = "launch" } /^ *Endpoint:/ { stage = "capture" } /clocked by/ { c = $NF; gsub(/[()]/, "", c); if (stage == "launch") lc = c; else cc = c } /^ *Path Group:/ { g = $3 } /^ *slack/ { printf "group=%-10s launch=%-11s capture=%-10s %8s [%s]\n", g, lc, cc, $3, (lc == cc ? "same domain" : "CROSS-DOMAIN"); lc=""; cc=""; g=""; stage="" }' hidden.rptgroup=clk_core launch=clk_core capture=clk_core -0.018 [same domain]
group=clk_ddr launch=clk_periph capture=clk_ddr -0.205 [CROSS-DOMAIN]
group=clk_io launch=clk_io capture=clk_io -0.023 [same domain]The stage variable is the technique: the Startpoint: and Endpoint: markers set which end the next clocked by line belongs to. Section 7 is about how fragile that is.
The middle path is the interesting one. It launches from clk_periph, is captured by clk_ddr, is filed under path group clk_ddr, and holds the worst slack in the report at −0.205. Which clock domain does that violation belong to? There are three defensible answers and the next section shows they are not interchangeable.
5. Three Clocks, Three Summaries
Two of those three summaries, built on hidden.rpt:
printf "by Path Group: "; awk '/^ *Path Group:/ { g = $3 } /^ *slack/ { n[g]++; if (!(g in s)) { s[g]=1; o[++k]=g } g="" } END { for (i=1;i<=k;i++) printf "[%s=%d]", o[i], n[o[i]]; print "" }' hidden.rpt
printf "by launch clock: "; awk '/^ *Startpoint:/ { stage="launch" } /clocked by/ { if (stage=="launch") { c=$NF; gsub(/[()]/,"",c); lc=c; stage="" } } /^ *slack/ { n[lc]++; if (!(lc in s)) { s[lc]=1; o[++k]=lc } lc="" } END { for (i=1;i<=k;i++) printf "[%s=%d]", o[i], n[o[i]]; print "" }' hidden.rptby Path Group: [clk_core=1][clk_ddr=1][clk_io=1]
by launch clock: [clk_core=1][clk_periph=1][clk_io=1]Two per-clock summaries of one report, naming different sets of clocks. clk_ddr exists only in the first and clk_periph only in the second, and both are labelled "paths per clock".
6. A Clock That Appears in No Path Group
The stronger version of the same problem. Which clocks does the report mention?
awk '/^ *Path Group:/ { print " " $3 }' hidden.rpt | sort -u clk_core
clk_ddr
clk_ioawk '/clocked by/ { c = $NF; gsub(/[()]/, "", c); print " " c }' hidden.rpt | sort -u clk_core
clk_ddr
clk_io
clk_periphThree path groups, four clocks. clk_periph is in the report, launches its worst violation, and is absent from every grouping built on Path Group.
So a per-clock summary has a prerequisite that is easy to skip: enumerate the clocks from the continuation lines, not from the path groups, and then say which of them the summary covers. A table with three rows on a report naming four clocks is incomplete regardless of how correct each row is.
awk '/^ *Path Group:/ { pg[$3] = 1 } /clocked by/ { c = $NF; gsub(/[()]/, "", c); ck[c] = 1 } END { for (c in ck) if (!(c in pg)) miss = miss " " c; printf "clock(s) named in the report but in no path group:%s\n", (miss == "" ? " none" : miss) }' hidden.rptclock(s) named in the report but in no path group: clk_periphawk '/^ *Path Group:/ { pg[$3] = 1 } /clocked by/ { c = $NF; gsub(/[()]/, "", c); ck[c] = 1 } END { for (c in ck) if (!(c in pg)) miss = miss " " c; printf "clock(s) named in the report but in no path group:%s\n", (miss == "" ? " none" : miss) }' timing.rptclock(s) named in the report but in no path group: noneOne rule, and it answers the question the summary cannot: is there a clock here I am not reporting on? Note that for (c in ck) is used only to accumulate into a string whose content does not depend on the order — the same reasoning as Chapter 10.4 section 8, where the order mattered and explicit keys were required. With one missing clock the order is unobservable; with several it would need sorting.
7. The Positional Parse Is Fragile
The stage variable in section 4 works by position: the first clocked by line after a Startpoint: marker is the launch clock. Remove the Endpoint: marker line while leaving its continuation line — the shape a header-wording change produces:
grep -v "Endpoint: u_mem" hidden.rpt > noend.rpt
printf "cleared: "; awk '/^ *Startpoint:/ { stage = "launch" } /^ *Endpoint:/ { stage = "capture" } /clocked by/ { c = $NF; gsub(/[()]/, "", c); if (stage == "launch") lc = c; else if (stage == "capture") cc = c } /^ *slack/ { printf "[%s->%s]", (lc==""?"?":lc), (cc==""?"?":cc); lc=""; cc=""; stage="" } END { print "" }' noend.rpt
printf "stale: "; awk '/^ *Startpoint:/ { stage = "launch" } /^ *Endpoint:/ { stage = "capture" } /clocked by/ { c = $NF; gsub(/[()]/, "", c); if (stage == "launch") lc = c; else cc = c } /^ *slack/ { printf "[%s->%s]", (lc==""?"?":lc), (cc==""?"?":cc) } END { print "" }' noend.rptcleared: [clk_core->clk_core][clk_ddr->?][clk_io->clk_io]
stale: [clk_core->clk_core][clk_ddr->clk_core][clk_io->clk_io]Both are wrong, and it is worth being exact about how.
The middle path's launch clock is reported as clk_ddr in both. With no Endpoint: marker, stage was still launch when the second continuation line arrived, so the capture clock overwrote the launch clock. clk_periph is gone from both outputs.
Clearing the state still helps, but less than it did in Chapters 10.2 and 10.4: it turns the capture clock into ?, which at least says something is missing here. The uncleared version instead reports clk_ddr -> clk_core, a cross-domain path between two clocks that has no counterpart in the design — it inherited path one's capture clock.
8. Count the Markers, Not the Continuation Lines
The obvious check is per path: each path should have two clocked by lines.
for f in hidden.rpt noend.rpt; do printf "%-12s " "$f"; awk '/^ *Startpoint:/ { nc = 0 } /clocked by/ { nc++ } /^ *slack/ { printf "[path %d: %d clock line(s)%s]", ++p, nc, (nc == 2 ? "" : " BAD"); nc = 0 } END { print "" }' $f; donehidden.rpt [path 1: 2 clock line(s)][path 2: 2 clock line(s)][path 3: 2 clock line(s)]
noend.rpt [path 1: 2 clock line(s)][path 2: 2 clock line(s)][path 3: 2 clock line(s)]The check passes on the broken file. Both have two continuation lines per path, because what was removed was the Endpoint: marker and not the line describing the endpoint's clock. A check counting the lines that carry the data cannot see a missing line that carries the structure.
Counting the markers does see it:
for f in hidden.rpt noend.rpt; do printf "%-12s " "$f"; awk '/^ *Startpoint:/ { ns++ } /^ *Endpoint:/ { ne++ } /clocked by/ { nc++ } /^ *slack/ { nk++ } END { printf "start=%d end=%d clocklines=%d slack=%d -> %s\n", ns+0, ne+0, nc+0, nk+0, (ns == ne && nc == 2 * nk ? "consistent" : "INCONSISTENT") }' $f; donehidden.rpt start=3 end=3 clocklines=6 slack=3 -> consistent
noend.rpt start=3 end=2 clocklines=6 slack=3 -> INCONSISTENTThree startpoints and two endpoints. The contract has three parts and each catches something different:
ns == ne— every path has both ends named. This is the one that fires here.nc == 2 * nk— every path has exactly two clock lines. Catches a missing continuation line, which the per-path version also catches.ns == nkfrom Chapter 10.2 section 8 — every path has a slack, and every slack has a path.
Together they say the file has the shape the positional parse assumes. None of them says the parse produced the right answer, which is the honest limit: a precondition buys you the right to trust a parse, and nothing more.
9. Common Mistakes
Assuming a path has one clock. It has a launch clock, a capture clock and a path group — section 4.
Grouping on Path Group and calling it per-clock. That is the capture clock, so a cross-domain violation is booked entirely against the receiving domain — section 5.
Enumerating clocks from the path groups. A clock can launch the worst violation and appear in no group — section 6.
Not seeding the per-group minimum. !(g in w) per key, or every group with only passing paths reports zero — Chapter 10.5 section 3.
Not clearing g, lc and cc. A path with a missing line inherits the previous one's.
Trusting the positional parse without checking the file's shape. Clearing makes a gap visible but cannot fix a mis-stepped position — section 7.
Counting the continuation lines as the precondition. It passes on a file with a missing marker — section 8.
Reporting a cross-domain path under one clock. It belongs to two; report the pair.
10. Exercises
Exercise 1 — Per-group summary
awk '/^ *Path Group:/ { g = $3 } /^ *slack/ { n[g]++; if ($2 == "(VIOLATED)") v[g]++; if (!(g in w) || $3 + 0 < w[g] + 0) w[g] = $3; if (!(g in seen)) { seen[g]=1; o[++k]=g } g = "" } END { for (i=1;i<=k;i++) { gg=o[i]; printf "%-11s %d path(s), %d violating, worst %s\n", gg, n[gg], v[gg]+0, w[gg] } }' hidden.rptclk_core 1 path(s), 1 violating, worst -0.018
clk_ddr 1 path(s), 1 violating, worst -0.205
clk_io 1 path(s), 1 violating, worst -0.023Exercise 2 — Name the cross-domain paths
awk '/^ *Startpoint:/ { s = $2; stage = "launch" } /^ *Endpoint:/ { e = $2; stage = "capture" } /clocked by/ { c = $NF; gsub(/[()]/, "", c); if (stage == "launch") lc = c; else if (stage == "capture") cc = c } /^ *slack/ { if (lc != cc) printf "%s (%s) -> %s (%s) %s\n", s, lc, e, cc, $3; s=""; e=""; lc=""; cc=""; stage="" } END { }' hidden.rptu_periph/req_reg[0] (clk_periph) -> u_mem/wdata_reg[0] (clk_ddr) -0.205One path, with both domains named — the row a review needs in order to send it to two owners rather than one.
Exercise 3 — Debug a missing clock
A per-clock table has three rows. A reviewer asks whether that is all the clocks.
awk '/^ *Path Group:/ { pg[$3] = 1 } /clocked by/ { c = $NF; gsub(/[()]/, "", c); ck[c] = 1 } END { for (c in ck) if (!(c in pg)) miss = miss " " c; printf "clock(s) named in the report but in no path group:%s\n", (miss == "" ? " none" : miss) }' hidden.rptclock(s) named in the report but in no path group: clk_periphclk_periph launches the report's worst violation and has no path group of its own, so no grouping on Path Group will ever produce a row for it.
Exercise 4 — Debug an invented clock crossing
A domain-crossing report lists a clk_ddr -> clk_core path. No such crossing exists in the design.
grep -v "Endpoint: u_mem" hidden.rpt > noend.rpt
awk '/^ *Startpoint:/ { stage = "launch" } /^ *Endpoint:/ { stage = "capture" } /clocked by/ { c = $NF; gsub(/[()]/, "", c); if (stage == "launch") lc = c; else cc = c } /^ *slack/ { if (lc != cc) printf "crossing %s -> %s %s\n", lc, cc, $3 }' noend.rptcrossing clk_ddr -> clk_core -0.205awk '/^ *Startpoint:/ { ns++ } /^ *Endpoint:/ { ne++ } /^ *slack/ { nk++ } END { printf "start=%d end=%d slack=%d -> %s\n", ns+0, ne+0, nk+0, (ns == ne && ns == nk ? "consistent" : "INCONSISTENT") }' noend.rptstart=3 end=2 slack=3 -> INCONSISTENTThree startpoints, two endpoints. One path has no Endpoint: marker, so its endpoint's clock was read as its startpoint's and the capture clock was inherited from the previous path. The crossing is an artefact of the parse, and the structural check is what says so — the crossing itself looks entirely plausible.
Exercise 5 — Engineering application
A per-clock summary that states its grouping, enumerates the clocks independently of it, reports cross-domain paths as pairs, and refuses a report whose shape it cannot trust.
# clocks.awk — per-clock timing summary with an explicit grouping.
#
# Usage: awk -f clocks.awk hidden.rpt # group on the path group
# awk -v by=launch -f clocks.awk hidden.rpt
# Exit: 0 clean 1 violations present 2 the report's shape is inconsistent
BEGIN { if (by == "") by = "group" }
/^ *Startpoint:/ { s = $2; stage = "launch"; ns++ }
/^ *Endpoint:/ { e = $2; stage = "capture"; ne++ }
/clocked by/ {
nc++
c = $NF; gsub(/[()]/, "", c)
allclk[c] = 1
if (stage == "launch") lc = c
else if (stage == "capture") cc = c
}
/^ *Path Group:/ { g = $3; pg[g] = 1 }
/^ *slack/ {
nk++
key = (by == "launch" ? lc : (by == "capture" ? cc : g))
if (key == "") key = "<unknown>"
if (!(key in n)) order[++nord] = key
n[key]++
if ($2 == "(VIOLATED)") { v[key]++; nviol++ }
if (!(key in w) || $3 + 0 < w[key] + 0) w[key] = $3
if (lc != "" && cc != "" && lc != cc) cross = cross sprintf(" %-20s %-11s -> %-11s %8s\n", s, lc, cc, $3)
s = ""; e = ""; g = ""; lc = ""; cc = ""; stage = ""
}
END {
if (ns != ne || nc != 2 * nk || ns != nk) {
printf "ERROR: start=%d end=%d clocklines=%d slack=%d -- shape inconsistent\n", ns+0, ne+0, nc+0, nk+0 > "/dev/stderr"
exit 2
}
printf "grouped by: %s\n", by
printf "%-12s %6s %6s %9s\n", "clock", "paths", "viol", "worst"
for (i = 1; i <= nord; i++) { k = order[i]; printf "%-12s %6d %6d %9.3f\n", k, n[k], v[k] + 0, w[k] }
for (c in allclk) if (!(c in n)) nonrow = nonrow " " c
if (nonrow != "") printf "# clock(s) in the report with no row under this grouping:%s\n", nonrow
if (cross != "") printf "cross-domain path(s):\n%s", cross
if (nviol) exit 1
}awk -f clocks.awk hidden.rpt
echo "status: $?"grouped by: group
clock paths viol worst
clk_core 1 1 -0.018
clk_ddr 1 1 -0.205
clk_io 1 1 -0.023
# clock(s) in the report with no row under this grouping: clk_periph
cross-domain path(s):
u_periph/req_reg[0] clk_periph -> clk_ddr -0.205
status: 1The table is the familiar one and the two lines under it are what the familiar one leaves out: a clock with no row, and the path whose two ends disagree.
The same report grouped on the launch clock:
awk -v by=launch -f clocks.awk hidden.rpt | head -6grouped by: launch
clock paths viol worst
clk_core 1 1 -0.018
clk_periph 1 1 -0.205
clk_io 1 1 -0.023
# clock(s) in the report with no row under this grouping: clk_ddrclk_periph now has a row and clk_ddr does not. Both tables are correct and neither is complete, which is why the grouped by: line is printed first — a per-clock summary without it is not a well-formed claim.
And the broken report is refused:
awk -f clocks.awk noend.rpt 2>&1 >/dev/null
echo "status: $?"ERROR: start=3 end=2 clocklines=6 slack=3 -- shape inconsistent
status: 2Exercise 6 — Challenge: show that the grouping choice is not detectable from the output
Both tables in Exercise 5 have three rows, one violation each, and the same three slack values. Show that nothing but the grouped by: line distinguishes them.
awk -f clocks.awk hidden.rpt | sed -n '3,5p' | awk '{ print $2, $3, $4 }'
awk -v by=launch -f clocks.awk hidden.rpt | sed -n '3,5p' | awk '{ print $2, $3, $4 }'1 1 -0.018
1 1 -0.205
1 1 -0.023
1 1 -0.018
1 1 -0.205
1 1 -0.023Strip the clock names and the two tables are byte-identical — the same three rows, in the same order, with the same counts and the same worst slack. The row order survives because both groupings happen to meet the three clocks in the same sequence, so even the one cue that might have hinted at a difference is absent.
The only thing that differs is the first column:
awk -f clocks.awk hidden.rpt | sed -n '3,5p' | awk '{ printf "[%s]", $1 } END { print "" }'
awk -v by=launch -f clocks.awk hidden.rpt | sed -n '3,5p' | awk '{ printf "[%s]", $1 } END { print "" }'[clk_core][clk_ddr][clk_io]
[clk_core][clk_periph][clk_io]One cell out of twelve. clk_ddr against clk_periph, in the middle row, and everything else about the two tables is the same.
Every aggregate agrees too:
for m in group launch capture; do printf "%-8s " "$m"; awk -v by=$m -f clocks.awk hidden.rpt | awk '/^clk/ { r++; n += $2; v += $3 } END { printf "rows=%d paths=%d violations=%d\n", r+0, n+0, v+0 }'; donegroup rows=3 paths=3 violations=3
launch rows=3 paths=3 violations=3
capture rows=3 paths=3 violations=3Identical under all three groupings. Three rows, three paths, three violations, whichever clock the summary was built on.
So the grouping cannot be recovered from the numbers. It has to be declared, which is the whole reason clocks.awk prints it on the first line. The same argument as Chapter 9.6's finding that the grouping key decides how many problems you appear to have: when a choice of key changes the answer and leaves no trace, the key becomes part of the result and belongs in the output.
11. Summary
- A path names three clocks: a launch clock, a capture clock, and a path group.
- The path group is the capture clock, so a cross-domain violation is booked entirely against the receiving domain — section 5.
- The clock name is in the continuation lines, extracted with
$NFandgsub(/[()]/, "", c). - A clock can launch the worst violation and appear in no path group —
clk_periphonhidden.rpt. Enumerate clocks independently of the grouping. - Seed each per-group minimum with
!(g in w), not once globally. - Clearing carried state makes a gap visible; it cannot fix a positional parse. A missing
Endpoint:marker made the capture clock overwrite the launch clock, and the uncleared version invented a crossing that does not exist — section 7. - The precondition must count the markers. Counting continuation lines passes on the broken file — section 8.
- Report a cross-domain path as a pair. It belongs to two domains.
- Declare the grouping. All three give identical aggregates, so the choice leaves no trace in the numbers.
Everything so far has read one line at a time with state carried between records. Chapter 10.7 — Parsing Multi-Line EDA Reports asks whether awk's paragraph mode is a better fit for the whole job, and answers it with a measurement: state-carrying survives both report shapes tested, paragraph mode survives one, and the difference between them is a single blank line.
Where this fits
Part of the awk curriculum.
