时序违例分析与能力提升实战 - 第3节
时钟定义:时序分析的“心跳”
做时序分析,第一步就是定义时钟。这就像给人做体检,得先知道心跳是多少。时钟定义错了,后面所有分析都是白搭。
我个人习惯,拿到一个新设计,第一件事就是打开SDC文件,看看时钟有没有定义完整。你想想看,如果连时钟周期都没说清楚,工具怎么知道你的电路能不能跑在目标频率上?
create_clock 的基本用法
最常用的命令就是 create_clock。它的核心参数就几个:
- 周期(period):时钟的周期,单位是ns
- 波形(waveform):上升沿和下降沿的位置
- 时钟名(name):给时钟起个名字,方便后面引用
# 创建一个100MHz的时钟,占空比50%
create_clock -name clk_sys -period 10.0 [get_ports clk_in]
# 创建一个有相移的时钟
create_clock -name clk_90 -period 10.0 -waveform {2.5 7.5} [get_ports clk_phase]
嗯,这里要注意一点:waveform 的写法。第一个值是上升沿时间,第二个是下降沿时间。默认是 {0 5},也就是50%占空比。
关键点:时钟周期决定了设计的最高工作频率。100MHz对应10ns周期,500MHz对应2ns周期。周期越短,时序收敛越难。
生成时钟:分频与倍频
实际项目中,我们很少直接用外部时钟。一般都是通过PLL产生各种频率的时钟。这时候就要用 create_generated_clock。
我在项目中遇到过,有人直接把分频后的时钟用 create_clock 定义,结果时序分析全乱了。为什么?因为工具不知道这个时钟和源时钟的关系,没法做跨时钟域分析。
# 源时钟
create_clock -name clk_pll_ref -period 10.0 [get_ports pll_ref]
# 2分频时钟
create_generated_clock -name clk_div2 \
-source [get_ports pll_ref] \
-divide_by 2 \
[get_pins pll/div2_out]
# 倍频时钟(2倍频)
create_generated_clock -name clk_x2 \
-source [get_ports pll_ref] \
-multiply_by 2 \
[get_pins pll/x2_out]
小技巧:生成时钟的 -source 一定要指向源时钟的时钟根节点,通常是PLL的输入引脚。指向错了,时序分析会报奇怪的违例。
输入输出延迟:芯片与外部世界的“握手”
芯片不是孤立存在的。它要和外部器件通信,比如DDR内存、ADC、DAC。这时候就需要定义输入输出延迟,告诉工具外部信号什么时候到达、什么时候需要被采样。
说白了,输入输出延迟就是描述芯片边界上的时序关系。我刚开始做STA时,这块经常搞混,后来总结了一个口诀:输入看外部,输出看内部。
输入延迟:信号从外部来
输入延迟描述的是:外部信号相对于时钟边沿,什么时候到达芯片引脚。
# 输入延迟:信号在时钟上升沿前2ns到达
set_input_delay -clock clk_sys -max 2.0 [get_ports data_in]
# 输入延迟:信号在时钟上升沿后1ns到达(负值表示在时钟之后)
set_input_delay -clock clk_sys -min -1.0 [get_ports data_in]
这里有个容易踩的坑:-max 和 -min 的含义。-max 对应建立时间检查,-min 对应保持时间检查。我曾经见过有人把这两个值写反了,结果芯片流片回来,输入接口死活不稳定。
避坑指南:我曾经在一个项目中,外部器件的输出延迟是3ns,但我在SDC里只写了 set_input_delay 3.0,没区分max和min。结果工具默认max和min都是3ns,导致保持时间违例没检查出来。后来花了整整两天才定位到这个问题。
输出延迟:信号要送到外部
输出延迟描述的是:芯片内部信号从时钟边沿开始,到外部器件需要它稳定,这中间有多少时间。
# 输出延迟:外部器件需要在时钟上升沿前1.5ns看到数据
set_output_delay -clock clk_sys -max 1.5 [get_ports data_out]
# 输出延迟:外部器件在时钟上升沿后0.5ns还需要数据保持
set_output_delay -clock clk_sys -min -0.5 [get_ports data_out]
你想想看,输出延迟其实是在告诉工具:留给芯片内部路径的时间还有多少。如果输出延迟设得太大,内部路径的时序裕量就变小了。
虚假路径:告诉工具“别管这里”
不是所有路径都需要做时序检查。有些路径在功能上永远不会被激活,或者即使有违例也不影响功能。这时候就要用 set_false_path 告诉工具:跳过这些路径。
虚假路径用得好,可以大大减少时序违例的数量,让你专注于真正需要关注的路径。用得不好,可能会漏掉真正的bug。
什么时候用虚假路径?
- 跨时钟域的同步器路径:经过两级或三级同步器的路径,不需要做STA
- 测试模式下的路径:scan_mode、mbist_mode等测试信号
- 复位信号:异步复位信号通常不需要做时序检查
- 静态配置信号:只在芯片初始化时配置一次的信号
# 跨时钟域虚假路径
set_false_path -from [get_clocks clk_a] -to [get_clocks clk_b]
# 测试模式虚假路径
set_false_path -from [get_ports scan_enable] -to [get_clocks clk_sys]
# 异步复位虚假路径
set_false_path -from [get_ports rst_n] -to [get_pins *reg*/D]
重要提醒:虚假路径不是万能的。对于跨时钟域路径,如果数据需要正确传递,必须使用同步器,并且用 set_clock_groups 或 set_false_path 告诉工具跳过这些路径。但同步器本身的时序还是要保证的。
set_clock_groups vs set_false_path
这两个命令都可以处理跨时钟域路径,但用法有区别:
| 命令 | 适用场景 | 特点 |
|---|---|---|
| set_false_path | 特定路径或特定时钟域之间 | 更灵活,可以指定from/to |
| set_clock_groups | 多个时钟域之间全部路径 | 一次性屏蔽所有跨时钟域路径 |
# 使用set_clock_groups屏蔽所有跨时钟域路径
set_clock_groups -asynchronous \
-group [get_clocks clk_a] \
-group [get_clocks clk_b] \
-group [get_clocks clk_c]
我个人更推荐用 set_clock_groups 来处理跨时钟域。为什么?因为它更清晰,一眼就能看出哪些时钟域是异步的。而 set_false_path 需要一条一条写,容易遗漏。
经验之谈:我在一个大型SoC项目中,有十几个时钟域。刚开始用 set_false_path 一条一条写,写了上百条。后来改用 set_clock_groups,代码量减少了90%,而且可读性大大提高。
知识体系总览
下面这张图,是我对本章知识点的总结。你可以把它当作一个快速参考:
这张图把本章的三个核心知识点串起来了。时钟定义是基础,输入输出延迟是边界约束,虚假路径是优化手段。三者配合好了,你的SDC文件才算合格。
好了,关于时钟定义、输入输出延迟和虚假路径,我就讲这么多。这些内容看起来简单,但真正用好需要大量实践。下次遇到时序违例,不妨先从SDC约束入手检查一遍,往往能发现不少问题。