
数字后端全流程里有一个阶段最容易被新手低估时钟树综合CTS。刚入行的时候我也觉得place都做完了检查一下congestion和DRC然后跑一趟CTS最后route收线好像CTS只是流程中间一个按钮而已。后来真正吃过几次亏才明白CTS做得好不好直接决定芯片最终能跑多高频率、时序能不能收敛、功耗会不会超标。这个系列前面几篇聊了数字后端的基本概念、布局和布线的入门思路这一篇把CTS里最核心的主角——时钟信号单独拎出来讲透。这篇笔记适合两类人看。一类是刚接触数字IC后端的学生或转行工程师想弄明白CTS到底在做什么、为什么要有这一大步另一类是已经用工具跑过几版流程但看报告时只知道skew越小越好、latency越小越好还想更深入理解背后逻辑的朋友。我会结合Innovus工具的实际操作经验把时钟信号的各种参数、CTS的输入与评估、常见问题排查讲清楚。1. 时钟树综合到底在解决什么问题1.1 先搞清楚CTS在数字后端流程中的位置数字后端完整流程大体是这样逻辑综合logic synthesis把RTL变成门级网表DFT插入扫描链然后布局placement把标准单元放到版图里接着就是时钟树综合再往后是布线routing最后做签核验证。CTS被放在placement之后、routing之前这个顺序是有道理的。Placement结束之后工具才知道每一个触发器flip-flop具体被摆在了版图的哪个坐标上。时钟信号要从时钟源比如PLL的输出或者芯片外部输入的时钟端口送到成千上万个触发器的时钟引脚它们的物理位置千差万别有的在左上角有的在右下角距离可能差出好几毫米。假如不做任何处理直接让时钟源一根信号线拉过去那距离近的触发器先收到时钟距离远的触发器后收到时钟这个时间差会直接反映在时序检查上setup和hold全都乱套。所以CTS的核心任务就是通过插入一串时钟缓冲器clock buffer或反相器clock inverter搭出一棵“时钟树”让时钟信号从源点出发经过几乎相等的路径延迟同时到达所有时序单元的时钟端。工具还会考虑时钟树上的低功耗、抗干扰、布线资源这些约束。做完CTS之后再走全局布线和详细布线时钟网络的物理实现才算完整。1.2 时钟信号为什么这么特殊常听人说时钟是芯片的心跳这句话一点不夸张。数据信号往往是点到点的一个单元把数据送到另一个单元路径就结束了。时钟信号不一样它几乎是全局性的要同时到达设计里成千上万个触发器扇出数量级动辄几万甚至十几万而且这是正常现象。另外时钟是持续翻转的信号。普通数据信号在大多数时间里可能保持不变时钟每次工作都在以目标频率反复跳变功耗贡献非常可观。先进工艺下时钟网络的功耗有时候能占到芯片总功耗的30%甚至更高这也是为什么低功耗设计里会有各种门控时钟clock gating技术。还有一点容易被忽略时钟信号的质量会直接转化为时序裕量。时钟边沿要是变得很缓也就是slew变差会让触发器产生不确定的采样时刻时钟抖动jitter和偏斜skew更是直接压缩时序预算。这个逻辑链条是时钟之前的偏差越大留给数据路径的建立时间、保持时间裕量就越少芯片能跑的最高频率就越低。1.3 CTS的目标不是为了建一棵好看的树而是让时序收敛很多初学者走到CTS这一步习惯性地盯着skew数字看觉得只要skew小CTS就成功。这种理解有偏差。CTS的最终目标是让整个设计的时序能够收敛尤其是建立时间setup和保持时间hold都要有足够的余量同时尽量减少功耗和面积开销。想要所有触发器的时钟到达时间完全一致理论上是可能的但代价极大。每加一级buffer时钟网络延迟会变大功耗会上去布线资源被占用。更聪明的做法是允许时钟有意识地偏向某一边这就是我们常说的useful skew。比如数据从触发器A传到触发器B如果时钟先到达B、后到达A相当于给这条数据路径多挤出了一点时间对修复setup违例很有帮助。反过来如果想让hold更容易满足可以让时钟晚点到B。这种让时钟“偏心”的做法在实际项目里天天都在用。所以CTS的目标可以概括成一句话在满足所有同步时序单元时序要求的前提下把时钟网络实现为功耗、面积、延迟都可控的物理网络。后面所有关于时钟信号的讨论都是围绕这句话展开的。2. 时钟信号的参数每一个都要吃透2.1 周期与占空比最基础也最容易被忽略时钟周期就是时钟信号重复一次的时间决定芯片的工作频率。比如一个10ns周期的时钟对应最高频率100MHz。这个参数通常在SDC约束里定义工具和综合脚本都围绕着它运转。占空比指高电平时间占整个周期的比例。50%占空比意味着高电平和低电平各占一半时间这是最常见的假设。但实际芯片里也不是所有时钟都严格50%有些内部生成时钟或者DDR相关时钟会有特殊要求。占空比会影响时序分析时对高电平有效逻辑和低电平有效逻辑的约束定义错了会在后面引起莫名其妙的violation。在SDC里定义时钟的时候周期和波形是用create_clock命令一起指定的。比如create_clock -name clk -period 10 [get_ports clk]这个写法隐含了50%占空比也就是上升沿在0ns、下降沿在5ns。如果需要非50%占空比可以显式写create_clock -name clk -period 10 -waveform {0 6} [get_ports clk]我建议无论是不是50%占空比都养成把waveform写出来的习惯。这样后面查看约束时一眼就能看出定义意图也方便做时钟树分析时快速判断。2.2 Clock Uncertainty给时序分析留出来的余量时钟不确定性clock uncertainty这个概念很多新手一上来会被搞晕因为它跟前面说的skew和jitter经常混在一起。我尽量用大白话解释。Jitter是时钟边沿在时间轴上的实际抖动来源包括PLL噪声、电源波动、串扰等是真实存在的物理现象。Skew是时钟到达不同触发器的时刻差同样真实存在。Uncertainty是时序分析工具在计算时序时对这两种现象以及其他不可预测因素的一种悲观化建模它直接加在时序检查的预算里。在静态时序分析中setup检查会要求数据比时钟采样边沿提前uncertainty 库器件建立时间到达hold检查会要求数据在时钟采样边沿之后再多保持uncertainty 库器件保持时间。uncertainty设得越大时序收敛越困难但越贴近真实设得太小芯片流片后可能因为环境变化而跑不到目标频率。SDC里设置uncertainty的常用写法是set_clock_uncertainty -setup 0.1 [get_clocks clk] set_clock_uncertainty -hold 0.05 [get_clocks clk]到了CTS阶段工具会通过控制skew来直接优化一部分uncertainty对应的余量。换句话说如果uncertainty把时钟预算吃得太多CTS怎么努力都可能有gap。所以CTS前花几分钟仔细核对uncertainty定义非常值得。2.3 Latency与Insertion DelayCTS结果里最直观的指标时钟延迟clock latency分两段理解source latency和network latency。Source latency指时钟从芯片外部输入端口或者PLL输出端到达时钟树根节点clock root之前的那一段延迟network latency指时钟从根节点出发经过时钟树上的各级buffer最终到达某个触发器时钟端的那一段延迟。CTS工具能直接控制的主要是network latencysource latency通常由约束指定SDC里用set_clock_latency描述。Insertion delay这个概念在很多报告里跟network latency混用指的就是时钟从根节点到某个sink的实际传播延迟。一棵时钟树的所有sink各自的insertion delay如果差得很大那就是skew变大如果整体insertion delay太大意味着时钟树级数过多延迟大、功耗大、抗干扰能力差。在Innovus里跑完CTS之后报告里会出现类似这样的信息Clock Tree Report Number of clock tree levels: 18 Total clock network delay: 1.42 ns Clock skew: 0.06 ns Clock uncertainty: 0.10 ns看到levels和total delay的时候别只盯着skew。树级数太多说明buffer链过长噪声和功耗都可能有隐患树级数太少则可能意味着某些sink的skew太大需要结合具体设计来权衡。3. CTS的输入准备时钟定义和CTS约束3.1 时钟定义不完整后面全是白费做CTS之前第一步永远是回头检查SDC里的时钟定义。你会发现很多CTS跑得乱七八糟的case根源不是工具参数设得不好而是时钟定义本身就有漏洞。常见问题包括只定义了主时钟忘了定义分频时钟、门控时钟、多路选择出来的时钟有些复用时钟或者异步时钟没有单独建时钟域有些create_generated_clock的源点找错了导致时钟树从根上就歪了。这些都会让工具在CTS阶段不知道时钟树要覆盖哪些sink甚至把本不应该穿过的逻辑当成时钟网络的一部分。我习惯在跑CTS前执行一次report_clock把所有时钟和generated clock的树根、目标点、类型全部过一遍。重点关注有没有timing loop的警告、有没有未定义的时钟引脚、有没有挂在组合逻辑上的时钟。CTS前花十分钟做这个检查能省下后面几天debug的功夫。3.2 从SDC到CTS spec工具在转换时到底做了什么Innovus里做CTS一般先根据SDC约束和设计信息生成CTS spec文件命令是create_ccopt_clock_tree_spec -file cts_spec.tcl生成的spec文件会明确指定每一棵时钟树的root pin、目标sink、需要覆盖的时钟域、时钟树布线使用的金属层、最大扇出、目标skew等信息。它会从SDC里解析出时钟根节点然后遍历所有被该时钟驱动的时序单元把它们列为sink。这里有个关键点spec文件生成之后一定要打开看一遍。工具自动生成的默认配置未必最优比如有些时钟域希望手动指定不同的目标skew有些时钟信号不希望穿过特定逻辑层有些clock gating cell需要被显式标记为时钟树节点。这些都需要你在spec里手动微调。调整完再source这个spec文件工具才会按照你的约束来建树source cts_spec.tcl有一个常见的坑就是当你修改了SDC里的时钟定义之后没有重新生成CTS spec导致工具还是按照旧的定义来建树。修改约束之后CTS spec必须重新create这个流程一定要养成习惯。3.3 时钟单元选型为什么多用CKBD和ICG时钟树的构成单元不是随便从标准单元库里抓一个buffer就能用的。专用时钟单元和普通数据缓冲单元在库里的电气特性有明显差别。时钟缓冲单元比如CKBD系列通常有更大的驱动能力对时钟边沿的整形能力更好延迟对负载的变化更平稳而且在典型PVT下延迟波动更小。时钟反相器单元CLKN/CLKP也经常用于时钟树因为它们功耗更低、延迟匹配更好很多后端工程师喜欢用反相器对来做时钟树因为它的翻转特性更对称。门控时钟单元ICGIntegrated Clock Gating Cell是现代低功耗设计里不可或缺的角色。它的作用是当寄存器组不需要翻转时直接切断时钟信号减少动态功耗。在CTS中ICG会被当作时钟树上的一个节点工具需要权衡ICG的位置确保门控后的时钟skew满足要求。实际项目里的经验是时钟树的非叶子节点尽量多用时钟专用单元不要随便用普通逻辑buffer能够合并的时钟门控尽量合并既能省功耗也能减轻后面时钟树上的负载。4. 实操用Innovus做一轮完整的时钟树综合4.1 跑CTS之前的例行检查规范的项目流程里CTS前有一个checklist我会逐步过一遍。首先检查Placement是否满足基本要求。Placement阶段的高扇出网络比如复位、使能有没有预先处理如果复位网络还是一根裸线直接驱动几万个寄存器CTS工具会很难受。其次用check_timing确认所有时序路径的起点和终点都有合法的时钟定义没有悬空的时钟引脚。然后是物理层面的检查。看floorplan里有没有blockage挡住了时钟树正常走线的通道看macro周围有没有拥挤区域容易造成时钟线绕远看有没有预布线堆叠在主要时钟通道上。最后检查库环境时钟单元在目标corner下的delay table范围是否覆盖了设计中的负载有没有某些极端电压下时钟单元根本不工作。所有这些检查都通过了再进入正式CTS流程才是一个可以复现、稳定的流程。4.2 从创建CTS spec到完成时钟树构建Innovus的CTS命令入口跟传统流程有一点区别现在的做法一般是这样create_ccopt_clock_tree_spec -file cts_spec.tcl source cts_spec.tcl clock_opt_designcreate_ccopt_clock_tree_spec会根据当前memory里的网表、约束和floorplan信息生成初始spec文件。clock_opt_design是真正执行时钟树综合的步骤它会自动插buffer、长时钟线、检测并修复时钟树上的一些基础DRC。跑完之后立刻看这几个报告report_clock_timing -type skew report_ccopt_timingreport_clock_timing -type skew会给出各时钟域内的skew分布哪些sink偏离平均延迟最远。report_ccopt_timing则会把每条时钟路径的延迟明细拉出来包括经过哪几级单元、每级贡献多少延迟。这时候你会一眼看到哪一段时钟树上延迟贡献最大是负载太重还是布线绕远。4.3 时钟树优化和修正的手段第一版CTS结果很少会完美达标通常要迭代几轮。常见问题可能是skew超标、某个时钟域上的插入延迟不一致、时钟树级数过多、或者某些时钟树的过渡时间slew在长距离上退化。如果skew超标优先检查是不是某个sink离其他sink特别远或者中间遇到了macro和blockage导致绕线。工具一般会在CTS阶段自动插入延迟匹配buffer来补偿但如果特定区域太拥挤工具可能找不到合适的插入位置。这时候需要回placement阶段或者调整该区域的布局。Innovus里也可以手动干预时钟树比如用add_clock_tree_balancing_delay给特定路径增加延迟或者手动删除工具插入的多余buffer。这些操作要非常谨慎因为手动改动很容易破坏其他时钟域的一致性每次改完都要重新分析。如果是时钟树级数太多、整体延迟太大我会先查是不是目标slew设得过紧导致工具不得不用更多级buffer来逐步整形。适当放松slew目标可以显著减少级数功耗也能降下来。工程实践就是在不同指标间找到平衡点。5. 时钟树质量评估看setup、看hold、还要看功耗5.1 Skew为什么不是越小越好前面说过skew是时钟到达时刻的差看到报告里skew是0.01ns就觉得完美这个习惯要改。更小的skew意味着工具在时钟树上加了更多的buffer去强行平衡路径这会直接导致两个问题整体插入延迟变大、功耗和面积上升。插入延迟变大对时序收敛并不总是坏事但对hold来说却有影响。另外时钟树buffer加多了会占用布线资源对后端工程来说可能引发局部拥塞反而更难收时序。这就是为什么后端工程师之间有一句经验skew是修出来的但不是越小越好而是恰好能满足时序要求即可。更有意思的是useful skew也就是故意让某个时钟域内的时钟边沿错开一点。后端工具在修复setup违例时会尝试让数据接收端的时钟来得更晚一些相当于偷了一点周期时间修复hold违例时会让接收端时钟来得更早一些。这种“主动偏心”在先进工艺项目里几乎成了标配优化手段。5.2 常见问题排查skew大、slew差、时钟树上DRC多我整理了CTS阶段最常碰到的几个问题用表格列出症状、原因和方向方便大家对照排查。症状可能原因处理方向skew超目标部分sink距离过远检查floorplan是否被blockage挡路手动约束平衡skew超目标时钟树根节点位置偏颇调整root pin或时钟源的位置约束时钟slew退化目标slew设得过紧buffer级数不足适当放宽slew目标或换大驱动buffer时钟slew退化长距离时钟线上负载过重插入中继buffer或改善时钟线布线层插入延迟过大时钟树级数过多检查目标slew和每级最大扇出是否合理时钟树功耗过高buffer数量太多精简树级数检查clock gating覆盖率时钟树上DRC违例时钟线位于拥塞区域调整CTS布线层或回改floorplan某条hold路径异常该路径所在时钟域skew过大查看是否属于不同时钟域检查useful skew排查手段方面Innovus的report_clock_timing -type skew -detail可以列出每个时钟域里偏差最大的几条路径。report_ccopt_timing -from X -to Y能单看某条时钟树路径的级联延迟。用GUI界面追踪时钟树结构也很方便可以一眼看出buffer在版图上的实际分布。5.3 CTS之后的时序收尾不可跳过CTS做完时钟网络具备了真实的物理延迟这时候时序分析的结果才真正开始有意义。因为在CTS前所有时钟延迟都是估计的、统一的而CTS后各个触发器的时钟到达时刻各不相同原先一些隐形的hold violition会在这时爆发出来。很多项目流程会在CTS之后马上做一轮快速时序分析专项检查hold。为什么专门看hold因为Data path上的setup问题在CTS前就能大致发现而hold路径的裕量极度依赖时钟skew。CTS后时钟到达时刻真实了hold violition才现形。所以标准流程里hold fixing通常放在CTS后、routing前有的也会在routing后做。我看报告的习惯是先看setup最差路径在哪段数据通路上再看hold最差路径是否扎堆在同一个时钟域然后用report_clock_timing -type skew确认这些违例路径上的时钟到达时刻是否与违例方向一致。把时钟树时序和数据路径时序放在一起看比单看任何一份报告都更能定位问题。最后再分享一点个人经验。做CTS越久越觉得不要抱着“一把跑过”的心态去处理时钟树综合。CTS是一个需要反复看报告、调spec、观察物理位置的迭代过程。时钟信号在概念上是一串脉冲但在物理实现里它是一群buffer、一段段金属线、一层层树结构它的每一次优化都跟floorplan、placement、功耗、信号完整性纠缠在一起。耐心把它拆开看芯片后端的大门才算真正推开了一多半。