1. 从一个真实的时序违例说起
去年冬天,我在做一个28nm的SoC后端项目,综合阶段跑完report_timing,发现有一条路径的setup slack是-0.43ns,路径起点是一个时钟选择器的输出端。当时第一反应是组合逻辑太深,准备去优化数据路径。但仔细看时序报告才发现,问题出在时钟树上——工具把经过MUX之后的时钟当成了一个普通的组合逻辑信号来传播,导致时钟到达时间(clock arrival time)的计算完全偏离了实际硬件行为。
这个问题的根源就是non-unate clock。
在DC(Design Compiler)综合中,时钟信号经过组合逻辑单元(比如MUX、AND、OR、XOR门)之后,工具需要判断时钟的传播极性。如果经过某个单元后,输出时钟的上升沿既可能对应输入时钟的上升沿,也可能对应下降沿,取决于另一路选择信号的状态,那么这个时钟就被称为non-unate clock。工具无法确定时钟极性,就会在时序分析中产生不确定性,要么过度悲观导致不必要的面积和功耗开销,要么过于乐观导致硅后时序违例。
set_clock_sense就是解决这个问题的关键命令。它告诉DC:这个时钟经过这个单元之后,极性是确定的(positive unate或negative unate),或者这个时钟根本不应该传播过去(stop propagation)。用好了,时序报告干净利落;用不好,要么约束报错,要么综合结果和实际硬件对不上。
这篇文章,我把自己在多个项目中处理non-unate clock的经验整理出来,从原理到实操,从约束写法到debug技巧,尽量讲透。适合已经有一定DC使用基础、正在被时钟树综合问题困扰的IC后端工程师和综合工程师参考。
2. non-unate clock到底是怎么产生的
2.1 从时钟传播的基本规则讲起
DC在做时序分析时,需要知道每个寄存器的时钟端到达的时钟信号是什么形态。时钟从源点(比如PLL输出或者时钟端口)出发,经过时钟树上的缓冲器、反相器、门控单元,最终到达寄存器的CK端。在这个过程中,工具会追踪时钟的极性(polarity)和到达时间(arrival time)。
对于简单的缓冲器(BUF),输入上升沿对应输出上升沿,这是positive unate。对于反相器(INV),输入上升沿对应输出下降沿,这是negative unate。这两种情况工具都能正确处理,因为它知道极性是确定的。
但遇到MUX就不一样了。假设一个2选1的MUX,S端是选择信号,A端接时钟clk_a,B端接时钟clk_b。当S=0时,输出跟随A;当S=1时,输出跟随B。如果S是一个静态信号(比如配置寄存器输出),那么工具在分析时不知道S到底是0还是1,就无法确定输出时钟的极性。这就是non-unate的典型场景。
再比如一个AND门,一个输入接时钟,另一个输入接使能信号。如果使能信号是动态翻转的,那么输出时钟的上升沿可能来自时钟的上升沿(使能为高时),也可能根本没有上升沿(使能为低时)。工具同样无法确定极性。
2.2 为什么工具会“懵”
DC内部的时序引擎使用时序图(timing graph)来表示信号传播。每个节点有一个到达时间和一个转换时间(slew)。对于unate信号,工具可以明确知道上升沿和下降沿的传播关系。但对于non-unate信号,工具必须做最坏情况假设:它可能同时考虑上升沿和下降沿的传播,导致时钟到达时间出现多个值,或者直接报出不确定性。
这种不确定性会带来两个后果:
第一,时序分析过度悲观。工具可能假设时钟到达时间有一个很大的不确定窗口,导致setup和hold的slack都被压缩。你可能会看到明明数据路径很短,但时序就是过不去,原因就在时钟上。
第二,时钟门控检查失效。如果时钟经过了non-unate单元,工具可能无法正确识别时钟门控结构,导致clock gating check报错或者漏报。
2.3 常见产生non-unate clock的电路结构
根据我的经验,以下几种结构最容易触发non-unate问题:
- 时钟MUX:用于多时钟源切换,比如测试时钟和功能时钟的切换。这是最常见的场景。
- 时钟门控单元:集成时钟门控单元(ICG)内部通常有一个锁存器加一个AND门。如果ICG的使能信号来自组合逻辑,工具可能无法确定门控后的时钟极性。
- 时钟分频电路:用触发器做分频时,如果分频比不是2的整数次幂,或者使用了组合逻辑做时钟切换,也可能产生non-unate。
- 复位/使能逻辑与时钟的组合:比如
clk & rst_n这种结构,如果rst_n是异步信号,工具无法确定输出时钟的极性。
注意:不是所有MUX都会产生non-unate clock。如果MUX的选择端被约束为常量(比如用
set_case_analysis固定),工具就能确定极性,不会报non-unate。所以很多时候,non-unate问题的根源是约束不完整。
3. set_clock_sense命令的语法与核心参数
3.1 命令基本语法
set_clock_sense的基本语法如下:
set_clock_sense -positive|-negative|-stop_propagation|-clock_gating \ [-clocks <clock_list>] \ [-pins <pin_list>] \ [-all_clocks]每个选项的含义:
-positive:指定时钟经过该引脚后极性为正,即输出上升沿对应输入上升沿。-negative:指定时钟经过该引脚后极性为负,即输出上升沿对应输入下降沿。-stop_propagation:停止时钟传播,工具不再追踪该引脚之后的时钟。-clock_gating:指定该引脚为时钟门控检查点,工具会在这里做clock gating setup/hold检查。-clocks:指定作用于哪些时钟。如果不指定,默认作用于所有经过该引脚的时钟。-pins:指定作用于哪些引脚。-all_clocks:作用于所有时钟。
3.2 参数选择背后的逻辑
选择-positive还是-negative,取决于实际电路中时钟经过该单元后的极性关系。比如一个反相器,输入时钟经过后极性反转,应该用-negative。但如果是MUX,选择端固定为0,输出跟随A端,那么极性取决于A端到输出是否反相。
选择-stop_propagation的场景是:这个时钟路径根本不应该被分析。比如测试时钟在功能模式下不活跃,你可以用-stop_propagation让工具忽略这条路径。但要注意,这会影响时序分析的完整性,必须确认该路径确实不需要检查。
-clock_gating选项比较特殊。它告诉工具:这个引脚是一个时钟门控点,需要做门控检查。通常用于ICG单元的输出端或者使能端。如果工具自动识别失败,可以手动指定。
3.3 与其他时钟约束命令的配合
set_clock_sense不是孤立的命令,它需要和以下命令配合使用:
create_clock:定义时钟源。create_generated_clock:定义生成时钟,比如分频后的时钟。set_case_analysis:固定某些控制信号的值,帮助工具确定极性。set_disable_timing:断开某些时序弧,阻止时钟传播。
在实际项目中,我通常先用set_case_analysis固定静态控制信号,如果还有non-unate问题,再用set_clock_sense手动指定极性。两者结合,基本能解决90%以上的non-unate问题。
4. 实战:一步步解决non-unate clock问题
4.1 案例背景与问题定位
回到开头提到的28nm项目。设计中有两个时钟源:功能时钟clk_func(500MHz)和测试时钟clk_test(100MHz)。它们通过一个2选1 MUX切换,选择端是test_mode信号。MUX输出clk_mux,然后经过一个ICG单元门控后送到各个模块。
综合后跑时序,发现clk_mux相关的路径slack异常。用report_clock -skew查看时钟树,发现clk_mux被标记为non-unate。进一步用report_timing -clock_path查看时钟路径,确认工具在MUX处产生了不确定性。
4.2 第一步:用set_case_analysis固定模式信号
首先确认test_mode在功能模式下是常量0。在综合脚本中加入:
set_case_analysis 0 [get_ports test_mode]重新跑综合,发现non-unate警告减少了,但clk_mux仍然被标记为non-unate。原因是ICG单元的使能信号来自组合逻辑,工具无法确定门控后的时钟极性。
4.3 第二步:用set_clock_sense指定MUX输出极性
由于test_mode固定为0,MUX输出跟随clk_func。假设MUX的A端接clk_func,B端接clk_test,S端接test_mode。当S=0时,输出等于A端,极性为正。所以:
set_clock_sense -positive -clocks [get_clocks clk_func] \ [get_pins mux_inst/Z]这条命令告诉工具:clk_func经过这个MUX后极性为正。重新跑综合,clk_mux的non-unate警告消失,时序报告中的时钟到达时间变得确定。
4.4 第三步:处理ICG单元的时钟门控检查
ICG单元的输出时钟clk_gated需要做clock gating检查。如果工具自动识别失败,手动指定:
set_clock_sense -clock_gating [get_pins icg_inst/CK]同时,确认ICG的使能信号enable的时序约束正确。通常需要在使能信号上设置set_clock_gating_check:
set_clock_gating_check -setup 0.1 -hold 0.05 [get_clocks clk_func]这里的0.1ns和0.05ns是根据工艺库和实际需求设定的门控检查余量。具体值需要和前端设计确认,不能随便填。
4.5 第四步:验证约束效果
约束加完后,用以下命令验证:
report_clock -skew report_timing -clock_path -max_paths 10 check_timing -verbose重点看check_timing的输出,确认没有no_clock、non_unate_clock等警告。同时对比约束前后的时序报告,确认slack有合理改善。
在我的案例中,加上约束后,那条-0.43ns的路径slack变成了+0.12ns,面积增加了约2%,但时序干净了。这个代价是可以接受的。
4.6 约束脚本的完整示例
把上面的步骤整合成一个完整的约束片段:
# 定义时钟 create_clock -name clk_func -period 2.0 [get_ports clk_func] create_clock -name clk_test -period 10.0 [get_ports clk_test] # 固定测试模式信号 set_case_analysis 0 [get_ports test_mode] # 指定MUX输出极性 set_clock_sense -positive -clocks [get_clocks clk_func] \ [get_pins mux_inst/Z] # 指定ICG门控检查点 set_clock_sense -clock_gating [get_pins icg_inst/CK] # 设置门控检查余量 set_clock_gating_check -setup 0.1 -hold 0.05 [get_clocks clk_func] # 生成时钟定义(如果有分频) create_generated_clock -name clk_div -source [get_pins mux_inst/Z] \ -divide_by 2 [get_pins div_inst/Q]这个脚本可以直接复用到类似结构中,只需要替换实例名和时钟名。
5. 常见问题与排查技巧实录
5.1 non-unate警告不消失怎么办
有时候加了set_clock_sense,工具还是报non-unate。常见原因有三个:
第一,作用域不对。-clocks指定的时钟名必须和create_clock中的名字完全一致,大小写敏感。用get_clocks确认时钟名。
第二,引脚路径不对。get_pins返回的引脚必须是时钟路径上的实际引脚。用report_timing -clock_path确认时钟经过的引脚。
第三,多个时钟经过同一引脚。如果MUX的两路输入都是时钟,且选择端没有固定,那么无论怎么设set_clock_sense,工具都无法确定极性。这时候必须用set_case_analysis固定选择端,或者用-stop_propagation停止其中一路。
5.2 set_clock_sense与set_disable_timing的区别
这两个命令容易混淆。set_clock_sense是告诉工具时钟的极性,时钟仍然传播,只是极性确定了。set_disable_timing是直接断开时序弧,时钟不再传播。前者用于确定极性,后者用于彻底阻断路径。
比如测试时钟在功能模式下不活跃,你可以用set_disable_timing断开测试时钟到MUX的路径。但如果你只是想让工具知道MUX输出跟随功能时钟,就用set_clock_sense -positive。
5.3 门控检查报错怎么排查
Clock gating check报错通常有两种:setup违例和hold违例。setup违例说明使能信号来得太晚,hold违例说明使能信号来得太早。
排查步骤:
- 用
report_timing -to [get_pins icg_inst/EN]查看使能信号的到达时间。 - 确认使能信号的时钟域和门控时钟的时钟域是否一致。
- 检查
set_clock_gating_check的余量是否合理。 - 如果使能信号来自组合逻辑,考虑是否需要插入寄存器打拍。
实操心得:门控检查的setup/hold余量不要设得太大,否则会过度约束使能路径,导致不必要的面积开销。一般建议setup余量在0.05~0.15ns之间,hold余量在0.02~0.08ns之间,具体看工艺和频率。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| non-unate警告不消失 | 时钟名或引脚名错误 | 用get_clocks/get_pins确认 |
| 时序slack异常悲观 | 时钟到达时间不确定 | 加set_clock_sense指定极性 |
| 门控检查漏报 | ICG未被识别 | 加-clock_gating选项 |
| 门控setup违例 | 使能信号太晚 | 检查使能路径,必要时打拍 |
| 门控hold违例 | 使能信号太早 | 检查使能路径,加延迟 |
| 综合面积突然增大 | 约束过紧 | 检查set_clock_sense是否必要 |
5.5 几个容易踩的坑
第一个坑:在MUX选择端未固定的情况下强行设set_clock_sense。这样工具虽然不报non-unate了,但时序分析结果和实际硬件行为可能不一致。硅后可能发现时钟极性反了,导致功能错误。所以一定要先确认选择端的状态。
第二个坑:set_clock_sense作用在错误的层级。如果设计是层次化的,get_pins需要指定完整的层次路径。用get_pins -hier可以跨层次查找,但要注意性能。
第三个坑:忽略generated clock的极性。如果MUX输出后还有分频电路,create_generated_clock的极性也需要确认。有时候non-unate问题不在MUX,而在分频器。
第四个坑:约束顺序不对。set_clock_sense必须在create_clock之后执行,否则工具找不到时钟对象。建议把时钟约束放在脚本的前面部分。
6. 进阶技巧:自动化检测与约束生成
6.1 用Tcl脚本自动检测non-unate clock
在大规模设计中,手动排查non-unate clock效率很低。我写了一个Tcl脚本,自动遍历时钟路径,检测non-unate单元并生成约束建议:
proc detect_nonunate {} { set clocks [get_clocks *] foreach clk $clocks { set pins [get_pins -of_objects [get_clocks $clk] -filter "is_clock_pin==true"] foreach pin $pins { set sense [get_attribute $pin clock_sense] if {$sense == "non_unate"} { puts "Non-unate detected: $pin (clock: $clk)" } } } }这个脚本可以快速定位问题引脚,然后根据实际电路手动确认极性。
6.2 结合set_case_analysis的自动化流程
更高效的做法是:先自动分析所有静态控制信号,用set_case_analysis固定,然后再检测剩余的non-unate单元。这样能大幅减少需要手动处理的引脚数量。
在实际项目中,我通常把这个过程集成到综合脚本的预处理阶段,每次跑综合前自动执行,确保约束完整。
6.3 约束的版本管理
时钟约束是设计的一部分,必须纳入版本管理。每次修改set_clock_sense或set_case_analysis,都要记录修改原因和影响范围。我见过太多项目因为约束文件混乱,导致综合结果和硅后不一致,debug成本极高。
建议把时钟约束单独放在一个文件里,比如clock_constraints.tcl,和RTL代码一起提交到版本控制系统。每次综合前,用source clock_constraints.tcl加载。
7. 一些个人体会
处理non-unate clock这件事,说到底是对设计意图的理解。工具不知道你的MUX选择端在功能模式下是0还是1,不知道ICG的使能信号什么时候有效,这些都需要你告诉它。set_clock_sense就是你和工具之间的沟通桥梁。
我刚开始做综合的时候,遇到non-unate警告就慌,到处加约束,结果越加越乱。后来慢慢明白,每一条约束背后都要有电路依据。你得先看懂电路,知道时钟是怎么走的,极性应该是什么,然后再写约束。约束不是越多越好,而是越准越好。
另外,时钟约束一定要和前端设计确认。我遇到过前端说“这个MUX在功能模式下永远选A”,结果RTL里选择端接的是一个可配置寄存器,硅后测试时有人改了配置,时钟极性反了,功能直接挂掉。所以,约束的前提是设计意图明确且不会变。
最后分享一个小技巧:在综合脚本里加一个check_timing的自动检查,如果发现non-unate警告,就打印出相关引脚和时钟,方便快速定位。这个习惯帮我省了很多debug时间。