
1. 分段长时钟树到底难在哪从sink type说起数字后端做久了你会发现真正让人头疼的往往不是面积和布线而是时钟树。尤其是那种跨度大、负载重、物理位置分散的时钟网络比如一颗SoC里跨了多个电压域、多个物理分区的根时钟或者大型交换芯片里从PLL出来要扇出到几十个模块的全局时钟。这类时钟树我习惯叫它“分段长时钟树”——它的特征很明显从时钟根到最远端的sink中间可能经过好几级buffer物理距离长沿途负载类型五花八门而且不同分支对skew、latency、transition的要求还不一样。在Innovus里跑CTSClock 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。第二步用命令创建sinkcreate_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输出 - 分段边界1IO buffer - 跨域隔离level shifter - gating边界clock gate - 分段边界2中间buffer - 末端补偿偏远flop。这一路上分段边界1用seg_boundary_sink跨域隔离用isolation_sinkgating边界用gating_sink分段边界2再用seg_boundary_sink末端用compensation_sink。如果中间还有未确定的负载再加一个virtual_load_sink。4.2 参数计算target_skew和target_latency怎么定这些sink的target_skew和target_latency不是拍脑袋定的需要根据整棵树的预算来分配。假设整棵树的target skew是100pslatency是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 CKseg_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相关命令在长时钟树后期修时序的时候很有用。但如果你之前定义了这些特殊sinkECO的时候需要确保这些sink定义还在。我的做法是把所有sink定义写在一个单独的tcl文件里CTS和ECO之前都source一遍。这样即使网表变了sink定义也不会丢。另外ECO buffer tree在插buffer的时候会参考sink的buffer_list。如果你在某个分段边界sink上只允许X8和X12ECO就不会在那里插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 1IO 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} # 跨域隔离sinklevel 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边界sinkclock 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降到了65pslatency从1.6ns降到了1.3nspower还省了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吃透是值得的。