3、时序约束入门:创建时钟约束、生成时钟约束、输入延迟约束、输出延迟约束
时序约束这东西,说白了就是告诉工具你的设计要在什么条件下跑。我刚开始做FPGA那会儿,总觉得约束可有可无,随便写个50MHz的时钟就完事了。结果呢?综合出来的时序报告一塌糊涂,上板调试更是噩梦。后来我才明白——没有约束的时序分析,就像没有地图的导航。
这一节,咱们就聊聊时序约束的四个基本功。嗯,都是我在项目里反复踩过的坑,希望能帮你少走弯路。
3.1 创建时钟约束:设计的“心跳”
时钟是FPGA设计的脉搏。没有它,所有寄存器都不知道什么时候该干活。创建时钟约束,就是告诉工具:你的时钟长什么样。
最基本的命令是 create_clock。我个人习惯在SDC文件里这样写:
# 创建一个50MHz的时钟,占空比50%
create_clock -name sys_clk -period 20.000 [get_ports clk]
这里有几个关键参数:
- -name:给时钟起个名字,方便后面引用
- -period:时钟周期,单位是ns。50MHz对应20ns
- [get_ports clk]:指定时钟从哪个端口进来
有时候时钟不是标准的50%占空比。比如DDR接口用的时钟,可能是45%或55%。这时候可以加个 -waveform 参数:
create_clock -name ddr_clk -period 5.000 -waveform {0.000 2.250} [get_ports ddr_clk_p]
这个例子表示:周期5ns(200MHz),上升沿在0ns,下降沿在2.25ns。占空比就是45%。
3.2 生成时钟约束:PLL出来的“孩子”
实际项目中,我们很少直接用晶振的原始时钟。通常会经过PLL或MMCM倍频、分频、移相。这些由PLL产生的时钟,工具不会自动识别,需要我们用 create_generated_clock 来定义。
举个例子,我用一个200MHz的输入时钟,通过PLL生成了一个400MHz的时钟:
create_generated_clock -name clk_400m -source [get_pins pll_inst/CLKIN] \
-multiply_by 2 -divide_by 1 [get_pins pll_inst/CLKOUT]
这里的关键是 -source 和 -multiply_by/-divide_by:
- -source:指定源时钟的引脚或端口
- -multiply_by:倍频系数
- -divide_by:分频系数
你想想看,如果PLL输出和输入之间有相位关系,比如移相90度,那还得加上 -phase 参数:
create_generated_clock -name clk_90 -source [get_pins pll_inst/CLKIN] \
-multiply_by 2 -divide_by 1 -phase 90 [get_pins pll_inst/CLKOUT90]
还有一个容易忽略的点:生成时钟的命名。我建议用有意义的名称,比如 clk_400m、clk_200m_90。别用 gen_clk1、gen_clk2 这种,否则时序报告里你根本分不清谁是谁。
3.3 输入延迟约束:数据从外面进来
数据从FPGA外部进来时,它相对于时钟是什么时候到达的?这就是输入延迟要解决的问题。说白了,就是告诉工具:数据在时钟沿之后多久才稳定。
输入延迟分两种:
- 最大输入延迟(set_input_delay -max):数据最晚什么时候到
- 最小输入延迟(set_input_delay -min):数据最早什么时候到
举个例子,一个SPI接口,数据在时钟上升沿之后2ns到达:
set_input_delay -clock sys_clk -max 2.000 [get_ports spi_data]
set_input_delay -clock sys_clk -min 0.500 [get_ports spi_data]
这里 -max 和 -min 的差值,就是数据的不确定性窗口。窗口越大,时序越难收敛。
有时候数据是相对于时钟下降沿的,那就要加个 -clock_fall:
set_input_delay -clock sys_clk -clock_fall -max 2.000 [get_ports data_in]
3.4 输出延迟约束:数据送到外面去
输出延迟和输入延迟是对称的。它告诉工具:数据从FPGA出去后,外部器件需要多长时间才能采到。
命令是 set_output_delay,用法类似:
set_output_delay -clock sys_clk -max 3.000 [get_ports data_out]
set_output_delay -clock sys_clk -min 1.000 [get_ports data_out]
这里的 -max 表示:外部器件在时钟沿之后最多等3ns才能采到数据。 -min 表示:最早1ns后数据就有效了。
你想想看,如果外部器件是一个DAC,它的datasheet说“数据必须在时钟沿之前2ns建立,之后1.5ns保持”,那输出延迟该怎么设?
嗯,这里有个小公式:
- 输出最大延迟 = 时钟周期 - 外部器件的建立时间
- 输出最小延迟 = 外部器件的保持时间
比如时钟周期10ns,建立时间2ns,保持时间1.5ns:
set_output_delay -clock sys_clk -max [expr 10 - 2] [get_ports dac_data]
set_output_delay -clock sys_clk -min 1.5 [get_ports dac_data]
3.5 知识体系总览
为了让你更直观地理解这四类约束的关系,我画了一张图:
这张图展示了时序约束的四个核心模块。你看,创建时钟和生成时钟是基础,必须先搞定。然后才是输入延迟和输出延迟。顺序搞反了,时序分析结果就是错的。
3.6 避坑指南
最后,分享几个我踩过的坑:
- 时钟约束遗漏:有时候设计中用了多个时钟,但只约束了主时钟。结果其他时钟域的路径全部没分析。我建议每次综合后,都检查一下
report_clock的输出,看看有没有未约束的时钟。 - 生成时钟的源选错:生成时钟的 -source 应该指向PLL的输入引脚,而不是输出引脚。选错了,工具会认为生成时钟和源时钟是同一个时钟,时序分析完全乱套。
- IO延迟值太激进:有些人为了追求性能,把输入输出延迟设得很小。结果时序报告一片红。我建议先从datasheet的典型值开始,等时序收敛了再慢慢收紧。
- 忘了约束异步时钟域:如果两个时钟之间没有关系,记得加
set_clock_groups -asynchronous。否则工具会去分析这些不存在的路径,浪费时间也浪费资源。
好了,这一节的内容就到这里。记住,约束写得好,上板调试少。下一节咱们聊聊更高级的约束技巧,比如多周期路径和虚假路径。嗯,到时候再细说。
可可精品资料库