3. SDC约束编写进阶:如何编写精确的时钟约束、输入输出延迟约束,以及常见的约束陷阱
SDC约束,说白了就是给芯片画个「时间红线」。你画得准,后端工具就知道往哪儿使劲。画歪了?工具要么瞎忙活,要么直接摆烂。我见过太多项目,最后时序崩了,追根溯源都是约束写得有问题。
今天咱们就聊聊,怎么把SDC约束写得更精确、更靠谱。我会把时钟约束、IO延迟约束,还有那些坑,一个一个掰开讲。
3.1 时钟约束:别只写个周期就完事
很多人写时钟约束,就写一句 create_clock -period 10 [get_ports clk]。嗯,这确实能跑。但你想过没有?真实时钟哪有这么完美?
3.1.1 时钟源延迟与不确定性
时钟从PLL出来,经过时钟树,到每个触发器的CK端,路径长度不一样。这个差异,就是时钟偏斜(clock skew)。
我个人习惯,在约束里明确写出时钟不确定性:
# 设置时钟不确定性,包含jitter和skew
set_clock_uncertainty -setup 0.2 [get_clocks clk_sys]
set_clock_uncertainty -hold 0.05 [get_clocks clk_sys]
为什么setup和hold要分开设?因为setup关心的是最坏情况(时钟来得早,数据来得晚),hold关心的是最好情况(时钟来得晚,数据来得早)。你想想看,这两个方向的不确定性,其实不一样。
3.1.2 生成时钟的传播
分频时钟、门控时钟,这些都要用 create_generated_clock 来定义。我见过有人直接用 create_clock 在分频器输出上再创建一个时钟——这其实是个坑。
正确的做法是:
# 主时钟
create_clock -name clk_sys -period 10 [get_ports clk]
# 二分频时钟,从主时钟衍生
create_generated_clock -name clk_div2 -source [get_ports clk] \
-divide_by 2 [get_pins u_divider/Q]
这样做的好处是,工具知道分频时钟和主时钟是同源的。做跨时钟域分析时,能自动识别它们的关系。
3.2 输入输出延迟约束:别让芯片边界成为盲区
芯片不是孤岛。它要和外部器件通信。输入输出延迟约束,就是告诉工具:外部世界的时间窗口是什么样的。
3.2.1 输入延迟:外部数据什么时候到?
假设你的芯片从一个外部ADC接收数据。ADC在时钟上升沿之后5ns才把数据送出来。那你的输入延迟就是5ns。
# 输入延迟约束
set_input_delay -clock clk_sys -max 5.0 [get_ports data_in]
set_input_delay -clock clk_sys -min 2.0 [get_ports data_in]
这里 -max 和 -min 分别对应最晚和最早到达时间。为什么要有两个?因为setup检查用最晚到达,hold检查用最早到达。
我建议你画个时序图,把外部器件的datasheet参数标上去。然后对着图写约束,不容易漏。
3.2.2 输出延迟:外部器件要等多久?
输出延迟正好反过来。它描述的是:外部器件在时钟沿之后多久,才需要看到稳定的数据。
# 输出延迟约束
set_output_delay -clock clk_sys -max 4.0 [get_ports data_out]
set_output_delay -clock clk_sys -min 1.0 [get_ports data_out]
这里 -max 对应外部器件的setup要求,-min 对应hold要求。说白了,就是告诉工具:你输出的数据,必须在时钟沿之后1ns到4ns这个窗口内保持稳定。
3.3 常见约束陷阱:我踩过的那些坑
做后端这么多年,我踩过的坑真不少。挑几个典型的说说,你遇到了能少走弯路。
3.3.1 陷阱一:忘了约束异步复位
异步复位信号,如果不加约束,工具会默认它和时钟同步。结果就是,复位释放的时序分析完全不对。
正确的做法是:
# 将异步复位设为false path
set_false_path -from [get_ports rst_n] -to [get_clocks clk_sys]
# 或者更精确:只约束复位释放
set_case_analysis 0 [get_ports rst_n]
我个人习惯用 set_false_path,简单粗暴。但要注意,这只适用于异步复位。如果是同步复位,就别这么干。
3.3.2 陷阱二:多时钟域交叉没处理好
两个不同源的时钟域之间传数据,如果不加约束,工具会试图分析所有路径。结果就是一堆假违例,浪费优化时间。
# 跨时钟域路径设为false path
set_false_path -from [get_clocks clk_a] -to [get_clocks clk_b]
set_false_path -from [get_clocks clk_b] -to [get_clocks clk_a]
但要注意:如果跨时钟域用了同步器(两级触发器),那同步器内部的路径还是要分析的。这时候可以用 set_clock_groups 来更精细地控制。
3.3.3 陷阱三:IO约束和实际PCB不匹配
这个坑我印象特别深。有一次,我按datasheet写了输入延迟,但PCB上走线太长,实际延迟比约束里大了一倍。结果芯片能工作,但时序余量几乎为零。
从那以后,我每次写IO约束,都会和PCB工程师对一下走线长度。算上PCB走线延迟,再留点余量,这样才保险。
set_input_transition,导致输入端口上的信号转换时间被工具默认成0.1ns。实际PCB上信号边沿很慢,setup直接崩了。现在我的习惯是:每个输入端口都显式设一下transition。
3.4 本章知识体系
下面这张图,把SDC约束的核心逻辑串起来了。你可以对照着看,检查自己有没有遗漏。
这张图从左到右、从上到下,展示了SDC约束的完整链条。从时钟源头开始,一步步定义清楚,最后落到IO边界上。每一步都环环相扣,漏掉一个,后面全乱套。
好了,关于SDC约束进阶,今天就聊到这儿。记住:约束写得好,后端没烦恼。多花点时间在约束上,后面能省十倍的时间。