☰
CTS时钟树CDB Cell过多?Innovus后端优化的关键策略与实践
2026/10/7 5:36:50 网站建设 项目流程

项目做到CTS这步,时钟树报告一拉,我脑子里直接跳出两个字:完了。Innovus的时钟树报告显示,CDB Cell数量比上一版失控版本还多出四成,更离谱的是,有一堆buffer只驱动了一两个触发器leaf,纯粹在给自己增加绕线负担。那一刻我就知道,问题不在工具,而在CTS spec里那些被我顺手填进去的“保守值”。

在数字后端流程里,Clock Tree Synthesis(CTS)是难度最容易被低估的一环。很多同学以为CTS就是把时钟从root推到大几千个sink,让skew尽量小就行,结果Innovus“忠实”地执行了你的意图,插出满满一屏CDB Cell——面积上去、功耗上去、congestion恶化、OCV悲观,反而把时序拖垮。这篇博文不聊教科书定义,只讲我在真实项目里怎么定位“为什么工具会疯插CDB Cell”、怎么通过spec和ECO操作把时钟树重新“减回标准体重”。

1. 一颗“胖”时钟树带来的连锁反应:过度CDB Cell到底坑在哪

1.1 CDB Cell的身份,以及时钟树里它该占多大体量

CDB Cell是时钟树综合过程中专门插入的时钟网络驱动单元,本质上是经过特殊设计和选型的buffer/inverter。它跟普通data path上的buffer有个重要区别:CDB Cell的型号选择、上升下降时间对称性、噪声鲁棒性和驱动能力,都是为了时钟信号大规模分发而准备的。在标准单元库里,这类cell通常会被标注为clock buffer/clock inverter,供CTS工具调用。

正常的CTS,CDB Cell数量应该和sink数量保持一个健康比例。以我这些年经手的项目经验,纯时钟buffer/inverter的总数量通常控制在sink数量的10%以内是比较合理的区间。举个例子,一个时钟域有2万个sink,时钟树里插了1500~2000个CDB Cell,这算正常;如果插到5000个以上,那就要警惕了。另外还要看树的级数,一颗从clock root到最远leaf之间的buffer级数,8级左右很常见,超过12级且平均每级延迟明显偏大,基本就是过度插入的信号。

1.2 过度插入的表征信号:先从报告和图上发现“三高”

工具不会直接告诉你“我插多了”,但它的行为会留下痕迹。我在项目里总结了三个最直观的表征,简称“三高”。

高比例:CDB Cell数量 / sink数量超过15%,甚至20%以上,说明大量buffer只是在打酱油。高冗余:报告里出现大量fanout极低的buffer,比如一个buffer就驱动1个或多或2个leaf pin。这种低效驱动是过度插入最典型的表现——正常工具不会为单leaf专门插一级buffer,除非它是在做延迟匹配或修复transition。高级数:report_clock_tree -detail拉出来,某些路径上的级数远超同domain均值,且latency明显偏大。

在Innovus GUI的Clock Tree Browser里,选中某棵树看结构图,你会看到一串串buffer像糖葫芦一样串在很短的net上。这种结构一旦出现在congestion hot spot区域,后面绕线阶段会非常痛苦。

1.3 面积只是一层皮:真正的代价在时序和绕线

有人觉得,CTS多插几百个buffer,面积也没超,有什么好慌的?我刚开始也这么想,直到被一个案子教育了。时钟树buffer过多,首先直接影响的是时钟延迟。buffer多了,insertion delay自然被推高,而现代工艺下OCV和derating对长路径非常不友好。你辛苦修完setup,结果CTS阶段因为时钟树过长,到post-CTS后setup slack直接掉一截,这属于“CTS种下苦果,signoff阶段还债”。

其次,大量CDB Cell占用了placement site,把原本留给data path cell的空间挤掉,congestion恶化。尤其在clock gating cell和macro附近,工具为了绕开拥塞区域会插入更多detour buffer,形成恶性循环。更隐蔽的一点:clock buffer数量多,意味着时钟网络的晶体管开关次数多,动态功耗上涨。芯片规模大了以后,时钟网络功耗占芯片总功耗的比例可以达到30%~50%,这一块的浪费是非常可惜的。

2. 动手之前先找根因:五个会诱导工具“只管多插”的CTS设置

2.1 第一个旋钮拧过头:target skew设成0不代表更优秀

这是我在新项目里最容易看到的问题。很多工程师觉得skew越小越好,于是直接SetTargetSkew 0.02甚至0。工具当然会“认真执行”:为了把远端sink和近端sink对齐,它会在路径较短的一端插一串delay buffer,让信号绕路、让buffer堆叠,硬生生把晚到达的路径给“等”出来。这些delay buffer并不改善任何signal integrity,纯粹是为了匹配延迟。更讽刺的是,在OCV环境下,skew设到极值往往导致时序更差,因为长路径的derating因素放大了。

我自己的习惯是把target skew从极端值放宽到可接受的范围内,比如60~100ps甚至更高,具体看时钟周期和库的单元延迟。请记住,CTS的目标不是skew=0,而是skew足够小、并且不会因为过度补偿带来额外的OCV和绕线代价。

2.2 被误解的max_fanout:设置越小反而引导工具插更多buffer

有一个常见的“反直觉”现象:把max_fanout设得非常小,比如8,工具反而会插出更多级的CDB Cell。原因在于,时钟树net如果fanout超过限制,工具会把大扇出拆成多个子树,每个子树都由一个新的buffer驱动。本来一个buffer能驱动16个sink,你非要让它最多驱动8个,那同样的sink数量就需要两倍的驱动buffer,这些驱动buffer之间为了满足skew,往往还需要额外的平衡buffer层。整体下来,buffer数量直接翻倍甚至更多。

我并不是说max_fanout可以随便设很大,过大的fanout会导致驱动不足、transition超限。关键是要在两者之间找到项目实际可行的平衡点。对大多数主流工艺库,14~20这个范围是比较常见的起点。如果你的库单元驱动能力很强,适当再放大也问题不大;如果库单元本身偏弱,就再收一点。总之,不要为了“看起来稳健”就把fanout压到个位数。

2.3 target delay的隐形上限:目标延迟设太小,工具就疯狂“推”

CTS spec里另一个容易埋雷的是延迟约束。有些流程会在spec里写SetTargetDelay -early值很小,甚至设置成0,试图让时钟尽快到达所有sink。工具为了实现极小的延迟目标,会不断加大每级buffer的驱动强度、缩短net长度,结果就是:在原本两三颗buffer就能完成任务的地方,硬生生堆出四五颗,只为把transition再压快一点、延迟再缩一点。

这里需要理解一个物理事实:在特定工艺和特定库下,时钟树存在一个“自然延迟”。你说:“我这里需要快一点”,工具能做的只是多加buffer、加宽wire、拉开间距。这些手段都存在收益递减的拐点。一旦过了拐点,再堆buffer换来的延迟改善微乎其微,面积功耗却直线上升。正确的做法是先跑一版不做强约束的CTS,看工具自然的latency分布,再决定要不要收敛,而不是一开始就把target delay打到最小值。

2.4 skew group跨区域平衡:让远水和近渴强行对齐的代价

当多个时钟域或同一域内物理距离悬殊的sink被放进同一个skew group时,Innovus就得想办法让它们对齐到达时间。最粗暴的办法就是给较近的那组插延迟buffer,给较远的组加大驱动。物理距离差异越大,需要的补偿buffer就越多。

我有一次处理过一个多时钟域模块,A域sink集中在左上角,B域sink散落在右下角,结果我把它们设到了一个group里,CTS完成后发现两棵子树之间密密麻麻全是平衡buffer。后来把跨域约束拆掉,各自独立build tree,再通过CTS后的useful skew微调来满足时序要求,CDB Cell数量立刻降了25%。所以skew group的定义里,物理位置、时序要求、macro sink的模型差异都要考虑进去,不能一刀切。

2.5 buffer/inverter类型配置:全buffer树比inverter树更容易“胖”

不少项目为了省事,CTS cell list里只配了clock buffer,没有配clock inverter。这相当于让工具只能用一种武器打仗。反相器结构在CMOS工艺下延迟更小、面积更小,而且可以通过奇偶级配对来实现同相传输,插入的级数天然比buffer树少。如果库里明明有高质量的clock inverter却不用,工具只能靠堆buffer来实现同样的驱动效果,CDB Cell数量自然会上去。

另外,cell list里不同驱动强度的型号也要选全。如果库里有驱动能力偏弱的小尺寸clock buffer,工具可能会在长距离传输时连续掉多级小buffer,而不是换成一级大驱动buffer。如果你在CTS spec里只给了中等驱动强度的几种,工具的可选项就受限,也会导致级数偏多。

3. 先学会“体检”:用报告和spec量化你的时钟树是否超重

3.1 从report_clock_tree里提取关键指标

判断时钟树是否过度插入,不能只靠感觉。我一直习惯在CTS之后第一时间跑三份报告:

report_clock_tree -structure,看树的整体层级和buffer分布。 report_clock_timing -type summary,看skew、latency是否在预期范围。 report_ccopt_clock_trees,这是在CCopt流程里看每棵树的拓扑情况。

具体看的时候,我会重点关注几个字段。一个是Number of Clock Tree Cells,注意它和sink数量的比值;另一个是Tree Levels,也就是最大级数。如果你发现同domain内不同branch的level差异很大,比如平均值5级,某一条支路却有9级,那这条支路上大概率就是被插了多余的平衡buffer。

3.2 用skew/latency分布判断“值不值得”

一颗健康的时钟树,latency应该相对均匀地分布在root到各sink之间,skew处在target范围内。我们可以做一个简单对比:如果把target skew从20ps放宽到80ps,CDB Cell数量能明显下降,而最终的skew依然在80ps内,那说明之前那套约束确实是无谓地逼工具加班。

实践中,我建议CTS后把关键domain的skew和实际latency打点成直方图或者直接在Innovus GUI里看地理分布。如果发现大量sink集中在极窄的延迟窗口里,但代价是整棵树级别暴增,这就是过度平衡的典型表现。有些情况下,放宽skew目标后,setup/hold反而更容易收敛,原因就是OCV悲观变小了,这是我们在评估“值不值得”时的核心量化依据。

3.3 哪些报表现象背后就是插太多的信号

经历了几个项目之后,我总结出几个特别“扎眼”的报表征象,一旦出现就要怀疑CDB Cell过度插入。

  • average fanout长期低于1.5:说明大量buffer在驱动少量leaf,属于明显的延迟匹配单元。
  • 同一片区域出现多个功能完全相同、驱动强度完全相同的buffer,且它们的输入都连到同一上游net:这多半是“clone buffer”过度使用的结果。
  • clock net在report_ccopt_clock_trees里显示为“detour”,物理距离比曼哈顿距离长30%以上:说明工具在绕路做平衡。
  • 某层级cell数量异常高,而下一层级network几乎没有sink增长:典型的延迟补偿层。

这些信号不需要等signoff,CTS阶段看到就能提前介入,省掉后面一大堆返工成本。

4. 从约束到落地的修正清单:让Innovus按需插buffer

4.1 spec文件的正确姿势:skew/delay/fanout到底怎么给

如果你用的还是传统CTS spec流程,我会给出下面这套可以当作起点的操作方式。

在spec文件里,先删掉那些“看起来很美好”的极端值。比如SetTargetSkew从0.02改到0.06~0.08,单位ns;SetTargetDelay不要同时压early和late,而是给一个范围:early设为0.1左右、late设为0.5~0.8(具体数值取决于你的时钟周期和库延迟)。不要小看这个改变,它对CDB Cell数量的影响往往是立竿见影的。

max_fanout方面,我在上面提到过14~20是个常见起点。如果你发现工具因为transition问题继续插buffer,优先检查的就是NDR的线宽线距和层设置,而不是继续调小fanout。另外一个好习惯是在spec里显式指定buffer和inverter的cell list,让工具知道它有哪些“武器”可用,而不是从库的所有cell里瞎挑。

4.2 善用inverter配对和cell list优化

在SetClockTreeOptions里,我会同时指定buffer和inverter,并且尽量选择不同驱动强度的型号。用inverter树时有一个细节:如果设计里有大量clock gating cell,要注意时钟门控单元输入端需要的通常是同相信号。这时可以用inverter pair来处理:两级inverter形成一个buffer功能,但因为它每一级的延迟更小,总面积往往比直接插一个大buffer更优。

实际操作时,我会在spec里设置类似这样的配置:

SetClockTreeOptions -buffer {CLKBUFX16 CLKBUFX24 CLKBUFX32} -inverter {CLKINVX16 CLKINVX24 CLKINVX32}

这里的核心思想是:给工具足够多的选择,同时限定在合理的候选范围。如果你只给一种大驱动buffer,工具遇到小负载也必须用大炮打蚊子;如果你给的全是小驱动buffer,长距离传输又会堆级数。两者都会让CDB Cell白白变多。

4.3 用CCopt与ECO buffer tree做局部外科手术

光改spec重新跑CTS是最理想的情况,但很多时候项目已经跑到了CTS之后,重新做全部CTS的代价太大。这时候就要靠CCopt流程里的ECO能力,也就是很多人搜索时用的“innovus eco buffer tree”这个方向。

在Innovus里,eco_ccopt_clock_trees可以在不重新做全局CTS的前提下,对指定clock net做局部的buffer插入或移除。我会先运行一个带报告模式的命令,让工具评估哪些buffer是冗余的。比如它可能会告诉你:这条net上的两个buffer可以合并成一个高驱动buffer,不会违反transition/cap,却能省掉一级。

如果发现某些buffer只驱动了一个leaf而且没有延迟匹配作用,我会直接把buffer删掉,让leaf直接连到上游net。但这里必须留个心眼:删buffer后,下游net的cap和transition可能发生变化,必须重新跑一遍时钟树时序分析,确认skew没有恶化到不可接受的范围。

4.4 迭代验证闭环:不要一次改到“面目全非”

工具优化的一个现实是:你调一个参数,可能解决了一个问题,却在另一个角落埋了新雷。所以我强烈建议迭代验证,不要一口气把所有约束都改掉。我的习惯是一次只改一到两个变量,然后对比三个指标:CDB Cell总数、最大skew、setup/hold关键path的slack变化。如果指标变好,保留改动再试下一组;如果变差,回滚再换一个方向。

这里有一点经验值得分享:CTS约束调整后,不只要看CTS report,还要跑一小段post-CTS的时序优化,比如ccopt_design -cts -post_legalize或者简单place_opt。因为时钟树变短之后,data path上的cell可能有一些重新优化的空间,这时候你能看到最真实的时序收益。

5. 真实案例复盘:一次16nm项目的CTS减肥记录

5.1 问题出现时的数据和现象

那个项目是一个16nm的SoC子系统,包含CPU cluster和周边总线,总共6个时钟域。最初版本CTS完成后,主时钟域的报告让我相当头疼:sink数量大概2.4万,CDB Cell总数却到了6500多个,比例接近27%。树的级数平均8级,个别branch到了13级。最扎眼的是,报告里出现大量fanout为1或2的buffer,平均fanout只有1.2。

从GUI上看,芯片中部的congestion map已经亮起了黄色。几个macro附近的绕线通道被整排的clock buffer占掉,之后placement optimization阶段,数据路径单元被挤到更远的row上,绕线长度增加,setup slack跟着掉了30多ps。我当时判断,问题核心就是CTS阶段的过度插入,而不是后面优化能救回来的。

5.2 一步步排查最终锁定的三个元凶

我先拉出CTS spec逐项审查,重点排查顺序是skew group、target delay、max fanout、cell list。

第一个问题很快就暴露了:spec里SetTargetSkew 0.02,20ps的目标skew,对16nm工艺下CPU cluster这种大跨度场景来说太激进。为了对齐远端大扇出sink和近端sink,工具在近端插了大量delay buffer。

第二个问题是SetTargetDelay的early设成了0.05ns,late设成0.1ns。这等于逼工具把时钟信号“吹”到所有sink,而且要求非常短。工具只能通过堆buffer、增大驱动来硬撑。

第三个问题是SetClockTreeOptions里max_fanout被设置成8,而实际上这个库的CLKBUFX24驱动能力很强,驱动16个sink完全没问题。再加上cell list里只有buffer没有inverter,工具就只能用低效的“多级小buffer”打法。

5.3 修改后的数值与收敛结果对比

针对三个元凶,我做了如下修改:

SetTargetSkew从0.02改为0.08ns,目标skew放宽到80ps。 SetTargetDelay改为early 0.15、late 0.5。 max_fanout从8调整到16。 SetClockTreeOptions里同时加入inverter cell,并且选用了驱动能力覆盖中到大范围的型号。

重新跑完CTS后的数据,CDB Cell总数从6500多降到3800多,降幅超过40%。最大skew保持在了75ps以内,完全满足设计目标。更关键的是,因为树的级数变少、insertion delay缩短,post-CTS阶段的setup slack比之前那版好了20ps以上,OCV悲观也因为路径变短而明显减弱。

5.4 事后清理直接挤出的面积/功耗余量

这版修改不仅让时钟树“瘦身”,还给整个floorplan带来了喘息空间。congestion map大范围转绿,绕线阶段明显顺畅。根据后来的估算,光时钟树部分减少的cell面积大概占了模块总面积的1.5%,对一个大芯片来说可能不算夸张,但对这个子模块来说,多出来的面积直接让后续的修复buffer有了落点。

功耗收益更明显。时钟树的动态功耗与CDB Cell数量正相关,减少了近半的clock buffer,意味着时钟网络功耗下降了接近30%。在低功耗设计里,这个数字可以写进报告作为优化成果了。

6. 经验与教训:这些我踩过的坑你最好别踩

6.1 不要迷信“先跑一版无约束”的原始数据,但一定要看自然延迟

有些工程师会说“CTS之前先别设约束,跑一版看看”。这个方法本身没问题,问题在于很多人跑完自然版本后,看到skew很大就慌了,然后开始疯狂收紧约束,反而走上了过度插入的老路。我的建议是:自然版本的价值不是看skew能不能接受,而是看每棵树的自然latency和级数。基于这个baseline去设定一个有挑战但不离谱的target delay和skew,这才是正确用法。

6.2 Clock Gating Cell周围的buffer需要单独关注

时钟门控cell(ICG)是CTS中CDB Cell过度插入的重灾区。因为ICG通常有enable端和clock端,工具在分析时钟树时会把ICG的clock pin当作一个特殊的sink point,为了修复ICG输出时钟的transition,有时会在ICG后面再插一串buffer。如果ICG大量聚集,这类局部buffer会迅速膨胀。遇到这种情况,我会给ICG的输出net单独设置一组更合理的transition和fanout限制,而不是用全局设置去约束它。

6.3 每次改完spec,提交给版本管理,写上“为什么改”

这条听起来像流程规定,但其实是我被现实教育出来的经验。有一回我改了target skew,过了两天另一个同事说“CTS怎么又胖了”,排查了半天才发现是某次merge时把我的改动覆盖了。从那之后,每轮CTS spec都提交版本库,commit message里写清楚改了什么、为什么改、期望达到什么效果。这样做不仅能防覆盖,也能让其他工程师review你的优化逻辑,减少团队里的无效争论。

6.4 如果真的只想快速验证一个方案,用ECO而不是全量重跑

项目后期经常会遇到一种情况:时序还差一口气,怀疑是某条clock path上多了一级uffer在拖后腿。这时候千万别冲动全量重跑CTS,影响面太大、风险太大。直接用eco_ccopt_clock_trees的report模式分析那条path,看看能不能安全地移除或合并buffer。一次典型的局部清理只影响几颗cell的级联关系,做完后跑一下局部时钟树的skew和transition检查,如果满足则继续,不满足就回滚。这种外科手术式的操作,我在多个项目后期都用过,效率非常高。

最后再分享一个我自己的小习惯:每次CTS完成,我都会强制自己看一眼“CDB Cell数量 / sink数量”这个比值。如果超过15%,我会默认不是时序有问题,而是约束或配置有问题,先停下来检查再继续。这个数字让我躲过了好几次“看起来能收敛、其实在给后端埋雷”的时钟树版本。CTS里的CDB Cell数量从来不是越多越好,工具插的每一个buffer背后都是你的约束逻辑在起作用。搞清楚它为什么插,你才能真正掌控时钟树的质量。

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

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

立即咨询