1. 一个让我返工三周的时钟约束事故
“时钟约束这种东西,写的时候不觉得,跑后端的时候全来讨债。”这是我跟后端同事对完第一轮CTS结果后,脑子里最真实的一句话。当时我负责一个多功能定时器子系统的DC综合,时钟结构其实不算复杂:一个PLL产生160MHz主时钟,一个分频寄存器产生20MHz低速时钟,还有一条从外部IO进来的32.768kHz RTC时钟,与主频域之间用异步FIFO交互。就是这样一个看起来“很标准”的结构,我交出去的第一版SDC,让整个后端流程翻了三周车。
第一次返工是因为uncertainty设得太大,160MHz主时钟本来周期只有6.25ns,我拍脑袋给了0.5ns的setup uncertainty,综合工具为了满足这个余量,把关键路径cell尺寸撑得巨大,功耗飙升,性能反而没过。第二次返工是APB接口的input/output delay约束里没有虚拟时钟,后端做CTS后,接口时序的slack全乱,排查半天才发现约束参考系就不对。第三次是时钟MUX的地方,我没有对输出端做处理,工具在综合报告里自动推断出一个新时钟,导致CTS阶段同一个模块上长了两棵时钟树。
三次返工的共同点都有一个:SDC语法本身没报错,check_timing也没有致命警告,但约束背后的物理意义不对。DC综合在整个RTL到GDSII流程里是最看重“语义”的一步——RTL是你描述的行为,SDC是你告诉工具“这个行为在真实芯片上如何被时钟采样”。你给错了,工具不会大喊大叫,而是默默把所有偏差留到后面的CTS或者signoff阶段爆发。
这也是我写这篇文章的动机。大部分教程和user guide只教你“这个命令是干什么的”,没人告诉你“在真实项目中,这个命令会在物理实现里引起什么连锁反应”。今天聊的这几点,都是我实际踩过、也帮别人排过的坑,每个都配了可以照着改的工程示例。文章比较适合正在做数字IC前端或者刚接触后端的同学;如果你已经跑过一两个项目,看到某些章节可能会心一笑,那就是我写它的意义了。
回到流程本身:DC综合之后,网表要经过DFT插入、Floorplan优化,再走CTS、布线、signoff STA。前端对物理实现影响最大的,一是RTL里写出的时钟结构,二就是这份SDC。后面的STA工具会读同一份约束,后端ECO也会以它为准。你可以把SDC理解成前端和后端之间的“时序契约书”——写得不严谨,后面每一步都要为这个不严谨买单。
2. 时钟不确定性:余量不是越大越稳,我吃了0.08的亏
2.1 Setup和Hold的不确定性必须分开算
很多工程师在DC里写set_clock_uncertainty时非常豪爽,一个0.2ns怼上去,觉得“余量大一点总归更安全”。但实际上,uncertainty不是时序余量,它是你提前支付给物理实现的“误差预算”。预算给得越多,工具在逻辑优化阶段能够使用的组合逻辑周期就越短,结果就是面积更大、功耗更高,甚至出现“为了满足一个不合理的误差预算,反而把真正的关键路径做坏了”的怪现象。
更关键的一点:setup和hold两种检查,物理上对应的误差来源完全不同。Setup检查关心的是同一个时钟沿在发起端和捕获端之间的偏斜,加上时钟源本身的周期抖动、片上变异(OCV)带来的偏差;而hold检查关心的是相邻两个时钟沿之间的相对偏差,jitter对它的影响远小于setup。所以SDC里通常要分开写:
set_clock_uncertainty -setup 0.150 [get_clocks cpu_clk] set_clock_uncertainty -hold 0.050 [get_clocks cpu_clk]这里0.15和0.05不是随便拍的。比如PLL输出时钟的周期jitter是30ps,CTS目标是将局部skew做到90ps以内,OCV再贡献大约40ps,那么setup uncertainty大致可以估算为:
sqrt(30ps^2 + 90ps^2) + 40ps ≈ 135ps,取整150pshold uncertainty主要取局部skew再加少量margin,所以50ps是一个比较常见的起点。如果你只设一个信号值,工具会同时用它约束setup和hold,结果就是hold路径被过度悲观,后端CTS之后会看到大片多余的hold修整buffer。
2.2 一次28nm项目的“拍脑袋”教训
我之前在28nm的MCU项目里,一度为了满足客户对频率的要求,把CPU时钟的setup uncertainty压到了0.08ns。DC综合报告非常漂亮,频率确实达标了,而且关键路径还有少量slack。可这个漂亮持续到后端同事做完CTS就破功了:由于OCV和时钟树本身的非理想性,signoff阶段setup slack直接变负,只能一轮接一轮ECO。后来我把uncertainty改回0.15ns重新综合,频率从480MHz掉到465MHz,但后端的时序收敛变得顺畅了很多,整体PPA反而更好。
那段时间我做了一个对比表格,发给团队作为后续项目的初始参考:
| 工艺节点 | 工作频率 | Setup uncertainty建议 | Hold uncertainty建议 | 备注 |
|---|---|---|---|---|
| 55nm | 300MHz | 0.2ns~0.25ns | 0.08ns~0.1ns | CTS目标skew较大 |
| 28nm | 500MHz | 0.12ns~0.18ns | 0.04ns~0.06ns | PLL jitter和OCV共同作用 |
| 16nm以下 | 1GHz+ | 0.1ns~0.15ns | 0.03ns~0.05ns | 需要配合更精细的OCV设置 |
注意:这张表只是起点,不是标准答案。真实项目里一定要结合PLL数据手册、后端Floorplan评估报告和CTS目标设定来定,不能直接抄作业。
3. 虚拟时钟:IO边界的约束不能用内部时钟硬凑
3.1 用内部时钟做参考为什么不准
IO边界约束是另一个重灾区。很多人写input delay时会顺手写成这样:
set_input_delay -clock [get_clocks cpu_clk] -max 3 [get_ports data_in]表面看没问题,因为data_in的确是由cpu_clk域的外部器件驱动的。但DC在计算IO路径时序时,会把这个参考时钟本身的时钟树延迟(clock latency)算进去。综合阶段的时钟网络是ideal model,工具不会为input delay自动补上时钟延迟的补偿。换句话说:你告诉工具“外部器件在时钟沿后3ns把数据送到芯片引脚”,工具理解成“内部时钟沿到达寄存器之前3ns数据就要稳定到引脚上”,两者能差出好几ns。
正确做法是定义一个虚拟时钟,专门代表外部器件接口侧的时钟,input/output delay全部相对于它设置:
create_clock -name vclk -period 40 -waveform {0 20} set_input_delay -max 8 -clock vclk [get_ports {addr data}] set_output_delay -max 6 -clock vclk [get_ports {ack}]为什么要强调“虚拟”?因为vclk不是芯片内部任何实际时钟树的根,工具不会对它做CTS,也不会在后端分析里给它插延迟。它只是一个纯粹的参考系,把外部接口的时序关系和内部时钟网络切开,这样才不会把时钟树延迟重复计算。
3.2 APB接口虚拟时钟约束实例
假设我们要综合一个带APB从机接口的子系统,APB时钟来自外部时钟发生器,频率25MHz,与内部CPU时钟无关。RTL里APB逻辑经过异步桥与CPU域交互,APB侧时钟是pclk,内部CPU域是hclk。正确的SDC应该是:
create_clock -name pclk -period 40 [get_ports pclk] create_clock -name vclk -period 20 -waveform {0 10} set_input_delay 6 -clock vclk [get_ports apb_sel] set_output_delay -max 4 -clock vclk [get_ports apb_ready]在这个例子里,vclk是APB总线master侧时钟的抽象。如果不建虚拟时钟,APB接口的setup/hold窗口会算错,综合后可能表面看着收敛,接入真实SoC后对外通信却时好时坏。虚拟时钟还有一个用法:约束两个PLL时钟域之间的边界IO时,virtual clock可以作为“对面时钟”的代理,避免在同一个时钟对象上同时做输入延迟和内部时序分析。
4. 生成时钟、时钟MUX和assign造钟:三个常见的约束雷区
4.1 分频寄存器:generated_clock不是可选项
RTL里做偶数分频很常见,类似这样:
always @(posedge clk_100m) begin if (rst_n) clk_div <= 1'b0; else clk_div <= ~clk_div; endDC里对这个clk_div的描述,必须用create_generated_clock,不能直接create_clock:
create_clock -name clk_100m -period 10 [get_ports clk_100m] create_generated_clock -name clk_50m -source [get_ports clk_100m] \ -divide_by 2 [get_pins clk_div_reg/Q]冷知识在于:如果在这个分频点直接create_clock,工具会认为clk_50m是一个独立时钟源,破坏它与主时钟的相位关系。后端CTS也会把clk_50m当成独立的时钟树根,导致两棵时钟树之间的同步关系无法被正确分析和优化。而用create_generated_clock之后,工具会推导出它是从clk_100m派生的,后端的CTS可以把它归并到同一棵时钟树里处理,时序收敛效率高很多。
4.2 时钟MUX输出端别乱create_clock
片上时钟切换是另一个藏雷点。比如CLK_100M和CLK_50M进入一个CLKMUX,由sel信号选择后输出给内部逻辑。如果你在mux输出端写:
create_clock -name clk_mux_out -period 10 [get_pins clkmux/z]这就出事了。工具会认为CLK_50M和CLK_100M同时作用在同一个节点上,产生大量虚假的跨时钟路径;CTS阶段也不知道该以哪条输入为主建树,常常在mux两个输入端都做平衡,面积功耗白白浪费。
正确的是分别在两个输入端定义时钟,然后用set_clock_sense阻断mux输出端的自动传播:
create_clock -name clk_100m -period 10 [get_pins clkmux/in0] create_clock -name clk_50m -period 20 [get_pins clkmux/in1] set_clock_sense -stop_propagation -pins clkmux/z如果mux选择信号在综合时已经固定,还可以用set_case_analysis指定选择值,工具会直接沿着被选中的一路时钟做分析,另一路时钟树在CTS阶段就不会被额外生长。
4.3 RTL里assign出来的时钟,比想象中更难收场
新人RTL代码里经常出现这种写法:
assign clk_gated = clk & en;这种组合逻辑产生的时钟在仿真里看起来没毛病,一进DC综合就原形毕露。工具默认会把它当普通组合逻辑网络,不认为它是时钟,于是所有由clk_gated驱动的触发器都可能出现“no clock”警告。就算你强行create_clock,工具又会保留这段组合逻辑,CTS之后的时钟树延迟和毛刺完全不可控。
正确姿势是在RTL里使用ICG单元,或者把门控逻辑写成数据条件:
always @(posedge clk) begin if (en) q <= d; end这种data gating写法可以让工具自动推断clock gating check。如果项目里确实因为特殊控制要求保留了门控时钟,DC里至少需要设置:
set_clock_gating_check -setup 0.2 -hold 0.1 [get_pins gated_clk_cell/CLK]热词里提到“RTL中assign的作用”——assign可以做数据选择、总线互连、逻辑运算,但唯独不适合用来“造”时钟。时钟网络必须来自PLL、分频寄存器或者库里的时钟门控单元,而不是随手assign出来的逻辑节点。
5. 跨时钟域的灰色地带:false_path不是万能药
5.1 异步FIFO路径、多比特跨域路径要区别对待
面对跨时钟域,很多工程师的处理方式是一刀切:
set_false_path -from [get_clocks clk_a] -to [get_clocks clk_b]这不完全正确。异步FIFO里的同步器路径可以false path,因为它的目的是解决亚稳态,不要求周期级收敛;但多比特数据总线如果直接跨域,一false了之就会让工具完全不检查,真实芯片上采样错误只在特定数据组合下出现,非常难排查。
在SDC里,我通常先用set_clock_groups划出异步域,再把真正需要同步的路径单独拿出来细化处理:
set_clock_groups -asynchronous -group {clk_a} -group {clk_b} set_false_path -from [get_cells sync_ff_1_reg] -to [get_cells sync_ff_2_reg] set_max_delay 10 -from [get_cells rx_sync_chain_reg*] -to [get_cells data_reg_reg*]set_clock_groups相当于告诉工具:这两个时钟域的相位关系不可预测,不要做跨域时序收敛。但它不会阻止工具沿这些路径做逻辑优化;如果异步路径上组合逻辑很深,DC依然会消耗面积去优化它。所以对真正需要控制收敛的路径,用set_max_delay或set_multicycle_path单独标注,才更贴近物理约束意图。
5.2 -asynchronous和-logically_exclusive差在哪
这是SDC里最容易被忽略的差异。两个“逻辑互斥”的时钟,比如由mux选择的两个输入,物理上来自同一时钟源或互斥通路,二者不会同时存在。这时应该用:
set_clock_groups -logically_exclusive \ -group {pll_a_clk} -group {pll_b_clk}而不是-asynchronous。原因在于:如果是逻辑互斥,工具在后端做时钟树时可以把两条路径合并或复用;如果-asynchronous,工具会认为两个时钟在物理上可能同时存在,CTS必须把它们当成独立时钟网络分别处理。我见过一个项目用-asynchronous约束两个工作模式下的时钟mux切换,CTS把PLL_A和PLL_B各建了一棵完整时钟树,功耗直接损失了百分之十几,后来改成-logically_exclusive才恢复。
5.3 一次量产后偶发失败的教训
我们之前有一款带UART和SPI接口的芯片,量产后零星出现SPI读寄存器偶尔错一个bit的现场。排查了很久,最后定位到约束:综合时把所有跨SPI时钟域的路径全部set_false_path,包括数据路径。SPI接收数据经过同步器进入总线域时,数据本身还要在一个有限窗口内稳定,但false path把这条路径的检查全取消了,工具就不会去优化对应的setup/hold,真实芯片上就有概率采样到跳变沿。
这类问题之所以难查,是因为它不报错、不固定复现,只在特定数据组合和温度电压漂移时冒出来。修复方法不复杂:把数据跨域路径从false path里拿出来,用set_max_delay约束收敛窗口,同步器路径保留false path。但它给团队的教训是:跨时钟域约束不能“先false上去再说”,要一条条路径想清楚背后到底是什么同步机制。
6. 给CTS留足温差:综合阶段就把时钟树延迟“预演”一遍
6.1 为什么DC里的时序到CTS后会变脸
DC综合阶段,时钟网络默认是ideal model,工具认为时钟沿在同一时刻到达所有触发器,不做时钟树延迟计算。到了后端CTS,时钟变成propagated model,真实的buffer/inverter延迟进来,时序自然变脸。这不是DC误报或者后端乱做,而是两个阶段对时钟网络的建模方式本来就不一样。
解决办法不是让DC直接去长时钟树,而是用set_clock_latency预估时钟网络延迟,让DC在优化路径时就把这部分时间留出来。比如后端评估报告说clk_100m从端口进去到最远寄存器大约1.2ns,那DC里可以写:
set_clock_latency 1.2 [get_clocks clk_100m]为了更接近CTS后的情况,还可以把latency设置成early和late范围。early代表时钟沿最早到达的情况,late代表最晚到达的情况,两者差值对hold分析影响很大。latency影响的是时钟沿的绝对到达时刻,uncertainty影响的是时序裕量里的偏差范围,两者角度不同,不能互相替代,但需要一起配合。
6.2 set_clock_latency怎么设才算合理
具体数值从哪来?第一个来源是老项目后端评估报告;第二个来源是时钟网络关键路径估算,根据扇出、线长模型粗算;第三个来源是PLL到时钟门控单元之间的走线延迟。一个实用的做法是分两轮综合:第一轮先用乐观latency看看逻辑深度极限,第二轮把latency上调到后端预估的数值,看真正的时序瓶颈出现在哪里。
一个容易被忽略的冷知识:set_clock_latency可以分别设置rise和fall。比如一个clock gating单元,时钟下降沿比上升沿晚到0.1ns,这个差值会让hold检查变严。如果DC不区分rise/fall,后端CTS后就可能冒出一批莫名其妙的hold violation。这类问题通常在report_timing的detail里才能看到,常规约束模板根本不会涉及。
6.3 综合完必跑的五分钟自查
最后给一份我每次综合完都会跑的自查命令:
check_timing report_clock -skew -attributes report_clock_gating -detail report_timing -delay_type min_max -clock_groupscheck_timing输出里一旦出现unconstrained endpoints或者no clock警告,说明约束一定有洞;report_clock能帮你确认虚拟时钟、生成时钟之间的关系是否符合预期。我尤其留意create_generated_clock列表里工具自动推导的时钟关系,如果突发出现奇怪的倍数关系,比如某个组合逻辑节点上自动推出3倍频,那基本可以断定约束里有冲突。
这些检查加起来不超过五分钟,但能提前暴露一大部分后期风险。我自己养成一个习惯:拿到新RTL版本后,先花半天整理一张时钟结构表,把PLL来源、分频点、MUX切换、跨域同步列清楚,再开始写SDC。这张表后来同步给后端和验证,大家讨论问题时有共同的参照物。时钟约束看起来是一堆命令,本质上是对全局时序关系的一种结构化表达。你在综合阶段偷的懒,后端都会加倍还给你。