awk Part III · Module 10 · Timing and STA Report Analysis
Setup and Hold Violations
A negative slack means a check failed. It does not say which check, and that distinction decides what anyone does next: a setup violation is a path that is too slow and a hold violation is a path that is too fast, so the two are fixed in opposite directions by different people. The report says which is which on a line four above the slack — and this chapter shows what happens when that line is missing and the parser, obeying Chapter 5.1's persistence rule, reports two hold violations as setup and a design with a hold problem as having no hold checks at all.
Advanced19 min readawkPart IIISTASetup and HoldClassification
Part III · Module 10 · Chapter 10.4 · Setup and Hold Violations
1. Why This Matters
Chapter 10.3 made the slack number trustworthy. A trustworthy number still answers only half the question.
Both of a chip's two fundamental timing checks report a violation as a negative slack. They are not variants of one problem:
- A setup violation means data arrived too late for the capturing clock edge. The path is too slow. It is fixed by making the logic faster — resizing gates, restructuring the logic, shortening the route — or by slowing the clock.
- A hold violation means data arrived too early and overwrote the value the capturing flop was still holding from the previous cycle. The path is too fast. It is fixed by making the logic slower, usually by inserting delay buffers.
Those are opposite fixes, and a hold violation is the more alarming of the two at signoff because slowing the clock does not help it — the race is between two paths, not against a clock period, so it persists at any frequency. A combined count of "4 violations" tells a reviewer nothing about whether the design needs a frequency drop or a buffer insertion pass.
This chapter is marked Critical because the obvious classification — the sign of the slack — cannot make this distinction at all, and the correct one has a failure mode that reports the alarming case as the routine one.
2. A Six-Path Report
Startpoint: u_regfile/dout_reg[3]
Endpoint: u_alu/sum_reg[7]
Path Group: clk_core
Path Type: max
slack (VIOLATED) -0.018
Startpoint: u_ctrl/state_reg[1]
Endpoint: u_alu/flag_reg[0]
Path Group: clk_core
Path Type: max
slack (MET) 0.067
Startpoint: u_alu/acc_reg[0]
Endpoint: u_alu/acc_reg[1]
Path Group: clk_core
Path Type: min
slack (VIOLATED) -0.041
Startpoint: u_io/pad_reg[0]
Endpoint: u_io/out_reg[0]
Path Group: clk_io
Path Type: min
slack (VIOLATED) -0.023
Startpoint: u_dma/len_reg[4]
Endpoint: u_dma/cnt_reg[4]
Path Group: clk_core
Path Type: max
slack (VIOLATED) -0.112
Startpoint: u_io/cfg_reg[2]
Endpoint: u_io/shift_reg[0]
Path Group: clk_io
Path Type: max
slack (MET) 0.031Six paths: four max and two min by the line counts, four violating and two meeting by the verdict words. The two classifications cut the set differently, and that is the chapter.
3. The Count That Cannot Be Acted On
awk '/^ *slack/ { if ($3 + 0 < 0) neg++; else nonneg++ } END { printf "%d violating, %d passing -- but WHICH CHECK FAILED?\n", neg+0, nonneg+0 }' sh.rpt4 violating, 2 passing -- but WHICH CHECK FAILED?Four violations. Everything a reviewer needs to decide what happens next is missing from that number: whether to ask for a frequency reduction, a buffer insertion pass, both, or neither.
Worse, the number is reassuring in the wrong direction. Four setup violations on a first timing run is routine and often closes with no intervention. Two hold violations is a different conversation. The combined figure reads like the first case.
4. Path Type Is the Classification
awk '/^ *Path Type:/ { print "Path Type line:", $0; print " $3 is [" $3 "]" }' sh.rpt | head -4Path Type line: Path Type: max
$3 is [max]
Path Type line: Path Type: max
$3 is [max]max and min are delay-analysis terms rather than check names — a setup check asks whether the maximum delay still arrives in time, and a hold check asks whether the minimum delay is slow enough not to race ahead. So:
Path Type | Check | Means | Fixed by |
|---|---|---|---|
max | setup | data too slow | faster logic, or a slower clock |
min | hold | data too fast | slower logic — delay buffers |
Splitting the report on it:
awk '/^ *Path Type:/ { ty = $3 } /^ *slack/ { chk = (ty == "max" ? "setup" : (ty == "min" ? "hold" : "unknown")); if ($2 == "(VIOLATED)") { v[chk]++; if (w[chk] == "" || $3 + 0 < w[chk] + 0) w[chk] = $3 } else m[chk]++; ty = "" } END { printf "setup %d VIOLATED %d MET worst %s\n", v["setup"]+0, m["setup"]+0, (w["setup"] == "" ? "n/a" : w["setup"]); printf "hold %d VIOLATED %d MET worst %s\n", v["hold"]+0, m["hold"]+0, (w["hold"] == "" ? "n/a" : w["hold"]); printf "unknown %d VIOLATED %d MET\n", v["unknown"]+0, m["unknown"]+0 }' sh.rptsetup 2 VIOLATED 2 MET worst -0.112
hold 2 VIOLATED 0 MET worst -0.041
unknown 0 VIOLATED 0 METThe same four violations, now actionable: two setup failures with the worst at −0.112, and two hold failures with nothing passing in that category at all. The 0 MET on the hold row is itself informative — every hold check in the report failed, which points at a systematic problem rather than two unlucky paths.
Three details in that command are deliberate and carried from earlier chapters:
- The verdict word classifies pass/fail, not the sign — Chapter 10.3 section 5.
ty = ""clears the carried type once it has been used — Chapter 10.2 section 7, and section 6 below is what it prevents.- An
unknownbucket exists and is printed even when empty, so a path that is neithermaxnormincannot vanish — Chapter 6.2 section 8's reconciling total.
5. The Flow
Steps 1 and 3 are a pair. Every later chapter in this module repeats them, and the next section is the reason.
6. One Uncleared Variable Hides a Hold Problem
Remove the two Path Type: min lines — the shape a tool-version change produces when one header's wording shifts:
grep -v "Path Type: min" sh.rpt > noty.rpt
grep -c "Path Type" noty.rpt
grep -c "slack" noty.rpt4
6Six paths, four type lines. Now the two versions of the classifier, differing only in whether ty is cleared:
awk '/^ *Path Type:/ { ty = $3 } /^ *slack/ { chk = (ty == "max" ? "setup" : (ty == "min" ? "hold" : "unknown")); c[chk]++; ty = "" } END { printf "setup=%d hold=%d unknown=%d\n", c["setup"]+0, c["hold"]+0, c["unknown"]+0 }' noty.rptsetup=4 hold=0 unknown=2awk '/^ *Path Type:/ { ty = $3 } /^ *slack/ { chk = (ty == "max" ? "setup" : (ty == "min" ? "hold" : "unknown")); c[chk]++ } END { printf "setup=%d hold=%d unknown=%d\n", c["setup"]+0, c["hold"]+0, c["unknown"]+0 }' noty.rptsetup=6 hold=0 unknown=0Six setup paths, no hold paths, nothing unknown. The uncleared version produces a report that is internally consistent, fully populated, and states that this design has no hold checks — on a report containing two hold violations.
The cleared version puts the same two paths in unknown, which says the honest thing: there are two paths here whose check I could not determine.
7. And It Erases the Worst Hold Slack
The count is not the only casualty. The same two versions, tracking the worst violating slack per check:
awk '/^ *Path Type:/ { ty = $3 } /^ *slack/ { chk = (ty == "max" ? "setup" : (ty == "min" ? "hold" : "unknown")); if ($2 == "(VIOLATED)" && (w[chk] == "" || $3 + 0 < w[chk] + 0)) w[chk] = $3; ty = "" } END { printf "cleared setup worst %-7s hold worst %-7s unknown worst %s\n", (w["setup"]==""?"n/a":w["setup"]), (w["hold"]==""?"n/a":w["hold"]), (w["unknown"]==""?"n/a":w["unknown"]) }' noty.rpt
awk '/^ *Path Type:/ { ty = $3 } /^ *slack/ { chk = (ty == "max" ? "setup" : (ty == "min" ? "hold" : "unknown")); if ($2 == "(VIOLATED)" && (w[chk] == "" || $3 + 0 < w[chk] + 0)) w[chk] = $3 } END { printf "stale setup worst %-7s hold worst %-7s unknown worst %s\n", (w["setup"]==""?"n/a":w["setup"]), (w["hold"]==""?"n/a":w["hold"]), (w["unknown"]==""?"n/a":w["unknown"]) }' noty.rptcleared setup worst -0.112 hold worst n/a unknown worst -0.041
stale setup worst -0.112 hold worst n/a unknown worst n/aIn the stale version the −0.041 hold violation leaves no trace at all. It was filed under setup, where the worst was already −0.112, so the minimum did not change. The violation was not merely mislabelled — it was absorbed.
That is the general hazard with a per-group extremum: a misfiled value that is not the group's extremum disappears entirely. The count would have shown it, which is the argument for reporting a count alongside every worst-case figure rather than the worst case alone.
8. Explicit Keys, Not Iteration
Both commands above print setup and hold by name. The alternative is to iterate:
awk '/^ *Path Type:/ { ty = $3 } /^ *slack/ { chk = (ty == "max" ? "setup" : "hold"); c[chk]++; ty = "" } END { for (k in c) printf "[%s=%d]", k, c[k]; print "" }' sh.rpt[setup=4][hold=2]That is the right answer, and it is not a reliable way to get it. POSIX defines no order for for (k in array) — the implementation may return keys in any sequence, and nothing about this run's output being in a sensible order means the next one will be. Chapter 6.1 section 9 covers the rule; here the consequence is a signoff table whose rows move.
Iteration has a second problem that matters more. It can only print categories that occurred. A report with no hold paths prints no hold row, and a missing row reads as no information when it should read as hold 0 VIOLATED. The explicit form says it:
awk '/^ *Path Type:/ { ty = $3 } /^ *slack/ { chk = (ty == "max" ? "setup" : (ty == "min" ? "hold" : "unknown")); c[chk]++; ty = "" } END { printf "setup=%d hold=%d unknown=%d\n", c["setup"]+0, c["hold"]+0, c["unknown"]+0 }' timing.rptsetup=2 hold=1 unknown=0Chapter 10.1's profile.awk named its array keys for the same reason, and so does every summary in the rest of this module.
9. Common Mistakes
Counting violations without splitting by check. Setup and hold need opposite fixes — section 3.
Classifying by the sign of the slack. Both checks report a violation as negative.
Reading max as "the worst case" and min as "the best case." They name which delay corner the check uses, not a ranking.
Not clearing ty. A path with no type line inherits the previous one, and hold violations become setup — section 6.
Reporting only a worst slack per check. A misfiled value that is not the extremum vanishes — section 7.
Omitting the unknown bucket. It is the only thing that distinguishes "no hold violations" from "I could not tell."
Using for (k in c) for the summary. No defined order, and absent categories print no row — section 8.
Expecting a frequency reduction to fix a hold violation. It is a race between two paths and does not depend on the period.
10. Exercises
Exercise 1 — Split the counts by check
awk '/^ *Path Type:/ { ty = $3 } /^ *slack/ { chk = (ty == "max" ? "setup" : (ty == "min" ? "hold" : "unknown")); if ($2 == "(VIOLATED)") v[chk]++; else m[chk]++; ty = "" } END { printf "setup %d VIOLATED %d MET\nhold %d VIOLATED %d MET\nunknown %d VIOLATED %d MET\n", v["setup"]+0, m["setup"]+0, v["hold"]+0, m["hold"]+0, v["unknown"]+0, m["unknown"]+0 }' sh.rptsetup 2 VIOLATED 2 MET
hold 2 VIOLATED 0 MET
unknown 0 VIOLATED 0 METExercise 2 — Name every violating path with its check
awk '/^ *Startpoint:/ { s = $2 } /^ *Path Type:/ { ty = $3 } /^ *slack/ { if ($2 == "(VIOLATED)") printf "%-6s %-24s %8s\n", (ty == "max" ? "setup" : (ty == "min" ? "hold" : "?")), s, $3; s = ""; ty = "" }' sh.rptsetup u_regfile/dout_reg[3] -0.018
hold u_alu/acc_reg[0] -0.041
hold u_io/pad_reg[0] -0.023
setup u_dma/len_reg[4] -0.112Four violations, each with the check that failed and the register it starts from — the form a reviewer can hand to two different people.
Exercise 3 — Debug a report claiming no hold checks
A timing summary reports six setup paths and no hold paths. The design has two hold violations.
grep -v "Path Type: min" sh.rpt > noty.rpt
awk '/^ *Path Type:/ { ty = $3 } /^ *slack/ { chk = (ty == "max" ? "setup" : (ty == "min" ? "hold" : "unknown")); c[chk]++ } END { printf "setup=%d hold=%d unknown=%d\n", c["setup"]+0, c["hold"]+0, c["unknown"]+0 }' noty.rpt
grep -c "Path Type" noty.rpt
grep -c "slack" noty.rptsetup=6 hold=0 unknown=0
4
6Six slack lines and four Path Type lines. Two paths have no type, and the uncleared ty gave them the previous path's.
awk '/^ *Path Type:/ { ty = $3 } /^ *slack/ { chk = (ty == "max" ? "setup" : (ty == "min" ? "hold" : "unknown")); c[chk]++; ty = "" } END { printf "setup=%d hold=%d unknown=%d\n", c["setup"]+0, c["hold"]+0, c["unknown"]+0 }' noty.rptsetup=4 hold=0 unknown=2Two paths are now reported as unclassified rather than as setup. Note what the fix does and does not do: it does not recover the two hold classifications, because the information is not in the file. It makes their absence visible, which is all a parser can honestly do.
Exercise 4 — Debug a vanished worst slack
awk '/^ *Path Type:/ { ty = $3 } /^ *slack/ { chk = (ty == "max" ? "setup" : (ty == "min" ? "hold" : "unknown")); if ($2 == "(VIOLATED)" && (w[chk] == "" || $3 + 0 < w[chk] + 0)) w[chk] = $3 } END { printf "worst setup %s, worst hold %s\n", (w["setup"]==""?"n/a":w["setup"]), (w["hold"]==""?"n/a":w["hold"]) }' noty.rptworst setup -0.112, worst hold n/aThe -0.041 hold violation is not in either figure. It was filed under setup, where -0.112 was already worse, so the comparison never changed.
awk '/^ *Path Type:/ { ty = $3 } /^ *slack/ { chk = (ty == "max" ? "setup" : (ty == "min" ? "hold" : "unknown")); if ($2 == "(VIOLATED)") { n[chk]++; if (w[chk] == "" || $3 + 0 < w[chk] + 0) w[chk] = $3 }; ty = "" } END { printf "setup %d violation(s), worst %s\nhold %d violation(s), worst %s\nunknown %d violation(s), worst %s\n", n["setup"]+0, (w["setup"]==""?"n/a":w["setup"]), n["hold"]+0, (w["hold"]==""?"n/a":w["hold"]), n["unknown"]+0, (w["unknown"]==""?"n/a":w["unknown"]) }' noty.rptsetup 2 violation(s), worst -0.112
hold 0 violation(s), worst n/a
unknown 2 violation(s), worst -0.041Clearing ty and reporting a count beside every extremum makes both facts visible: two violations are unclassified, and the worst of them is −0.041.
Exercise 5 — Engineering application
The per-check signoff summary, with the preconditions from Chapter 10.2 section 8 and the verdict discipline from Chapter 10.3.
# checks.awk — per-check violation summary from a timing report.
#
# Usage: awk -f checks.awk sh.rpt
# Exit: 0 no violations
# 1 violations present
# 2 the report cannot be classified (unknown paths, or counts disagree)
/^ *Path Type:/ { ty = $3; ntype++ }
/^ *slack/ {
nslack++
chk = (ty == "max" ? "setup" : (ty == "min" ? "hold" : "unknown"))
if ($2 == "(VIOLATED)") {
v[chk]++
if (w[chk] == "" || $3 + 0 < w[chk] + 0) w[chk] = $3
} else if ($2 == "(MET)") m[chk]++
else other[chk]++
ty = ""
}
END {
printf "%-8s %10s %10s %12s\n", "check", "violating", "meeting", "worst slack"
split("setup hold unknown", order, " ")
for (i = 1; i <= 3; i++) {
c = order[i]
printf "%-8s %10d %10d %12s\n", c, v[c] + 0, m[c] + 0, (w[c] == "" ? "-" : sprintf("%.3f", w[c]))
}
printf "# %d slack line(s), %d Path Type line(s)\n", nslack + 0, ntype + 0
if (ntype != nslack) { printf "ERROR: %d type line(s) for %d path(s) -- classification unreliable\n", ntype, nslack > "/dev/stderr"; exit 2 }
if (v["unknown"] + m["unknown"] + other["unknown"] > 0) { print "ERROR: unclassified path(s) present" > "/dev/stderr"; exit 2 }
if (v["setup"] + v["hold"] > 0) exit 1
}awk -f checks.awk sh.rpt
echo "status: $?"check violating meeting worst slack
setup 2 2 -0.112
hold 2 0 -0.041
unknown 0 0 -
# 6 slack line(s), 6 Path Type line(s)
status: 1split("setup hold unknown", order, " ") fixes the row order without hardcoding three printf calls — Chapter 6.1 section 10's idiom for iterating an array in a defined sequence, and the answer to section 8's problem.
The broken report is refused rather than summarised:
awk -f checks.awk noty.rpt 2>&1 >/dev/null
echo "status: $?"ERROR: 4 type line(s) for 6 path(s) -- classification unreliable
status: 2And the three-path report from Chapter 10.1, for continuity:
awk -f checks.awk timing.rpt
echo "status: $?"check violating meeting worst slack
setup 1 1 -0.018
hold 1 0 -0.023
unknown 0 0 -
# 3 slack line(s), 3 Path Type line(s)
status: 1One setup violation and one hold violation — and the hold one is the numerically worse of the two, which Chapter 10.5 shows is exactly the comparison a single worst-path figure gets wrong.
Exercise 6 — Challenge: show that the stale-type bug cannot be caught by a total
Exercise 3's broken parser reports setup=6 hold=0. Show that the slack-line count, the violation count and the worst setup slack are all unchanged by the bug, so none of them can detect it.
printf '%-10s ' 'cleared'; awk '/^ *Path Type:/ { ty = $3 } /^ *slack/ { n++; if ($2 == "(VIOLATED)") v++; chk = (ty == "max" ? "setup" : (ty == "min" ? "hold" : "unknown")); if ($2 == "(VIOLATED)" && chk == "setup" && (ws == "" || $3 + 0 < ws + 0)) ws = $3; ty = "" } END { printf "paths=%d violations=%d worst-setup=%s\n", n+0, v+0, ws }' noty.rpt
printf '%-10s ' 'stale'; awk '/^ *Path Type:/ { ty = $3 } /^ *slack/ { n++; if ($2 == "(VIOLATED)") v++; chk = (ty == "max" ? "setup" : (ty == "min" ? "hold" : "unknown")); if ($2 == "(VIOLATED)" && chk == "setup" && (ws == "" || $3 + 0 < ws + 0)) ws = $3 } END { printf "paths=%d violations=%d worst-setup=%s\n", n+0, v+0, ws }' noty.rptcleared paths=6 violations=4 worst-setup=-0.112
stale paths=6 violations=4 worst-setup=-0.112Identical on all three aggregates. Six paths, four violations, worst setup −0.112, from a correct parser and a broken one.
The only figure that moves is the per-category breakdown, which is the thing the bug corrupts:
printf '%-10s ' 'cleared'; awk '/^ *Path Type:/ { ty = $3 } /^ *slack/ { chk = (ty == "max" ? "setup" : (ty == "min" ? "hold" : "unknown")); c[chk]++; ty = "" } END { printf "setup=%d hold=%d unknown=%d\n", c["setup"]+0, c["hold"]+0, c["unknown"]+0 }' noty.rpt
printf '%-10s ' 'stale'; awk '/^ *Path Type:/ { ty = $3 } /^ *slack/ { chk = (ty == "max" ? "setup" : (ty == "min" ? "hold" : "unknown")); c[chk]++ } END { printf "setup=%d hold=%d unknown=%d\n", c["setup"]+0, c["hold"]+0, c["unknown"]+0 }' noty.rptcleared setup=4 hold=0 unknown=2
stale setup=6 hold=0 unknown=0So the detection has to be structural, not statistical: compare the number of Path Type lines against the number of slack lines, which is what checks.awk does and what Chapter 10.2 section 8 established as the module's precondition. Four type lines for six paths is a fact about the file that no amount of reading the output would reveal.
This is Chapter 10.3 section 6's lesson in a second setting — an aggregate check cannot find an error that does not change the aggregate — and Chapter 9.2 section 7's in a third: the check has to be placed where the information still exists.
11. Summary
- Setup and hold both report a violation as a negative slack and are fixed in opposite directions.
- A hold violation does not go away when the clock slows down. It is a race between paths, not against a period.
- A combined violation count is not actionable — section 3.
Path Typeis the classifier:maxis setup,minis hold.- Clear
tyonce it is used. Without it, hold violations are reported as setup and the design appears to have no hold checks — section 6. - A misfiled value that is not its group's extremum vanishes entirely — section 7. Report a count beside every worst case.
- Keep an
unknownbucket and print it even when empty. It distinguishes "none" from "could not tell." - Name array keys, or order them with
split.for (k in c)has no defined order and cannot print a category that did not occur. - The stale-type bug changes no aggregate — path count, violation count and worst setup slack are all identical. Detection is structural: compare the
Path Typecount against the slack count.
Violations are now classified. Chapter 10.5 — Worst Timing Paths takes the ranking, and starts from the observation this chapter's last output makes: the numerically worst path in timing.rpt is a hold check, so a single "worst slack" figure across the whole report compares two quantities that are not comparable.
Where this fits
Part of the awk curriculum.
