
数字后端设计里时钟不确定性clock uncertainty是绕不开的一环。很多刚接触Design Compiler的朋友在做时序约束时会把setup和hold的uncertainty随手填一个0.1或者0.2觉得“差不多就行”。但等到PR阶段发现时序怎么都收敛不了回头一查发现是uncertainty设得太乐观或者根本没区分jitter和skew导致综合阶段过度乐观后端拼命修也修不回来。这篇文章就围绕set_clock_uncertainty这个命令把时钟抖动的建模逻辑、参数计算、实战配置和常见坑点讲透适合正在做DC综合、对时序约束有一定基础但想搞清楚“为什么这么设”的工程师。1. 时钟不确定性到底在约束什么1.1 uncertainty不是skew但包含了skew很多人第一次看到set_clock_uncertainty会下意识觉得它就是用来描述时钟偏斜skew的。这个理解只对了一半。在Design Compiler的时序模型里clock uncertainty是一个“预算”概念它代表的是在时钟到达两个不同触发器时除了理想时钟树延迟之外所有可能导致时序余量恶化的因素总和。具体来说uncertainty通常包含三部分时钟抖动jitter、时钟偏斜skew以及设计余量margin。jitter是时钟源本身周期不稳定带来的相位波动skew是时钟树走线长度不同导致的到达时间差异margin则是工程师为了给后端留余地而人为加的一层保护。DC在综合阶段还没有真实的时钟树所以它无法知道实际的skew是多少这时候就需要你用uncertainty把这个预算“喂”给它。注意DC的set_clock_uncertainty命令本身不区分jitter和skew它只接受一个数值。但你在计算这个数值的时候必须把两者都考虑进去否则综合结果会过于乐观。1.2 setup和hold的uncertainty方向相反这是最容易搞混的地方。对于setup检查uncertainty是“减”在数据路径上的也就是说它会让setup的时序余量变小对于hold检查uncertainty是“加”在数据路径上的它会让hold的余量也变小。换句话说无论setup还是holduncertainty的作用都是让时序变得更紧而不是更松。为什么方向不同因为setup关心的是数据能不能在时钟捕获沿之前稳定下来时钟来得越早或者数据来得越晚setup越容易违例而hold关心的是数据在时钟捕获沿之后能不能保持足够长时间时钟来得越晚或者数据来得越早hold越容易违例。所以uncertainty在setup和hold上的施加方向是相反的但效果都是压缩时序窗口。在DC里你可以分别对setup和hold设置不同的uncertainty值set_clock_uncertainty -setup 0.15 [get_clocks clk_main] set_clock_uncertainty -hold 0.08 [get_clocks clk_main]如果不加-setup或-hold默认是同时作用于两者。实际项目中setup的uncertainty通常比hold大因为setup对jitter和skew更敏感而且后端留给setup的余量一般也更多。1.3 为什么不能直接填0有些工程师觉得反正后端会做时钟树综合CTS到时候真实的skew就出来了综合阶段随便填个小值就行。这个想法很危险。DC在综合时会根据你给的uncertainty来优化逻辑如果uncertainty设得太小DC会认为时序很宽松于是它可能选择面积更小但速度更慢的单元或者减少缓冲器的插入。等到PR阶段真实skew进来时序一下子恶化后端只能靠加buffer和upsize来救但这时候逻辑结构已经固定了能修的空间很有限。反过来如果uncertainty设得太大DC会过度优化面积和功耗都会上去甚至可能因为约束太紧而无法收敛。所以uncertainty的设定是一个“预算分配”的问题你需要根据时钟频率、工艺节点、时钟树复杂度来给出一个合理的估计。2. 从jitter到uncertainty参数是怎么算出来的2.1 周期抖动和相位抖动的区别时钟抖动分两种周期抖动period jitter和相位抖动phase jitter也叫long-term jitter。周期抖动描述的是单个时钟周期长度的波动它影响的是同一对触发器之间的时序相位抖动描述的是时钟边沿相对于理想位置的累积偏移它影响的是经过多个周期后的时序关系。在大多数数字设计里我们关心的是周期抖动因为它直接决定了setup和hold的余量。周期抖动又分为随机抖动random jitterRJ和确定性抖动deterministic jitterDJ。随机抖动服从高斯分布通常用RMS值或者峰峰值peak-to-peak来描述确定性抖动是有界的比如占空比失真、电源噪声引起的抖动等。总抖动total jitterTJ的常用估算公式是TJ DJ n × RJ其中n取决于你要求的误码率BER。对于常见的10^-12 BERn大约取14对于10^-9 BERn大约取12。这个n值来自高斯分布的尾部概率具体推导涉及统计数学这里不展开你只需要知道BER要求越高n越大TJ也越大。2.2 一个实际的计算例子假设你的时钟源规格书里写着RMS随机抖动0.5ps确定性抖动3ps目标BER 10^-12。那么TJ 3ps 14 × 0.5ps 3 7 10ps这10ps就是峰峰值总抖动。但注意这是时钟源本身的抖动还没有考虑时钟树上的抖动放大。在实际设计中时钟树上的buffer和走线会引入额外的抖动通常需要再乘一个放大系数经验值在1.2到1.5之间。假设取1.3TJ_clock_tree 10ps × 1.3 13ps这13ps就是你需要在uncertainty里体现的jitter部分。然后再加上skew预算和margin才是最终的uncertainty值。2.3 skew预算怎么估在综合阶段你还没有时钟树但你可以根据设计规模和工艺节点来估算skew。一般来说对于小规模设计几千个触发器以内时钟树深度浅skew可以估在50ps到100ps。对于中等规模设计几万个触发器skew通常在100ps到200ps。对于大规模设计几十万触发器以上skew可能达到200ps到300ps甚至更高。这些数值是经验值具体还要看你的时钟树综合策略。如果你打算用H-tree或者clock meshskew会小一些如果用普通的CTSskew会大一些。另外工艺越先进skew通常越小因为走线延迟在总延迟中的占比下降但on-chip variationOCV的影响会变大。2.4 margin留多少合适margin是一个“保险系数”它用来覆盖那些你无法精确建模的因素比如OCV、电源噪声、温度变化等。在综合阶段margin通常取uncertainty总值的10%到20%。如果你已经在jitter和skew里考虑了OCVmargin可以小一些如果没有margin就要大一些。一个常用的经验公式是uncertainty_setup jitter skew margin uncertainty_hold jitter skew_hold margin_hold其中hold的skew通常比setup小因为hold检查是在同一个时钟沿附近而setup检查可能跨越半个周期甚至多个周期。所以hold的uncertainty一般比setup小30%到50%。3. 在DC里怎么设命令、选项和常见写法3.1 基本命令格式set_clock_uncertainty的基本语法是set_clock_uncertainty [‑setup] [‑hold] uncertainty_value [get_clocks clock_name]uncertainty_value是一个时间值单位默认是ns除非你在set_units里改了。clock_name是你用create_clock定义的时钟名。几个常用的写法# 对单个时钟设置setup和hold相同的uncertainty set_clock_uncertainty 0.12 [get_clocks clk_core] # 分别设置setup和hold set_clock_uncertainty -setup 0.15 [get_clocks clk_core] set_clock_uncertainty -hold 0.07 [get_clocks clk_core] # 对多个时钟同时设置 set_clock_uncertainty 0.10 [get_clocks {clk_core clk_ddr}] # 对所有时钟设置 set_clock_uncertainty 0.10 [all_clocks]3.2 跨时钟域怎么处理当两个时钟域之间有数据交互时uncertainty的设置会更复杂。如果两个时钟是同步的比如同源分频你可以用set_clock_groups或者set_false_path来约束但uncertainty仍然需要设置因为两个时钟域的时钟树可能不同jitter和skew的叠加方式也不同。对于异步时钟域通常用set_clock_groups -asynchronous来切断时序路径这时候uncertainty就不重要了因为路径已经被忽略。但如果你要做跨时钟域的时序分析比如用set_max_delay那uncertainty还是要设的而且要考虑两个时钟的jitter叠加。一个常见的做法是对每个时钟单独设置uncertainty然后在跨时钟域路径上用set_clock_uncertainty -from和-to来覆盖set_clock_uncertainty -from [get_clocks clk_a] -to [get_clocks clk_b] 0.20这个0.20是两个时钟uncertainty的叠加值具体怎么算要看两个时钟的jitter是否相关。如果两个时钟来自同一个PLLjitter是相关的叠加值会小一些如果来自不同PLLjitter不相关叠加值要用均方根RMS方式计算。3.3 和set_clock_latency的配合set_clock_uncertainty经常和set_clock_latency一起用。latency描述的是时钟从源到触发器时钟端的延迟uncertainty描述的是这个延迟的不确定性。在综合阶段你通常会用set_clock_latency -source来建模时钟源延迟用set_clock_latency来建模时钟树延迟。一个典型的配置create_clock -name clk_core -period 2.0 [get_ports clk_in] set_clock_latency -source 0.5 [get_clocks clk_core] set_clock_latency 0.3 [get_clocks clk_core] set_clock_uncertainty -setup 0.15 [get_clocks clk_core] set_clock_uncertainty -hold 0.08 [get_clocks clk_core]这里source latency是0.5nsnetwork latency是0.3nssetup uncertainty是0.15ns。DC在做setup检查时会把source latency和network latency都算进去然后再减去uncertainty。提示如果你在综合阶段还没有时钟树network latency可以设一个估计值等CTS之后再更新。uncertainty则是在CTS之前用来覆盖skew和jitter的CTS之后可以用真实的skew来替换。3.4 用report_clock_timing检查设置设完uncertainty之后一定要用report_clock_timing检查一下report_clock_timing -type uncertainty这个命令会列出每个时钟的uncertainty设置包括setup和hold的值。如果你发现某个时钟的uncertainty没设上或者设错了可以及时修正。另外report_timing的时候也会显示uncertainty的影响。在时序报告里uncertainty会出现在clock uncertainty那一行你可以看到它具体扣了多少余量。4. 那些年我踩过的uncertainty坑4.1 坑一setup和hold用了同一个值刚开始做项目的时候我图省事setup和hold都设了0.1。结果hold违例一大堆setup反而很宽松。后来才明白hold的uncertainty应该比setup小因为hold检查是在同一个时钟沿附近skew的影响小jitter的相关性也高。把hold改成0.05之后hold违例少了很多而且没有影响setup的收敛。4.2 坑二忘了跨时钟域的uncertainty叠加有一次做DDR接口clk_ddr和clk_core之间有数据交互。我分别设了0.12和0.10的uncertainty但没有设跨时钟域的覆盖值。结果DC在分析跨时钟域路径时只用了单个时钟的uncertainty导致时序过于乐观。后来加了set_clock_uncertainty -from clk_ddr -to clk_core 0.18时序才真实反映出来。跨时钟域的uncertainty叠加不是简单相加如果两个时钟不相关应该用RMSuncertainty_cross sqrt(uncertainty_a^2 uncertainty_b^2)如果相关比如同源分频可以用线性相加但通常要乘一个相关系数。4.3 坑三uncertainty设得太大导致综合不收敛有一次为了“保险”我把uncertainty设到了0.25ns结果DC怎么都收敛不了面积暴涨。后来把uncertainty降到0.15ns重新综合时序轻松满足。这说明uncertainty不是越大越好它需要和你的时钟周期匹配。一般来说uncertainty不应该超过时钟周期的10%到15%。如果时钟周期是2nsuncertainty最好在0.2ns到0.3ns之间超过这个范围就要重新审视你的时钟树方案了。4.4 坑四CTS之后忘了更新uncertainty综合阶段用的uncertainty是估计值CTS之后真实的skew出来了应该用真实的skew替换掉uncertainty里的skew部分。但很多人忘了这一步导致PR阶段的时序分析仍然用着综合阶段的保守估计时序怎么都修不干净。正确的做法是CTS之后用report_clock_timing -type skew查看真实skew然后重新计算uncertainty把skew部分去掉只保留jitter和margin。5. 不同工艺节点下的uncertainty策略5.1 成熟工艺28nm及以上在28nm及以上的成熟工艺里走线延迟占主导skew通常比较大但jitter相对较小。uncertainty的设置可以偏保守一些setup的uncertainty可以取时钟周期的12%到15%hold取5%到8%。这个阶段的时钟树通常用普通的CTS就能满足要求不需要太复杂的时钟树结构。5.2 先进工艺16nm及以下在16nm及以下走线延迟占比下降但OCV和jitter的影响变大。uncertainty的设置需要更精细setup的uncertainty可以取时钟周期的8%到12%hold取4%到6%。这个阶段要特别注意OCV的建模通常需要用AOCV或者POCV来替代传统的derateuncertainty里的margin也要相应调整。另外先进工艺下时钟树的功耗占比很高uncertainty设得太大可能会导致过度插入buffer功耗飙升。所以在这个阶段uncertainty的设定要和功耗目标一起考虑。5.3 一个对比表格工艺节点setup uncertainty占周期比例hold uncertainty占周期比例时钟树策略28nm及以上12%-15%5%-8%普通CTS16nm-22nm10%-13%4%-7%CTS有用skew7nm-14nm8%-12%4%-6%H-tree或clock mesh7nm以下6%-10%3%-5%定制时钟树这个表格是经验值具体项目还要根据时钟频率、设计规模和功耗目标来调整。6. 从综合到PRuncertainty的交接6.1 综合阶段的目标综合阶段设uncertainty的目标是让DC在一个“合理悲观”的假设下优化逻辑。这个假设不能太乐观否则后端修不回来也不能太悲观否则面积和功耗浪费。一般来说综合阶段的uncertainty应该比最终签核signoff时的uncertainty大20%到30%给后端留出优化空间。6.2 PR阶段怎么更新到了PR阶段CTS做完之后你有了真实的skew数据。这时候应该用report_clock_timing -type skew查看每个时钟域的skew然后重新计算uncertaintyuncertainty_pr jitter margin注意这里不再加skew因为skew已经体现在实际的时钟树延迟里了。margin可以保留用来覆盖OCV和电源噪声。如果你在PR阶段用了AOCVmargin可以适当减小。6.3 签核阶段的最终值签核阶段uncertainty通常只保留jitter和很小的margin。有些团队甚至会把uncertainty设为零完全依赖OCV derate和真实的时钟树分析。但这取决于你的签核流程和工具配置。如果签核工具已经能精确建模jitteruncertainty可以设得很小如果签核工具还是用传统的derate方法uncertainty仍然需要保留一定的margin。7. 几个容易被忽略的细节7.1 时钟反相和分频的影响如果你的时钟经过了反相器或者分频器uncertainty的设置需要特别注意。反相器会引入额外的抖动分频器会改变jitter的传递特性。对于分频时钟jitter通常会按分频比放大因为分频器的输出边沿依赖于输入边沿的累积。所以在设置分频时钟的uncertainty时要把jitter乘以分频比。7.2 多路时钟选择器clock mux的处理当设计里有clock mux时不同的时钟路径可能有不同的jitter和skew。这时候你需要对每个输入时钟分别设置uncertainty然后在mux输出端用最坏情况的值。DC通常会自动取最坏情况但你还是应该用report_clock_timing确认一下。7.3 时钟门控clock gating的影响时钟门控单元会引入额外的延迟和抖动。在综合阶段如果你用了clock gatinguncertainty需要适当增加通常加5ps到10ps。这个值看起来不大但在高频设计里可能会影响时序收敛。7.4 别忘了检查generated clockGenerated clock是从主时钟派生出来的它的uncertainty默认会继承主时钟的值。但如果你对generated clock有特殊要求可以用set_clock_uncertainty单独覆盖。记得用report_clocks确认generated clock的uncertainty是否正确继承。8. 一个完整的约束脚本示例下面是一个实际项目里的时钟约束脚本片段供参考# 创建主时钟 create_clock -name clk_sys -period 2.5 -waveform {0 1.25} [get_ports clk_sys_in] # 设置时钟延迟 set_clock_latency -source 0.8 [get_clocks clk_sys] set_clock_latency 0.4 [get_clocks clk_sys] # 设置时钟不确定性 # jitter 15ps, skew 120ps, margin 15ps set_clock_uncertainty -setup 0.15 [get_clocks clk_sys] set_clock_uncertainty -hold 0.07 [get_clocks clk_sys] # 创建分频时钟 create_generated_clock -name clk_div2 -source [get_ports clk_sys_in] -divide_by 2 [get_pins div_reg/Q] # 分频时钟的uncertaintyjitter按分频比放大 set_clock_uncertainty -setup 0.18 [get_clocks clk_div2] set_clock_uncertainty -hold 0.09 [get_clocks clk_div2] # 跨时钟域约束 set_clock_groups -asynchronous -group {clk_sys} -group {clk_div2} # 检查设置 report_clock_timing -type uncertainty report_clocks这个脚本里clk_sys的setup uncertainty是0.15nshold是0.07ns。clk_div2是二分频jitter放大后setup取0.18nshold取0.09ns。跨时钟域用set_clock_groups -asynchronous切断所以不需要额外的跨时钟域uncertainty。9. 怎么验证uncertainty设得合不合理9.1 用report_timing看余量分布设完uncertainty之后跑一次report_timing看看最差路径的余量是多少。如果余量在-0.1ns到0.1ns之间说明uncertainty设得比较合理如果余量很大比如0.5ns以上说明uncertainty可能设得太松如果余量很差比如-0.3ns以下说明uncertainty可能设得太紧或者逻辑本身有问题。9.2 做一次uncertainty扫描如果你不确定uncertainty设多少合适可以做一次扫描从0.05ns到0.25ns每隔0.05ns跑一次综合看看面积和时序的变化。通常你会看到一个拐点uncertainty小于某个值时面积变化不大超过这个值后面积急剧上升。这个拐点就是比较合理的uncertainty值。9.3 和后端对齐最后一定要和后端工程师对齐uncertainty的假设。综合阶段用的uncertainty应该和后端CTS之后的真实skewjitter匹配。如果后端发现综合阶段的uncertainty太乐观他们可以要求你重新综合如果太悲观他们可以要求你放宽约束。这个沟通环节很重要能避免很多返工。10. 写在最后的一些个人体会做了这么多年综合我最大的体会是uncertainty不是一个“填个数字就行”的约束它背后是对时钟系统的理解。你得知道你的时钟源是什么、jitter有多大、时钟树大概长什么样、后端会用什么策略做CTS才能给出一个合理的uncertainty。另外uncertainty不是一成不变的。从综合到PR到签核它应该是一个逐步收敛的过程综合阶段最保守PR阶段用真实skew替换签核阶段只保留jitter和最小margin。如果你在整个流程里都用同一个uncertainty值要么综合阶段太乐观要么签核阶段太悲观总有一头会出问题。最后分享一个小技巧如果你不确定jitter和skew怎么分配可以先设一个总的uncertainty然后在PR阶段用真实的skew去反推jitter。比如综合时设了0.15nsPR阶段真实skew是0.10ns那jittermargin就是0.05ns。这个值可以反馈到下一个项目的综合阶段作为jitter的参考。这样迭代几次你对uncertainty的估计会越来越准。