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关心的是最好情况(时钟来得晚,数据来得早)。你想想看,这两个方向的不确定性,其实不一样。

我的经验:setup uncertainty一般设时钟周期的2%~5%。hold uncertainty可以设小一点,0.05ns~0.1ns就够。别设太大,否则工具会过度悲观,白白浪费面积和功耗。

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]

这样做的好处是,工具知道分频时钟和主时钟是同源的。做跨时钟域分析时,能自动识别它们的关系。

注意:生成时钟的 -source 要指向时钟源(主时钟的端口或引脚),而不是分频器的时钟输入引脚。我曾经在这个细节上吃过亏,工具报了一堆莫名其妙的违例。

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约束编写核心逻辑 主时钟约束 生成时钟约束 时钟不确定性 (jitter+skew) 输入延迟约束 输出延迟约束 常见陷阱:异步复位未约束 | 跨时钟域未处理 | IO与PCB不匹配

这张图从左到右、从上到下,展示了SDC约束的完整链条。从时钟源头开始,一步步定义清楚,最后落到IO边界上。每一步都环环相扣,漏掉一个,后面全乱套。

好了,关于SDC约束进阶,今天就聊到这儿。记住:约束写得好,后端没烦恼。多花点时间在约束上,后面能省十倍的时间。