SDC 编写流程:从时钟到 IO,一份约束的正确落地顺序
静态时序分析 STA 入门 讲过,STA 工具本身只懂拓扑和延迟,芯片要跑多快、边界留多少预算,全部来自一份 SDC(Synopsys Design Constraints)约束文件。很多新手拿到一个设计就把 create_clock、set_input_delay、set_false_path 混写一气,结果要么报 clock not found,要么例外根本没生效。本篇按时钟 → IO 边界 → 例外的正确顺序写一份最小 SDC,每加一类约束就用 OpenSTA 2.0.17 真跑一次 report_checks,用前后 slack 数字证明约束真的进了计算。约束在整个流程里如何分层传播,见 SDC 时序合同的生命周期。
本文要回答的问题
- 一份 SDC 里时钟、IO 延迟、例外约束为什么不能随便排序,写反了会怎样?
create_clock的周期、-waveform、set_clock_uncertainty各自在报告里变成哪一行数字?set_input_delay/set_output_delay的数值是「芯片内部延迟」还是「芯片外部延迟」?set_clock_groups、set_false_path、set_multicycle_path怎么写才算生效,多周期路径为什么会意外打穿 hold?
读这张卡抓住一条主线:第 1、2 层是在建立完整的时序合同,第 3 层才允许在合同上开口子;每一层写完都不是看脚本退出码,而是看第 4 层的报告数字。
一、这一步在 STA 流程里的位置
上游给约束编写者的是综合后的门级网表和单元库(liberty),下游是 report_checks 产生的 setup/hold slack 报告,供综合与布局布线迭代收敛:
图里红、蓝、紫三色就是约束的三层语义,绿色报告是唯一验收点。注意反馈环:例外写错(最典型是多周期只写 setup),常常要到报告里 hold 变红才发现,所以约束不是一次写完,而是「写一层、验一层」。
二、约束命令逐段解读
第一层,时钟。 create_clock -name clk -period 10.0 -waveform {0.0 5.0} [get_ports clk]:-period 是周期(ns),决定所有同域路径的要求时间;-waveform {上升沿 下降沿} 定义波形,省略时默认上升沿在 0、占空比 50%,建议显式写出;末尾的端口集合是时钟源。时钟建完再加余量:set_clock_uncertainty -setup 0.3 [get_clocks clk] 表示给 setup 要求时间预留 0.3ns 给抖动与偏差。它必须写在 create_clock 之后,因为参数按名解析时钟对象,顺序颠倒只会得到一条 Warning、约束静默丢弃,详见 OpenSTA clock uncertainty 未生效排查。
第二层,IO 边界。 两个命令描述的都是芯片外部占用的时间预算:
| 命令 | 数值含义 | 对 slack 的影响 |
|---|---|---|
set_input_delay 1.0 -clock clk [get_ports d_in] | 外部器件在 clk 沿之后 1.0ns 才把数据放到端口 | 数据到达时间加 1.0,内部可用预算少 1.0 |
set_output_delay 0.5 -clock clk [get_ports q_out] | 下游器件要求输出在下一个 clk 沿之前 0.5ns 稳定 | 要求时间减 0.5,输出路径预算少 0.5 |
-clock 引用的时钟同样必须已存在,否则报 clock not found,排查见 OpenSTA set_input_delay 时钟引用错误。需要给慢角、快角分别预算时再加 -max/-min:-max 作用于 setup 报告、-min 作用于 hold 报告,两个都不写则同一数值同时进两类检查。不写 IO 延迟时,输入端口没有起点、输出端口没有终点,这些路径根本不进报告,看似全绿,实际是漏检。
第三层,例外。 例外是对默认「单周期全检查」合同的削减,必须最后写、且要知道每条削减了什么:
set_clock_groups -asynchronous -group [get_clocks clk] -group [get_clocks clk2]:声明两个时钟异步,互跨路径双向切除,STA 不再为它们背书(跨域要靠同步器等 CDC 手段保证,深读见 SDC 时钟组约束)。set_false_path -from [get_ports en_in] -to [get_pins u_b/D]:切断指定起止对的时序检查,适合近乎静态的配置/使能;滥用会掩盖真问题,方法论见 false path 审计。set_multicycle_path -setup 2 -from [get_cells u_a] -to [get_cells u_b]:数据两拍才用一次,捕获沿推迟一个周期。OpenSTA 里-from/-to给的是时序单元 cell,给 Q 引脚会被判为非法起点。
三、输入输出文件长什么样
输入三件:教学库 teaching.lib(含 DFF_P、NAND2、BUF_X 三个单元,延迟为标量常数,仅用于演示约束解析,产线替换为 PDK 库);网表 top.v(clk 域两级流水线 u_a→u_b,u_c 工作在异步 clk2 域);按层拆分的 s1_clock.sdc、s2_io.sdc、s3_exc.sdc。第一层 SDC 全文很短,对应文件里的注释逐行说明了每个数值进报告的位置:
create_clock -name clk -period 10.0 -waveform {0.0 5.0} [get_ports clk]
create_clock -name clk2 -period 15.0 [get_ports clk2]
set_clock_uncertainty -setup 0.3 [get_clocks clk]输出是 report_checks 文本,固定读四段:Startpoint/Endpoint(路径谁到谁)、data arrival time(到达时间)、data required time(要求时间)、末尾的 slack (MET/VIOLATED)。同一起讫对只列最差的一条,例如阶段 2 里 u_b 的最差路径是 8.45 的 en_in 输入路径,9.25 的寄存器间路径就被折叠不显示,读报告时要意识到这一点,别把「没列出」当成「不存在」。
四、常见错与检查点
- 顺序颠倒:先写 IO/uncertainty 后
create_clock。现象是加载期clock not found警告但退出码仍为 0。检查加载期输出有没有 Warning;纠正办法是把所有create_clock提到文件最前。 - 漏写 IO 延迟误判全绿:现象是报告 Startpoint 全是触发器、没有任何端口。检查报告里输入/输出端口是否出现;漏约束端点的系统排查见 unconstrained endpoint 排查。
- 多周期只放宽 setup:同频同相路径上
-setup 2会让工具默认把 hold 检查沿前推一拍,实测 hold slack 变成 −9.65,下一节给出数字。纠正:同域多周期成对补set_multicycle_path -hold 1,规则深读见 多周期路径约束。 - 跨时钟域不切组:不写
set_clock_groups时,STA 会一本正经地给异步路径算一个 slack,这个数字毫无保证意义。检查报告中是否还有「clocked by clk → clocked by clk2」的路径;有就说明分组没生效。
五、真实跑一遍:四阶段 report_checks 对照
四个阶段各启动一次独立的 sta -no_init -exit 进程。阶段 1 只建时钟(clk 10ns、clk2 15ns、uncertainty 0.3ns),同域 u_a→u_b 路径的 setup 报告:
读这张终端卡:到达时间 0.35 = DFF 的 C→Q 0.20 + NAND2 0.15;要求时间 9.60 = 周期 10 − uncertainty 0.30 − 库 setup 0.10;slack 9.25。此时报告的 Startpoint 只有 u_a、u_b 两个触发器,四个 IO 端口一条路径都没有——它们正处于漏检状态。跨域路径 u_b→u_c 反而被算了一个 4.70 的假 slack。
阶段 2 补上 IO 约束后,输入端口第一次进入报告,en_in→u_b 路径:
到达时间 1.15 = 片外输入延迟 1.0 + NAND2 0.15,要求时间仍是 9.60,slack 8.45;同阶段 d_in→u_a 为 8.60,输出路径 u_b→q_out 为 8.90(要求时间被 output delay 压到 9.20)。IO 预算从此真实参与收敛。
阶段 3 加入三类例外,报告发生三件事:en_in 起点消失(false path);跨域路径整条消失(clock groups);u_a→u_b 的 setup 因多周期变成 19.25(捕获沿推到第 20ns)。但 hold 报告同时翻红:
Startpoint: u_a (rising edge-triggered flip-flop clocked by clk)
Endpoint: u_b (rising edge-triggered flip-flop clocked by clk)
Path Group: clk
Path Type: min
0.00 clock clk (rise edge)
10.00 10.00 clock clk (rise edge)
...
-9.65 slack (VIOLATED)这正是第四节第 3 个坑:hold 检查沿被前推到 10ns。同一份 hold 报告里,输入与输出路径仍然是正裕量(d_in 为 1.00、q_out 为 0.80),说明红的只有多周期这一条,问题定位不会有歧义。阶段 4 补一条 set_multicycle_path -hold 1 -from [get_cells u_a] -to [get_cells u_b],hold 检查沿回到 0ns,slack 恢复 0.35 MET,而 setup 的 19.25 保持不变——放宽与保持同时正确。四个阶段退出码均为 0,加载期零 Warning。
代码获取:完整教学库、网表、四个阶段的 SDC/Tcl 与运行脚本位于 geek-doc.cn/experiments/flow-opensta-sdc-writing,执行 bash run.sh,或用 docker run --rm -v "$PWD":/work -w /work geek-eda:0.3-sta bash -lc 'bash run.sh' 一次复现四个阶段的全部对照数字。
实验环境
- 机器/系统:本地 Docker 容器,Ubuntu 24.04;宿主为 macOS。
- 工具版本:OpenSTA 2.0.17(
sta -version真实输出2.0.17,镜像 geek-eda:0.3-sta)。 - 日期:2026-11-03。
- 完整命令:在实验目录执行
bash run.sh,依次运行stage1.tcl~stage4.tcl,每个进程独立读库、读网表、source 对应阶段的 SDC 并输出 setup/hold 两份report_checks。 - 实验边界:
teaching.lib仅含 3 个单元、标量延迟,数值只用于演示约束如何进入时序计算,不能外推为任何工艺的签核结论;OpenSTA 2.0.17 没有report_clock_groups命令,例外是否生效以路径在报告中的出现/消失为准。
这一篇在知识体系里的位置
本篇讲 SDC 的正常编写顺序,属于约束入门的操作层:时钟建不出来先看 OpenSTA no clocks found 排查,单条命令失效的报错类问题走 OpenSTA set_input_delay 时钟引用错误 与 clock uncertainty 未生效;想深入三个例外的方法论,再读 多周期路径约束、SDC 时钟组约束 与 SDC 约束验证。
总结
写 SDC 的正确顺序是「先有时钟、再划边界、最后开口子」:create_clock 加 set_clock_uncertainty 定义周期与余量,set_input_delay/set_output_delay 描述片外器件占用的预算并让 IO 路径进入报告,set_clock_groups、set_false_path、set_multicycle_path 最后按功能语义削减检查;每加一层都用 report_checks -path_delay max/min 的数字复验——实测中时钟层同域 slack 9.25,IO 层让端口路径以 8.45/8.60/8.90 出现,例外层让伪路径与跨域路径消失、多周期 setup 放宽到 19.25,而同域多周期必须成对补 -hold 1 才能把被前推的 hold 检查沿拉回、避免 −9.65 的假违例。