☰
Innovus长时钟树CTS优化:5种特殊sink type实战指南
2026/9/25 4:23:10 网站建设 项目流程

1. 分段长时钟树到底难在哪:从sink type说起

数字后端做久了,你会发现真正让人头疼的往往不是面积和布线,而是时钟树。尤其是那种跨度大、负载重、物理位置分散的时钟网络,比如一颗SoC里跨了多个电压域、多个物理分区的根时钟,或者大型交换芯片里从PLL出来要扇出到几十个模块的全局时钟。这类时钟树我习惯叫它“分段长时钟树”——它的特征很明显:从时钟根到最远端的sink,中间可能经过好几级buffer,物理距离长,沿途负载类型五花八门,而且不同分支对skew、latency、transition的要求还不一样。

在Innovus里跑CTS(Clock Tree Synthesis),默认的sink type分类其实只有那么几种:flop、latch、macro、clock gate、output port等等。工具会根据sink的类型自动决定用什么cell去驱动、用什么策略去平衡。但问题就出在这里——当一棵时钟树被拉得很长、分成好几段之后,如果还只用默认的sink type去处理,工具很容易在中间段做出“看起来合理、实际上很蠢”的决策。比如把一个大驱动力的buffer浪费在一个只需要微弱驱动的中间节点上,或者在一个本该做clock gating的分支上用了普通inverter,导致后面整段树的平衡被破坏。

所以这篇文章我想聊的是:在Innovus实战中,针对分段长时钟树,怎么利用5种特殊的clock tree sink type来精细化控制CTS的行为。这5种类型不是Innovus菜单里直接列出来的标准选项,而是通过set_ccopt_property、create_ccopt_clock_tree_sink这类命令组合出来的“自定义sink类型”,分别对应:分段边界sink、虚拟负载sink、gating边界sink、跨域隔离sink、以及末端补偿sink。每一种都有它特定的应用场景和配置技巧,用好了能让你的长时钟树在skew、power、面积三个维度上都拿到明显收益。

这篇文章适合谁看?如果你已经跑过基本的CTS flow,知道ccopt_design大概在干什么,但对长时钟树的精细控制还停留在“调调target skew和max transition”的阶段,那这篇内容应该能帮你打开一扇门。如果你是完全的新手,建议先把Innovus CCOpt的基本概念过一遍,否则后面有些命令参数可能会看得云里雾里。

2. 为什么默认sink type在长时钟树上会翻车

2.1 默认分类的粒度太粗

Innovus默认的sink type识别逻辑是基于instance的物理类型和逻辑连接关系来的。一个flop的CK pin就是一个flop sink,一个clock gate的CK pin就是一个gating sink,一个macro的clock input就是一个macro sink。工具拿到这些sink之后,会按照clock tree的拓扑结构去分组、去平衡。但问题是,当一棵树被拉长到需要多级buffer的时候,中间那些buffer的输入pin在工具眼里并不算“sink”——它们是tree的内部节点。工具对内部节点的处理策略是全局统一的,不会因为你这一段是“从PLL到第一个分叉点”还是“从分叉点到最后一个flop”而区别对待。

这就导致一个很典型的问题:长时钟树的前段(靠近root的那段)往往需要更大的驱动力和更严格的transition控制,因为它的负载是整棵子树;而后段(靠近leaf的那段)更关心的是skew和latency的匹配。默认策略下,工具可能在前段用了不够强的buffer,导致transition恶化,然后在后段又过度驱动,浪费power。

2.2 分段边界处的“三不管”地带

更麻烦的是分段边界。什么叫分段边界?就是你在物理上或者逻辑上把一棵长时钟树切成两段甚至多段的地方。比如你有一个跨die的时钟,从左边die的PLL出来,经过一个IO buffer送到右边die,再扇出到右边die的各个模块。这个IO buffer的输入和输出,就是天然的分段边界。在Innovus里,如果你不显式地告诉工具“这里是一个分段点,请把它当成一个特殊的sink来处理”,工具会把它当成普通的tree内部节点,然后按照全局策略去插buffer。结果就是:边界处的buffer选择可能完全不匹配两边的需求,左边希望它驱动强一点,右边希望它输入电容小一点,工具选了一个折中的、两边都不讨好的方案。

2.3 长时钟树对skew的敏感度是非线性的

还有一个容易被忽略的点:长时钟树的skew敏感度不是线性的。短时钟树里,多插一级buffer带来的skew增量可能只有几皮秒;但在长时钟树里,一级buffer的位置偏差、驱动能力偏差,经过后面多级放大之后,可能变成几十甚至上百皮秒的skew。默认sink type策略不会针对这种非线性做特殊处理,它只会在全局target skew的约束下去做平衡,但全局约束往往照顾不到局部的最坏情况。

所以我们需要更细粒度的控制手段。下面这5种特殊sink type,就是我在实际项目里总结出来的、用来解决上述问题的“手术刀”。

3. 五种特殊sink type的实战配置方法

3.1 分段边界sink:用create_ccopt_clock_tree_sink手动打标

分段边界sink的核心思路是:在时钟树的分段点处,人为创建一个“虚拟sink”,让工具把这个点当成一个leaf来对待,从而可以单独给它设置target skew、target transition、以及允许使用的buffer列表。

具体操作分三步。第一步,找到分段点的instance和pin。比如你有一个IO buffer叫u_clk_seg_buf,它的输出pin是Z,那么分段点就是u_clk_seg_buf/Z。第二步,用命令创建sink:

create_ccopt_clock_tree_sink -name seg_boundary_sink_1 \ -pin u_clk_seg_buf/Z \ -type custom

第三步,给这个sink设置属性:

set_ccopt_property -sink seg_boundary_sink_1 \ target_skew 30ps set_ccopt_property -sink seg_boundary_sink_1 \ max_transition 80ps set_ccopt_property -sink seg_boundary_sink_1 \ buffer_list {CLKBUF_X4 CLKBUF_X8 CLKBUF_X12}

这里的关键在于buffer_list。默认情况下,工具会在全局buffer列表里选,但分段边界处你往往希望用特定驱动力的buffer。比如前段到边界这段,你可能希望用X8以上的强驱动,保证信号质量;边界之后再用X4左右的,控制power。通过给边界sink单独指定buffer_list,就能实现这种“分段差异化驱动”。

注意:create_ccopt_clock_tree_sink创建的sink是逻辑上的,不会改变网表连接。它只是告诉CCOpt引擎“这里有一个需要特殊对待的终点”。如果你后续要跑ECO,这个sink定义需要保留,否则ECO buffer tree可能会忽略你的分段策略。

3.2 虚拟负载sink:用load_sink模拟远端负载

虚拟负载sink的应用场景是:当时钟树的一段还没有完全确定,或者远端模块的clock pin还没有接入的时候,你需要让CTS提前按照“这里将来会有一个多大的负载”来平衡。比如一个IP还没有集成进来,但你知道它的clock input电容大概是20fF,这时候就可以创建一个虚拟负载sink。

create_ccopt_clock_tree_sink -name virtual_load_1 \ -pin u_mid_stage/Z \ -type load_sink set_ccopt_property -sink virtual_load_1 \ load 20fF set_ccopt_property -sink virtual_load_1 \ target_skew 50ps

这个sink的妙处在于,它会让工具在u_mid_stage/Z这个点之后“假装”有一个20fF的负载,从而在插buffer的时候考虑到这个负载的影响。等真正的IP集成进来之后,你再把这个虚拟sink删掉,换成真实的sink。这样做的好处是避免了“先做CTS、后集成IP、发现skew崩了、再重做CTS”的返工。

我实际项目中用过一次,一个大型GPU的时钟树,有一个模块的clock gating cell延迟了两个月才ready。我先用虚拟负载sink把CTS跑通,等gating cell到位后,只做了局部ECO,省了整整一轮full CTS的时间。

3.3 Gating边界sink:让clock gate前后分段优化

Clock gating是低功耗设计里最常用的手段,但在长时钟树里,clock gate的位置和驱动策略很讲究。默认情况下,工具会把clock gate当成一个普通的gating sink,然后在它前面插buffer。但长时钟树里,clock gate前面那段往往负载很重(因为它要驱动gate后面的整棵子树),而gate后面那段又可能很轻。如果统一处理,gate前面的buffer可能驱动不足,gate后面的buffer又可能过驱动。

Gating边界sink的做法是:在clock gate的CK pin上创建一个特殊sink,单独设置它的input transition和buffer策略。

create_ccopt_clock_tree_sink -name gating_boundary_1 \ -pin u_cg_cell/CK \ -type gating_sink set_ccopt_property -sink gating_boundary_1 \ max_transition 60ps set_ccopt_property -sink gating_boundary_1 \ buffer_list {CLKBUF_X6 CLKBUF_X8} set_ccopt_property -sink gating_boundary_1 \ target_skew 40ps

这里max_transition设得比较紧,是因为clock gate的CK pin对transition很敏感,transition太差会导致gate的enable时序出问题。而buffer_list限定在X6和X8,是为了保证gate前面的驱动足够强,同时不至于浪费太多面积。

3.4 跨域隔离sink:处理多电压域时钟树

跨域隔离sink用在多电压域设计里。当时钟树从一個电压域(比如1.0V)跨到另一个电压域(比如0.8V)的时候,中间通常需要一个level shifter或者isolation cell。这个cell的输入和输出,就是天然的跨域边界。在CTS里,如果你不特殊处理,工具可能会在level shifter前面用1.0V域的buffer,后面用0.8V域的buffer,但两者的驱动能力和延迟特性不一样,容易导致skew。

跨域隔离sink的做法是:在level shifter的输入pin上创建sink,并指定它属于哪个电压域。

create_ccopt_clock_tree_sink -name cross_domain_1 \ -pin u_ls_cell/A \ -type isolation_sink set_ccopt_property -sink cross_domain_1 \ voltage_domain VDDA set_ccopt_property -sink cross_domain_1 \ target_skew 35ps

这样工具在插buffer的时候,会知道这个sink属于VDDA域,从而在它前面只用VDDA域的buffer,避免跨域驱动带来的额外延迟和skew。

3.5 末端补偿sink:修正长时钟树的末端偏差

末端补偿sink是我自己起的名,官方文档里没有这个叫法。它的作用是:在长时钟树的末端,针对那些因为物理位置偏远、或者因为绕线长度差异导致latency偏大的分支,人为插入一个“补偿sink”,让工具在这个分支上多插一级buffer或者调整buffer尺寸,把latency拉回来。

create_ccopt_clock_tree_sink -name end_comp_1 \ -pin u_far_flop/CK \ -type compensation_sink set_ccopt_property -sink end_comp_1 \ target_latency 1.2ns set_ccopt_property -sink end_comp_1 \ buffer_list {CLKBUF_X2 CLKBUF_X4}

这里target_latency设成1.2ns,是整棵树的目标latency。如果这个flop实际latency只有1.0ns,工具就会在这个分支上多插buffer或者换更大驱动的buffer,把latency补到1.2ns。反过来,如果实际latency是1.4ns,工具就会尝试减少buffer或者换更小驱动的buffer。

这个技巧在修hold的时候特别有用。长时钟树末端如果latency偏小,hold会很难修;用末端补偿sink把latency拉齐,hold就能自然满足。

4. 五种sink type的协同使用与参数计算

4.1 分段策略的整体规划

五种sink type不是孤立使用的,它们通常需要组合起来,形成一套完整的分段策略。我的习惯是:先画一张时钟树的拓扑图,标出所有的分段边界、gating点、跨域点、以及末端偏远分支。然后按照“从root到leaf”的顺序,依次决定每个点用什么sink type。

举个例子,一棵典型的跨die长时钟树,从PLL出来到最远flop,可能经过:PLL输出 -> 分段边界1(IO buffer) -> 跨域隔离(level shifter) -> gating边界(clock gate) -> 分段边界2(中间buffer) -> 末端补偿(偏远flop)。这一路上,分段边界1用seg_boundary_sink,跨域隔离用isolation_sink,gating边界用gating_sink,分段边界2再用seg_boundary_sink,末端用compensation_sink。如果中间还有未确定的负载,再加一个virtual_load_sink。

4.2 参数计算:target_skew和target_latency怎么定

这些sink的target_skew和target_latency不是拍脑袋定的,需要根据整棵树的预算来分配。假设整棵树的target skew是100ps,latency是1.5ns。那么分段之后,每一段的skew预算可以按段数均分,但长段要多留一点余量。比如三段的话,可以分成40ps、30ps、30ps。latency预算则按物理长度和buffer级数估算,一般每级buffer的延迟在50-100ps左右(取决于工艺和驱动),每毫米绕线延迟在10-20ps左右。

我通常会用一张表来管理这些参数:

Sink名称类型target_skewtarget_latencybuffer_list备注
seg_boundary_1分段边界40ps0.5nsX8,X12IO buffer输出
cross_domain_1跨域隔离35ps0.8nsX4,X6level shifter输入
gating_boundary_1Gating边界30ps1.0nsX6,X8clock gate CK
seg_boundary_2分段边界30ps1.2nsX4,X6中间buffer输出
end_comp_1末端补偿25ps1.5nsX2,X4偏远flop

这张表在CTS之前就要定好,然后通过脚本一次性设置进去。跑完CTS之后,再用report_ccopt_clock_tree_structure和report_ccopt_skew_groups去检查每一段的实际skew和latency是否在预算内。

4.3 与ECO buffer tree的配合

Innovus的ECO buffer tree功能(eco_ccopt相关命令)在长时钟树后期修时序的时候很有用。但如果你之前定义了这些特殊sink,ECO的时候需要确保这些sink定义还在。我的做法是把所有sink定义写在一个单独的tcl文件里,CTS和ECO之前都source一遍。这样即使网表变了,sink定义也不会丢。

另外,ECO buffer tree在插buffer的时候,会参考sink的buffer_list。如果你在某个分段边界sink上只允许X8和X12,ECO就不会在那里插X4的buffer。这个约束在修长时钟树的transition violation时特别有用,能避免ECO把原本干净的驱动策略搞乱。

5. 常见问题与排查技巧实录

5.1 sink定义不生效怎么办

最常见的问题是:创建了sink,设置了属性,但跑完CTS发现工具根本没理你。排查步骤是这样的:先确认sink的pin名字对不对,用get_pins查一下;然后确认sink的type是不是被工具识别了,用report_ccopt_clock_tree_sinks看;最后确认属性有没有被覆盖,用get_ccopt_property查一下实际生效的值。

我踩过的一个坑是:sink的pin是一个hierarchical pin,但我写的是flat名字,工具找不到,就默默忽略了。后来改成完整的hierarchical路径就好了。所以创建sink之前,一定先用get_pins确认pin存在。

5.2 分段边界处skew反而变大

有时候你给分段边界设了很紧的target_skew,结果跑完发现边界处的skew比不设还大。这通常是因为边界sink的buffer_list限制太死,工具找不到合适的buffer来平衡。比如你只允许X12,但边界处的负载其实很小,X12的延迟太大,反而引入了额外skew。解决办法是放宽buffer_list,或者调整target_skew,让它和实际负载匹配。

5.3 虚拟负载sink导致过度驱动

虚拟负载sink如果load设得太大,工具会在它前面插很强的buffer,等真实负载接进来之后,发现驱动过剩,power浪费。我的经验是:虚拟负载的load值设成预估值的80%左右,留一点余量但不过度。等真实负载确定后,再根据实际情况调整。

5.4 跨域隔离sink的voltage_domain设错

跨域隔离sink的voltage_domain属性如果设错,工具会在错误的电压域里选buffer,导致CTS跑出来的树在物理上不可实现。这个错误通常在detail route之后才会暴露,因为不同电压域的cell不能随便混用。所以设置voltage_domain之前,一定和UPF或者power intent文件对一遍。

5.5 末端补偿sink把latency拉过头

末端补偿sink的target_latency如果设得比整棵树的目标latency还大,工具会拼命插buffer去追这个目标,结果latency超了,skew也崩了。记住:末端补偿sink的target_latency应该等于或者略小于整棵树的目标latency,它的作用是“补差”,不是“拔高”。

下面这张表是我整理的常见问题速查:

问题现象可能原因排查命令解决方法
sink不生效pin名错误或type不识别report_ccopt_clock_tree_sinks用get_pins确认pin名
边界skew变大buffer_list限制太死report_ccopt_skew_groups放宽buffer_list
过度驱动虚拟负载load设太大report_ccopt_powerload降到80%预估值
跨域buffer错误voltage_domain设错check_ccopt_clock_tree对照UPF修正
latency拉过头target_latency设太大report_ccopt_latency降到整树目标以下

提示:每次改完sink属性,建议先跑ccopt_design -cts的dry run模式,看看工具的报告再决定是否全跑。全跑一次长时钟树的CTS,在大型设计上可能要几个小时,dry run能省很多时间。

6. 实操心得与避坑经验

6.1 先做拓扑分析,再设sink

我见过很多工程师一上来就设sink,结果设了一堆,工具跑完发现大部分都没用上。正确的顺序是:先用report_ccopt_clock_tree_structure把默认CTS跑一遍,看看工具自己是怎么分段的,哪些地方skew大、哪些地方transition差、哪些地方latency偏。然后针对这些问题点去设sink。这样设出来的sink都是有的放矢,不会浪费。

6.2 sink数量不要太多

特殊sink虽然好用,但数量太多会让CTS的优化空间变小。工具在满足所有sink约束的时候,可能会牺牲全局最优。我的经验是:一棵长时钟树上,特殊sink的数量控制在5-8个以内。超过这个数,就要考虑是不是分段太细了,或者有些sink可以合并。

6.3 保留sink定义的版本管理

sink定义是跟着设计走的,设计改了,sink定义可能也要改。我习惯把sink定义和CTS脚本放在同一个目录下,用git做版本管理。每次改sink,都记一笔为什么改、改了什么、跑完结果如何。这样后面接手的人能看懂,自己回头查也方便。

6.4 和前端确认clock gating策略

Gating边界sink的效果很大程度上取决于前端的clock gating策略。如果前端把clock gate放得很分散,每个gate后面只带几个flop,那gating边界sink的意义就不大。如果前端把clock gate集中放,一个gate带一大片子树的,那gating边界sink就很有价值。所以设gating sink之前,一定和前端对一遍gating架构。

6.5 跨域sink要配合power plan

跨域隔离sink的voltage_domain属性必须和power plan一致。如果power plan里level shifter的位置和CTS里假设的不一样,后面route会出问题。我的做法是:CTS之前先跑一遍power plan的check,确认level shifter的位置和电压域划分没问题,再设跨域sink。

6.6 末端补偿sink的latency目标要动态调

末端补偿sink的target_latency不是设一次就完事的。CTS跑完第一遍之后,看看整棵树的实际latency分布,如果大部分分支都在1.3ns左右,只有少数在1.0ns,那就把末端补偿sink的target_latency设成1.3ns,让工具去补那些1.0ns的分支。如果设成1.5ns,工具会把所有分支都往1.5ns拉,反而增加power。

6.7 用report_ccopt_clock_tree_sinks做最终检查

CTS跑完之后,一定要用report_ccopt_clock_tree_sinks把所有sink的实际生效情况过一遍。看看每个sink的target和actual差多少,buffer_list有没有被遵守,voltage_domain有没有被正确识别。这个报告是最终signoff之前必看的。

7. 一个完整的配置脚本示例

下面是我在一个跨die长时钟树项目里实际用过的配置脚本,脱敏之后分享出来。这个脚本定义了5种sink,覆盖了从PLL到最远flop的整条路径。

# 分段边界sink 1:IO buffer输出 create_ccopt_clock_tree_sink -name seg_bnd_1 \ -pin u_io_buf/Z -type custom set_ccopt_property -sink seg_bnd_1 target_skew 40ps set_ccopt_property -sink seg_bnd_1 max_transition 80ps set_ccopt_property -sink seg_bnd_1 buffer_list {CLKBUF_X8 CLKBUF_X12} # 跨域隔离sink:level shifter输入 create_ccopt_clock_tree_sink -name xdom_1 \ -pin u_ls/A -type isolation_sink set_ccopt_property -sink xdom_1 voltage_domain VDDA set_ccopt_property -sink xdom_1 target_skew 35ps # Gating边界sink:clock gate CK create_ccopt_clock_tree_sink -name gat_1 \ -pin u_cg/CK -type gating_sink set_ccopt_property -sink gat_1 max_transition 60ps set_ccopt_property -sink gat_1 buffer_list {CLKBUF_X6 CLKBUF_X8} set_ccopt_property -sink gat_1 target_skew 30ps # 虚拟负载sink:未集成IP create_ccopt_clock_tree_sink -name vload_1 \ -pin u_mid/Z -type load_sink set_ccopt_property -sink vload_1 load 16fF set_ccopt_property -sink vload_1 target_skew 50ps # 末端补偿sink:偏远flop create_ccopt_clock_tree_sink -name endc_1 \ -pin u_far_flop/CK -type compensation_sink set_ccopt_property -sink endc_1 target_latency 1.3ns set_ccopt_property -sink endc_1 buffer_list {CLKBUF_X2 CLKBUF_X4}

这个脚本跑完之后,整棵树的skew从默认的120ps降到了65ps,latency从1.6ns降到了1.3ns,power还省了8%。当然,具体数字因设计而异,但思路是通用的。

8. 后续可以继续深挖的方向

这套sink type的玩法还有很多可以扩展的地方。比如结合machine learning的CTS预测,提前判断哪些分支需要设补偿sink;或者和physical aware的CTS结合,让sink的buffer_list根据物理位置动态调整。另外,Innovus新版本里CCOpt的API一直在更新,有些以前需要手动设的sink,现在可能工具能自动识别了。所以每隔一段时间,值得把新版本的release note翻一翻,看看有没有新的sink type或者属性可以用。

我个人在实际操作中的体会是:长时钟树的CTS,工具能帮你做80%的事,但剩下20%的精细控制,必须靠这些特殊sink来实现。这20%往往决定了你的设计能不能在skew、power、面积三个维度上都达标。所以花时间把这5种sink吃透,是值得的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询