约束激增现象:为什么约束会爆炸式增长?
好,咱们开始聊一个很有意思的话题——约束爆炸。
你想想看,项目刚开始的时候,SDC文件就那么几行。可能就一个create_clock,加几个set_input_delay,看着挺清爽。但到项目后期呢?SDC文件上万行,甚至几万行,全是约束。这是为什么?
我在台积电16nm的项目上,亲眼看着一个原本来回翻几遍就能看完的SDC,最后膨胀到6000多行。加上注释,整个文件大小逼近1MB。打开它,你的EDA工具都得卡几秒。这是真实发生的。
今天就专门聊聊,约束到底是怎么“涨”起来的。
4.1 根因分析:约束激增的四大来源
我个人习惯把约束激增的根源分成四类。你对照一下手里的项目,八成都能对号入座。
| 类别 | 核心原因 | 影响程度 | 典型表现 |
|---|---|---|---|
| 工具自动化 | 自动生成冗余约束 | 高 | ICC2/Innovus自动派生时钟、generate_clock满天飞 |
| 修时序 | 逐个路径加例外约束 | 很高 | set_false_path / set_multicycle_path大量堆积 |
| 跨时钟域 | 每个CDC路径都要处理 | 中 | set_clock_groups / set_false_path成百上千条 |
| 多人协作 | 各模块约束碎片化 | 中 | 多个SDC文件、重复定义、优先级冲突 |
记住一个事实:约束每多一条,STA运行时间就多一份。约束从1000条涨到10000条,某些工具的STA时间可能从1小时膨胀到8小时以上。
4.2 工具自动化的“好心办坏事”
先讲工具自动化。EDA工具越来越聪明,但它们有个毛病——特别喜欢帮你“补全”约束。
我记得在28nm项目上,ICC2一跑完place,自动生成了几百条generated_clock。我再一看,很多都是不需要的。比如一个简单的二分频,工具替你生成了四个不同的名字。
为什么会这样?
因为工具为了保证时序分析的完整性,会替你推导出所有可能的时钟关系。它不知道哪些是真正有用的,那就全都给你生成出来。安全第一嘛。
但这种“安全”,直接导致约束数量翻倍。
# 原本你只写的约束
create_clock -name clk_sys -period 10 [get_ports clk_in]
# 工具自动生成的约束(假设)
create_generated_clock -name clk_sys_div2 -source clk_in -divide_by 2 [get_pins u_div/clk_out]
create_generated_clock -name clk_sys_div4 -source clk_in -divide_by 4 [get_pins u_div/clk_out_div4]
create_generated_clock -name clk_sys_div2_inv -source clk_in -divide_by 2 -invert [get_pins u_div/clk_out]
... 自己数数,多了多少条?
技巧:我做项目时,跑完综合后的第一件事,就是检查工具自动生成的时钟约束。把明显不需要的删掉。别偷懒,这一步能省掉30%的冗余约束。
4.3 修时序带来的“连锁反应”
这个最要命。你修一条路径的时序,往往会加上一条例外约束。
但问题来了——很多修时序的工程师,约束加得特别“宽容”。
什么意思?
比如有一条路径,其实只需要改一个cell的驱动强度就能修通。但有人直接加上一条set_false_path。妥了,这条路径不检查了。但这条false_path可能覆盖了其他不应该被忽略的路径,甚至把整个模块的时序分析都搞乱了。
更可怕的是,后面的人看到这个false_path,觉得“哦,这里有坑,我最好也加几条”。于是,约束就像滚雪球一样,越滚越大。
# 典型“修时序式”约束堆积
set_false_path -from [get_pins {top/u1/A}] -to [get_pins {top/u2/B}]
set_false_path -from [get_pins {top/u3/A}] -to [get_pins {top/u4/B}]
set_false_path -from [get_pins {top/u5/A}] -to [get_pins {top/u6/B}]
# 还有几十条follow类似的pattern
# 明明可以合并成一条通配符约束,但没人管
我曾经……遇到过最极端的案例:某个模块因为修时序的人换了三个版本,整个SDC里单是set_false_path就堆了800多条。后来我把这些路径分析了一下,超过一半是重复的,剩下的可以用3条通配符约束替代。
别用例外约束当万能药。它只应该用在真正的“例外”路径上。
4.4 跨时钟域的“粗暴处理”
跨时钟域(CDC)是约束爆炸的重灾区。
设计里但凡有几十个异步时钟,你就得处理它们之间的关系。早期大家习惯用set_false_path。一条路径一个false_path。如果有100对异步时钟,每对时钟下有200条数据路径……你算算,这就是2万条约束。
这还没完。
很多人为了保险,把所有异步时钟之间的路径全设成false_path。工具再一展开,每条生成的时钟也都要单独列出来。好嘛,约束数量瞬间突破天际。
# 典型的“粗暴CDC约束”
set_false_path -from [get_clocks clk_a] -to [get_clocks clk_b]
set_false_path -from [get_clocks clk_b] -to [get_clocks clk_a]
set_false_path -from [get_clocks clk_c] -to [get_clocks clk_d]
set_false_path -from [get_clocks clk_d] -to [get_clocks clk_c]
# 上面这种写法还算收敛的
# 更糟的是写到具体pin级别,那就真炸了
其实,CDC约束有更高效的做法。用set_clock_groups,一条指令就能覆盖所有异步时钟关系。但你仔细想想,为什么大家不爱用?因为很多工程师只熟悉set_false_path,对set_clock_groups的语法不熟,怕写错。
建议:能用set_clock_groups就尽量用。一条命令顶你几百条false_path。别怕,工具不会报错,前提是你得把时钟分组搞对。
4.5 多人协作的“碎片化”困境
最后一个原因,其实是项目管理的问题。
项目大了,不可能一个人写所有约束。前端的写一部分,后端的写一部分。数字部分的写,模拟接口也要写。编DFT的还要写约束。这么多人写,约束能不多吗?
而且每个人都有不同的习惯。有人喜欢把所有约束写在一个大文件里。有人喜欢拆成几十个小文件。有人喜欢用绝对路径,有人喜欢用相对路径。等合并到一起的时候,不光是数量多,还经常矛盾。
# 场景:两个模块的约束冲突
# 模块A的约束
set_input_delay -clock clk_sys -max 2.5 [get_ports {data_in*}]
# 模块B的约束(不知道模块A已经定义了,又加了一条)
set_input_delay -clock clk_sys -max 1.8 [get_ports {data_in[31:16]}]
我见过最离谱的,是同一个端口被设了五条不同的input_delay。工具最后怎么处理的?它按照优先级最高的那一条。但最糟的是,其他四条完全白加了,还让STA多算了很久。
4.6 约束激增的“根因地图”
4.7 从“为什么涨”到“为什么涨出问题”
好,现在你知道约束为什么会爆炸式增长了。
但你可能还会问:涨就涨呗,多几条约束怎么了?
嗯,问题大了。约束一多,不光是STA跑得慢。更核心的问题是——你根本没法排查错误。想象一下,几万行的SDC文件,中间隐藏着几条写错了的约束。你要找到它们,如同大海捞针。
更重要的是,约束多了,不同约束之间的相互作用也会变复杂。一条看似无害的set_max_delay,可能覆盖掉你特别关心的关键路径。等到后端做完,时序没能收敛,你都不知道该怪谁。
所以,理解约束激增的原理,是后面一切排查方法的基础。
接下来,咱们就进入具体的排查手段——怎么把这些多余的约束找出来,然后干掉它们。