1. 为什么非要聊_clock_gen skew group这个话题
做数字后端的人,多多少少都跟Innovus的CTS(时钟树综合)打过交道。很多项目跑到CTS阶段就卡壳,时钟树长完,时序报告一出来,setup/hold一大堆violation,折腾半天发现都是同一个源头——_clock_gen的skew group被工具自动分组,然后分组方式跟你的设计意图完全拧着来。
我刚入行那会儿也踩过这个坑。芯片规模中等偏大,几十万个寄存器,时钟树CTS跑了几个小时,长出来的树looks great,skew报告也漂亮,结果到了signoff阶段,timing closure死活做不完。后来一层层排查,才发现问题出在一个毫不起眼的_clock_gen上。
先说清楚什么叫_clock_gen。在Innovus的时钟树综合流程里,工具会为每一个时钟源(clock source)自动创建一组时钟树信息,这个信息对象的名字通常会带_clock_gen后缀。它不是你在SDC里定义的clock,而是工具内部为CTS生成的"管理节点"。每个_clock_gen对应一棵独立的时钟子树,带自己的skew group、target skew、insertion delay约束。正常情况下,工具会根据时钟拓扑和物理位置自动划分这些skew group,但"自动"这两个字恰恰是最大的变量。
这个内容适合谁看?两类人。第一类是刚接触数字后端、正在跑第一个真实芯片flow的初级工程师,你需要知道CTS不是"工具全自动跑完就完事"的黑盒;第二类是已经在做timing closure、被莫名其妙的时钟偏移问题折磨过的中高级工程师,你可能需要一次系统地回头看,_clock_gen的自动分组到底在你项目里埋了多少雷。
下面我按实际项目的排查思路来拆,从原理讲到实操,最后给一份可以直接拿去对表的排查清单。
2. _clock_gen skew group的机制拆解与自动分组逻辑
2.1 skew group到底在管什么
时钟树的终极目标,是让所有时钟到达寄存器时钟端的时间一致。但这只是理想态,真实芯片里不可能每个寄存器的clock latency完全相同。skew group就是工具用来管理"哪些寄存器的时钟到达时间需要被拉近"的最小单位。
一个skew group会记录这几类信息:
- group内的所有clock pin(寄存器的CK端)
- 期望的target skew(组内最大允许偏差)
- 时钟树的起点(root pin)
- 目标插入延迟(target insertion delay)
- 时钟网络的RC特性权衡模式(balance mode)
Innovus在CTS时会为每个group单独做时钟树优化。group内部的寄存器,工具会尽量把它们平衡到target skew以内;group与group之间,则不做严格平衡,只要求在skew budget内。换句话说,如果你的设计里有100个寄存器,工具把它们分成了两个skew group,每个group内部虽然很平衡,但两组之间可能有很大的group-to-group skew——这正是许多时序violation的藏身之处。
2.2 自动分组的触发条件
Innovus的ccopt引擎在做时钟树综合时,会根据SDC中定义的时钟关系自动决定每个时钟引脚归属哪个skew group。常见的自动分组规则包括:
- 同一个clock root下,物理距离接近的寄存器会自动聚到同一组;
- 同一条clock path上的寄存器,如果通过同一级buffer驱动,倾向于分到同一组;
- 不同的时钟域,默认严格分开,不共享skew group;
- 由于某些特殊cell(比如ICG、clock mux、divider)产生的衍生时钟,工具会在衍生点创建新的_clock_gen节点,并自动衍生出新的skew group。
这里最容易出问题的就是最后一条。一个时钟经过ICG或者divider之后,Innovus会在该cell的输出端生成一个新的_clock_gen,这个_clock_gen会自己成为一个skew group的root。问题在于,工具默认的处理逻辑不可能完全理解你的设计意图——它看到的是"这个电信号被分叉了",所以它按拓扑去分组;但你真正想要的是"这两簇寄存器虽然被分开了,但它们之间的skew必须受到严格约束"。
2.3 工具视角与设计意图的差异
举一个很常见的场景。某个模块里有一个由MUX选择出来的异步时钟切换结构,两条输入时钟A和B经过MUX之后,输出时钟C驱动了200个寄存器。在SDC里,你只在A和B上定义了create_clock,但C是MUX的输出,需要定义create_generated_clock。CTS跑完之后,Innovus会为MUX输出端生成一个_clock_gen,并且为C创建一个独立的skew group。
到这里一切还好。
问题出现在另一个场景:同一条时钟线路上,既有ICG单元,又有普通的buffer。ICG使能端还有另一组控制逻辑产生的时钟信号,两者在设计上是同步的,你希望它们在时钟树层面被平衡。但工具默认把ICG输出的时钟当成一个新的skew group,控制逻辑那部分又归到另一个group里。两条group的target skew、insertion delay都被各自优化,结果就是两组寄存器的时钟到达时间差得离谱,setup/hold全都hold不住。
有一个非常典型的案例:某个项目里,一条功能时钟线上挂了几个ICG,工具在这几个ICG输出端都生成了独立的_clock_gen,自动划分了将近20个skew group。从report_clock_tree看,每个group自己的skew只有几十ps,非常理想;但group之间的真实skew到了1ns以上。而SDC约束里,这些寄存器的逻辑是跨ICG交互的,要求300ps以内的时钟偏斜,结果可想而知。
这时你才意识到,工具自动分组产生的"漂亮报告"其实掩盖了真实的设计限度。
3. 实际项目里最常踩的三个具体坑
3.1 坑一:跨group的时钟偏移被"合理分配"吃掉
第一个坑,也是最隐蔽的坑,就是跨skew group的时钟偏移被"合理分配"到了不同预算里,表面看起来每个group都达标,实际整体时序根本收不了。
它的成因其实不复杂。Innovus做CTS时,每个skew group的target skew是独立定义的。一般默认是全芯片统一的一个数值(比如250ps或者300ps),但group之间的相对关系,工具默认只是尽量去balance,并没有硬性约束。一旦有跨group的数据交互,setup/hold分析就要考虑双方的clock arrival time差,也就是group-to-group skew。
如果工具把寄存器A分到了group1,把与A交互的寄存器B分到了group2,那么这两个group之间的时钟偏移就需要由后端工程师手动用约束去限制。很多人不太理解这一点,以为SDC里定义了时钟就完事了,其实CTS的group划分和SDC的时钟定义是两套机制,SDC管功能约束,skew group管物理实现约束。
提示:如果你在CTS之后看timing,发现两个时钟域之间存在大量交互路径,但是时序violation的pattern特别怪异——不是集中在一个group内部,而是分布在不同group的边界路径上,第一反应就应该是去查这两个group之间的相对约束。
我通常会用下面的命令快速查看当前芯片里所有的_clock_gen和它们的group划分情况:
# 列出所有skew group及其root report_clock_tree -skew_group -detail这个命令会在Innovus的log里打印出每个skew group的root pin、group内的sink数量、以及当前skew值。如果你看到group数量比预期的多出很多,或者某个group的sink数量特别少(比如只有两三个寄存器),就要警惕自动分组是否太碎了。
碎group的危害在于,工具会为每一个小group单独做平衡,这样每个小group都"达标"了,但group之间的skew反而变大。一个本来可以整体平衡的时钟网络,被切成七八个group之后,整体skew反而比切之前更差。
3.2 坑二:生成时钟_clock_gen的位置导致路径断开
第二个坑比较隐蔽,它和SDC里create_generated_clock的定义位置有直接关系。
我遇到过一个项目,里面有个分频器模块,输出二分频时钟给后面的逻辑。在SDC里,我把create_generated_clock定义在divider的输出pin上。CTS跑完,Innovus在divider输出端自动生成了一个_clock_gen,然后从这个点开始为后面的寄存器构建时钟树。
理论上这没问题。
但实际在physical design时,divider的输出pin和它驱动的寄存器之间存在好几层逻辑,Innovus在长时钟树的时候,是从divider输出pin开始平衡的,导致divider之前的路径(也就是源时钟到divider的路径)没有被时钟树平衡覆盖。而这条源时钟路径上,还带着另外一组寄存器,它们直接由源时钟驱动,不进过divider。
结果就是,源时钟直接驱动的寄存器和分频后的寄存器,时钟到达时间差出了一个buffer chain的长度——正常情况下这是可以预见的,但如果你没有在SDC里明确约束两者之间的skew,工具不会替你管理这部分偏移。
解决这个问题,通常需要在SDC里用set_clock_group或者set_clock_tree_options把这些时钟设置成逻辑上相关、物理上需要平衡的group。如果不想改SDC,也可以在Innovus里直接用ccopt的约束API做干预:
# 强制把两个_clock_gen拉入同一个balance group ccopt_check_group -root [get_pins u_div/clk_out] -balance true这类修法需要一定经验,因为并不是所有衍生时钟都需要和源时钟放在同一个balance group。如果两个时钟在逻辑上其实是异步的,硬绑到一起反而会让CTS更加难收敛。核心判断标准是:这两个时钟驱动的寄存器之间是否存在真实的数据交互路径。
3.3 坑三:工具自动插入的delay cell作用被skew group间接抵消
第三个坑常见于需要做时钟延迟匹配(clock delay matching)的设计,比如DDR接口或者高速同步接口。这类接口通常有严格的output delay要求,需要在clk路径上手动插入delay buffer来匹配data path。
Innovus在CTS阶段支持自动插入delay cell(balance delay),这本身没什么问题。问题出在,当你用set_clock_delay或者set_clock_tree_options -target_insertion_delay给某个_clock_gen指定了插入延迟后,如果这个_clock_gen恰好处于一个自动分组的group里,工具在处理时可能把你这部分延迟调整分摊到整个group内部的其他路径上,导致原本预期只延迟A路径的buffer,实际被用来平衡了B路径,最终结果和你的预期完全不一致。
还有另一种情况更让人头疼:某个_clock_gen在时序上并不关键,但因为它被自动归入了一个较大的skew group,而group里其他时钟路径要求很紧的skew,为满足这个group的约束,工具自动在这个_clock_gen的输出路径上插入了多级delay cell。这个_clock_gen驱动的是一个低频时钟域,本来不需要那么好的skew,结果反而因为自动分组白白多出了几百ps的insertion delay,影响了整个模块的时序收敛。
碰到这种情况,一般的处理方案有两种。一是把不关键的时钟从group里摘出来:用set_clock_tree_options -exclude_clock让工具长树时忽略它;二是显式降低这个_clock_gen的target skew要求:
set_clock_tree_options -clock [get_clocks clk_func] -target_skew 0.5这里把target skew放到0.5ns,工具就不会在它上面花费太多buffer资源去强行平衡了。
4. 如何用report和排查命令定位skew group异常
4.1 用report_clock_tree拆解真实的skew分布
当CTS跑完,时序出了violation,你的第一件事不应该是马上去修路径。先把skew report打开,看清楚当前芯片的skew分布到底长什么样。
report_clock_tree -metric skew_group -group [get_clock_nets clk_func] report_clock_tree -skew_group -detail第一行报告的是每个_clock_gen/metric上的skew分布,它会列出每个group的root、sink数量、max/min arrival time、以及group内skew。第二行报告的是所有group的聚合信息。
我看report的时候,习惯先关注两个数值:
- 每个group的skew值是否小于该group的target skew
- 所有group里,max arrival和min arrival之间的是不是有特别大的gap
如果第二个gap很大,基本可以断定有group之间的skew失控问题。这时再用下面的命令看看具体是哪个group拖了整个时钟的后腿:
report_clock_timing -type skew -group [get_groups clock_group_xxx] -late这个报告会用表格列出这个group里所有sink的clock arrival time,按late、early两种corner分别统计。通过这个表,你能看到这个group里走得最慢和最快的两条路径分别是哪两条,它们的物理位置在哪里,之后就能针对性到版图里去看。
4.2 在Innovus GUI里快速可视化skew group
命令行之外,Innovus的GUI里也提供了很直观的可视化方式。
打开GUI之后,在菜单栏的Tools -> Timing -> Clock Tree -> Skew Group,可以把所有skew group以不同颜色高亮显示在版图上。此时如果看到某块逻辑区域被分成了好几种颜色,每种颜色覆盖的范围又特别小,那就说明工具在这个区域做了过度的自动分组。
还有一个非常有用的操作:在GUI里选中某一个_clock_gen节点,按F9,工具会高亮显示这个节点覆盖的全部sink。你可以直接目测这些sink是不是一块连续的区域。如果高亮的sink散落在版图各个角落,甚至跨越了block边界,那么这个group本身就可能是工具"乱分组"的产物。
4.3 结合PG信息排查特殊单元的影响
除了skew group本身,还有一类经常被忽略的问题是特殊单元(比如ICG的PG pin、标准单元的PG term)在CTS分组过程中产生的干扰。
Innovus里标准单元的PG(Power/Ground)连接信息对时钟树综合并不是完全透明的。ICU(Integrated Clock Gating cell)的电源网络如果连接不完整,或者某个标准单元的PG term悬空,会导致时序分析时该单元的lib timing信息不完整,CTS在估算它的delay时做出错误的余量判断。
这时候可以用下面的命令检查标准单元PG连接情况:
# 检查标准单元名字为biasnw的PG term连接 dbQuery -obj [dbGet top.insts.name biasnw] -port PG如果你在CTS之前发现某些标准单元的PG term没有连上,最好先重新做PG连接(比如用addRequiredPG或者重新跑globalNetConnect),否则工具对这些单元的RC估算会产生偏差,进而影响时钟树平衡的结果。
另外一个更隐蔽的问题是,有些单元的PG pin在库文件里被定义成了"pg_pin"类型,但它在时序上还承担着额外的寄生电容。CTS估算时钟网络的RC时,如果工具没有把这个单元当作clock buffer来处理,而是当成普通逻辑单元,那么它插入的delay就会比实际偏大。这类问题很难通过调整skew group参数来解决,需要你回到library和netlist层面去核查单元定义。
5. 干预自动分组:可落地的三种实操手段
5.1 在SDC层面用约束约束skew group的划分
最干净、最可持续的做法是在SDC层面就把时钟关系约束清楚,让工具在自动分组时遵循你的意图。
第一招是用set_clock_group -logically_exclusive或者-physically_exclusive明确告诉工具哪些时钟不会同时工作,这些时钟可以被分到不同的skew group,且不会产生group-to-group skew的时序分析问题。这是最常规的做法,适用于涉及时钟MUX切换的设计。
第二招是用set_clock_tree_options -balance_group手动指定哪些时钟应该被放在同一个balance group里:
set_clock_tree_options -balance_group [list clk_a clk_b] -balance_group_name grp_ab这种写法对Innovus来说非常有效。指定balance_group之后,工具会把这些时钟的sink合并到一起做平衡,保证它们之间的group-to-group skew尽量小。
注意:balance_group不是随便用的。如果这两个时钟的工作频率差异非常大(比如一个100MHz一个1GHz),强行平衡会导致低频时钟被加入大量buffer来匹配高频时钟的树长,bloat面积和功耗。所以用这招之前,一定要确认两条时钟路径在高频情况下确实有数据交互的需求。
第三招是针对generated clock的:在定义create_generated_clock时,加上-master_clock参数,并且在set_clock_tree_options里设置-target_clock_skew,让工具在生成_clock_gen时就按你给的skew目标来处理。
create_generated_clock -name clk_div -source [get_pins u_pll/clkout] -divide_by 2 [get_pins u_div/clkout] -master_clock clk_in set_clock_tree_options -clock [get_clocks clk_div] -target_skew 0.355.2 在Innovus里用ccopt命令动态修正
如果设计已经floorplan完成,SDC不方便再动,可以在Innovus里通过命令动态调整。
Innovus的ccopt引擎提供了一组skew group管理接口。最常用的是这两个:
# 把某个pin从自动group中拆出来 ccopt_split_group -pin [get_pins u_icg/CK] -name split_grp_1 # 把split出来的group合并到另一个group ccopt_join_groups -source split_grp_1 -target [get_clock_nets clk_func]ccopt_split_group会把指定的sink pin从它原本的skew group中拆分出来生成一个新的group。这个命令很实用,比如你想让某个特殊寄存器的时钟树长法跟旁边其他寄存器不一样(比如需要多一级buffer来匹配一个clock gating check),就可以把它单独拆出来调。
ccopt_join_groups则相反,它把两个或多个group合并成一个,合并之后CTS的balance逻辑会覆盖所有sink,强制它们共享同一套树形结构。
这两个命令一般情况下不建议在flow里大量使用,因为它们属于"手动暴力修正",用多了会让时钟树的结构失去规律,后续ECO(工程变更单)也不好做。但在timing closure的关键时刻,它们往往是救火最快的办法。
我记得有一次做项目,一条时钟线上有6个ICG,工具生成了7个_clock_gen。我把其中4个ICG输出端的sink全部join到了主时钟的group里,时钟树整体skew从800ps降到了300ps以内,setup violation少了三分之二。代价是整个CTS跑了更久,但毕竟结果更重要。
5.3 通过CTS脚本控制工具行为
另一种思路是在CTS综合阶段直接控制工具的行为,而不是事后再去做post-CTS修timing。
Innovus里做CTS时,工具可以从一组指定的leaf pin开始构建时钟树。如果你已经知道哪些寄存器是关键路径上的高频交互点,可以在CTS开始前,用set_clock_tree_exceptions把它们的时钟pin定义成关键pin:
set_clock_tree_exceptions -pins [get_pins {u_mod_a/reg_a/CK u_mod_b/reg_b/CK}] -dont_balance false这样工具在自动分组时,会优先保证这些pin之间的平衡,而不是把它们随随便便分到不同group。
还有一招是用set_ccopt_property去调整工具对时钟树延迟匹配的偏好。比如你可以设置工具在chocktree构建时,更倾向于把经过ICG的分支插入delay cell来补偿ICG内部延迟,而不是让它重新切分group:
set_ccopt_property balance_tool_option -icg_delay_compensation true这条命令的原理是,ICG细胞本身会产生几百ps的内部延迟。如果工具不补偿这部分延迟,那么经过ICG的sink比不经过ICG的sink天然慢很多,工具为了平衡这些sink,就会自动把它们分开。开了补偿之后,工具会主动在非ICG路径上插入delay buffer,让所有sink的arrival time保持一致,从而避免过度分组。
6. 和PG term、标准单元相关的一个常见联动问题
6.1 PG连接不完整对skew group的间接影响
前面提到了标准单元的PG term,很多后端工程师在跑CTS之前根本不会去检查PG连接情况,反正place已经做完了,PG通不通后面还有IR drop分析去兜底。但CTS阶段,PG连接不完整对时钟树balance的影响是实实在在的。
InnovusCTS过程中,工具调用delay calculation的时候,需要从库中提取单元的时序弧(timing arc)。如果单元的PG pin没有正确连接,工具对当前电压域的判定会出错,可能出现某个单元被当作另一个power domain来处理的情况。此时该单元的电压变化、温度变化对delay的影响都会按错误模型计算,这在multi-voltage design里甚至会导致CTS生成完全错误的树结构。
我之前在一个双电压域设计里,SRAM周围的标准单元PG连错了域,结果CTS之后这些单元的clock path delay比其他同类单元多了整整600ps——不是物理路径变长,而是timing model算错了。后来我加了一段检查脚本,在CTS之前把所有标准单元的PG term连接状态都dump出来检查一遍,把漏连的补上,CTS结果立刻正常了。
6.2 怎么快速检查标准单元PG term
检查PG term连接状态,可以用Innovus内置的dbQuery来查:
# 查询所有名字匹配biasnw的实例的PG pin连接 dbGet [dbGet top.insts.name biasnw*].pgTerms.name dbGet [dbGet top.insts.name biasnw*].pgTerms.net.name第一条命令返回指定实例的所有PG term名称,第二条返回每个PG term当前连接到哪个net。如果某个PG term显示的net是<no net>或者VSS连到VDD这类明显错误,就需要在CTS前修掉。
批量检查所有标准单元的PG连接情况,可以用下面的循环:
set failed_pg_insts {} foreach inst [dbGet top.insts.pgTerms.name -e] { set inst_name [dbGet $inst.instance.name] set pg_net [dbGet $inst.net.name] if {$pg_net == "VSS"} { lappend failed_pg_insts $inst_name } } foreach inst_name $failed_pg_insts { puts "ERROR: instance $inst_name has missing PG connection" }这个脚本会遍历所有instance的所有PG term,找出那些没有正确连接的。实际项目里跑一遍大概几分钟,但能省下后面好几个小时的排查时间。
6.3 PG term和skew group的时序分析联动
还有一点值得提:在Innovus做时钟树后仿真和时序signoff时,如果标准单元的PG网络定义有误,report_clock_timing里显示的clock arrival time可能会和后续STA工具(比如Tempus)算出来的对不上。这种GBA/PBA不一致问题非常折磨人,因为它往往只在特定corner下出现,而且出现的pattern没有规律。
我遇到过一次,CTS阶段skew report很漂亮,tempus跑setup也没什么问题,但hold时序在低电压corner疯狂violation。后来发现是某个lib cell在低电压下的内部电容模型和实际PG连接状态不匹配,导致CTS阶段估算的hold margin严重偏乐观。这种问题的root cause虽然不在skew group上,但排查时序问题时,PG term状态和skew group划分经常搅在一起,需要并行排查。
7. 基于真实场景的完整排障流程演示
7.1 场景描述与初步现象
为了让上面的理论更直观,我拿一个我做过的一个28nm车规级MCU项目来举例。
这个芯片里有一个外设通信模块,包含一个SPI主控制器和一个UART控制器,它们共用一个系统时钟clk_sys,频率48MHz。SDC里只定义了一个clk_sys,然后内部经过两个ICG分别给SPI和UART供时钟。
CTS完成后,STA报出来,SPI模块的setup时序有300多条violation。奇怪的是,UART模块完全干净。
打开report_clock_tree -skew_group,发现工具为clk_sys生成了3个_clock_gen:一个是根时钟的(root在PLL输出),一个在SPI的ICG输出上,一个在UART的ICG输出上。SPI的group和根时钟group之间的group-to-group skew有1.2ns,而UART和根时钟的group skew只有200ps。
7.2 逐步排查与命令操作
第一步,先查SPI模块里violation路径的时钟到达时间。用report_clock_timing -type skew -group看了之后,确认vio路径全部集中在SPI第一个ICG输出之后的寄存器组与直接由clk_sys驱动的寄存器组之间的交互路径上。
第二步,到GUI里高亮这两个group的sink。目测发现SPI ICG后面的寄存器与clk_sys直接驱动的寄存器在版图上实际上是交错的,说明工具只是根据拓扑做了分组,并没有考虑到它们在物理上混在一起。
第三步,我查看了ICG的library定义。发现这个ICG cell的内部delay大概要400ps左右。工具自动分组时,把ICG输出端的sink单独拿出来作为新group,没有在ICG输入侧做delay compensation。结果就是经过ICG的寄存器天然比直接由clk_sys驱动的寄存器晚了将近400ps。UART那边之所以没出问题,是因为UART模块的sink数量少、且位置紧凑,工具的skew优化把这段差距吸收掉了。
第四步,用了两条命令修复:
# 启用ICG延迟补偿,让工具在非ICG路径上自动插入buffer匹配延迟 set_ccopt_property balance_tool_option -icg_delay_compensation true # 将三个_clock_gen全部join到同一个balance group,强制统一平衡 ccopt_join_groups -source spi_icg_clk_gen -target clk_sys_clk_gen ccopt_join_groups -source uart_icg_clk_gen -target clk_sys_clk_gen改完之后重新跑CTS。整棵时钟树的skew从之前的1.2ns降到180ps,setup violation剩下不到30条。再经过一轮regular修正,基本就干净了。
7.3 复盘总结
事后复盘,这个问题的本质其实很简单:工具的自动分组遵循的是"拓扑分支优先"原则,看到ICG就往分支节点插,但这颗ICG的使能时钟与数据时钟属于同一功能域,它们之间的同步关系要求时钟偏移被严格约束。
如果你对工具自动分组的理解不到位,一上来就直接去手动修setup violation的data path,会耗费大量时间在错误的方向上,甚至连修10轮都收不干净。先确认skew group划分是否合理,是CTS后timing排查的第一步,也是性价比最高的一步。
8. 一份可以直接抄的排查清单与避坑心得
8.1 时钟树综合前的预防性检查
我把每次做CTS之前必做的检查项整理成了一份清单,分享出来,大家可以直接抄:
| 检查项 | 命令/方法 | 通过标准 |
|---|---|---|
| SDC时钟定义完整性 | check_timing,检查generated clock的source是否正确 | 无unconstrained endpoint |
| 时钟关系约束 | report_clock_groups,确认exclusive/async关系已正确设置 | 需要互斥的时钟已正确标注 |
| PG连接完整性 | 用dbQuery遍历所有标准单元PG term | 不存在未连接或连错的PG term |
| 关键时钟balance group | 列出所有_clock_gen,确认数量在预期范围 | 没有异常碎group |
| 特殊单元list | report_clock_tree -sink_type | ICG/mux/divider全部在预期位置 |
| 时钟网络上的非时钟cell | report_clock_tree -net | 无逻辑单元混挂在时钟网络上 |
这个清单看起来简单,但每一条都踩过坑。第4条尤其容易被忽略,很多人SDC没问题就直接睡等在CTS跑完,结果CTS跑出来一堆小group,浪费大量时间。
8.2 CTS后的快速验证动作
CTS跑完之后,不要急着往下走。我用最快的时间做三件事:
第一,打开report_clock_tree -skew_group -detail,统计group数量。如果group数量比SDC里定义的时钟数多出50%以上,多半有自动分组过度的情况。
第二,看每个group的sink数量。如果一个group里只有1~3个sink,而且没有特别的原因(比如高驱动强度的特殊寄存器),大概率是工具把关键sink拆出来了,可以顺手合并回去。
第三,跑一个early版本的report_clock_timing -type skew -group -setup和-hold,对比late/early两种corner下的skew。如果两者差异超过正常范围,很可能是时钟树在特殊corner下存在结构性问题,需要回到PG或者library层面排查。
8.3 我的个人经验与建议
最后说几句心里话。
做数字后端的时间越长,越觉得CTS不是一个"跑完就完"的步骤。它更像是一个需要持续维护和审视的过程。_clock_gen skew group这种看起来不起眼的内部对象,恰恰是决定时钟树质量的关键杠杆。工具很强大,但它默认帮你做的决定,不一定是好的决定。
我的建议是,每到一个新项目、或者换了一颗新工艺,CTS阶段一定要花半天时间把_clock_gen的划分逻辑彻底看懂。这个过程不亏。它可能帮你省下后面整整两周的timing closure时间。
另外,如果你遇到的问题是skew group本身怎么调都调不好,可以试着往前查一步:看看时钟树的clock root是否选得合理。很多group划分异常其实是root选错了导致的。比如某个时钟在传播过程中经历了多级mux,工具默认在第一个mux的输出端作为root开始长树,但如果你希望它从mux的输入端开始长,可以用set_clock_tree_roots去指定。一个小改动,后面的分组逻辑可能就完全不一样了。
时钟树是这个行业里最"诚实"的环节之一——你以为你懂它的时候,它总会给你一个惊喜。但正是在这些细枝末节里死磕出来的经验,才真正构成了一个数字后端工程师不可替代的竞争力。