☰
分频电路CTS优化:Skew Group划分策略与Innovus实战指南
2026/10/7 22:46:53 网站建设 项目流程

先交代一个背景:我去年做过一个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 Group

Skew 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_expanded

report_clock_interaction非常关键,它会列出任意两个时钟之间是否存在逻辑路径。看到某个时钟对之间有大量路径且没有被约束隔离,就把这个时钟对记为重点关注对象。反之,如果两个时钟之间只有可怜的几条路径,而且功能上本来就可以放松,那么在SDC里直接用set_clock_groups -asynchronous隔开,再从Skew Group方案里排除掉,是更省面积的做法。

确认完路径关系后,再回到物理层面,看分频器的位置。分频器在物理上靠近哪一簇寄存器,也会影响Skew Group内部平衡难度。如果分频器和主时钟域寄存器分布太分散,那即使放在同一组,CTS为了平衡它们之间的延迟也可能插很多buffer,这时候要结合布局约束考虑,甚至预先对分频器做区域约束。

3.3 第三步:创建Skew Group

Innovus里创建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 -detail

report_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周期的multicycle:

set_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迭代时间。

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

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

立即咨询