awk Part IV · Module 13 · Real-World AWK Engineering Projects
Project: Timing Violation Extractor
The second script a timing flow keeps: it walks a report, emits one row per violated path with its endpoint, clock and slack, and reports the worst negative slack — which is where the design decision lives. A single worst-slack figure is the number everyone asks for and the one thing this script refuses to produce, because on this report it would be a setup failure on one clock standing in for a hold failure on another. Every rule in it answers a failure measured in Module 10, and the report's own marker counts decide whether any of it is trustworthy.
Advanced19 min readawkPart IVProjectSTATiming
Part IV · Module 13 · Chapter 13.2 · Project: Timing Violation Extractor
1. What the Script Has to Do
Three outputs, and the third is the one with a judgement in it:
- One row per violated path — startpoint, endpoint, clock, which check, and the slack. This is what gets handed to whoever fixes it.
- A violation count against the path count, so the row list has a denominator.
- The worst negative slack. Everyone wants one number. Module 10.5 measured why there isn't one, and this script reports it per
(check, clock)instead.
2. The Report
No STA tool runs in this environment, so the report is generated — eight paths in the standard report_timing shape, across three clocks and both check types:
awk 'BEGIN {
n = split("u_regfile/dout_reg[3]:u_alu/sum_reg[7]:clk_core:max:-0.018|u_ctrl/state_reg[1]:u_alu/flag_reg[0]:clk_core:max:0.067|u_dma/len_reg[4]:u_dma/cnt_reg[4]:clk_core:max:-0.112|u_alu/acc_reg[0]:u_alu/acc_reg[1]:clk_core:min:-0.041|u_io/pad_reg[0]:u_io/out_reg[0]:clk_io:min:-0.023|u_io/cfg_reg[2]:u_io/shift_reg[0]:clk_io:max:0.031|u_mem/rd_reg[0]:u_mem/dout_reg[0]:clk_ddr:max:-0.205|u_mem/wr_reg[1]:u_mem/din_reg[1]:clk_ddr:min:0.014", p, "|")
print "****************************************"
print "Report : timing"
print "Design : soc_top"
print "****************************************"
print ""
for (i = 1; i <= n; i++) {
split(p[i], f, ":")
v = (f[5] + 0 < 0 ? "VIOLATED" : "MET")
printf " Startpoint: %s\n", f[1]
printf " (rising edge-triggered flip-flop clocked by %s)\n", f[3]
printf " Endpoint: %s\n", f[2]
printf " (rising edge-triggered flip-flop clocked by %s)\n", f[3]
printf " Path Group: %s\n", f[3]
printf " Path Type: %s\n", f[4]
print ""
print " Point Incr Path"
print " ----------------------------------------------------------"
print " data required time 0.220"
print " data arrival time -0.238"
print " ----------------------------------------------------------"
printf " slack (%s) %38.3f\n", v, f[5]
print ""
}
}' > timing.rpt
wc -l < timing.rpt | awk '{ printf " %d lines\n", $1 }' 117 linesOne path of it, read back from the file rather than quoted separately — so what the chapter shows and what the commands read cannot drift apart:
sed -n '6,18p' timing.rpt 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
Point Incr Path
----------------------------------------------------------
data required time 0.220
data arrival time -0.238
----------------------------------------------------------
slack (VIOLATED) -0.018awk '/^ *slack/ { nk++ } /^ *slack \(VIOLATED\)/ { nv++ } /^ *Path Group:/ { g[$3]++; if (!($3 in gs)) { gs[$3]=1; go[++ng]=$3 } } /^ *Path Type:/ { t[$3]++; if (!($3 in ts)) { ts[$3]=1; to[++nt]=$3 } }
END { printf " %d path(s), %d violating\n", nk, nv
printf " clocks:"; for (i=1;i<=ng;i++) printf " %s(%d)", go[i], g[go[i]]; print ""
printf " types: "; for (i=1;i<=nt;i++) printf " %s(%d)", to[i], t[to[i]]; print "" }' timing.rpt 8 path(s), 5 violating
clocks: clk_core(4) clk_io(2) clk_ddr(2)
types: max(5) min(3)Five violating of eight, four paths on clk_core, two each on clk_io and clk_ddr, five setup checks and three hold. The clock and type listings are built from insertion-ordered arrays rather than for (k in …), because that has no defined order — Chapter 10.4 section 8 — and an unordered listing here would differ between runs.
3. The Pipeline
4. Why There Is No Single Worst Slack
The three groupings of the same five violations:
awk '/^ *Path Group:/ { g = $3 } /^ *Path Type:/ { ty = $3 } /^ *slack \(VIOLATED\)/ {
c = (ty == "max" ? "setup" : "hold")
if (!(c in wt) || $3 + 0 < wt[c]) wt[c] = $3 + 0; nt[c]++
if (!(g in wg) || $3 + 0 < wg[g]) wg[g] = $3 + 0; ng[g]++
k = c "/" g; if (!(k in wb) || $3 + 0 < wb[k]) wb[k] = $3 + 0; nb[k]++
if ($3 + 0 < (ov == "" ? 1 : ov)) { ov = $3 + 0; ovc = c; ovg = g }
g = ""; ty = "" }
END { printf " by check only: setup %.3f (n=%d), hold %.3f (n=%d)\n", wt["setup"], nt["setup"], wt["hold"], nt["hold"]
printf " by clock only: clk_core %.3f (n=%d), clk_io %.3f (n=%d), clk_ddr %.3f (n=%d)\n", wg["clk_core"], ng["clk_core"], wg["clk_io"], ng["clk_io"], wg["clk_ddr"], ng["clk_ddr"]
printf " worst overall: %.3f -- a %s check on %s\n", ov, ovc, ovg }' timing.rpt by check only: setup -0.205 (n=3), hold -0.041 (n=2)
by clock only: clk_core -0.112 (n=3), clk_io -0.023 (n=1), clk_ddr -0.205 (n=1)
worst overall: -0.205 -- a setup check on clk_ddr5. The Complete Script
# viol.awk — timing violation extractor.
#
# Usage: awk -f viol.awk timing.rpt
# awk -v fmt=tsv -f viol.awk timing.rpt
# Exit: 0 no violations 1 violations present
# 2 the report's structure is inconsistent -- nothing can be trusted
#
# Design notes, each from a measured failure in Module 10:
# - Carries the header forward and CLEARS IT on use; without the clear a
# missing Startpoint line pairs two slacks with one register. (10.2)
# - Classifies pass/fail by the VERDICT WORD, never by the sign: a report
# rounds a sub-precision violation to -0.000. (10.3)
# - Separates setup (max) from hold (min). A stale Path Type reports hold
# violations as setup and shows a design with no hold checks. (10.4)
# - Reports worst negative slack PER (check, clock), never one figure: the
# worst path overall need not belong to a check you can act on. (10.5)
# - Counts the markers as a precondition; the DIRECTION of a mismatch names
# the fault. (10.2 s8)
BEGIN { if (fmt == "") fmt = "table"; TOL = 1e-6 }
/^ *Startpoint:/ { s = $2; nstart++ }
/^ *Endpoint:/ { e = $2; nend++ }
/^ *Path Group:/ { g = $3 }
/^ *Path Type:/ { ty = $3 }
/^ *slack/ {
nslack++
if (NF < 3) { shape++; s = ""; e = ""; g = ""; ty = ""; next }
word = $2; gsub(/[()]/, "", word)
val = $3 + 0
chk = (ty == "max" ? "setup" : (ty == "min" ? "hold" : "unknown"))
if (chk == "unknown") shape++
npath++
if (word == "VIOLATED") {
nviol++
key = chk "/" (g == "" ? "<none>" : g)
cnt[key]++
if (!(key in worst) || val < worst[key]) { worst[key] = val; where[key] = e }
if (!(key in kseen)) { kseen[key] = 1; korder[++nk] = key }
row[nviol] = sprintf("%-20s %-20s %-9s %-6s %8.3f", (s == "" ? "<MISSING>" : s), (e == "" ? "<MISSING>" : e), g, chk, val)
tsv[nviol] = sprintf("%s\t%s\t%s\t%s\t%.3f", s, e, g, chk, val)
}
s = ""; e = ""; g = ""; ty = ""
}
END {
if (nslack == 0) { print "ERROR: no slack lines -- is this a timing report?" > "/dev/stderr"; exit 2 }
if (nstart != nslack) {
printf "ERROR: %d startpoint(s) but %d slack line(s) -- %s\n", nstart, nslack,
(nstart > nslack ? "report looks truncated" : "a header line is missing") > "/dev/stderr"
exit 2
}
if (nstart != nend) { printf "ERROR: %d startpoint(s) but %d endpoint(s)\n", nstart, nend > "/dev/stderr"; exit 2 }
if (shape) { printf "ERROR: %d path(s) with an unreadable shape or unknown Path Type\n", shape > "/dev/stderr"; exit 2 }
if (fmt == "tsv") {
print "startpoint\tendpoint\tclock\tcheck\tslack"
for (i = 1; i <= nviol; i++) print tsv[i]
} else {
printf "VIOLATED PATHS (%d of %d)\n", nviol + 0, npath
if (nviol) printf " %-20s %-20s %-9s %-6s %8s\n", "startpoint", "endpoint", "clock", "check", "slack"
for (i = 1; i <= nviol; i++) printf " %s\n", row[i]
print ""
print "WORST NEGATIVE SLACK (per check and clock -- never one figure)"
printf " %-18s %6s %9s %s\n", "check/clock", "count", "worst", "endpoint"
for (i = 1; i <= nk; i++) { k = korder[i]
printf " %-18s %6d %9.3f %s\n", k, cnt[k], worst[k], where[k] }
print ""
printf "SUMMARY paths=%d violating=%d groups=%d\n", npath, nviol + 0, nk + 0
}
if (nviol) exit 1
}Two structural choices worth naming. All four preconditions run before any output, so a report that cannot be trusted produces a diagnostic and nothing else — a partial table is worse than none. And the rows are built into an array and printed in END, rather than streamed, because the (check, clock) summary cannot be computed until every path has been seen and the two must not be interleaved.
6. Running It
awk -f viol.awk timing.rpt
echo "status: $?"VIOLATED PATHS (5 of 8)
startpoint endpoint clock check slack
u_regfile/dout_reg[3] u_alu/sum_reg[7] clk_core setup -0.018
u_dma/len_reg[4] u_dma/cnt_reg[4] clk_core setup -0.112
u_alu/acc_reg[0] u_alu/acc_reg[1] clk_core hold -0.041
u_io/pad_reg[0] u_io/out_reg[0] clk_io hold -0.023
u_mem/rd_reg[0] u_mem/dout_reg[0] clk_ddr setup -0.205
WORST NEGATIVE SLACK (per check and clock -- never one figure)
check/clock count worst endpoint
setup/clk_core 2 -0.112 u_dma/cnt_reg[4]
hold/clk_core 1 -0.041 u_alu/acc_reg[1]
hold/clk_io 1 -0.023 u_io/out_reg[0]
setup/clk_ddr 1 -0.205 u_mem/dout_reg[0]
SUMMARY paths=8 violating=5 groups=4
status: 1Four groups, four pieces of work. setup/clk_ddr at −0.205 is the largest number and setup/clk_core has twice as many paths — which of those is more urgent is an engineering judgement the script deliberately does not make.
The first row's startpoint overflows its column (u_regfile/dout_reg[3] is 21 characters in a 20-wide field), which %-20s does not truncate — it pushes the rest of the row right. That is the correct behaviour for a report a person reads: losing a character of a register name would be worse than losing the alignment.
The delimited form, for anything downstream:
awk -v fmt=tsv -f viol.awk timing.rpt > viol.tsv
echo "status: $?"
head -3 viol.tsvstatus: 1
startpoint endpoint clock check slack
u_regfile/dout_reg[3] u_alu/sum_reg[7] clk_core setup -0.018
u_dma/len_reg[4] u_dma/cnt_reg[4] clk_core setup -0.112Note the redirect rather than a pipe into head. Chapter 12.1 section 7 measured that $? after a pipeline is the last command's, and an earlier draft of this chapter reported status: 0 for exactly that reason — head succeeded while awk exited 1.
7. The Refusals
awk 'NR <= 100' timing.rpt > trunc.rpt
awk -f viol.awk trunc.rpt 2>&1 >/dev/null
echo "status: $?"ERROR: 7 startpoint(s) but 6 slack line(s) -- report looks truncated
status: 2grep -v 'Startpoint: u_dma' timing.rpt > nostart.rpt
awk -f viol.awk nostart.rpt 2>&1 >/dev/null
echo "status: $?"ERROR: 7 startpoint(s) but 8 slack line(s) -- a header line is missing
status: 2The direction of the mismatch names the fault — Chapter 10.2 section 8. More startpoints than slacks is a truncation; fewer is a missing header line, which is the condition that would otherwise pair two slacks with one register.
And a report with nothing wrong:
sed 's/(VIOLATED)/(MET) /' timing.rpt | sed 's/\(slack (MET) *\)-/\1 /' > clean.rpt
awk -f viol.awk clean.rpt | tail -2
echo "status: $?"
SUMMARY paths=8 violating=0 groups=0
status: 0Zero violations and status 0 — the case a gate needs to be able to return, and the one Chapter 8.7 argued must be demonstrated rather than assumed.
8. Every Decision and Its Source
| decision | alternative that fails | measured in |
|---|---|---|
| carry the header, clear on use | two slacks pair with one startpoint | 10.2 |
| classify by the verdict word | -0.000 is a violation the report rounded | 10.3 |
guard NF < 3 before reading $3 | $NF on a valueless line is the verdict word | 10.3 |
Path Type for setup/hold | the slack sign cannot distinguish them | 10.4 |
clear ty too | hold violations get reported as setup | 10.4 |
worst per (check, clock) | one figure mixes incomparable quantities | 10.5 |
| a count beside every extremum | a misfiled value that is not the extremum vanishes | 10.4 |
| insertion-ordered output arrays | for (k in …) has no defined order | 10.4 |
| marker counts before any output | a partial table is worse than none | 10.2 |
redirect, not a pipe, when reading $? | head replaces awk's status | 12.1 |
9. Common Mistakes
One worst-slack figure. It was a setup check here and a hold check in Module 10.5 — section 4.
Comparing setup against hold. Different causes, different fixes, frequency-independent in one case.
Classifying by the slack sign. A sub-precision violation prints as -0.000.
Not clearing the carried header. A missing line pairs two slacks with one register.
Not clearing Path Type. Hold failures are reported as setup and the design appears to have no hold checks.
Reporting a worst slack with no count. A misfiled value that is not the extremum leaves no trace.
Emitting rows before the preconditions run. A refusal has to refuse everything.
Piping into head when you need awk's status. Redirect instead — section 6.
10. Exercises
Exercise 1 — Violations by check
awk '/^ *Path Type:/ { ty = $3 } /^ *slack \(VIOLATED\)/ { c = (ty == "max" ? "setup" : "hold"); n[c]++; if (!(c in w) || $3 + 0 < w[c]) w[c] = $3 + 0; ty = "" }
END { printf " setup %d violation(s), worst %.3f\n hold %d violation(s), worst %.3f\n", n["setup"], w["setup"], n["hold"], w["hold"] }' timing.rpt setup 3 violation(s), worst -0.205
hold 2 violation(s), worst -0.041Exercise 2 — The TSV stream
awk -v fmt=tsv -f viol.awk timing.rpt > v.tsv
awk -F'\t' 'NR > 1 { n++; if ($5 + 0 < w) { w = $5 + 0; k = $4 "/" $3 } } END { printf " %d row(s); worst %.3f in %s\n", n, w, k }' v.tsv 5 row(s); worst -0.205 in setup/clk_ddrFive rows and the worst named with its group — the downstream consumer never needs to parse the formatted table.
Exercise 3 — Debug a report that is refused
awk 'NR <= 100' timing.rpt > trunc.rpt
awk '/^ *Startpoint:/ { ns++ } /^ *Endpoint:/ { ne++ } /^ *slack/ { nk++ } END { printf " startpoints=%d endpoints=%d slack=%d\n", ns, ne, nk }' trunc.rpt
awk -f viol.awk trunc.rpt 2>&1 >/dev/null startpoints=7 endpoints=7 slack=6
ERROR: 7 startpoint(s) but 6 slack line(s) -- report looks truncatedSeven paths started and six reached a slack line, so the eighth path's delay table was cut off. The counts are the diagnosis.
Exercise 4 — Debug hold violations reported as setup
grep -v 'Path Type: min' timing.rpt > noty.rpt
awk '/^ *Path Type:/ { ty = $3 } /^ *slack \(VIOLATED\)/ { c = (ty == "max" ? "setup" : (ty == "min" ? "hold" : "unknown")); n[c]++ } END { printf " WITHOUT clearing ty: setup=%d hold=%d unknown=%d\n", n["setup"]+0, n["hold"]+0, n["unknown"]+0 }' noty.rpt
awk '/^ *Path Type:/ { ty = $3 } /^ *slack \(VIOLATED\)/ { c = (ty == "max" ? "setup" : (ty == "min" ? "hold" : "unknown")); n[c]++; ty = "" } END { printf " WITH clearing ty: setup=%d hold=%d unknown=%d\n", n["setup"]+0, n["hold"]+0, n["unknown"]+0 }' noty.rpt WITHOUT clearing ty: setup=5 hold=0 unknown=0
WITH clearing ty: setup=3 hold=0 unknown=2Five setup and no hold without the clear, on a report containing two hold violations — Chapter 10.4's result, reproduced here. With the clear they become unknown, which is honest: the information is not in the file.
The script itself refuses this report outright, because all three paths that lost their Path Type line trip its shape guard:
awk -f viol.awk noty.rpt 2>&1 >/dev/null
echo "status: $?"ERROR: 3 path(s) with an unreadable shape or unknown Path Type
status: 2Exercise 5 — Engineering application
Wiring it into a flow, with the three statuses routed:
#!/bin/sh
# check_timing.sh — run the extractor and route the outcome.
#
# Two disciplines from Chapter 12.1 section 7:
# - awk is NOT at the end of a pipeline, so $? is awk's own status.
# - `status=$?` is captured on the line immediately after the command whose
# status it is; everything afterwards uses the VARIABLE, not $?, which is
# overwritten by every command in between.
awk -f viol.awk timing.rpt > timing_report.txt 2> timing_errors.txt
status=$?
awk -v fmt=tsv -f viol.awk timing.rpt > timing_viol.tsv 2>/dev/null
nviol=$(awk -F'\t' 'END { print NR - 1 }' timing_viol.tsv)
case $status in
0) echo "TIMING CLEAN: no violated paths" ;;
1) echo "TIMING VIOLATIONS: $nviol path(s); see timing_report.txt" ;;
2) echo "TIMING REPORT UNUSABLE:"; cat timing_errors.txt ;;
*) echo "EXTRACTOR FAILED (status $status)" ;;
esac
exit $statussh check_timing.sh
echo "status: $?"TIMING VIOLATIONS: 5 path(s); see timing_report.txt
status: 1Status 1 reaches the caller intact. Three things make that work, and all three were measured earlier: awk is not at the end of a pipeline, status is captured on the very next line, and the case reads the variable rather than $? — which by then belongs to the awk and the command substitution that ran in between.
On a report the extractor refuses:
awk 'NR <= 100' timing.rpt > trunc.rpt
cp timing.rpt timing_full.rpt && cp trunc.rpt timing.rpt
sh check_timing.sh
echo "status: $?"
cp timing_full.rpt timing.rptTIMING REPORT UNUSABLE:
ERROR: 7 startpoint(s) but 6 slack line(s) -- report looks truncated
status: 2The flow gets 2 and the diagnostic, rather than a violation count computed from a truncated report. The file is swapped back afterwards so the remaining fences see the full report — a chapter's fixtures are shared state, which is itself a reason a real script takes its input path as an argument rather than hardcoding it.
Exercise 6 — Challenge: show that the four groups cannot be reduced to one number
The script reports four (check, clock) groups. Show that no single figure derived from them preserves what a reviewer needs.
awk -v fmt=tsv -f viol.awk timing.rpt > v.tsv 2>/dev/null
awk -F'\t' 'NR > 1 { n++; s += $5; if (NR == 2 || $5 + 0 < mn) mn = $5 + 0; k = $4 "/" $3; c[k]++ }
END { printf " count %d\n", n
printf " minimum %.3f\n", mn
printf " mean %.3f\n", s / n
printf " sum %.3f\n", s
m = 0; for (g in c) m++
printf " distinct groups %d\n", m }' v.tsv count 5
minimum -0.205
mean -0.080
sum -0.399
distinct groups 4Five candidate headlines, and each loses the thing that matters:
count = 5— five paths to fix, with no idea whether that is one problem or four.minimum = -0.205— the worst number, which belongs tosetup/clk_ddrand says nothing about the twosetup/clk_corepaths or either hold failure.mean = -0.080— averages a setup failure on one clock against a hold failure on another. Chapter 11.8's error exactly.sum = -0.399— has no physical meaning at all; slacks do not add.distinct groups = 4— the most honest single number, and it still omits every magnitude.
What a reviewer needs is the mapping from group to worst slack and count, which is a relation of four rows. Chapter 13.1 Exercise 6 reached the same conclusion about failure signatures: some findings are relations, not figures, and a reduction that insists on one number has to discard one of the two axes.
The practical compromise, when a single cell is unavoidable, is the one Chapter 11.8 section 6 recommended — name the governing group rather than blend:
awk -F'\t' 'NR > 1 { k = $4 "/" $3; if (!(k in w) || $5 + 0 < w[k]) w[k] = $5 + 0; c[k]++ }
END { for (k in w) if (bk == "" || w[k] < bw) { bw = w[k]; bk = k }
printf " governing group: %s at %.3f over %d path(s)\n", bk, bw, c[bk] }' v.tsv governing group: setup/clk_ddr at -0.205 over 1 path(s)"setup/clk_ddr, −0.205, 1 path" survives being questioned: it names the check, the clock, the magnitude and how much work it represents, and it is reproducible. −0.205 alone does not.
11. Summary
- Three outputs: the violated-path rows, the count against the path count, and the worst slack — which is the one with a decision in it.
- There is no single worst slack. It was a setup check on
clk_ddrhere and a hold check in Module 10.5; the answer is a property of the report. - Setup and hold are not comparable — different causes, different fixes, and hold is frequency-independent.
- Report per
(check, clock)with a count — four rows here, four pieces of work. - Carry the header and clear it; classify by the verdict word, not the sign; guard
NF < 3before reading$3. - Clear
Path Typetoo. Without it this report shows 5 setup and 0 hold violations while containing 2 hold failures. - Run every precondition before any output. A refusal must refuse the whole table.
- The direction of a marker mismatch names the fault — truncation against a missing header.
- Redirect rather than pipe when you need awk's status, and capture
$?immediately — Exercise 5's script exited 5 instead of 1 because a command substitution overwrote it. - Some findings are relations, not figures. Count, minimum, mean, sum and group count each discard what a reviewer needs; naming the governing group is the honest single cell.
Chapter 13.3 — Project: Coverage Summary Generator builds the third script: per-block coverage aggregated the way the tool weights it, the blocks below target flagged, and both a readable table and a CSV from one pass.
Where this fits
Part of the awk curriculum.
