在数字后端实现里,时钟树综合(CTS)永远是那个最容易被低估、却又最能决定芯片成败的环节。很多人跑完place、做完时序优化,觉得剩下的就是把CTS脚本一跑、让工具自动balance一下,结果流片回来发现某些模块的时钟偏斜大得离谱,或者hold修到崩溃。我做了这么多年后端,踩过最多的坑基本都集中在一种场景上:分段长时钟树。这种结构通常出现在多电压域、跨物理分区、或者时钟需要经过长距离绕线才能到达sink点的设计里。Innovus提供了多种clock tree sink type,默认的flop/cell类型大家都熟,但真正能救命的,是那几种特殊sink type在分段长树里的组合用法。这篇就围绕这5种特殊sink type,把我在实际项目里怎么用、为什么这么用、以及踩过的坑,完整拆一遍。
1. 先搞清楚分段长时钟树到底难在哪
1.1 长时钟树的物理现实:绕线延迟不再是配角
短时钟树里,leaf到root的距离可能就几百微米,绕线延迟相对于buffer延迟可以忽略。但分段长时钟树不一样,sink点可能分布在芯片的两端,中间要穿过好几个物理分区,绕线长度动辄几毫米。这时候绕线延迟(net delay)会占到总insertion delay的很大比例,甚至超过buffer本身的延迟。
我遇到过最极端的一个case:一个跨两个die-to-die接口的时钟,从PLL出来到最远sink点,绕线延迟占了整个insertion delay的65%。这种情况下,如果你还用默认的CTS策略,工具会拼命在中间插buffer来balance,结果就是buffer数量爆炸、功耗飙升,skew还不一定收敛。
所以分段长树的核心矛盾是:你要在绕线延迟主导的前提下,让工具理解哪些sink是可以"分组对待"的,而不是一视同仁地balance。
1.2 为什么默认sink type在长树里会失效
Innovus默认把leaf pin识别为flop的clock pin、latch的gate pin、或者macro的clock input。工具会把这些都当成需要严格balance的sink。但在分段长树里,有些sink其实不需要和主树严格对齐,比如:
- 某些低速模块的时钟,允许有较大的skew
- 某些测试逻辑的时钟,只在scan mode下用
- 某些跨域同步的时钟,本身就要做clock gating处理
如果你不告诉工具这些sink的特殊性,它就会浪费大量资源去balance它们,反而把真正关键的路径搞乱。这就是特殊sink type存在的意义——给工具"分类指令",让它知道哪些sink该严格管、哪些可以放松、哪些干脆单独处理。
1.3 五种特殊sink type的定位速览
在展开细节之前,先给一个整体定位,方便你建立全局认知。Innovus里常用的特殊sink type包括:stop pin、exclude pin、float pin、through pin、以及non-stop pin(不同版本命名略有差异,但语义一致)。它们各自的作用方向不同:
| Sink Type | 核心作用 | 典型场景 |
|---|---|---|
| Stop Pin | 时钟树在此终止,不再往下balance | 分段树的边界点 |
| Exclude Pin | 完全排除在CTS之外 | 测试逻辑、旁路时钟 |
| Float Pin | 参与balance但不强制对齐 | 低速模块时钟 |
| Through Pin | 时钟穿过但不作为sink | 跨分区馈通 |
| Non-stop Pin | 继续往下但不在此点收敛 | 中间馈点 |
这张表建议你先记住,后面每一节我都会结合具体操作和踩坑经验展开。
2. Stop Pin:分段长树里最关键的"断点"设计
2.1 Stop Pin的本质:让时钟树分段独立收敛
Stop pin是我在分段长树里用得最多的特殊sink type。它的语义很直接:时钟树传播到这个pin就停止,不再继续往它的fanout方向balance。换句话说,这个pin成了当前这段时钟树的leaf,工具只负责把时钟送到这里,后面的子树由你单独处理。
为什么这个机制在长树里这么重要?因为长时钟树如果一次性从头balance到尾,工具会试图用一条统一的树去覆盖所有sink,导致中间节点被过度约束。而实际上,长树往往可以拆成几段:PLL到分区入口是一段,分区入口到模块内部是另一段。每段的延迟特征、绕线环境都不一样,分开收敛反而更容易控制skew。
2.2 设置Stop Pin的具体操作与参数
在Innovus里设置stop pin通常有两种方式。一种是在CTS spec文件里用set_clock_tree_stop_pins命令,另一种是在MMMC或者floorplan阶段通过set_db属性标记。我一般用spec文件的方式,因为可读性好、方便版本管理。
# 在CTS spec中标记stop pin set_clock_tree_stop_pins [get_pins u_partition_A/clk_in] set_clock_tree_stop_pins [get_pins u_partition_B/clk_in] # 也可以批量标记 set stop_pins [get_pins -of_objects [get_cells {u_part_A u_part_B}] -filter "direction==in && name==clk*"] set_clock_tree_stop_pins $stop_pins设置完之后,工具在CTS时会把这两个pin当作leaf处理,不再往partition内部插buffer。partition内部的时钟树你需要单独跑一次CTS,或者用手工balance的方式处理。
注意:stop pin设置后,这个pin的downstream逻辑不会被自动CTS,必须确保你有后续的CTS步骤覆盖它,否则会出现unbalanced的时钟。
2.3 Stop Pin在跨电压域场景下的实战心得
跨电压域是stop pin最典型的应用场景。不同电压域的时钟树如果混在一起balance,工具会忽略level shifter带来的额外延迟,导致skew计算失真。我通常会在level shifter的输入侧设置stop pin,让每个电压域的时钟树独立收敛。
这里有个细节很多人会忽略:stop pin的位置选择会影响后续ECO的难度。如果你把stop pin设在太靠前的位置,后续要调整分区内部时钟树时,就得重新跑整个分区的CTS;如果设在太靠后,又起不到隔离作用。我的经验是设在分区入口的第一个buffer之后,这样既隔离了域间影响,又保留了分区内部一定的调整空间。
另外,stop pin和clock gating cell的配合也很关键。如果stop pin下游有ICG,要确保ICG的enable路径也被正确处理,否则会出现gating失效或者时钟被意外切断的问题。
3. Exclude Pin:把不该管的sink彻底踢出去
3.1 什么时候必须用Exclude Pin
Exclude pin的语义是:这个pin完全不参与时钟树综合,工具既不给它插buffer,也不把它算进skew计算。听起来很粗暴,但在某些场景下这是唯一正确的做法。
最典型的场景是测试逻辑。比如scan chain的clock、DFT专用的时钟mux,这些逻辑在function mode下根本不工作,但如果你不exclude,工具会认真地去balance它们,浪费大量buffer资源。更糟糕的是,有些测试时钟的fanout很大,工具为了balance它会插一堆buffer,结果影响了正常功能时钟的树结构。
还有一种场景是旁路时钟(bypass clock)。有些设计里存在多条时钟路径,实际工作时只选一条,其他都是备选。这些备选路径如果不exclude,工具会试图同时balance所有路径,导致资源浪费和skew恶化。
3.2 Exclude Pin的配置方法与验证
配置exclude pin的命令和stop pin类似,但语义完全不同:
# 排除测试逻辑时钟 set_clock_tree_exclude_pins [get_pins u_dft/scan_clk] set_clock_tree_exclude_pins [get_pins u_dft/test_mux/Z] # 排除旁路时钟路径 set exclude_list [get_pins -of_objects [get_cells u_bypass*] -filter "direction==in"] set_clock_tree_exclude_pins $exclude_list设置完之后,一定要验证。我通常用report_clock_tree -exclude_pins来确认排除列表是否正确,然后用check_clock_tree检查有没有遗漏的sink。
提示:exclude pin设置后,这些pin的时钟延迟不会被工具优化,你需要手工确保它们的时序满足要求,或者在后续ECO阶段单独处理。
3.3 Exclude Pin的常见误用与避坑
我见过最常见的误用,是把exclude pin当成了"万能解药",一遇到难balance的sink就exclude掉。这是很危险的。因为exclude之后,工具完全不管这个sink,如果它恰好是功能路径上的关键sink,时序会直接崩掉。
我的原则是:只有确认在function mode下不工作、或者有独立处理方案的sink,才用exclude。每次exclude之前,我都会问自己三个问题:这个sink在function mode下有时序要求吗?它的时钟延迟有人管吗?如果工具不管它,我有没有后续手段保证它不出问题?
另外一个坑是exclude pin的传播性。有些版本的Innovus里,exclude一个pin会连带影响它的fanout,导致下游sink也被排除。这时候要用-no_propagate选项控制传播行为,具体要看版本手册确认。
4. Float Pin:给低速模块时钟"松绑"的正确姿势
4.1 Float Pin与Stop Pin的本质区别
很多人会把float pin和stop pin搞混,其实它们的语义差别很大。Stop pin是"到此为止,不再往下",而float pin是"继续往下,但不强制和主树对齐"。换句话说,float pin仍然参与时钟树构建,但工具不会为了它去牺牲主树的skew。
这个区别在分段长树里非常关键。比如一个长树上有几个低速模块的sink,它们的时钟频率只有主时钟的1/4,对skew的要求很宽松。如果你用stop pin,就得单独给它们建树;如果用float pin,工具会在主树的基础上"顺带"把时钟送过去,只是不保证严格对齐。
4.2 Float Pin的适用判断标准
什么时候该用float pin?我的判断标准是看sink的时序裕量和时钟频率:
- 时钟频率低于主时钟的1/2,且setup裕量大于insertion delay的20%
- sink数量少,且分布不集中
- 这些sink的skew即使偏大,也不会导致功能错误
满足这些条件,就可以考虑float pin。具体配置:
# 标记低速模块时钟为float set_clock_tree_float_pins [get_pins u_low_speed_mod/clk] # 可以配合skew group使用 set_clock_tree_skew_group -name low_speed_grp -pins [get_pins u_low_speed_mod/clk] -target_skew 500ps这里我通常会配合skew group一起用,给float pin一个宽松的target skew,让工具知道"这些sink可以放松,但别完全不管"。
4.3 Float Pin在长树里的实测效果与调优
我在一个多核处理器项目里大量使用了float pin。当时的情况是:主时钟要送到8个core,每个core内部还有一堆低速外设时钟。如果全部严格balance,buffer数量会多出30%以上。
用了float pin之后,工具只对主时钟路径做严格balance,低速外设的时钟通过float pin"搭便车"送过去。实测下来,主时钟skew控制在50ps以内,低速外设的skew在300ps左右,完全满足时序要求,buffer数量减少了约25%。
调优的关键是float pin的target skew设置。设得太紧,工具还是会拼命balance;设得太松,又可能出现时序问题。我的经验值是:float pin的target skew设为主树target skew的5到10倍,具体要看低速模块的时序裕量。
5. Through Pin与Non-stop Pin:长树馈通场景的精细控制
5.1 Through Pin:时钟穿过但不收敛的中间点
Through pin的语义是:时钟信号经过这个pin,但工具不把它当作sink,也不在这里收敛。它更像是一个"透明"的节点,时钟穿过去继续往后面的sink传播。
这个类型在跨分区馈通场景下特别有用。比如时钟从分区A传到分区B,中间经过分区A的一个边界buffer。如果你把这个buffer的输出pin设为through pin,工具就知道"这里只是路过,真正的sink在分区B里面",从而不会在分区A里浪费资源去balance这个点。
配置方式:
# 设置through pin set_clock_tree_through_pins [get_pins u_part_A/boundary_buf/Z] # 配合stop pin使用,形成完整的馈通路径 set_clock_tree_stop_pins [get_pins u_part_A/clk_out]Through pin和stop pin经常配合使用:stop pin标记分区的出口,through pin标记路径上的中间节点。这样工具就能清晰地理解时钟的传播路径,不会在中间节点上做无谓的balance。
5.2 Non-stop Pin:继续传播但不在此收敛
Non-stop pin和through pin很像,但有一个细微差别:non-stop pin会参与时钟树的延迟计算,只是不作为最终的balance目标。它通常用在那些"需要被看到,但不需要被严格对齐"的中间节点上。
在实际项目里,我一般用non-stop pin来处理那些有monitor或者debug逻辑的中间节点。这些节点需要时钟信号,但它们的时序要求很宽松,不值得为它们牺牲主树的skew。
# 设置non-stop pin set_clock_tree_non_stop_pins [get_pins u_debug/monitor_clk]5.3 五种sink type的组合使用策略
单独用某一种sink type往往解决不了复杂问题,真正的威力在于组合。我在一个跨三个物理分区的长时钟树项目里,是这样组合使用的:
- 分区入口设stop pin,隔离各分区的时钟树
- 分区内部的低速模块设float pin,放松skew要求
- 跨分区的馈通路径设through pin,避免中间节点被误balance
- 测试逻辑设exclude pin,彻底排除
- debug逻辑设non-stop pin,保证信号可达但不影响主树
这套组合下来,整个长树的buffer数量比默认策略少了约35%,skew反而更好了。核心逻辑就是:让工具把精力集中在真正重要的sink上,其他sink用合适的类型"分流"处理。
| 组合场景 | 使用的sink type | 效果 |
|---|---|---|
| 跨电压域 | stop + through | 域间隔离,馈通透明 |
| 多核+低速外设 | float + stop | 主树严格,外设放松 |
| 含DFT逻辑 | exclude + non-stop | 功能路径干净 |
| 跨分区长树 | stop + through + float | 分段收敛,资源最优 |
6. 踩坑实录:那些让我熬夜的sink type配置问题
6.1 Stop Pin设错位置导致的分区时钟失配
有一次我在一个双分区设计里,把stop pin设在了分区入口的时钟mux输出上。结果CTS跑完,两个分区的时钟延迟差了将近200ps,导致跨分区接口的setup直接violation。
排查过程是这样的:先用report_clock_tree -summary看各分区的insertion delay,发现分区B的延迟明显偏大。然后report_clock_tree -pin追到stop pin的位置,才意识到问题——mux本身有延迟,而我把stop pin设在mux之后,导致分区B的时钟树从mux输出开始算,自然比分区A多了一段mux延迟。
修复方法很简单:把stop pin移到mux的输入侧,让mux的延迟被包含在上一段时钟树里。改完之后,两个分区的延迟差缩小到30ps以内。
这个坑的教训是:stop pin的位置要选在"延迟特征一致"的边界上,不要把有额外延迟的单元夹在中间。
6.2 Exclude Pin传播导致的意外断链
另一个坑是exclude pin的传播行为。我在一个项目里exclude了一个测试mux的输出pin,本以为只影响这个pin本身,结果工具把它的整个fanout都排除了,包括一个功能路径上的clock divider。CTS跑完后,那个divider的时钟完全没被balance,时序崩得一塌糊涂。
排查的时候用check_clock_tree -exclude才发现,exclude列表里多了一堆我没设置的pin。后来查手册才知道,某些版本的Innovus默认会传播exclude属性。解决办法是加-no_propagate选项,或者改用stop pin来达到类似效果。
6.3 Float Pin的target skew设置过紧
float pin的target skew如果设得太紧,工具会把它当成普通sink来balance,float的意义就没了。我有一次把float pin的target skew设成了100ps,结果工具为了达到这个目标,在主树和float sink之间插了一堆buffer,buffer数量不降反升。
后来我把target skew放宽到500ps,工具才真正"放松"下来。所以float pin的关键不是设不设,而是target skew要设得足够宽松,让工具明确知道这些sink可以牺牲精度换资源。
6.4 Through Pin与Stop Pin顺序错误
through pin和stop pin如果设置顺序不对,工具可能无法正确识别路径。我遇到过一次:先设了stop pin,后设through pin,结果工具把through pin当成了stop pin的下游sink,反而在中间插了buffer。
正确的做法是先设through pin,再设stop pin,让工具先理解路径的透明性,再确定收敛点。这个顺序在Innovus的CTS spec里很重要,建议养成习惯。
7. 从CTS到ECO:sink type配置的后续维护
7.1 CTS完成后如何验证sink type是否生效
CTS跑完不代表万事大吉,sink type是否真正生效需要验证。我通常用这几个命令组合检查:
# 检查stop pin是否被正确识别 report_clock_tree -stop_pins # 检查exclude pin列表 report_clock_tree -exclude_pins # 检查float pin的skew group report_clock_tree -skew_groups # 整体检查时钟树结构 check_clock_tree -verbose如果发现某个sink type没生效,先检查pin的层次名是否正确,再检查是否有其他约束覆盖了它。有时候MMMC里的约束会和CTS spec冲突,导致sink type被忽略。
7.2 ECO阶段sink type的调整策略
ECO阶段如果改了时钟结构,sink type可能需要同步调整。比如你新增了一个分区,就要给它的入口加stop pin;如果删除了某个低速模块,对应的float pin也要清理掉。
我的习惯是在ECO之前先report_clock_tree导出当前的sink type配置,改完之后对比一遍,确保没有遗漏。ECO buffer tree的插入也要注意不要跨越stop pin的边界,否则会破坏分段结构。
7.3 跨项目复用的sink type配置模板
最后分享一个我常用的配置模板思路。不同项目虽然细节不同,但sink type的配置逻辑是相通的。我会把配置分成三层:
- 基础层:所有项目通用的exclude规则(比如DFT逻辑)
- 结构层:根据分区和电压域设置的stop/through pin
- 优化层:根据时序裕量设置的float/non-stop pin
这样复用的时候只需要改结构层和优化层,基础层直接拿来用。模板化的好处是减少遗漏,坏处是可能忽略项目特殊性,所以每次复用都要重新review一遍。
# 基础层示例 set_clock_tree_exclude_pins [get_pins -hier -filter "name=~*scan_clk*"] set_clock_tree_exclude_pins [get_pins -hier -filter "name=~*test_clk*"] # 结构层示例(需按项目修改) foreach part {u_part_A u_part_B u_part_C} { set_clock_tree_stop_pins [get_pins $part/clk_in] } # 优化层示例(需按时序裕量调整) set_clock_tree_float_pins [get_pins -hier -filter "name=~*low_speed*/clk"] set_clock_tree_skew_group -name float_grp -target_skew 500ps这套模板我在好几个项目里用过,基本能覆盖80%的场景,剩下的20%靠具体分析补。关键是理解每种sink type的语义,而不是死记命令。
我在实际使用中最大的体会是:sink type不是越多越好,而是越精准越好。每设置一个特殊sink,都要清楚它为什么存在、不设会怎样、设了之后谁来保证它的时序。想清楚这三个问题,分段长时钟树就不再是噩梦,反而成了你可以精细调控的舞台。