2. 时序约束概念:SDC约束、时钟定义、输入输出延迟

说到时序约束,很多刚入行的朋友会觉得这就是一堆烦人的命令。其实不然。时序约束说白了,就是告诉工具你的芯片要跑多快,数据从哪里来,要到哪里去。我刚开始做项目时,也曾觉得约束差不多就行,结果有一次综合后的时序报告惨不忍睹...从那天起,我才真正开始认真对待每一行约束。

核心观点:时序约束是连接设计意图与工具实现的桥梁。没有它,工具就像盲人摸象.

2.1 什么是SDC?

SDC是Synopsys Design Constraints的缩写。它是业界标准约束格式,几乎所有数字芯片设计工具都支持。你想想看,无论是Synopsys的DC、PrimeTime,还是Cadence的Genus、Tempus,底层拿到的都是同一套约束语言。

SDC文件本质上是一个Tcl脚本。每一行都是一条命令。工具按顺序执行这些命令,构建出时序分析的“游戏规则”。我个人习惯把SDC文件分为三大块:

  • 时钟定义——告诉工具时钟长什么样
  • 输入输出延迟——告诉工具芯片边界的数据怎么走
  • 异常路径——告诉工具哪些路径不用查

这章我们先聊前两块。异常路径后面有专门章节讲。

2.2 时钟定义——时序分析的“心跳”

时钟是时序分析的基准。没有时钟,就没有建立时间和保持时间的概念。

最常见的时钟定义命令是create_clock。来看一个实际例子:

# 定义一个100MHz的主时钟,占空比50%
create_clock -name clk_sys -period 10.000 \
             -waveform {0.000 5.000} [get_ports clk_in]

这里有几个关键参数:

参数 含义 常见坑点
-period 时钟周期,单位ns 别忘了单位是ns。100MHz就是10ns
-waveform 上升沿和下降沿的位置 默认就是50%占空比,但不能省略
[get_ports] 时钟源端口 必须指定物理端口,不能是内部节点

注意:时钟定义必须写在SDC文件最前面。因为后面的输入输出延迟、异常路径都依赖时钟信息。我曾经见过有人把create_clock写在文件末尾,结果前面所有约束都被工具忽略——查了整整半天才找到原因。

除了主时钟,还有生成时钟。比如PLL分频出来的时钟:

# 定义一个由clk_sys二分频得到的时钟
create_generated_clock -name clk_div2 \
                       -source [get_ports clk_in] \
                       -divide_by 2 \
                       [get_pins u_pll/clk_out]

这里要注意-source指定的是源时钟的物理节点,不是时钟名。很多新手把时钟名写进去,工具直接报错。我见过最离谱的是有人写了clk_sys——那是时钟名,不是pin或port,工具当然不认识。

2.3 输入延迟——数据进芯片的“第一关”

数据从片外进入芯片,肯定有延迟。这个延迟来自PCB走线、片外器件的输出延迟等等。set_input_delay就是告诉工具这个延迟有多大。

举个例子:假设片外器件在时钟上升沿后2ns才输出数据,那输入延迟就是2ns。

# 设置输入延迟,相对时钟clk_sys的上升沿
set_input_delay -clock clk_sys -max 2.500 [get_ports data_in*]
set_input_delay -clock clk_sys -min 1.000 [get_ports data_in*]

这里为什么要分-max和-min?

  • -max:最大延迟,用于建立时间分析。数据来得越晚,留给内部逻辑的时间越少。
  • -min:最小延迟,用于保持时间分析。数据来得越早,越容易和上一拍数据冲突。

小技巧:在做输入延迟约束时,我建议先拿到片外器件的datasheet,找到它的t_co(时钟到输出延迟)参数。输入延迟就等于t_co_max和t_co_min。不要自己瞎猜,数据手册说了算。

2.4 输出延迟——数据出芯片的“最后一关”

输出延迟的思路和输入延迟是对称的。set_output_delay告诉工具,数据从芯片内部到达片外器件,还有多少时间预算留给芯片内部。

# 设置输出延迟,相对时钟clk_sys的上升沿
set_output_delay -clock clk_sys -max 4.000 [get_ports data_out*]
set_output_delay -clock clk_sys -min 1.500 [get_ports data_out*]

输出延迟的物理意义是:片外器件需要数据在时钟沿之前建立好。假设片外器件的建立时间要求是4ns,那输出延迟就是4ns。同样,保持时间对应-min。

我自己总结了一个小技巧:

  1. 输出延迟 = 片外器件的建立时间要求(对-max来说)
  2. 输出延迟 = 片外器件的保持时间要求(对-min来说)

这样记就不会搞反了。

2.5 知识体系总览

下面这张图概括了时序约束的核心逻辑。画的是数据从片外进来,经过内部逻辑,再到片外的完整路径。你看一眼就能明白约束之间是怎么配合的。

时序约束核心逻辑结构图 时序约束核心逻辑:从输入到输出 set_input_delay 定义外部输入路径延迟 create_clock 定义时钟周期与波形 set_output_delay 定义外部输出路径延迟 内部组合逻辑 + 寄存器时序路径 由工具根据网表自动分析,受约束边界限制 建立时间分析(setup) 检查数据是否来得足够快 保持时间分析(hold) 检查数据是否变得太快 输入输出延迟 + 时钟定义 → 构成完整时序约束边界 → 驱动STA分析

2.6 避坑指南:我踩过的几个典型坑

坑1:忘记加-clock参数

很多人写set_input_delay时,漏掉了-clock。工具不会报错,但会默认使用所有时钟,结果分析结果完全不对。我记得有一次帮同事查问题,他折腾了两天没搞定,我一看——就是少了-clock。

坑2:max和min搞反

这其实是对概念理解不透。我建议你记住一句话:数据来得越晚,setup越紧张;数据来得越早,hold越紧张。所以-max对应晚到,-min对应早到。

坑3:生成时钟的-source指向了时钟名

-source必须指向物理pin或port。你可以用get_pins或者get_ports,但不要用get_clocks。这是新手最容易犯的错。

实用建议:每写完一条约束,立刻跑一遍check_timing或者report_timing看有没有unclocked register。如果发现大量没有时钟的寄存器,那八成是时钟定义出了问题。别等到综合跑完才检查。

2.7 总结

时序约束说白了就是三件事:

  • 定义时钟——定下节奏
  • 定义输入延迟——数据什么时候进来
  • 定义输出延迟——数据什么时候出去

这三件事做好了,后面的STA分析就有个靠谱的基础。我见过太多项目因为约束写错,导致最后一版芯片频率上不去,或者功能不稳定。花点时间把约束写对,是最划算的投资。