set_false_path 伪路径与异步时钟组的边界:OpenSTA 双写法实测
更新时间:2026-10-28。本文是「集成电路大类 · 芯片设计」中间层(intermediate)的一篇实验文。写时序约束的人都知道一个常识:跨异步时钟域的路径不该做 setup/hold 检查。但落到 SDC 里,「不检查」有两种主流写法——
set_false_path -from clk_a -to clk_b和set_clock_groups -asynchronous -group clk_a -group clk_b。站内 set_disable_timing 与 set_false_path 的区别 讲了伪路径在约束家族里的位置,CDC 基础 讲了跨域为什么危险,但「这两条命令在真实 STA 工具里跑出来是不是一回事」这个问题,不亲眼看报告是答不踏实的。本篇用一个带两级同步器的双时钟设计,在 OpenSTA 上把两种写法各跑一遍,报告摆在一起对照。
本文要回答的问题
set_false_path和set_clock_groups -asynchronous的语义边界各是什么?谁管「路径」、谁管「时钟关系」?- 对一个标准的 CDC 同步器结构,两种写法在
report_checks里的检查集合是否真的一致? - 工程上该选哪一种?什么场景下两者不等价?
- 报告里哪些字段能证明「跨域路径确实被豁免了」?
先把两条命令的分工摆出来,这是全文的对照基线:
这张对比卡的要点在最后一条:两者最大的区别不在「结果」而在「声明的意图」。set_false_path 对工具说的是「这条路径你别查,原因我自己负责」;set_clock_groups -asynchronous 说的是「这两个时钟本来就是异步的,物理上没有有意义的相位关系」。后者把设计知识显式交给了工具,前者只是打了个豁免补丁。这个差别在纯 CDC 结构里看不出数字差异,但到后面「什么时候不等价」一节会看到它咬人的方式。
一、实验设计:一个 clk_a 10ns / clk_b 7ns 的同步器结构
为了让两种写法的对照足够干净,RTL 刻意做成最小可复现结构:两个时钟域,之间只有一条两级触发器同步链。
// sta_async.v — two asynchronous clock domains with a 2-flop synchronizer crossing
module sta_async (
input wire clk_a,
input wire clk_b,
input wire [7:0] din_a,
output wire [7:0] dout_b
);
reg [7:0] reg_a;
reg [7:0] sync1;
reg [7:0] reg_b;
always @(posedge clk_a)
reg_a <= din_a;
always @(posedge clk_b) begin
sync1 <= reg_a;
reg_b <= sync1;
end
assign dout_b = reg_b;
endmodule结构一眼就能读完:reg_a 在 clk_a 域采样输入;sync1 和 reg_b 都在 clk_b 域,构成经典的两级同步器——第一拍 sync1 用来消化亚稳态,第二拍 reg_b 才是给后级用的稳定信号(两级同步器的原理见 CDC 基础,这里不展开)。关键路径有三类:clk_a 域内的 reg_a 到自身(本例域内无组合逻辑,退化为寄存器直查)、跨域的 reg_a → sync1、clk_b 域内的 sync1 → reg_b。跨域那条就是约束要豁免的对象。
时钟选择也有讲究:10 ns 和 7 ns 没有整数倍关系,相位关系随时间不断漂移——这正是「异步」的定义,也是为什么要用波形图看一眼两者到底是什么关系:
读这张波形抓住一点:clk_a 每 10 ns 一个沿、clk_b 每 7 ns 一个沿,两者的最小公周期在 70 ns 之外还在漂移,STA 工具若硬要检查 reg_a → sync1,就得在漂移的相位里找一个「最坏沿对」——这个沿对在物理上没有意义,因为两个时钟根本没有同步关系,找出来的「最坏」只是数学上的巧合,既不真实也无法收敛。这就是跨异步域路径必须豁免的根本原因:不是路径太长,是「检查」这个动作本身不成立。
可以算一笔账让这件事更直观:不做任何豁免时,STA 会在 70 ns 的公共周期窗口里枚举所有「clk_a 发射沿 → 其后最近的 clk_b 捕获沿」组合,找到间隔最短的那一对。10 与 7 的最小间隔可以逼近 1 ns 甚至更小的量——也就是说工具会要求 reg_a 的输出在不到 1 ns 内穿过跨域线到达 sync1 的 D 端,并满足建立时间。这不是「时序紧张」,而是一个编造出来的需求:真实的 sync1 本来就被允许在任意相位上采样,亚稳态由第二拍 reg_b 兜底。豁免约束干的事,就是告诉工具「这条路径的时序正确性由结构(两级同步器)保证,不由延迟数字保证」,把它从延迟检查的管辖范围里划出去。
二、两种写法各自的命令长什么样
综合出门级网表后(yosys 把 sta_async.v 映射到 sky130_fd_sc_hd 单元库),两次运行只有约束那一行不同。
写法 A,set_false_path:
read_liberty $env(STA_LIB)
read_verilog sta_async_net.v
link_design sta_async
create_clock -name clk_a -period 10.0 [get_ports clk_a]
create_clock -name clk_b -period 7.0 [get_ports clk_b]
set_false_path -from [get_clocks clk_a] -to [get_clocks clk_b]
report_checks写法 B,set_clock_groups -asynchronous:
read_liberty $env(STA_LIB)
read_verilog sta_async_net.v
link_design sta_async
create_clock -name clk_a -period 10.0 [get_ports clk_a]
create_clock -name clk_b -period 7.0 [get_ports clk_b]
set_clock_groups -asynchronous -group [get_clocks clk_a] -group [get_clocks clk_b]
report_checks逐条对照能看清语义差。两份脚本的前五行完全相同,也值得花一句说清楚各自在干什么:read_liberty 读入 sky130_fd_sc_hd 单元库的时序模型(tt 工艺角、1.8V、25°C),read_verilog 加载 yosys 综合出的门级网表,link_design 把网表与库单元绑定成可分析的实例图,两条 create_clock 分别在两个时钟端口上声明 10 ns 与 7 ns 周期。到这一行为止,工具眼里 reg_a → sync1 还是一条普通待查路径——真正分流的是第六行。
写法 A 的豁免是路径级的,-from clk_a -to clk_b 声明了方向——如果担心 clk_b → clk_a 反向也有路径,得再补一条反向的 set_false_path。写法 B 的豁免是关系级的,-group 一旦把两个时钟分进不同组,组与组之间所有交叉路径(无论方向)全部不再检查,一条命令封死双向。另外注意写法 B 的颗粒度:它是按时钟分组的,同一时钟组内可以有多个时钟(比如派生时钟跟着主时钟走),这条特性在 SDC 时钟分组 一篇里有更完整的讨论。
| 维度 | set_false_path | set_clock_groups -asynchronous |
|---|---|---|
| 豁免对象 | 指定起终点之间的路径 | 时钟组之间的一切交叉路径 |
| 方向性 | 单向,反向需另写 | 双向,一条封死 |
| 同频但异步的时钟 | 可以逐个豁免 | 同样适用(异步是逻辑声明,与频率无关) |
| 误伤风险 | 低:只动列出的路径 | 较高:组内所有交叉路径一刀切 |
| 与 max_delay 约束叠加 | false_path 优先级更高,会压掉 max_delay | 同样压掉,需谨慎 |
| 表达的设计意图 | 「这条路我负责」 | 「这两个时钟本就无关」 |
三、实测:两种写法各跑一遍 report_checks
两次运行均走 OpenLane 镜像里的独立 OpenSTA 二进制(OpenSTA 2.4.0),网表、单元库、时钟定义完全一致,只有那一行约束不同。先看写法 A 的报告:
这份报告最有信息量的是开头三行:Startpoint: _00_、Endpoint: _16_、Path Group: clk_b——起点和终点都是 clk_b 域的触发器,整条报告里没有任何一条 Startpoint 属于 clk_a。这就是豁免生效的证据:跨域路径 reg_a → sync1 已经从检查集合里消失了,工具只汇报了域内的 sync1 → reg_b(综合后对应 _00_ → _16_)。如果伪路径没写对,这里会冒出一个 clk_a 起点的路径,并且因为 10 ns 对 7 ns 的漂移相位给出一个小得离谱甚至为负的 slack。
再看写法 B:
对照着读:写法 B 的报告与写法 A 逐字段相同——同样的 _00_ → _16_、同样的 Path Group clk_b、同样的 slack 数值。把报告主体原样摘录出来方便逐行核对:
Startpoint: _00_ (rising edge-triggered flip-flop clocked by clk_b)
Endpoint: _16_ (rising edge-triggered flip-flop clocked by clk_b)
Path Group: clk_b
Path Type: max
Delay Time Description
---------------------------------------------------------
0.00 0.00 clock clk_b (rise edge)
0.00 0.00 clock network delay (ideal)
0.00 0.00 ^ _00_/CLK (sky130_fd_sc_hd__dfxtp_1)
0.27 0.27 v _00_/Q (sky130_fd_sc_hd__dfxtp_1)
0.00 0.27 v _16_/D (sky130_fd_sc_hd__dfxtp_1)
0.27 data arrival time
7.00 7.00 clock clk_b (rise edge)
0.00 7.00 clock network delay (ideal)
0.00 7.00 clock reconvergence pessimism
7.00 ^ _16_/CLK (sky130_fd_sc_hd__dfxtp_1)
-0.11 6.89 library setup time
6.89 data required time
---------------------------------------------------------
6.89 data required time
-0.27 data arrival time
---------------------------------------------------------
6.62 slack (MET)这份报告本身就是 clk_b 域内一条最普通的 setup 检查:发射沿 0 ns,捕获沿 7 ns(clk_b 周期),数据经触发器 clk-to-q 延迟 0.27 ns 到达,required time 6.89 ns,slack 6.62 MET。两次运行报告一致,说明对本设计而言两种写法的检查集合完全相同——差异只在声明意图的层面,STA 数字上看不出区别。
报告里还有一行值得顺带认识:clock reconvergence pessimism(CRPR,时钟重收敛悲观度)为 0。它出现在发射时钟与捕获时钟是同一条(都是 clk_b)时,工具把共享时钟路径上的悲观度扣回去。这条路径发射与捕获同钟,CRPR 为 0 是合理的;而跨域路径若没被豁免,发射是 clk_a、捕获是 clk_b,根本不共享时钟树,这一项也谈不上。另一个细节是 clock network delay (ideal):本次分析没做时钟树综合后的传播延迟建模,时钟按理想网络处理,所以发射与捕获的网络延迟都是 0——真实项目的签核报告里这两行会是非零值,读法相同。
这里有一个读报告的方法论值得记住:验证伪路径/时钟组是否生效,不要去看「有没有 VIOLATED」,而要去看 Startpoint/Endpoint 的时钟归属。豁免生效的标志是「该消失的路径消失了」,报告里只剩域内路径;如果跨域路径还在报告里且 slack 异常小,才是约束没写对。这个检查思路与 STA 报告阅读方法 的口径一致:先看路径归属,再看数字。
四、既然结果一致,工程上该选哪个
数字一致不代表随便选。两者的边界在三个场景会真正分化。
第一,意图表达与审计。 set_clock_groups -asynchronous 把「这两个时钟异步」这个事实写成了机器可读的声明。后续的 CDC 结构检查工具、约束审计脚本(见 伪路径审计方法)都能直接消费这层信息——工具知道这些路径是「故意的异步」,而不是「被主人拍脑袋豁免的」。set_false_path 没有这层语义,审计时只能看到一堆零散豁免,无法区分「豁免的是 CDC 路径」还是「豁免的是真的查不过的慢路径」。团队约束评审时,前者一行可读,后者需要逐条人工确认。
第二,颗粒度。 如果两个时钟之间大部分路径是异步的,但有一两条例外(比如同步握手逻辑确实要保证延迟上界),set_clock_groups 一刀切之后想再放行某条路径就麻烦了——时钟组的豁免优先级很高,需要额外手段捞回来。反过来 set_false_path 天然支持精确豁免,配合 set_max_delay(注意 false_path 优先级更高,得小心别互相压掉)能做「豁免 setup 但限制最大延迟」这类细活。很多公司 CDC 约束的实际写法是混合的:用 clock_groups 声明大关系,用 false_path + max_delay 处理特殊路径。
第三,派生时钟的跟随。 set_clock_groups 分组后,组内时钟的派生时钟(generated clock)自动跟随主时钟的组属性——clk_a 的分频时钟不需要再单独声明。set_false_path 则是点对点豁免,新增一个派生时钟就要补新的豁免条目,约束维护成本随时钟数量线性增长。大设计几十上百个时钟时,这一点是决定性的。
一句话归纳选型:纯异步时钟域之间,优先 set_clock_groups -asynchronous(意图清晰、自动跟随派生时钟、一条封双向);需要路径级精细控制或混合约束时,用 set_false_path。两者叠加写也不算错,很多签核套件里确实同时存在。
最后补一个常被忽略的操作习惯:无论选哪种写法,约束改完之后第一件事不是看 slack 有没有变绿,而是用 report_checks 的路径归属或工具的 report_clock_groups / 豁免清单类命令核对豁免清单本身。伪路径和时钟组属于「静默生效」的约束——写错了不会报错,只会让你漏查或错查路径。本篇的对照方法(同一设计、同一网表、只换一行约束、比对报告 Startpoint 集合)可以直接搬进日常约束评审:两次报告的路径集合做差集,差集必须恰好等于你预期豁免的那批路径,多一条少一条都说明约束写飞了。
这一篇在知识体系里的位置
- 理论承接:CDC 基础(为什么跨异步域路径不能检查)与 SDC 约束总览(约束家族全景);
- 横向同类:set_disable_timing 与 set_false_path 的区别(约束家族内另一对易混命令)、SDC 时钟分组(asynchronous 与 logically/physically exclusive 的全貌);
- 方法延伸:伪路径审计方法(大设计里如何批量核查豁免是否过多)、多周期路径(另一类时序例外);
- 报告读法:STA 报告阅读方法(Startpoint/Endpoint 归属、slack 字段的标准解读)。
一句话总结
set_false_path 豁免的是「路径」、set_clock_groups -asynchronous 切断的是「时钟关系」;对一个 clk_a 10ns / clk_b 7ns、跨域走两级同步器的设计,OpenSTA 2.4.0 实测两种写法报告逐字段一致(均只剩 clk_b 域内路径,slack 6.62 MET)——纯 CDC 场景数字上无差别,但 clock_groups 显式声明了异步意图、自动跟随派生时钟、一条封死双向,是异步时钟域约束的首选;只有需要路径级精细豁免或叠加 max_delay 时才退回 set_false_path。
实验环境
- 机器/系统:云主机 16C32G / Ubuntu 22.04(eda-golden-v1 镜像)
- 工具版本:OpenSTA 2.4.0 41a51eaf4c(OpenLane docker 镜像内
/build/bin/sta)、Yosys(综合出 sky130_fd_sc_hd 门级网表) - 日期:2026-10-28
- 完整命令:
yosys -p "read_verilog sta_async.v; synth -top sta_async; dfflibmap -liberty $LIB; abc -liberty $LIB; clean; write_verilog sta_async_net.v"综合网表;然后两次docker run --rm -v $PWD:/w -w /w -e STA_LIB=$LIB efabless/openlane:<sha>-amd64 /build/bin/sta -exit run_false_path.tcl/... run_clock_groups.tcl各跑一遍report_checks
代码获取
完整 RTL、两份 tcl 脚本与原始报告已同步到公开仓库:geek-doc.cn/experiments/opensta-false-path-async-clock,克隆后先跑 yosys 综合生成 sta_async_net.v,再分别执行两份 run_*.tcl 即可复现本文两份报告。