STA 时序报告提取 TCL 脚本:从 OpenSTA 报告抓 WNS/TNS 与违例条数
更新时间:2026-11-02。这是芯片设计入门的脚本模板,给已经能在 OpenSTA 里跑出 STA 时序报告、希望把结果接进回归脚本或电子表格的读者使用。模板只负责把时序结果压缩成机器可读的一行摘要,不替代人工阅读完整报告。
首屏答案:用 find_timing_paths -unique_paths_to_endpoint -group_count N -path_delay max -sort_by_slack 拿到路径对象列表,逐条 get_property $path slack,最小值即 WNS(worst negative slack,最差裕量),负 slack 求和即 TNS(total negative slack,负裕量总和),负值个数即违例路径条数。结果打印为固定格式的 STA_STATUS ... status=MET|VIOLATED 摘要行。setup 与 hold 必须分两次收集;设计没有时钟时路径列表为空,脚本输出 NO_CLOCKS 而不是伪造一组零。
本文要回答的问题
- 怎样不复制粘贴
report_checks文本,就让脚本拿到 WNS、TNS 和违例路径条数? - 为什么默认的
report_checks会漏掉第二个违例端点,模板是怎么避开的? - 设计没有约束时钟、路径对象为空时,脚本为什么不能静默输出 0?
- setup 和 hold 能不能一次抓完?OpenSTA 的退出码为什么不能直接用来判失败?
- 怎样把摘要改成 CSV,或者加上自己的裕量阈值?
一、这个模板用在什么场景
时序收敛是一个反复迭代的过程:综合后看一次、布局后看一次、修完一条关键路径再看一次。每次都人肉打开报告读三个数字,既慢又容易看错行。这个模板面向三个典型场景:回归脚本在综合或布局结束后自动跑一遍 STA(静态时序分析),只看摘要行决定放行还是打回;把多个工程或多个时钟角的结果汇总成一张趋势表;给新人一个统一口径,避免「TNS 是负的所以越小越好还是越负越差」这类口头歧义。
读这张图抓住数据的方向:蓝色区是人工维护的输入,紫色区是 OpenSTA 在内存里生成的路径对象,橙色区是脚本唯一新增的东西。模板不重新计算时序,所有数字都从路径对象的属性读出,因此它和图形界面里看到的报告同源,不会出现两套结果。
二、脚本的数据流:从一次 sta 运行到一行摘要
下图是模板内部的处理流水线。蓝色节点是准备动作,紫色节点是核心汇总,橙色节点是输出;红色支路专门处理「设计里没有任何时钟」的异常工况。
判空节点是这个模板和「一段能跑的临时脚本」最大的区别:忘记写 create_clock 的设计照样能完成 link_design,此时 find_timing_paths 返回空列表。若把空列表直接喂给汇总逻辑,要么报错退出,要么被误读成「零条违例、全部通过」。模板先问一句 [all_clocks] 是否为空,空就走红色支路,明确输出 NO_CLOCKS,把「没约束」和「约束后全过」区分开。这个坑的完整案例可参考 OpenSTA 无约束端点排查。
三、完整可复制的 TCL 模板
下面是全部代码,在 OpenSTA 2.0.17 上真实跑通。保存为 extract_sta_summary.tcl,在读完库、网表、SDC 之后用 source 引入即可;参数在 source 之前用 set 覆盖。
# extract_sta_summary.tcl
# OpenSTA timing summary extractor (verified on OpenSTA 2.0.17).
# Source this file AFTER read_liberty/read_verilog/link_design and the SDC.
#
# Parameters (set before sourcing this file, or edit the defaults):
# sta_path_delay max = setup check, min = hold check (default: max)
# sta_max_paths paths collected per path group (default: 20)
# sta_fail_slack WNS below this value marks STA_VIOLATED (default: 0.0)
# sta_csv 1 = also emit one CSV row per path (default: 0)
if {![info exists sta_path_delay]} { set sta_path_delay "max" }
if {![info exists sta_max_paths]} { set sta_max_paths 20 }
if {![info exists sta_fail_slack]} { set sta_fail_slack 0.0 }
if {![info exists sta_csv]} { set sta_csv 0 }
proc sta_check_name {path_delay} {
switch -glob -- $path_delay {
"max*" { return "setup" }
"min*" { return "hold" }
default { error "sta_path_delay must be max or min, got \"$path_delay\"" }
}
}
proc sta_collect_paths {path_delay max_paths} {
if {[current_design] == "null"} {
error "no linked design; run read_liberty/read_verilog/link_design first"
}
if {[llength [all_clocks]] == 0} {
return -code 10 "NO_CLOCKS"
}
set paths [find_timing_paths -unique_paths_to_endpoint \
-group_count $max_paths -path_delay $path_delay -sort_by_slack]
lsearch -all -inline -not -exact $paths {}
}
proc sta_summarize {paths} {
set wns ""
set tns 0.0
set violating 0
foreach path $paths {
set slack [get_property $path slack]
if {$wns eq "" || $slack < $wns} {
set wns $slack
}
if {$slack < 0.0} {
set tns [expr {$tns + $slack}]
incr violating
}
}
list $wns $tns $violating [llength $paths]
}
proc sta_print_summary {check_name paths summary fail_slack csv} {
lassign $summary wns tns violating total
puts "------------------------------------------------------------"
puts "STA check : $check_name"
puts "Paths collected: $total"
if {$total == 0} {
puts "WNS/TNS : n/a (no constrained timing path)"
puts "STA_STATUS check=$check_name status=NO_CLOCKS wns=NA tns=NA violating_paths=0 total=0"
return
}
puts "WNS : [format "%.3f" $wns]"
puts "TNS : [format "%.3f" $tns]"
puts "Violating paths: $violating (slack < 0)"
if {$wns < $fail_slack} {
set status "VIOLATED"
} else {
set status "MET"
}
puts "STA_STATUS check=$check_name status=$status wns=[format "%.3f" $wns] tns=[format "%.3f" $tns] violating_paths=$violating total=$total"
if {$csv} {
puts "STA_PATH_CSV,check,startpoint,endpoint,slack"
foreach path $paths {
set startpin [get_property $path startpoint]
set endpin [get_property $path endpoint]
set slack [get_property $path slack]
puts [format "STA_PATH_CSV,%s,%s,%s,%.3f" $check_name \
[get_property $startpin full_name] \
[get_property $endpin full_name] $slack]
}
}
}
set check_name [sta_check_name $sta_path_delay]
if {[catch {sta_collect_paths $sta_path_delay $sta_max_paths} paths err_opts]} {
if {[dict get $err_opts -code] == 10} {
puts "STA_STATUS check=$check_name status=NO_CLOCKS wns=NA tns=NA violating_paths=0 total=0"
puts "NOTE: no clock object exists; create_clock is required before STA."
} else {
puts "STA_STATUS check=$check_name status=SCRIPT_ERROR wns=NA tns=NA violating_paths=0 total=0"
puts "ERROR: [dict get $err_opts -errorinfo]"
}
} else {
sta_print_summary $check_name $paths [sta_summarize $paths] $sta_fail_slack $sta_csv
}代码分四段。参数区给四个变量兜底默认值,外部 set 过的不覆盖;sta_collect_paths 用 return -code 10 把「无时钟」作为一类独立信号抛出,和真正的 Tcl 错误分开;sta_summarize 是一次线性遍历,同时算出三个指标,WNS 初值用空字符串而不是 0,否则全正裕量的设计会被错误地记成 WNS=0;最后一段统一打印,STA_STATUS 行字段顺序固定,方便上层用 sed 或 awk 提取。
一个版本相关的细节:OpenSTA 2.0.17 的 find_timing_paths 用 -group_count 而不是某些新文档里的 -max_paths,用 -path_delay 而不是 -delay_type。换版本前先执行 help find_timing_paths 对一下关键字,不认的关键字只会产生一条警告,随后返回空结果——这正是最危险的「静默无数据」。
四、真实运行:收紧时钟后的两条违例链
实验设计是同一个发射触发器驱动两条组合链:一条 15 级缓冲、一条 20 级缓冲,每级缓冲延迟 0.15 ns,触发器 clk-to-q 为 0.20 ns、setup 约束 0.10 ns。时钟 5 ns 时两链都过;把时钟收紧到 2 ns 后,两链分别违例 −0.55 ns 和 −1.30 ns。下面是收紧工况的真实输出。
输出里三个数字可以手工对账:最差路径在 20 级链末端 u_c/D,2 − 0.10 − 0.20 − 20×0.15 = −1.30,即 WNS;两条链都为负,TNS = −1.30 + (−0.55) = −1.85;违例条数为 2。Paths collected: 2 说明两个端点都进了统计——这不是默认行为,下一节展开。
五、使用注意事项
第一,report_checks 默认每个时钟组只报一条路径。 在同一设计上直接执行不带参数的 report_checks,报告里只有最差的 u_c 一条,15 级链的 u_b/D 违例不会出现;加上 -group_count 10 -unique_paths_to_endpoint 后两个端点才都报出来。模板在 find_timing_paths 上固定带了这两个选项,并通过 sta_max_paths 暴露上限。如果工程里有上千个违例端点,记得把上限调大,否则 TNS 和违例条数只覆盖前 N 条。
第二,setup 和 hold 分两次抓。 -path_delay max 对应 setup(建立时间检查),min 对应 hold(保持时间检查),一次调用不能同时返回两类。收紧时钟的实验里 setup 违例 −1.30 ns,但 hold 的 WNS 是 +2.45 ns,依然 MET——长组合链对 setup 不利、对 hold 反而有利,混在一个数字里会丢失这个信息。检查类型会打印在摘要行的 check= 字段中。关于两类检查的物理含义,可读 setup 检查原理与 OpenSTA 实操。
第三,路径对象为空不是「零违例」。 忘记约束时钟、读错了顶层、SDC 没 source 上,都会让路径列表为空。模板用 all_clocks 判空并输出 NO_CLOCKS,上层应把它映射为退出码 2(输入问题),与「有约束且存在违例」的退出码 1 区分。排查无时钟约束的完整过程见 OpenSTA no clocks found 报错。
第四,OpenSTA 的进程退出码不能直接用来判失败。 实测在脚本里写 exit 1 甚至 exit 3,配合 -no_init -exit 运行后进程仍返回 0;STA 违例本身也不改变退出码。正确做法是让外层 shell 解析 STA_STATUS 行再映射退出码,配套 run.sh 就是这么做的:MET 为 0、VIOLATED 为 1、NO_CLOCKS 为 2。
第五,摘要不替代完整报告。 WNS/TNS 回答「过没过、差多少」,不回答「为什么差、怎么修」。定位根因时仍要打开 report_checks -path_delay max -fields {slew capacitance} 看具体是哪级门、哪段线在拖后腿。
六、修改示范:加 CSV 输出和自定义裕量阈值
两处修改都只动参数区或打印段,不需要改收集逻辑。第一次修改打开逐路径 CSV,方便把结果贴进电子表格,只需在 source 模板前加一行:
set sta_csv 1
source extract_sta_summary.tcl收紧工况下真实输出如下,路径按 slack 升序排列,最差端点排在第一行:
STA_PATH_CSV,check,startpoint,endpoint,slack
STA_PATH_CSV,setup,u_a/Q,u_c/D,-1.300
STA_PATH_CSV,setup,u_a/Q,u_b/D,-0.550第二次修改把验收标准从「非负即可」收紧为「必须保留 2 ns 裕量」。5 ns 时钟下 WNS 为 1.700,按默认阈值本是 MET,改阈值后状态翻转:
-# 默认:sta_fail_slack = 0.0
+set sta_fail_slack 2.0
source extract_sta_summary.tclSTA_STATUS check=setup status=VIOLATED wns=1.700 tns=0.000 violating_paths=0 total=2
CASE_EXIT case_margin=1 (status=VIOLATED)注意此时没有任何负 slack,violating_paths=0,但状态仍是 VIOLATED——因为判定依据从「slack 是否为负」变成了「WNS 是否达到工艺留量要求」。流片前的多角收敛常用这种口径,阈值可以再按 corner 分别设置。
实验环境
- 机器/系统:Mac(x86_64)/ Darwin 25.5.0,实验在 Docker 容器
geek-eda:0.3-sta(Ubuntu 24.04)内运行 - 工具版本:OpenSTA 2.0.17(Debian 官方包)
- 日期:2026-11-02
- 完整命令:
docker run --rm -v "$PWD":/work -w /work geek-eda:0.3-sta bash -lc 'bash run.sh';六个工况依次为 5 ns 时钟 setup、2 ns 时钟 setup、2 ns 时钟 hold、无时钟、2 ns 裕量阈值、CSV 输出
代码获取
完整模板、教学 liberty、两级链网表、六个工况脚本与 run.sh 位于公开仓 https://github.com/derekzhuo/geek-doc.cn/tree/main/experiments/tpl-sta-report-extract-tcl。进入该目录执行 docker run --rm -v "$PWD":/work -w /work geek-eda:0.3-sta bash -lc 'bash run.sh' 可复现本文全部输出;接入自己的工程时,把驱动脚本里的库、网表和 SDC 换成实际文件,在约束之后 source extract_sta_summary.tcl 即可。
这一篇在知识体系里的位置
本篇处在 STA 流程的自动化环节:前置是约束与报告的基础操作,可从 OpenSTA 创建时钟与报告生成 和 STA 入门:静态时序分析在干什么 开始;看到违例后如何解读,可参考 负 slack 与 VIOLATED 报告。同为「结果汇总」模板,仿真侧的 仿真日志解析 Python 模板 处理的是回归用例状态,本篇处理的是时序裕量,两者可以接在同一条回归流水线的首尾。
总结
STA 时序报告提取 TCL 脚本的核心,是用 find_timing_paths 直接拿路径对象、逐条读 slack 属性,线性汇总出 WNS、TNS 与违例条数,并打印字段固定的 STA_STATUS 摘要行。2 ns 收紧时钟实验真实测得 WNS=−1.300、TNS=−1.850、两条违例路径;无时钟设计输出 NO_CLOCKS,setup 与 hold 分开统计,退出码由外层 shell 解析摘要行映射。摘要只管门禁和汇总,具体违例原因仍需回到完整 report_checks 报告人工分析。