1. 分段长时钟树到底难在哪:从sink type说起
做数字后端这行的朋友,尤其是经常跟Innovus打交道的,应该都有过这样的经历:跑完CTS,report一看,某条clock path的latency大得离谱,skew倒是压得不错,但insertion delay高得让人心里发毛。再仔细一看,这条clock tree从root到leaf穿过了大半个die,中间还跨了好几个power domain。这种场景下,clock tree的sink type选择就变得非常关键了。
所谓分段长时钟树,说白了就是clock tree的物理跨度很大,从clock root到最终的sink点,路径上可能经过多个hierarchy、多个voltage island、甚至多个clock domain的边界。这种树如果按常规方式一刀切地设成同一种sink type,要么工具优化不动,要么优化出来的结果在某个局部看起来还行,全局一看全是问题。
Innovus里CTS阶段支持的sink type有好几种,常用的包括stop pin、exclude pin、float pin、through pin、leaf pin等等。每种type背后对应的是工具对这条path的约束策略和优化自由度。选对了,工具知道哪里该停、哪里该穿、哪里该重点balance;选错了,要么工具在该停的地方继续往下buffer,要么在该穿的地方给你硬生生截断,最后latency和skew两头不讨好。
我见过不少项目,CTS阶段为了图省事,把所有sink都设成stop pin,结果工具在长路径上疯狂插buffer,insertion delay直接爆掉,后面再怎么调useful skew都救不回来。也见过把该设成through pin的地方设成了leaf pin,导致工具在中间节点就开始做balance,最后leaf端的skew反而压不住。
这篇文章主要面向有一定Innovus CTS经验的数字后端工程师,尤其是那些正在处理大规模SoC、多power domain、长clock latency场景的同行。我会把5种特殊sink type在分段长时钟树里的应用技巧拆开来讲,包括每种type的适用场景、设置方法、常见坑点,以及怎么配合其他CTS约束一起用。内容基于我自己在多个项目里的实操经验,有些是踩过坑之后总结出来的,有些是跟同行交流时学到的,希望能帮你在下次跑CTS时少走点弯路。
2. 五种特殊sink type的核心机制与选型逻辑
2.1 stop pin:什么时候该让工具“到此为止”
stop pin是CTS里最常用也最容易被滥用的sink type。它的核心语义是告诉工具:这条path到这里就结束了,不要再往下trace,也不要在这个点之后插任何CTS buffer。工具会把stop pin当作一个clock tree的终点来对待,围绕它做balance和optimization。
在分段长时钟树里,stop pin的典型应用场景是clock gating cell的enable端或者某个子模块的clock input port。比如你有一个大的CPU cluster,它的clock是从顶层root分下来的,但cluster内部有自己的clock tree。这时候你可以在cluster的clock input port上设stop pin,让顶层CTS只负责把clock推到cluster入口,cluster内部的tree由它自己的CTS run去处理。这样做的好处是顶层CTS的优化范围可控,不会因为cluster内部复杂的clock结构而把顶层tree搞得过于臃肿。
但这里有个坑:stop pin设多了,clock tree会被切得太碎。我见过一个项目,为了控制latency,在中间hierarchy上设了十几个stop pin,结果顶层tree的skew怎么都压不下去,因为每个stop pin都是一个独立的balance目标,工具在多个目标之间来回妥协,最后哪个都做不好。所以stop pin的使用原则是:只在真正的clock domain边界或者物理分区边界上设,不要为了局部优化而随意设。
设置方法上,Innovus里可以通过set_ccopt_property sink_type来指定,也可以在create_ccopt_clock_tree_spec文件里直接写。推荐的做法是在spec文件里显式声明,这样可追溯性好,后面ECO的时候也容易改。
# 在ccopt spec文件里设置stop pin set_ccopt_property sink_type stop [get_pins cpu_cluster/ck_in]注意:stop pin一旦设定,工具就不会再往该pin的fanout方向trace。如果这个pin后面还有你希望工具balance的leaf,那就要慎重,因为工具根本看不到它们。
2.2 exclude pin:把不需要balance的点摘出去
exclude pin的语义比stop pin更彻底:工具不仅不往这个pin后面trace,而且完全不把这个pin纳入clock tree的balance目标。换句话说,这个pin在CTS眼里就是透明的,它既不是sink也不是through,工具不会为它做任何latency匹配。
这个type在分段长时钟树里的典型用途是测试逻辑的clock端或者某些always-on的monitor clock。比如DFT的scan clock,在function mode下你根本不关心它的latency,那就可以设成exclude pin,让工具把精力集中在function clock上。再比如一些performance monitor或者debug logic的clock,它们对skew不敏感,设成exclude可以避免工具为了balance它们而牺牲主clock的质量。
但exclude pin有个隐藏风险:如果这个pin后面实际上还有function logic的clock,你把它exclude了,那部分logic的clock就完全没人管了。我遇到过一种情况,某个模块的clock在RTL里被mux过,function mode下走主clock,test mode下走scan clock。工具默认会把mux的输出当作一个sink来balance,但如果你把mux的某个input设成exclude,工具可能会误判整个mux的输出都不需要balance。所以设exclude之前,一定要确认这个pin的fanout里没有你关心的function clock。
# 设置exclude pin set_ccopt_property sink_type exclude [get_pins dft_scan_ck]2.3 float pin:让工具“看着办”的灵活选项
float pin是五种type里最灵活的一种。它的语义是:工具可以把这个pin当作sink来balance,也可以当作through来trace,具体怎么处理由工具根据全局优化目标来决定。换句话说,你把决定权交给了工具。
在分段长时钟树里,float pin适合用在那些你不太确定该stop还是该through的中间节点上。比如某个hierarchy的clock input,你既希望工具能balance它到和其他sink差不多的latency,又希望工具能继续往下trace到内部的leaf。这时候设成float,工具会根据实际情况判断:如果往下trace对全局skew有利,它就往下走;如果停在这里更有利于balance,它就停。
但float pin的问题是结果不可预测。同一个设计,两次CTS run可能会得到不同的结果,因为工具的优化算法有一定的随机性。所以如果你的clock tree对一致性要求很高,比如要做ECO或者要跟其他corner做对比,float pin可能会给你带来麻烦。我的建议是:在项目初期可以用float pin来探索工具的优化倾向,但到了signoff阶段,最好把float pin替换成明确的stop或through,让结果可控。
# 设置float pin set_ccopt_property sink_type float [get_pins sub_module/ck_in]2.4 through pin:长路径上的“穿针引线”
through pin是分段长时钟树里最重要的type之一。它的语义是:工具必须穿过这个pin继续往下trace,但这个pin本身不作为一个balance目标。换句话说,这个pin在clock tree里是一个“路过”的节点,工具会在它上面插buffer来驱动后面的load,但不会为了它单独做latency匹配。
through pin的典型应用场景是长clock path上的中间buffer节点或者跨power domain的level shifter。比如你的clock从顶层root出发,经过一个always-on的power domain,再进入一个可关断的domain。在always-on domain里有一个level shifter或者isolation cell,它的clock input就是一个天然的through pin。工具需要穿过它继续往下走,但不需要为它做balance,因为它本身不是最终的sink。
through pin用得好,可以显著减少长路径上的buffer数量。我做过一个对比:同样一条跨三个power domain的clock path,如果中间节点都设成stop pin,工具会在每个domain边界都插一堆buffer来做balance,总buffer数多了将近40%。改成through pin之后,工具只在必要的地方插buffer,latency反而更小,因为路径上的逻辑级数少了。
但through pin也有坑:如果through pin后面的load太大,工具可能会在through pin前面插很多buffer来驱动,这时候through pin就变成了一个事实上的sink,latency会集中在它前面。所以设through pin的时候,要关注它后面的fanout情况,如果fanout太大,考虑在它后面再加一级buffer或者设一个stop pin来分担。
# 设置through pin set_ccopt_property sink_type through [get_pins level_shift/ck_in]2.5 leaf pin:最终sink的精确控制
leaf pin是clock tree的最终终点,也就是flop的clock pin或者latch的gate pin。在分段长时钟树里,leaf pin的设置直接决定了工具在最后一级的balance策略。
默认情况下,工具会把所有flop的clock pin都当作leaf pin来处理。但在分段长时钟树里,有时候你需要手动指定某些pin为leaf,比如当某个flop的clock是从一个非标准的cell过来的时候,或者当你想把某个pin从through改成leaf来强制工具在那里结束tree的时候。
leaf pin的一个关键属性是leaf pin的balance group。工具会把leaf pin按照它们的clock domain、voltage island、以及你指定的group来分组,然后在组内做balance。在分段长时钟树里,如果你不显式地指定group,工具可能会把不同domain的leaf混在一起balance,结果就是跨domain的skew怎么都压不下去。所以我的习惯是:在CTS之前,先根据clock domain和power domain把leaf pin分好组,然后在spec里显式指定每个group的balance目标。
# 设置leaf pin并指定balance group set_ccopt_property sink_type leaf [get_pins cpu_core/reg_*/CK] set_ccopt_property balance_group cpu_core_grp [get_pins cpu_core/reg_*/CK]提示:leaf pin的balance group不要设得太细,否则工具会在每个小组内单独balance,全局skew反而会变大。一般建议按clock domain来分,最多再按power domain细分一层。
3. 分段长时钟树里的组合应用与参数计算
3.1 怎么根据路径长度和domain数量选type
在实际项目里,一条clock path上往往需要组合使用多种sink type。我的经验是先看路径长度,再看domain数量,最后看leaf的分布。
如果路径长度超过2000微米,或者穿过3个以上的power domain,那中间节点大概率要用through pin来减少buffer级数。如果路径上某个节点后面有独立的clock tree,那这个节点设stop pin。如果某个节点只是路过,后面还有大量leaf需要balance,那设through。如果某个节点的latency你完全不关心,设exclude。
具体怎么判断?我一般会先跑一次CTS,用report_ccopt_clock_trees看一下工具默认是怎么处理的,然后根据report里的latency和buffer分布来调整。比如report显示某条path上插了20个buffer,latency 1.2ns,那就要考虑把中间的一些节点从stop改成through,让工具少插点buffer。
3.2 latency目标怎么定:一个实际的计算例子
假设你的clock period是2ns,root的latency是0.3ns,leaf端的setup time是0.1ns,clock uncertainty是0.05ns。那么leaf端的clock latency最大不能超过:
2ns - 0.3ns - 0.1ns - 0.05ns = 1.55ns但这只是理论上限。实际做的时候,你要留足够的margin给OCV和后面的ECO。我一般会把目标latency定在理论上限的70%左右,也就是1.1ns左右。然后根据这个目标,反推每个中间节点的latency预算。
比如路径上有3个中间节点,那每个节点的latency预算大概是(1.1 - 0.3) / 3 ≈ 0.27ns。如果某个节点的实际latency超过了这个预算,就要考虑调整它的sink type,比如从stop改成through,让工具把buffer分散到后面的节点去。
3.3 跟useful skew的配合:别让sink type打架
useful skew是CTS里常用的技巧,通过故意让某些leaf的clock早到或晚到来改善setup或hold。但在分段长时钟树里,useful skew和sink type可能会打架。
比如你把某个中间节点设成stop pin,工具会围绕它做balance,这时候如果你又对这个节点后面的leaf施加useful skew,工具可能会因为stop pin的约束而无法实现你想要的skew。所以我的做法是:先确定sink type,再施加useful skew。如果某个leaf需要useful skew,那它的上游节点最好设成through或者float,给工具留出调整latency的空间。
# 先设sink type set_ccopt_property sink_type through [get_pins mid_node/ck_in] # 再施加useful skew set_ccopt_property useful_skew -0.1 [get_pins leaf_reg/CK]4. 实操流程:从spec编写到CTS signoff
4.1 spec文件的编写要点
Innovus的CTS spec文件是控制sink type的主要入口。我一般会把spec分成几个部分:clock定义、sink type设置、balance group设置、以及exception设置。
clock定义部分要写清楚每个clock的root、period、以及相关的generated clock。sink type设置部分按hierarchy来组织,先设顶层的stop和through,再设中间层的float,最后设leaf的balance group。exception部分用来处理一些特殊情况,比如某个pin需要单独设成exclude。
# 示例spec结构 # 1. clock定义 create_ccopt_clock_tree -name func_clk -source [get_ports clk_in] set_ccopt_property target_skew 0.05 -clock_tree func_clk # 2. sink type设置 set_ccopt_property sink_type stop [get_pins cpu_cluster/ck_in] set_ccopt_property sink_type through [get_pins level_shift/ck_in] set_ccopt_property sink_type float [get_pins sub_module/ck_in] # 3. balance group set_ccopt_property balance_group cpu_grp [get_pins cpu_core/reg_*/CK] # 4. exception set_ccopt_property sink_type exclude [get_pins dft_scan_ck]4.2 CTS run的检查清单
跑完CTS之后,我一般会按以下顺序检查:
- report_ccopt_clock_trees:看每个tree的latency、skew、buffer数量。重点看latency有没有超过预算,buffer数量有没有异常多。
- report_ccopt_skew_groups:看每个balance group的skew。如果某个group的skew特别大,检查它的sink type设置是不是有问题。
- check_ccopt_clock_tree_convergence:看CTS有没有收敛。如果没收敛,看log里的warning和error。
- report_ccopt_latency:看每条path的latency分布。如果某条path的latency明显高于其他,检查它的sink type是不是设成了stop但实际应该设through。
4.3 ECO阶段的sink type调整
CTS做完之后,ECO阶段可能还需要调整sink type。比如某个模块的clock latency在post-CTS timing里成了critical path,那就要考虑把它的sink type从stop改成through,让工具在ECO时重新balance。
ECO阶段调整sink type要注意:不要一次性改太多。每次改一两个节点,跑一次ECO,看结果。因为sink type的改变会影响工具对整个tree的优化策略,改多了容易导致ECO不收敛。
# ECO阶段调整sink type set_ccopt_property sink_type through [get_pins critical_module/ck_in] ccopt_design -cts5. 常见问题与排查技巧实录
5.1 sink type设了没生效?先查这几项
这是最常见的问题。你明明在spec里设了stop pin,但report里显示工具还是往下trace了。排查顺序如下:
- 检查spec文件有没有被正确加载。Innovus里用
ccopt_design -spec来加载spec,如果路径写错了或者文件格式有问题,spec会被忽略。 - 检查pin的hierarchy path对不对。
get_pins的path必须跟网表里的完全一致,大小写敏感。 - 检查有没有被后面的设置覆盖。比如你先设了stop,后面又设了through,那最终生效的是through。
- 检查这个pin是不是被设成了exclude。exclude的优先级比stop高,如果同时设了,exclude生效。
5.2 latency压不下去?试试调整through pin的位置
如果某条path的latency怎么都压不下去,大概率是through pin的位置不对。我的经验是:through pin应该设在路径的中间偏后位置,而不是最前面。因为工具在through pin前面插的buffer会直接累加到latency上,如果through pin太靠前,前面的buffer级数就会很多。
比如一条path有5个中间节点,through pin设在第4个节点上,那工具只需要在前4个节点之间插buffer,级数可控。如果设在第1个节点上,工具要在第1个节点前面插buffer来驱动后面所有的load,级数就会爆炸。
5.3 skew在跨domain边界变大?检查balance group
跨power domain的skew是分段长时钟树的经典难题。工具默认会把所有leaf放在一个balance group里,但不同domain的leaf可能因为voltage不同而latency差异很大。这时候要把不同domain的leaf分到不同的balance group,然后分别设target skew。
但分group之后,group之间的skew可能会变大。这时候可以用useful skew来补偿:让latency大的group的target skew设小一点,latency小的group设大一点,这样group之间的skew就能压下来。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| sink type设了没生效 | spec未加载/路径错误/被覆盖 | 检查spec加载log,确认pin path | 重新加载spec,修正path |
| latency过大 | through pin位置太靠前 | report_ccopt_latency看buffer分布 | 把through pin往后移 |
| 跨domain skew大 | balance group未分或分得太细 | report_ccopt_skew_groups | 按domain分group,配合useful skew |
| CTS不收敛 | sink type冲突 | check_ccopt_clock_tree_convergence | 减少float pin,明确stop/through |
| ECO后latency变差 | sink type改动太多 | 对比ECO前后的report | 每次只改一两个节点 |
提示:CTS的sink type设置没有“万能模板”,每个设计都要根据实际的clock结构、power domain划分、以及timing目标来调整。我的习惯是在项目初期多花点时间做sink type的探索,把各种组合都试一遍,找到最适合当前设计的方案,后面ECO阶段就会轻松很多。
6. 一些个人体会
做CTS这些年,我越来越觉得sink type的选择本质上是在工具优化自由度和结果可控性之间找平衡。stop pin和exclude pin给工具的自由度小,结果可控但可能不是最优;float pin给工具的自由度大,结果可能更优但不可控;through pin和leaf pin介于两者之间,用好了能兼顾。
分段长时钟树之所以难,是因为它把这种平衡问题放大了。一条短clock path,你随便设什么type,工具都能给你balance得差不多。但一条长path,type设错一个节点,latency可能就差出几百个ps,后面怎么调都调不回来。
我自己的做法是:在项目早期就用report_ccopt_clock_trees把每条长path的latency和buffer分布看清楚,然后针对性地设sink type。不要等到CTS跑完了发现latency爆了再回头改,那时候改的成本会高很多。另外,spec文件一定要写得清晰、可追溯,每个sink type的设置都要有注释说明为什么这么设,这样后面ECO或者换人接手的时候,不至于一脸懵。
最后再分享一个小技巧:如果你不确定某个节点该设什么type,可以先设成float跑一次CTS,看工具怎么处理。如果工具把它当成了sink,那说明它后面的load不大,设成stop可能更合适;如果工具把它当成了through,那说明它后面的load很大,设成through让工具继续往下trace可能更好。这个方法我用了很多次,基本上一次就能判断个八九不离十。