☰
数字IC后端CTS实战:Skew Group划分与引擎优化指南
2026/10/7 11:43:39 网站建设 项目流程

数字IC后端实现里,时钟树综合(CTS)是那种"做好了没人夸、做砸了全流程返工"的环节。我做过几个从RTL到GDSII全流程的项目,也在28nm、16nm、7nm几个节点上踩过CTS的坑,最深的体会是:Skew Group的划分方式,直接决定了CTS引擎的优化空间和最终时序收敛的难度。很多人跑CTS就是打开工具、加载CTS脚本、点run,结果hold修不完、clock latency大得离谱、OCV余量被吃掉一大截,回头查半天发现是Skew Group没配对。这篇内容就是把我自己在实际项目中关于Skew Group配置和CTS引擎优化的经验整理出来,从概念到脚本到debug,尽量讲透。适合已经接触过数字后端流程、正在做CTS或者准备面试的工程师参考,也适合想理解CTS底层逻辑的验证和前端同学。

1. 先搞清楚Skew Group到底在管什么

1.1 从时钟树的结构说起

时钟树综合的本质,是把一个时钟源点(clock root)通过缓冲器(buffer)和反相器(inverter)组成的树形网络,扇出到成千上万个时序单元的时钟端口(clock pin)。理想情况下,所有clock pin应该在同一时刻收到时钟沿,但物理实现上不可能——线长不同、负载不同、buffer延迟不同,到达时间必然有差异。这个差异就是skew。

CTS引擎在优化时,并不是把所有clock pin当成一个整体来平衡。它会把时钟域内的sink点划分成若干个组,每个组内部做skew平衡,组与组之间允许存在一定的偏移。这个组就是Skew Group。你可以把它理解成"CTS引擎的优化单元"——引擎在每个Skew Group内部尽量把skew压到目标值以内,而跨组的偏移则由你通过配置来控制。

为什么要有这个概念?因为实际设计里,不是所有寄存器都需要严格对齐。比如一个模块内部的寄存器之间需要严格同步,但模块A和模块B之间可能通过异步FIFO或握手信号交互,它们的时钟到达时间差个几百皮秒完全没问题。如果强行把所有sink拉到同一个skew目标,CTS引擎会插入大量buffer去平衡那些本来不需要平衡的路径,面积、功耗、latency全部恶化。Skew Group就是让你告诉引擎:"这几组寄存器要严格对齐,那几组可以松一点。"

1.2 Skew Group和Clock Domain的区别

这是新手最容易混淆的地方。Clock Domain是逻辑层面的概念,由时钟定义决定——同一个时钟源驱动的所有寄存器属于同一个clock domain。Skew Group是物理实现层面的概念,是你在CTS阶段人为划分的优化分组。

一个clock domain可以包含多个Skew Group,一个Skew Group也可以跨多个clock domain(虽然不常见)。举个例子:一个CPU core的时钟域里,整数执行单元和浮点执行单元可能被划成两个Skew Group,因为它们之间的时序路径很少,不需要严格对齐;但每个单元内部的寄存器必须在同一个Skew Group里,保证内部skew可控。

我见过有人把整个clock domain当成一个Skew Group来跑CTS,结果clock tree上挂了上万個sink,引擎优化时收敛极慢,而且为了平衡一些根本不相关的路径插了一堆buffer。后来拆成四个Skew Group,latency降了15%,buffer数量少了20%。

1.3 什么时候需要拆Skew Group

不是所有设计都需要精细划分Skew Group。以下几种情况建议拆:

  • 跨时钟域交互少的模块:比如一个SoC里,CPU子系统、GPU子系统、外设子系统各自独立,交互通过总线桥接,这种天然适合拆成不同Skew Group。
  • 频率差异大的区域:高频区域对skew敏感,低频区域可以放宽,拆开后高频区域能获得更多优化资源。
  • 物理位置分散的sink:如果两个模块在floorplan上离得很远,强行放一个Skew Group里,CTS引擎会为了平衡它们之间的skew插入长线buffer,反而恶化latency。
  • 特殊时序要求:比如某些寄存器需要做clock gating,gating cell的时钟到达时间有特殊要求,可以单独成组。

反过来,如果设计规模不大(比如几万instance的小芯片),或者所有模块交互都很紧密,那可能一个Skew Group就够了,拆多了反而增加配置复杂度。

2. Skew Group的配置参数与脚本实战

2.1 核心参数:target skew和target latency

配置Skew Group时,最核心的两个参数是target skew和target latency。

Target skew是你希望组内clock pin之间的最大到达时间差。这个值不是拍脑袋定的,要根据时钟周期和工艺节点来算。经验公式是:target skew ≤ 时钟周期的5%~10%。比如1GHz时钟(周期1ns),target skew可以设在50~100ps。7nm以下节点,由于OCV影响更大,建议压到5%以内。

Target latency是clock root到sink的平均延迟。这个值影响插入延迟(insertion delay)和时钟树功耗。latency设得越小,CTS引擎会插更少的buffer,但可能skew收敛难度增加。一般设成时钟周期的10%~20%比较合理。

在Innovus里,配置命令大概是这样的:

# 创建Skew Group create_ccopt_clock_tree -name cpu_core_clk -source [get_pins clk_gen/CLKOUT] \ -skew_group cpu_core_sg # 设置target skew和latency set_ccopt_property -skew_group cpu_core_sg target_skew 80ps set_ccopt_property -skew_group cpu_core_sg target_latency 300ps # 把sink加入Skew Group set_ccopt_property -skew_group cpu_core_sg -sinks [get_pins -hier */CLK]

在ICC2里对应的是:

create_clock_tree -name cpu_core_clk -source [get_pins clk_gen/CLKOUT] set_clock_tree_options -clock_tree cpu_core_clk -target_skew 80ps set_clock_tree_options -clock_tree cpu_core_clk -target_latency 300ps

注意,不同工具对Skew Group的称呼和命令不一样,Innovus叫ccopt skew group,ICC2叫clock tree,但概念是通的。

2.2 用sink type区分leaf和non-leaf

Skew Group里的sink分两类:leaf sink(寄存器的clock pin)和non-leaf sink(比如clock gating cell、分频器、其他时钟树的root)。这两类sink的优化策略不同。

Leaf sink是CTS优化的主要目标,引擎会尽量平衡它们之间的skew。Non-leaf sink则需要特殊处理——比如clock gating cell的时钟到达时间会影响gating后的时钟树,如果gating cell的clock pin skew太大,可能导致gating后的寄存器出现毛刺或时序问题。

在配置时,可以用sink type来区分:

# 只对leaf sink做skew平衡 set_ccopt_property -skew_group cpu_core_sg -sink_type leaf # non-leaf sink单独设置 set_ccopt_property -skew_group cpu_core_sg -sink_type non_leaf \ -target_skew 50ps

我一般会把non-leaf sink的target skew设得比leaf更严,因为它们的偏移会传递到下游时钟树,放大误差。

2.3 跨Skew Group的balance配置

多个Skew Group之间如果需要保持一定的相位关系,可以用balance配置。比如两个Skew Group共享同一个时钟源,你希望它们的latency尽量接近,避免跨组路径的时序余量被吃掉。

# 设置两个Skew Group之间的balance set_ccopt_property -skew_group cpu_core_sg -balance_group cpu_balance set_ccopt_property -skew_group gpu_core_sg -balance_group cpu_balance

这样CTS引擎会在优化时同时考虑两个组的latency,尽量让它们对齐。但要注意,balance group会增加引擎的优化复杂度,如果两个组的sink数量差异很大,收敛时间会明显增加。

2.4 一个完整的CTS脚本框架

下面是我在一个16nm项目里用的CTS脚本框架,简化后分享出来:

# 1. 时钟定义检查 check_clock_tree -clock_tree all # 2. 创建Skew Group create_ccopt_clock_tree -name sys_clk -source [get_pins pll/CLKOUT] create_ccopt_clock_tree -name cpu_clk -source [get_pins clk_div/CLKOUT] # 3. 配置Skew Group参数 set_ccopt_property -skew_group sys_clk_sg target_skew 100ps set_ccopt_property -skew_group sys_clk_sg target_latency 400ps set_ccopt_property -skew_group cpu_clk_sg target_skew 60ps set_ccopt_property -skew_group cpu_clk_sg target_latency 250ps # 4. 指定sink set_ccopt_property -skew_group sys_clk_sg -sinks [get_pins -hier \ -filter "full_name=~*sys_domain*" */CLK] set_ccopt_property -skew_group cpu_clk_sg -sinks [get_pins -hier \ -filter "full_name=~*cpu_domain*" */CLK] # 5. 设置CTS引擎选项 set_ccopt_property -skew_group sys_clk_sg -useful_skew true set_ccopt_property -skew_group cpu_clk_sg -useful_skew true # 6. 运行CTS ccopt_design -cts # 7. 检查结果 report_ccopt_clock_trees -file cts_report.rpt report_ccopt_skew_groups -file skew_report.rpt

这个脚本里有个关键点:useful skew。开启后,CTS引擎会利用时序余量来调整skew,而不是一味追求零skew。比如某条路径setup余量很大,引擎可以故意让这条路径的sink时钟晚到一点,把余量让给hold紧张的路径。这个功能在时序紧张的设计里非常有用,但前提是你的时序约束要准确,否则引擎会"优化"出错误的方向。

3. CTS引擎的优化策略与底层逻辑

3.1 引擎是怎么做skew平衡的

CTS引擎的优化过程大致分三步:聚类、拓扑生成、缓冲器插入与 sizing。

聚类阶段,引擎会根据sink的物理位置和时钟需求,把它们分成若干簇(cluster)。每个簇内的sink会被尽量放在一起,减少后续布线长度。聚类算法通常基于几何距离和时序权重——时序紧张的sink会被优先聚在一起。

拓扑生成阶段,引擎会构建一棵树形结构,从root到各个簇。树的结构直接影响latency和skew——平衡树(balanced tree)可以让各路径延迟接近,但可能增加总线长;H-tree在规则布局下效果好,但实际设计里sink分布不规则,H-tree往往不是最优。

缓冲器插入与sizing阶段,引擎会在树的节点上插入buffer,并调整buffer的尺寸(drive strength)。这里有个权衡:大尺寸buffer驱动能力强、延迟小,但功耗和面积大;小尺寸buffer省功耗,但可能需要多级级联,增加latency。

我实测过一个对比:同一个设计,用大尺寸buffer少级数 vs 小尺寸buffer多级数,前者latency低15%,但功耗高20%。最终选了折中方案,latency和功耗都在可接受范围。

3.2 Useful Skew的利与弊

Useful skew是CTS引擎最强大的功能之一,但也是最容易出问题的。它的原理是:利用时序路径的余量,故意让某些sink的时钟早到或晚到,从而改善整体时序。

举个例子:一条路径从寄存器A到寄存器B,setup余量有200ps,hold余量只有20ps。如果CTS引擎让B的时钟晚到100ps,那么A到B的setup余量变成100ps(仍然够),但hold余量变成120ps(大幅改善)。这就是useful skew的价值。

但问题在于:引擎判断余量依赖时序约束的准确性。如果SDC里有多周期路径(multicycle path)或虚假路径(false path)没设对,引擎会误以为某些路径有余量,做出错误的skew调整。我踩过一次坑:一个跨时钟域路径没设false path,引擎以为有余量,把时钟调得乱七八糟,后来修SDC重新跑CTS才解决。

另外,useful skew会增加时钟树的不确定性,对OCV分析不利。所以我的建议是:在时序紧张的设计里开启useful skew,但必须确保SDC干净;在时序宽松或安全性要求高的设计里,可以关闭,用保守的零skew策略。

3.3 多源时钟树的处理

实际设计里,一个时钟树可能有多个源点——比如一个时钟经过MUX选择后驱动下游,或者多个PLL输出汇聚到一个时钟网络。这种多源时钟树的CTS处理更复杂。

引擎需要先确定每个源点的驱动范围,然后在源点之间做平衡。如果两个源点的时钟频率不同,引擎会把它们当成独立的Skew Group处理;如果频率相同但相位不同,则需要配置phase关系。

我遇到过一个case:两个PLL输出同频但相位差180度,CTS时没配置phase关系,引擎把它们当成独立时钟树优化,结果下游寄存器采样出错。后来在SDC里加了set_clock_groups -asynchronous,并在CTS配置里指定了phase关系,问题解决。

3.4 时钟树上的clock gating处理

Clock gating cell在CTS里是个特殊存在。它既是上游时钟树的sink,又是下游时钟树的source。CTS引擎需要保证gating cell的clock pin skew可控,同时gating后的时钟树也要满足skew要求。

处理方式通常有两种:把gating cell当成non-leaf sink单独设skew目标,或者把gating后的寄存器划到独立的Skew Group。我一般用前者,因为gating后的时钟树通常规模不大,单独成组反而增加配置复杂度。

但要注意:gating cell的enable信号时序也很关键。如果enable信号在时钟沿附近变化,可能导致gating后的时钟出现毛刺。CTS阶段虽然不直接优化enable路径,但可以通过调整gating cell的时钟到达时间来间接改善。我的经验是:让gating cell的时钟比下游寄存器的时钟早到一点,这样enable信号有更充裕的稳定时间。

4. 实测中的典型问题与排查链路

4.1 Skew收敛不了:从sink分布查起

CTS跑完发现某个Skew Group的skew远大于目标值,比如目标80ps,实际200ps。这时候别急着调参数,先查sink分布。

用report_ccopt_skew_groups -verbose看每个sink的到达时间,找出偏离最大的那些。常见原因有几个:

  • sink物理位置过于分散:如果最远和最近的sink距离超过500um,CTS引擎很难在合理latency内平衡。解决办法是拆Skew Group,或者调整floorplan让相关sink靠近。
  • sink负载差异大:有些sink的clock pin电容大(比如大尺寸寄存器),有些小,引擎需要插不同尺寸的buffer来匹配,如果差异太大,skew收敛困难。可以在CTS前做clock pin的load balancing,或者手动调整关键sink的驱动。
  • 时钟树上有高扇出net:如果某个节点扇出超过50,引擎的buffer插入策略可能不够优化。可以手动约束max fanout,让引擎提前规划。

我遇到过一次:一个Skew Group里混了两种寄存器,一种clock pin电容是另一种的3倍,skew怎么都收敛不了。后来把这两种寄存器拆到不同Skew Group,各自设不同的target skew,问题解决。

4.2 Latency过大:buffer级数失控

Latency过大通常意味着时钟树上的buffer级数太多。用report_ccopt_clock_trees -latency看每级buffer的延迟贡献,找出瓶颈。

常见原因:

  • target latency设得太小:引擎为了满足latency目标,拼命插buffer,反而增加了级数。这时候要放宽target latency,让引擎有更多优化空间。
  • buffer尺寸选择不当:如果引擎用了太多小尺寸buffer,级数会增加。可以约束buffer的可用尺寸范围,让引擎优先用大尺寸buffer。
  • 布线绕行:如果时钟树布线绕了远路,线延迟会增加。检查floorplan和placement,确保时钟树路径畅通。

我的经验是:target latency不要设得比实际可达值小太多。可以先跑一次CTS,看实际latency是多少,然后设成实际值的90%左右,给引擎一点优化压力但又不至于失控。

4.3 Hold违例修不完:CTS和后续流程的配合

CTS跑完,hold违例一大堆,修了半天修不完。这时候要回头看看CTS阶段是不是留了太多余量给hold。

Hold违例的根本原因是时钟到达时间差不够。如果CTS阶段把skew压得很小,hold余量自然就少。解决办法有两个:一是CTS阶段适当放宽skew目标,给hold留余量;二是开启useful skew,让引擎主动调整时钟到达时间来改善hold。

但要注意:useful skew改善hold的前提是setup有余量。如果setup本身就很紧,useful skew可能顾此失彼。我一般会在CTS前先跑一次时序分析,看看setup和hold的余量分布,再决定useful skew的策略。

另外,CTS后的hold修复通常用insertion delay或buffer插入,但这些操作会改变时钟树,可能影响skew。所以hold修复最好在CTS阶段就规划好,而不是留到后面。

4.4 跨时钟域路径的skew问题

跨时钟域路径的skew问题往往被忽视。如果两个时钟域的时钟到达时间差太大,跨域路径的setup和hold都会受影响。

处理方式:在CTS阶段配置balance group,让两个时钟域的latency尽量接近。如果两个时钟域频率不同,无法完全balance,则需要在SDC里正确设置clock group关系,并在时序分析时单独检查跨域路径。

我见过一个设计,两个时钟域频率比是2:1,CTS时没做balance,结果跨域路径的hold违例特别多。后来在CTS配置里加了balance group,虽然不能完全消除skew,但把跨域路径的余量改善了30%。

5. 进阶:CTS与PR流程的协同优化

5.1 Placement阶段就要为CTS做准备

CTS的质量很大程度上取决于placement阶段。如果placement时寄存器摆得乱七八糟,CTS再怎么优化也救不回来。

我的做法是:在placement阶段就设置clock-aware的约束。比如用set_clock_tree_options -clock_aware_placement让工具在摆放寄存器时考虑时钟树的需求,把同一Skew Group的寄存器尽量摆在一起。另外,可以用create_clock_tree -sink_type提前标记关键sink,让placement阶段优先处理。

在Innovus里,可以用ccopt_design -place在placement阶段就做初步的时钟树规划,这样CTS阶段收敛更快。

5.2 Routing阶段的时钟树保护

CTS后的routing阶段,时钟树网络需要特殊保护。因为时钟树对线延迟敏感,如果routing时走了远路或串扰严重,skew会恶化。

常用手段:

  • 设置non-default rule(NDR):给时钟树网络设置更宽的线宽和更大的间距,减少电阻和串扰。
  • 屏蔽层(shielding):在时钟线两侧加接地屏蔽线,减少串扰。
  • 限制layer:让时钟树走高层金属,电阻小、延迟稳定。

我一般会给时钟树设置双倍线宽和双倍间距的NDR,虽然面积会增加,但skew和latency的稳定性明显提升。

5.3 Post-CTS的时序修复策略

CTS后的时序修复要分优先级:先修setup,再修hold。因为setup违例影响功能,hold违例影响可靠性,但setup修复可能会改变时钟树,进而影响hold。

修复setup时,优先用useful skew和buffer sizing,尽量避免动时钟树结构。修复hold时,可以用insertion delay或小尺寸buffer,但要注意不要破坏skew。

另外,Post-CTS的时序修复要设置合理的margin。因为后续还有routing和signoff,如果Post-CTS阶段把时序修得太紧,routing后可能又出现违例。我一般会留10%~15%的余量给后续流程。

5.4 一个完整的CTS-PR协同流程

总结一下我在实际项目中用的流程:

  1. Placement阶段:开启clock-aware placement,初步规划时钟树。
  2. Pre-CTS时序分析:检查setup和hold余量,确定useful skew策略。
  3. CTS配置:划分Skew Group,设置target skew和latency,配置balance group。
  4. CTS运行:先跑一次无useful skew的CTS,看baseline;再开useful skew优化。
  5. Post-CTS时序分析:检查skew、latency、setup、hold,定位问题。
  6. 时序修复:先修setup,再修hold,留余量给后续流程。
  7. Routing:设置NDR和shielding,保护时钟树。
  8. Post-Route时序分析:检查最终时序,确认skew和latency在目标范围内。

这个流程不是固定的,要根据设计规模和时序难度调整。比如小设计可以跳过Pre-CTS分析,大设计可能需要多轮CTS迭代。

6. 几个容易被忽视的细节

6.1 Clock pin的load balancing

CTS前做clock pin的load balancing,可以显著改善skew收敛。具体做法是:检查所有sink的clock pin电容,如果差异超过2倍,考虑插入dummy buffer或调整驱动来平衡。

在Innovus里可以用report_ccopt_clock_trees -load查看每个sink的负载。如果发现某些sink负载特别大,可以在CTS前手动插buffer。

6.2 时钟树的power优化

时钟树是芯片功耗的大头,通常占动态功耗的20%~40%。CTS阶段可以通过以下方式优化功耗:

  • 减少buffer数量:在满足skew和latency的前提下,尽量少插buffer。
  • 用低功耗buffer:如果工艺库里有低功耗buffer,优先选用。
  • clock gating:在CTS阶段合理插入clock gating cell,减少不必要的时钟翻转。

但要注意:clock gating会引入额外的skew和latency,需要在功耗和时序之间权衡。

6.3 多corner下的CTS

先进工艺节点下,CTS需要在多个corner下验证。比如SS corner下latency大,FF corner下latency小,如果只在一个corner下优化,另一个corner可能出问题。

我的做法是:在typical corner下做CTS优化,然后在SS和FF corner下检查skew和latency。如果差异太大,需要在CTS配置里设置corner-aware的约束,让引擎在优化时考虑多个corner。

6.4 CTS报告的解读

CTS跑完会生成一堆报告,重点看这几个:

  • skew report:看每个Skew Group的实际skew,和目标值的差距。
  • latency report:看clock root到sink的平均延迟和最大延迟。
  • buffer report:看插了多少buffer,各级buffer的尺寸分布。
  • violation report:看有没有skew或latency违例。

我一般会把这些报告和Pre-CTS的时序报告对比,看看CTS对时序的改善程度。如果改善不明显,说明CTS配置有问题,需要调整。

7. 面试中常被问到的CTS问题

7.1 Skew和Jitter的区别

面试高频题。Skew是空间上的到达时间差——不同sink在同一时钟沿的到达时间差异。Jitter是时间上的不确定性——同一sink在不同时钟周期的到达时间波动。Skew可以通过CTS优化减小,Jitter主要由PLL和时钟源决定,CTS只能间接改善。

7.2 为什么CTS后要做hold修复

CTS后时钟树确定了,寄存器之间的时钟到达时间差也确定了。如果这个差值太小,hold违例就会出现。Hold修复的本质是增加时钟到达时间差,或者增加数据路径延迟。CTS阶段可以通过useful skew主动调整,Post-CTS阶段则用buffer插入或delay cell。

7.3 Useful skew的风险

Useful skew的风险主要有三个:一是依赖SDC准确性,SDC错了引擎会优化错方向;二是增加时钟树不确定性,对OCV分析不利;三是可能顾此失彼,改善了hold但恶化了setup。所以用useful skew要谨慎,必须配合完整的时序分析。

7.4 如何选择target skew

Target skew的选择要综合考虑时钟周期、工艺节点、设计规模。一般规则是:时钟周期的5%~10%,先进节点取小值,成熟节点取大值。另外要考虑OCV余量,如果OCV影响大,target skew要设得更小。

8. 写在最后

CTS这个环节,工具能帮你做很多事,但工具不知道你的设计意图。Skew Group怎么划、target skew怎么设、useful skew开不开,这些决策依赖你对设计的理解。我见过太多人把CTS当成一个"跑一下就行"的步骤,结果后面修时序修到崩溃。

我自己的习惯是:CTS前花半天时间分析时钟结构和时序余量,把Skew Group划分和参数配置想清楚,CTS跑完后花半天时间看报告、定位问题。这比反复跑CTS、反复修时序效率高得多。

另外,CTS的经验很难从文档里学到,更多是从项目里踩坑踩出来的。每次CTS出问题,别急着调参数,先搞清楚问题的根因——是sink分布问题、约束问题、还是引擎配置问题。搞清楚根因,下次就不会再踩同样的坑。

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

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

立即咨询