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]:指定时钟从哪个端口进来
小技巧:如果你不确定时钟频率,可以先用一个保守值。比如目标100MHz,先约束80MHz,等时序收敛了再慢慢往上提。我在一个项目中就是这么干的,省了不少反复综合的时间。

有时候时钟不是标准的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]
核心要点:生成时钟的源时钟,一定要是已经约束过的时钟。否则工具会报错,或者时序分析结果完全不对。我见过有人把PLL的输入引脚写错了,结果生成时钟的波形完全乱套。

还有一个容易忽略的点:生成时钟的命名。我建议用有意义的名称,比如 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 的差值,就是数据的不确定性窗口。窗口越大,时序越难收敛。

经验之谈:输入延迟的值怎么定?我一般看外部器件的datasheet。比如ADC的datasheet会写“数据在时钟沿之后1.5ns到3.5ns之间有效”,那 -min 就是1.5ns,-max 就是3.5ns。千万别自己瞎猜,否则上板调试有你受的。

有时候数据是相对于时钟下降沿的,那就要加个 -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]
核心要点:输出延迟的 -max 和 -min,本质上是在约束FPGA内部逻辑的输出路径。工具会根据这些值,去优化从寄存器到输出端口的路径延迟。

3.5 知识体系总览

为了让你更直观地理解这四类约束的关系,我画了一张图:

时序约束知识体系 时序约束 创建时钟约束 生成时钟约束 输入延迟约束 输出延迟约束 关键参数 • -name:时钟名称 • -period:时钟周期 • -waveform:波形定义 关键参数 • -source:源时钟 • -multiply_by:倍频 • -divide_by:分频 关键参数 • -max:最大延迟 • -min:最小延迟 • -clock_fall:下降沿 关键参数 • -max:最大延迟 • -min:最小延迟 • 参考外部器件 核心原则:先约束时钟,再约束IO 没有时钟约束,IO约束就是空中楼阁

这张图展示了时序约束的四个核心模块。你看,创建时钟和生成时钟是基础,必须先搞定。然后才是输入延迟和输出延迟。顺序搞反了,时序分析结果就是错的。

3.6 避坑指南

最后,分享几个我踩过的坑:

  • 时钟约束遗漏:有时候设计中用了多个时钟,但只约束了主时钟。结果其他时钟域的路径全部没分析。我建议每次综合后,都检查一下 report_clock 的输出,看看有没有未约束的时钟。
  • 生成时钟的源选错:生成时钟的 -source 应该指向PLL的输入引脚,而不是输出引脚。选错了,工具会认为生成时钟和源时钟是同一个时钟,时序分析完全乱套。
  • IO延迟值太激进:有些人为了追求性能,把输入输出延迟设得很小。结果时序报告一片红。我建议先从datasheet的典型值开始,等时序收敛了再慢慢收紧。
  • 忘了约束异步时钟域:如果两个时钟之间没有关系,记得加 set_clock_groups -asynchronous。否则工具会去分析这些不存在的路径,浪费时间也浪费资源。
一句话总结:时序约束不是写完了就完事的。每次综合后,都要仔细看时序报告。红灯亮了不可怕,可怕的是你不知道为什么亮。

好了,这一节的内容就到这里。记住,约束写得好,上板调试少。下一节咱们聊聊更高级的约束技巧,比如多周期路径和虚假路径。嗯,到时候再细说。


可可精品资料库