先交代一个背景我去年做过一个MCU级别的SoC项目主时钟100MHz里面为了低速外设加了一个二分频器产生50MHz时钟。功能本身很简单但后端CTS做完以后我花了一整个星期在跟时钟树较劲——光时钟树就吃掉了将近15%的单元面积主时钟域到分频时钟域之间还有一批hold违例怎么修都压不干净。后来排查来排查去问题的根源就出在时钟树综合策略上工具默认把主时钟的所有sink和分频后时钟的所有sink全都拉齐到同一个延迟水平两边互相拖累面积、功耗、时序一起崩。这篇文章就把我从分频电路原理到Skew Group落地优化的完整思路整理出来包括为什么要重新审视分频时钟、Innovus里Skew Group到底怎么划分和配置、以及实际项目中反复踩过的几个坑。如果你正在做数字后端或者在设计里遇到分频电路导致CTS结果不理想的情况这份经验应该能帮你少走不少弯路。1. 分频电路为什么会成为CTS的“特殊分子”1.1 二分频电路的本质一个DFF的双重身份先回到最基础的东西。所谓二分频电路最典型的实现就是用一个D触发器把Q反接到D端时钟从CK输入。由于D端在每个时钟上升沿采到的是上一次Q值的反相Q端输出频率就变成输入时钟的一半。也就是输入10个脉冲输出5个脉冲占空比在理想情况下是50%。这个电路看起来简单但在时钟树综合的视角里它的身份非常特殊。一方面分频器本身是一级时序单元它有CK端需要被时钟树驱动所以它是一个sink。另一方面它的Q端输出了一个新的时钟信号驱动下游一大片寄存器所以它又是分频后时钟树的源点。一个单元同时扮演“sink”和“source”两个角色这种情况在整个时钟树设计里都算得上少数派。用一个生活化的比喻分频器就像一座城市里的地铁换乘站。从主时钟方向来的“人流”在换乘站停下经过一个固定时间转换变成另一条线路继续向前。CTS工具要做的不只是把换乘站门口的人流整理整齐还要保证换乘后新线路上的各站到站时间一致。这两个任务听起来相关但如果用同一套标准去管理所有站点就会出问题。1.2 工具默认行为的误区全局拉齐不是免费午餐在默认配置下Innovus的时钟树综合会把能识别到的时钟sink尽量做平衡。注意这里的“尽量”是一个很微妙的状态如果不做任何Skew Group约束工具倾向于把所有sink的插入延迟insertion delay拉到同一个水平包括主时钟域寄存器、分频器CK端、分频后时钟域的寄存器。这种做法在工具看来是“最安全”的因为所有时钟沿到达时间一致时序分析时skew自然很小。但代价非常明显。第一是buffer数量暴增。全芯片如果只有100MHz主时钟域也就算了如果还带一个分频后时钟域所有时钟域的sink被绑在一起做平衡就意味着要给分频后时钟树额外插入大量buffer来弥补分频器CK延迟加上clk-to-q延迟造成的相位差让它追上主时钟树的延迟。第二是时钟树的级数变深插入延迟变大。延迟越大对片上偏差OCV越敏感signoff阶段的悲观度越高。第三则是功耗时钟树通常占总动态功耗的30%以上盲目插入buffer等于在给功耗预算挖坑。那是不是说默认行为就完全错了也不是。在没有分频电路、所有寄存器都在同一时钟域时默认的全局平衡确实能带来很好的时序结果。问题恰恰出现在“不同频率、不同相位的时钟”同时存在时工具并不知道哪些sink之间的skew真正有时序意义它只会做数学上的拉齐。1.3 跨时钟域隐藏路径真正需要对齐的对象要理解Skew Group怎么划分必须先搞清楚分频场景下哪些时钟沿之间的skew会影响时序。我把分频器相关的数据路径分成三类来看这样思路会更清晰。第一类是主时钟域内部的路径也就是由主时钟直接驱动的寄存器A到寄存器B两个都属于主时钟域。这类路径需要主时钟树内部的sink相互平衡。第二类是分频后时钟域内部的路径由分频器Q端驱动的寄存器C到寄存器D都属于分频后时钟域。这类路径需要分频后时钟树内部的sink相互平衡。第三类是跨时钟域路径比如主时钟域的寄存器A输出数据被分频后时钟域的寄存器C采样。这时launch沿来自主时钟capture沿来自分频后时钟。真正影响这类路径时序的是主时钟在A寄存器CK端的到达时刻与分频后时钟在C寄存器CK端的到达时刻之间的差值也就是跨时钟域的skew。由此可以得到一个关键结论只有当第三类路径真实存在且需要做时序收敛时才需要考虑把主时钟sink和分频后sink放进同一个Skew Group。如果两个时钟域之间没有任何数据交互或者所有交互都已经被set_false_path、set_clock_groups等约束隔离了那么两个时钟域的时钟树完全可以各自独立平衡互相之间保留任意相位差都无所谓。很多工程师在这里会犯一个直觉性错误觉得既然是同一个源衍生出来的时钟天然就该在物理上对齐。实际上时序工具关心的是“数据能不能在每个时钟周期内稳定采集”而不是“所有时钟沿是不是同时到达”。只要跨域路径不存在或者路径的时序余量满足要求skew大一点并没有关系。2. Skew Group的核心机制把“全局平衡”拆成“局部对齐”2.1 什么是Skew GroupSkew Group简单说就是一组在时钟树综合时被要求相互平衡到相近延迟的sink集合。同一Skew Group内部的sink之间工具会尽量减小skew不同Skew Group之间工具不强制做平衡允许存在较大的skew。我在项目里最喜欢把Skew Group的机制比作“分批排队”。假设一个体育馆要同时开放多个入口如果所有观众都必须从同一个入口进场那队伍会排到几条街外效率极低。但如果按区域划分成几个入口每个入口内部各自排队整体通行速度反而快得多。Skew Group就相当于给时钟树划分了不同的“入口”让工具知道哪些sink必须排在一起、哪些不用。在Innovus的时序引擎里Skew Group会直接影响时钟树综合阶段的平衡算法。工具在插buffer和调整路径时优先保证组内skew收敛而不是把计算资源和buffer资源浪费在无关sink之间。2.2 一个具体数字例子分组前后的buffer数量和延迟对比我拿一个真实项目中简化后的数据来说明分组前后的差异。假设设计里有主时钟域寄存器1000个分频器1个分频后时钟域寄存器500个。主时钟100MHz分频后时钟50MHz。所有寄存器之间有正常的业务逻辑路径跨时钟域路径只占总路径的5%且已经通过SDC约束隔离。先看默认不分组的情况。Innovus把所有sink拉齐后我的实测报告里主时钟树的插入延迟约1.2ns分频后时钟树为了追上主时钟树的延迟需要额外插入约12级buffer时钟树总buffer面积占全芯片面积约12%。而且因为分频后时钟树路径变深不考虑时钟树时的setup余量有0.3ns在考虑OCV之后反而变成负的这是非常典型的“过度平衡导致过度悲观”的例子。再看分组后的情况。我把主时钟域寄存器加分频器CK端划成一个Skew Group分频后时钟域寄存器单独划成一个Skew Group两个组之间不做平衡。调整后主时钟树插入延迟降到0.8ns分频后时钟树插入延迟0.6ns时钟树总buffer面积降到6%左右。由于两条树的物理深度都变浅了OCV悲观度也随之下降最终时序收敛起来反而更容易。这个例子不是说分组一定能让所有指标变好但它很直观地说明了一个道理时钟树平衡是有成本的精准地平衡“需要平衡的部分”才是CTS优化的核心思路。2.3 分频场景下Skew Group划分的通用原则经过几个项目的验证我总结出四条在分频电路设计中划分Skew Group的通用原则可以给新人直接当checklist用。第一同频同相、有实际数据交互的sink必须放同一组。比如所有受主时钟直接驱动的寄存器sink放在一组所有受分频后时钟直接驱动的寄存器sink放在另一组。第二分频器CK端应当与主时钟域sink放在同一组。因为分频器D端通常直接接QN它自身的时序由主时钟沿决定。如果分频器CK和主时钟域寄存器之间的skew失控分频后时钟的相位就会相对主时钟发生偏移可能导致后续所有跨域路径的建立时间余量被吃掉。第三分频后的sink单独成组。分频后时钟域内部的路径只要自身组内平衡好整体时序就能收敛。分频器Q端会有固定clk-to-q延迟这个延迟可以通过合理的时钟树插入延迟来吸收不需要和主时钟域强行对齐。第四条是动态判断如果跨时钟域路径没有被约束隔离且确实存在主时钟域到分频后时钟域的时序路径就需要额外分析这些路径的时序余量。发现余量不足时再考虑把相应的sink合并到同一个Skew Group里。注意这里说的是“考虑”因为合并带会增加时钟树负载和延迟成本必须和跨域路径违例的修复代价做一个权衡。3. Innovus中的完整实操流程从SDC约束到CTS结果检查3.1 第一步定义好Generated Clock很多CTS问题根子上从SDC阶段就埋下了。如果分频器Q端的时钟没有正确定义为generated clock工具就无法理解分频后时钟和主时钟的相位关系后续所有Skew Group操作都失去依据。最简单的二分频定义方式是这样# 主时钟定义 create_clock -name clk_100m -period 10.0 [get_ports clk_100m] # 分频器Q端定义分频后时钟 create_generated_clock -name clk_50m \ -source [get_pins u_divider/CK] \ -divide_by 2 \ [get_pins u_divider/Q]以u_divider/CK作为源点-divide_by 2告诉工具这是二分频关系。这里有一个很容易被忽略的细节源点选择的是分频器CK端而不是主时钟输入端口。这样工具才能正确计算主时钟从端口到分频器CK端的延迟并把这段延迟纳入分频后时钟的相位计算中。如果你在source里写的是[get_ports clk_100m]工具会丢失分频器CK到端口之间的时钟树延迟信息CTS做出来以后跨域路径的时序分析就是错的。另外对于三分频、四分频这类更复杂的情况-edges或-edge_shift的描述会比-divide_by更精确。不过二分频用-divide_by 2已经足够。3.2 第二步分析数据路径确定分组方案不要一上来就写命令。先用Innovus的时序报告工具把设计里的时钟和路径关系摸清楚这一步决定了Skew Group划分是否合理。我在实际操作里会先做这几件事# 检查所有已定义的时钟 report_clocks # 看各时钟域之间的路径数量 report_clock_interaction # 抽取具体跨域路径做详细分析 report_timing -from [all_registers -clock clk_100m] \ -to [all_registers -clock clk_50m] \ -path_type full_clock_expandedreport_clock_interaction非常关键它会列出任意两个时钟之间是否存在逻辑路径。看到某个时钟对之间有大量路径且没有被约束隔离就把这个时钟对记为重点关注对象。反之如果两个时钟之间只有可怜的几条路径而且功能上本来就可以放松那么在SDC里直接用set_clock_groups -asynchronous隔开再从Skew Group方案里排除掉是更省面积的做法。确认完路径关系后再回到物理层面看分频器的位置。分频器在物理上靠近哪一簇寄存器也会影响Skew Group内部平衡难度。如果分频器和主时钟域寄存器分布太分散那即使放在同一组CTS为了平衡它们之间的延迟也可能插很多buffer这时候要结合布局约束考虑甚至预先对分频器做区域约束。3.3 第三步创建Skew GroupInnovus里创建Skew Group的命令在不同版本间略有差异但整体思路一致。下面是我在CCopt流程里常用的形式# 把主时钟域寄存器sink和分频器CK端放同一组 create_balance_group -name bg_main_100m \ -sinks [get_sinks -clock clk_100m] \ -balance inter # 把分频后时钟域寄存器sink单独放一组 create_balance_group -name bg_div_50m \ -sinks [get_sinks -clock clk_50m] \ -balance inter注意两点。第一get_sinks的过滤条件里分频器CK端本身属于clk_100m时钟所以第一条命令已经把分频器包含进去了。第二条命令单独圈定分频后时钟的sink。第二-balance inter表示组内所有sink之间两两平衡这是最常见的选择。如果你的组里既有需要严格平衡的区域又有可以稍微放松的区域可以用-balance intra或者进一步细分更小的组。有些版本可能只支持set_ccopt_property balance_groups这类属性式写法或者要求在使用前先声明ccopt模式。真实操作时先敲help create_balance_group看一下当前版本的具体选项。这里我想强调核心要素永远是三个维度选哪些sink、组内怎么平衡、组间要不要平衡。3.4 第四步CTS运行与报告解读Skew Group配置完成后运行CTS的命令和普通流程没有区别clock_opt -from clock_opt_cts关键是跑完之后怎么看结果。我会按下面这个顺序逐项检查# 查看时钟树综合报告 report_ccopt_clock_trees # 查看Skew Group报告 report_balance_groups # 检查时钟树DRC report_clock_tree -detailreport_balance_groups是验证分组是否符合预期的第一道关口。确认每个组里包含的sink数量和自己预期的差不多如果发现分频器没有进到主时钟组或者分频后时钟域的sink被错误并到别的组里立即回头检查命令里的时钟过滤条件。接下来看report_ccopt_clock_trees中每个时钟的插入延迟和组内最大skew。你需要关注的不只是skew数值本身还要看skew在总插入延迟中的占比。比如分频后时钟树插入延迟0.6ns组内skew 0.15ns虽然绝对值看起来不大但对50MHz时钟来说相对比率已经不低。如果后续时序收敛困难优先处理占比最高的那一组。最后是时钟树DRC重点看max transition和max capacitance有没有违例。分频后时钟树的负载通常比较集中容易出现tran违例如果报告里有优先在TCL脚本里对分频器Q端单独设置set_clock_tree_options -max_transition来约束而不是对整个时钟域一刀切。3.5 第五步跨时钟域路径的时序约束配合Skew Group是物理层的手段SDC约束是逻辑层的手段两者不是二选一而是配合使用。在分频电路场景下我建议按这样的优先级来处理先审核所有跨时钟域路径的功能属性。确实不同步、不需要时序收敛的路径用set_clock_groups -asynchronous或set_false_path明确隔离。这里要特别注意隔离之后对应的Skew Group就可以放心拆开。另一部分需要进行时序检查的跨域路径先保留Skew Group的关联然后根据实际情况决定是否增加set_multicycle_path。举个例子主时钟100MHz分频后时钟50MHz从100MHz域发射数据到50MHz域接收如果数据在100MHz域只变化一次且能保持两个周期那么可以设置2周期的multicycleset_multicycle_path 2 -setup -from [get_clocks clk_100m] -to [get_clocks clk_50m] set_multicycle_path 1 -hold -from [get_clocks clk_100m] -to [get_clocks clk_50m]这样做的作用是在时序约束层面放宽跨域路径的采样窗口间接降低对物理skew的敏感度。我在实际项目中通常的做法是先用约束把能放松的路径都放松然后把剩余的跨域路径数量控制在极少范围这时候再判断这些残留路径是否需要主时钟sink和分频后sink进同一个Skew Group。大多数情况下经过约束放松后真正需要物理对齐的跨域路径所剩无几Skew Group划分的负担就小很多。4. 实测案例复盘一个ADC采样链路的时钟平衡优化4.1 设计背景和原始约束这个项目是一个感测器SoC里的ADC数字控制子模块。主时钟来自PLL频率96MHz主时钟域包含数字控制逻辑和配置寄存器一共约800个触发器。为了给模拟前端提供采样时钟设计里用了一个二分频器把96MHz变成48MHz分频后时钟域主要负责采样数据缓存和输出接口约300个触发器。项目最初接手时SDC里只定义了主时钟分频器的Q端没有单独定义generated clock。也就是说工具把分频器Q输出当作普通数据信号处理根本不认为它是一个时钟。这种状态下的CTS结果自然一塌糊涂分频器Q端到下游寄存器的路径延迟巨大而且由于工具不知道这里是时钟时钟树综合根本没对它做任何平衡。第一步修改SDC按照3.1节的命令补上generated clock定义然后重新跑CTS。这次的时钟树报告能看了但结果依然不理想主时钟树插入延迟0.9ns分频后时钟树插入延迟1.1ns全局skew约0.25ns时钟树buffer面积占模块总面积的11%。更要命的是分频后时钟域内部出现了一批hold违例每条路径都要插好几个delay buffer面积进一步膨胀。4.2 第一次CTS失败skew过大导致hold违例为什么分频后时钟域内部会出现hold违例我抓了一条典型路径分析。分频器Q端的clk-to-q延迟本身有0.3ns而主时钟到分频器CK端的延迟0.9ns也就是说分频后时钟沿到达下游寄存器的时间比主时钟沿晚了大概1.2ns左右。下游寄存器之间的data path延迟却在0.5ns以内。在默认的set_propagated_clock分析下这种skew直接导致hold违例。当时的我犯了一个错误想着“既然分频后时钟域内部hold违例那把分频后时钟域的sink单独做成一组就好了”结果CTS跑出来以后分频后时钟域内部skew确实收住了但主时钟域到分频后时钟域的setup路径猛地冒出上百条违例。原因很清楚跨域路径存在而我把两个域彻底拆开了物理上没有平衡时序上又没做约束放松setup自然崩掉。这个教训让我意识到Skew Group划分必须在“路径约束”框定的前提下进行而不是拍脑袋决定。于是回到SDC把该隔开的异步路径全部用set_clock_groups隔开剩下的跨域路径逐条审查发现大部分可以加multicycle最终真正需要保持物理对齐的跨域路径只剩下十几条。4.3 用Skew Group重构后的结果做了以下调整第一SDC里定义好generated clock并隔离异步路径设置合理的multicycle第二创建两个Skew Group主时钟域寄存器加分频器CK端一组分频后时钟域寄存器一组第三针对剩余的十几条真实跨域路径单独约束它们必须满足时序并靠工具在时钟树优化阶段自然收敛。最终CTS报告里主时钟树插入延迟降到0.7ns分频后时钟树插入延迟0.55ns分频后时钟域组内skew缩小到0.08ns。时钟树buffer面积从11%降到5.5%hold违例数量从几十条降到零setup余量从原来的负数变成0.12ns。这个案例的参考价值不在于数字本身而在于完整的优化链路先补时钟定义再梳理跨域路径然后决定Skew Group的颗粒度最后用CTS报告验证。每一步都有明确的目的而不是盲目堆命令。5. 容易踩的坑Skew Group不是万能的5.1 坑一过度分组导致跨时钟域路径时序崩溃这是我第一次用Skew Group时踩过的坑。当时做一个小模块里面有主时钟和二分频时钟两个时钟域之间有很多数据交互但我图省事直接按频率把两个域彻底拆成两个独立Skew Group。结果CTS后setup违例暴增检查发现那些被分开的sink恰恰存在于跨时钟域路径上物理上没有对齐时序上又没做约束处理等于把问题直接暴露在了时序分析里。正确的做法是先问自己这些路径在约束层面是否已经放松如果放松不了物理上就必须给它们保留平衡条件。Skew Group拆分得越细物理层对齐的负担越小但逻辑层的约束必须同步跟上。两个域之间的交互路径如果没有被set_clock_groups或multicycle约束兜底拆组就是给自己挖坑。5.2 坑二分频器CK端忘掉加入主时钟Skew Group还有一个高频问题创建主时钟Skew Group时只选了[all_registers -clock clk_100m]结果漏掉了分频器的CK端。这个pin本身也是主时钟的sink如果它没有被纳入主时钟组工具就不会把主时钟到达分频器的时间和其他主时钟域寄存器对齐。后果是分频器输出的相位相对主时钟发生随机偏移跨域路径的时序结果在每次CTS后都不一样稳定性极差。排查办法很简单CTS后用report_balance_groups看主时钟组里有没有分频器的CK端如果你用get_sinks -clock clk_100m去过滤它会自动包含分频器CK端。前提是分频器的generated clock已经在SDC里定义好否则工具不会把分频器CK端识别为主时钟的sink。5.3 坑三ECO阶段破坏了Skew Group结构芯片迭代过程中功能ECO经常需要在某个时钟域里加几级缓存寄存器。新加的寄存器默认情况下不会自动归类到既有的Skew Group里。如果后端工程师在ECO之后直接跑时钟树优化新寄存器可能被工具放入一个全新的默认组导致它附近的时钟skew和周围寄存器不一致。处理方式是每次ECO后重新检查report_balance_groups把新增的sink手动加入对应Skew Group。如果新增逻辑不多可以直接在Innovus里用增量方式更新UI不必重跑整个CTS。但必须确保组的成员关系是“最新状态”。我在实际项目中试过只加几个寄存器不更新组关系结果CTS优化时工具为了平衡这个“孤儿sink”在它附近插了整整一长串buffer面积和时序都吃了大亏。从分频电路的原理走到Skew Group的实际落地这套思路我现在已经用到团队的标准流程里。每当遇到设计中有分频器、门控时钟使能、以及任何“由寄存器再生成新时钟”的结构我都会先花半小时把时钟关系梳理清楚再决定Skew Group怎么划。这半小时省下的往往是一整周的CTS迭代时间。