set_multicycle_path 多周期路径实例:慢速使能场景下 OpenSTA 约束前后报告对照
更新时间:2026-10-28。本文是「集成电路大类 · 芯片设计」中间层(intermediate)的一篇实验文。概念篇 多周期路径与伪路径:什么时候该对 STA 说谎 已经讲清了两类时序例外的语义边界,多周期路径什么时候该设 则给出了判据清单;SDC 约束入门 与 STA 报告怎么读 是读本文报告的基础。概念讲完还差一环:同一份网表,加不加 set_multicycle_path,报告到底差在哪几行?本篇用 OpenSTA 在 sky130 工艺库上对同一个设计跑两遍 report_checks,把差异钉在数字上。
本文要回答的问题
- 什么样的电路结构天然就是多周期路径,不设例外会怎样?
- set_multicycle_path 2 -setup 之后,报告里哪几个数字会变、哪几行纹丝不动?
- 为什么 setup 放宽到 2 拍之后,hold 必须显式拉回 1 拍?
- 哪些场景看起来像多周期、其实不该用这条约束?
先把两遍报告的核心差异摆上来,四个数字全部出自后文实录日志:
这张对照卡的读法:左边红色区是默认行为——STA 工具不知道你的数据两拍才用一次,它永远按「下一拍就要采样」的最严口径检查;右边绿色区加了例外之后,到达时间(arrival)那一整列数字一个都没变,因为组合逻辑还是那串门,变的只有 required time 那一侧的捕获沿。这正是多周期例外的本质:它不改变电路,只告诉 STA 这条路径的真实使用节奏。
一、什么样的电路配得上多周期例外
多周期路径最经典的合法场景是慢速使能(slow enable)寄存器:一个寄存器不是每拍都更新,而是由一个每隔 N 拍才拉高一次的使能信号 en 控制。本实验的被测设计就是这样一个计数器:
module sta_demo (
input wire clk,
input wire rst_n,
input wire en,
output reg [7:0] q
);
always @(posedge clk) begin
if (!rst_n) q <= 8'h00;
else if (en) q <= q + 1;
end
endmodule当 en 每两拍才有效一次时,q 的翻转只发生在 en 有效的那一拍;q 变化之后,下游寄存器有一整拍的空闲时间才需要采样到稳定值。也就是说,从「q 的某个触发器翻转」到「下游必须采到正确数据」之间,真实可用的预算是两个时钟周期,不是一个。其他典型合法场景还包括:按 N 分频节奏工作的数据通路、多拍才能完成的功能单元(如迭代式乘法器的中间级)、以及明确两拍锁存一次的配置寄存器链。
这里有一个判别上很容易混淆的点:多周期路径不是「这条路径延迟大」,而是「这条路径的使用协议允许它慢」。一条组合逻辑延迟 19 ns、时钟周期 10 ns 的路径,如果下游每拍都要采样,它就是真实的 setup 违约,只能靠流水线拆分或逻辑优化去修;同样一条 19 ns 的路径,如果下游两拍才采样一次,它就是一条合法的多周期路径,设个例外就完事。延迟数值本身不参与判断,参与判断的是采样节奏——这也是为什么多周期例外必须由懂电路协议的设计者手工设,STA 工具自己永远推不出来。
反过来说,不设例外的代价是真实存在的。STA 报出的 setup 违约会驱动后端工具的优化引擎:换更大驱动的单元、插缓冲器调整树、甚至推动布局重排。这些修复动作消耗面积、功耗和布线资源,去满足一个协议上根本不存在的单拍要求。设计里慢速路径越多(配置链、分频数据通路往往成片出现),这份浪费越可观。所以设多周期例外不是「偷懒放过」,而是把优化资源留给真正需要收敛的路径。
时序上这件事看得更清楚。下面这张波形画的是 en 每两拍有效一次时,数据窗口与两个捕获沿候选的关系:
读这张图抓两个 H 高电平柱:默认捕获沿(A)落在第 1 拍末——STA 默认认为下游在这个沿就要采样,于是 q 从 D0 翻到 D1 之后只有一拍时间稳定下来;而 en 的协议决定了下游真正采样 D1 是在第 2 拍末(B)。set_multicycle_path 2 -setup 做的事就是把检查用的捕获沿从 A 挪到 B,让 STA 的检查口径与电路的真实协议对齐。注意它挪的是检查点,不是数据本身——这也是后文报告里到达时间一列完全不变的原因。
二、实验设计:同一网表,两遍 STA
为了让「约束是唯一变量」,实验设计刻意极简:先用 Yosys 把上面的 RTL 综合成 sky130_fd_sc_hd 工艺的门级网表(synth -top sta_demo 做通用综合,dfflibmap 把触发器映射到库单元,abc 做工艺映射优化),然后同一个网表文件 sta_demo_net.v 进 OpenSTA 跑两遍,唯一的差别是第二遍多两条例外命令。时钟统一 create_clock -name clk -period 10.0,即 10 ns 周期。
为什么要强调「同一网表」?因为任何逻辑差异都会污染对照——换一个综合种子、换一个优化等级,最差路径上的门就可能不是同一串,届时 slack 的变化就说不清是约束带来的还是网表带来的。把网表钉死、两遍只改约束,slack 差值才可以精确归因到 set_multicycle_path 这一条命令上。这也是读别人实验数据时该养成的习惯:先看变量控制干不干净,再看数字。
两遍脚本的全文如下,对照着看差异一目了然:
# run_nomcp.tcl —— 第一遍:无例外,默认单拍检查
read_liberty $env(STA_LIB)
read_verilog sta_demo_net.v
link_design sta_demo
create_clock -name clk -period 10.0 [get_ports clk]
report_checks -path_delay max# run_mcp.tcl —— 第二遍:加多周期例外
read_liberty $env(STA_LIB)
read_verilog sta_demo_net.v
link_design sta_demo
create_clock -name clk -period 10.0 [get_ports clk]
set_multicycle_path 2 -setup -from [all_registers] -to [all_registers]
set_multicycle_path 1 -hold -from [all_registers] -to [all_registers]
report_checks -path_delay max这里 set_multicycle_path 2 -setup 的语义是:setup 检查用的捕获沿向后推迟 2-1=1 个周期(数字 2 表示「数据窗口跨 2 拍」);配套的 set_multicycle_path 1 -hold 把 hold 检查的捕获沿拉回默认位置——原因在第四节展开。-from [all_registers] -to [all_registers] 是演示用的全量写法,真实项目里应当用 -from/-to 精确圈定慢速路径,否则会把你没打算放宽的路径一起放掉,这一点 多周期路径什么时候该设 里有更细的判据。
三、第一遍:默认单拍检查的报告
先看没有例外的基准跑。report_checks -path_delay max 给出最差 setup 路径的完整明细:
这份报告分上下两半:上半是 data arrival path——从源触发器 _37_ 的时钟沿出发,经 and4、nand4、or3、a21boi 四个组合门,1.17 ns 后到达目的触发器 _44_ 的 D 端;下半是 data required path——捕获沿取第 10.00 ns 那个上升沿(10 ns 周期的下一拍),扣掉 0.08 ns 的库建立时间,得到 required time 9.92。9.92 减 1.17,slack 8.76,MET。
注意这份报告本身没有任何报错——8.76 的 slack 看起来很健康。问题在于它是按错误的口径算出来的健康:电路协议明明给了两拍预算,STA 却只认一拍。如果这个设计时序收敛不了(比如周期压到 1.5 ns),后端工具会为了这个虚假的违约去插缓冲器、换大驱动单元,白白消耗面积和功耗去满足一个根本不存在的约束。这就是「不设例外会怎样」的答案:不是报不出来,而是把工具引向过度修复。
顺着这份报告再交代一下读法:上半页 arrival path 每一行三个数字分别是本级延迟、累计时刻、跳变方向(^ 上升、v 下降),从源触发器时钟引脚的 0.00 一路累加到目的触发器 D 端的 1.17;下半页 required path 从捕获沿时刻倒推出扣除库建立时间后的最后期限。两半相减即 slack,正数 MET、负数 VIOLATED。这套三段式结构在所有 STA 工具里通用,更系统的字段讲解见 STA 报告怎么读,本文只盯两遍之间的差异行。
四、第二遍:加了 set_multicycle_path 之后
同一网表加上两条例外再跑,报告变成这样:
逐行对比两遍报告,差异精确地集中在 required path 一侧。把关键三行原样摘录对照:
# 无例外(run1.log):
10.00 10.00 clock clk (rise edge)
-0.08 9.92 library setup time
9.92 data required time
8.76 slack (MET)
# set_multicycle_path 2 -setup(run2.log):
20.00 20.00 clock clk (rise edge)
-0.08 19.92 library setup time
19.92 data required time
18.76 slack (MET)捕获沿时刻从 10.00 变成 20.00,恰好后移一个时钟周期;arrival path 那半页(_37_/CLK 到 _44_/D 的每一行延迟)两遍逐字一致,这从机器输出层面印证了第一节的判断:多周期例外只挪检查点,不碰数据路径。slack 从 8.76 涨到 18.76,差值恰为 10.00——一个完整周期,与约束语义严丝合缝。
还有一个容易漏掉的细节:为什么第二遍脚本里必须跟一句 set_multicycle_path 1 -hold?STA 的默认规则是 hold 检查沿跟着 setup 捕获沿走——setup 捕获沿后移一拍之后,hold 的检查沿也会被带着后移,变成「前一个数据必须保持到下一拍之后」,这会把原本根本不存在的 hold 违约凭空造出来,后端工具再去修这些假违约(往数据路径里插延迟单元),又是白烧面积,极端情况下还会把短路径修出新的问题。-hold 数字写 1 的意思就是「hold 检查沿回到默认位置」,即数据只需保持过同一拍沿之后的一小段时间。
把这个规则推广开:setup 放 N 拍,hold 就写 N-1。setup 2 配 hold 1、setup 3 配 hold 2,永远成对出现。只写 setup 不写 hold 是多周期约束新手最高频的错误,症状很有迷惑性——setup 报告一片祥和,hold 报告却冒出一堆莫名其妙的违约,不知情的人会以为是时钟树或单元延迟出了问题,实际上是自己的例外命令少写了半句。在商用工具(PrimeTime 等)里语义完全一致,这条习惯可以跨工具带走。
五、像多周期、其实不该用的场景
这条约束的反面教材同样值得记。下面这张表把「该用」与「不该用」的边界一次划清:
| 场景 | 是否该用 set_multicycle_path | 原因 |
|---|---|---|
| en 每 N 拍有效一次的慢速寄存器 | 该用 | 数据真实稳定窗口就是 N 拍 |
| 迭代式运算单元的中间级 | 该用 | 中间结果多拍后才被采样 |
| 跨异步时钟域的路径 | 不该用 | 两时钟无确定相位,应 set_false_path 或 set_clock_groups |
| 「这条路径太难收敛,放宽一下」 | 不该用 | 电路协议并没有给它多拍预算,是在掩盖真实违约 |
| 到输出端口的慢速外设路径 | 多半不该用 | 应改用 set_output_delay 描述外部时序 |
| 上电后只配置一次的寄存器 | 谨慎用 | 更常用 set_false_path 或 max_delay 约束 |
判据其实只有一条:电路的功能协议是否真的给了这条路径多于一个周期的预算。给得了,就设例外让 STA 口径对齐现实;给不了,设了就是把真实违约扫到地毯下,芯片上板后以偶发错误的形式讨债。异步时钟域那条尤其常见——两个时钟相位关系不确定,谈不上「第 2 拍捕获」,正确做法见 多周期路径与伪路径 里伪路径一侧的讨论。
落地时还有两条工程纪律值得带上。其一,-from/-to 要精确圈定到具体的寄存器组或层次实例,本文演示用的 [all_registers] 全量写法在真实设计里几乎总是过宽——它会把同频正常路径一并放宽,等于关掉了大半个设计的 setup 检查。其二,例外写完后要反过来验证:用 report_checks 加 -path_delay min 确认 hold 没被误伤,用 report_disable_timing 或等效报告确认例外的作用范围与预期一致,而不是只看 setup slack 变好就收工。约束文件是设计的一部分,值得和 RTL 一样走评审。
这一篇在知识体系里的位置
- 概念承接:多周期路径与伪路径:什么时候该对 STA 说谎(两类例外的语义边界)与 多周期路径什么时候该设(判据清单);
- 基础工具篇:SDC 约束入门(create_clock 等基本命令)与 STA 报告怎么读(arrival/required/slack 各段的读法);
- 相邻实验:OpenSTA create_clock 报告实测与本篇共用同一份 RTL 与综合网表,是同一实验台上的基础档;
- 后续方向:时序收敛见 时序收敛基础,亚稳态与捕获沿的关系见 setup/hold 与亚稳态。
一句话总结
set_multicycle_path 的唯一合法用途是让 STA 的检查口径对齐电路的真实节奏:慢速使能、分频数据通路这类「数据天然多拍才用一次」的路径才配得上它。本篇在 OpenSTA 2.4.0 上对同一 sky130 网表做对照实验:默认单拍检查 slack 8.76,加 set_multicycle_path 2 -setup(hold 回 1)后捕获沿从 10.00 移到 20.00、slack 变 18.76,而到达路径逐行不变——例外的全部效果就是挪动检查点。记住两件事:setup 放几拍 hold 就写拍数减一、永远成对写;-from/-to 要精确圈定,别用全寄存器写法把不该放宽的路径一起放掉。
实验环境
- 机器/系统:云主机 16C32G / Ubuntu 22.04(eda-golden-v1 镜像)
- 工具版本:Yosys 0.9(git sha1 1979e0b,综合 sta_demo_net.v)+ OpenSTA 2.4.0(41a51eaf4c,OpenLane docker 镜像内
/build/bin/sta);工艺库 sky130_fd_sc_hd tt_025C_1v80 - 日期:2026-10-28
- 完整命令:
# 综合门级网表(Yosys,sky130 库由 $STA_LIB 指向)
yosys -p "read_verilog sta_demo.v; synth -top sta_demo; \
dfflibmap -liberty $STA_LIB; abc -liberty $STA_LIB; \
clean; write_verilog sta_demo_net.v"
# 第一遍:无例外,默认单拍检查
/build/bin/sta -exit run_nomcp.tcl # 输出存 run1.log
# 第二遍:set_multicycle_path 2 -setup + 1 -hold
/build/bin/sta -exit run_mcp.tcl # 输出存 run2.log代码获取
本文完整 RTL、两份 OpenSTA 脚本与两遍原始报告已同步到公开仓库:geek-doc.cn/experiments/opensta-multicycle-path-example,克隆后进入该目录,按 README 先跑 Yosys 综合生成网表、再分别执行两份 tcl 脚本即可复现本文全部数字(需可访问 sky130 工艺库与 OpenSTA 可执行文件)。