第二章:时序约束入门

各位同学,咱们今天聊聊时序约束的入门功夫。说白了,就是告诉工具你的时钟长什么样、数据什么时候来、什么时候走。我刚开始学FPGA那会儿,觉得约束可有可无,随便写两句就完事。结果呢?综合出来的电路要么跑不快,要么根本跑不对。后来我才明白——约束不是写给工具看的,是写给自己的设计看的。

2.1 创建主时钟

主时钟,就是整个设计的“心跳”。没有它,时序分析根本没法做。你想想看,工具连时钟周期都不知道,怎么判断路径是否满足时序?

创建主时钟用 create_clock 命令。语法很简单:

create_clock -name <时钟名> -period <周期(ns)> [ -waveform <上升沿 下降沿> ] [get_ports <端口名>]

举个例子。假设板子上有个100MHz的晶振,接到FPGA的 sys_clk 引脚上:

create_clock -name sys_clk -period 10.000 [get_ports sys_clk]

这里周期是10ns,对应100MHz。默认波形是50%占空比,上升沿在0ns,下降沿在5ns。如果你的时钟不是标准占空比,比如40%,那就得手动指定波形:

create_clock -name custom_clk -period 10.000 -waveform {0.000 4.000} [get_ports custom_clk]
小提示: 我个人习惯给时钟起个有意义的名字,别用默认的。比如 clk_100m 比 clk1 好得多。调试时一眼就能看出是哪个时钟。

2.2 生成时钟

生成时钟,就是由主时钟通过PLL、MMCM或者分频逻辑派生出来的时钟。这类时钟不能直接用 create_clock,得用 create_generated_clock。

为什么?因为生成时钟和主时钟之间有明确的相位和频率关系。工具需要知道这个关系,才能做准确的时序分析。

语法长这样:

create_generated_clock -name <时钟名> -source <源时钟> -divide_by <分频系数> -multiply_by <倍频系数> [get_pins <输出引脚>]

举个例子。PLL输出一个200MHz的时钟,源时钟是100MHz:

create_generated_clock -name pll_clk_200m -source [get_ports sys_clk] -multiply_by 2 [get_pins pll_instance/clk_out]

再比如,用寄存器做二分频:

create_generated_clock -name div2_clk -source [get_ports sys_clk] -divide_by 2 [get_pins div2_reg/Q]
注意: 生成时钟的 -source 必须是主时钟或者另一个已定义的生成时钟。我曾经见过有人把源时钟设成普通信号,结果时序分析全乱套了。嗯,这个坑我踩过。

2.3 输入延迟约束

输入延迟,说白了就是数据从外部芯片发出,到FPGA引脚接收,中间花了多少时间。这个时间包括:

  • 外部芯片的时钟到输出延迟(Tco)
  • PCB走线延迟
  • 其他外部延迟

约束用 set_input_delay 命令:

set_input_delay -clock <时钟名> -max <最大延迟(ns)> -min <最小延迟(ns)> [get_ports <数据端口>]

举个例子。外部ADC输出数据,时钟是10ns周期,Tco最大2ns,最小1ns,PCB走线延迟0.5ns:

set_input_delay -clock sys_clk -max 2.5 [get_ports adc_data*]
set_input_delay -clock sys_clk -min 1.5 [get_ports adc_data*]

这里 -max 是2+0.5=2.5ns,-min 是1+0.5=1.5ns。工具会用 -max 检查建立时间,用 -min 检查保持时间。

关键点: 输入延迟约束一定要同时给 -max 和 -min。只给一个的话,另一个会被当成0,结果就是要么建立时间过紧,要么保持时间过紧。我刚开始就吃过这个亏。

2.4 输出延迟约束

输出延迟和输入延迟是对称的。它描述的是数据从FPGA引脚输出,到外部芯片接收,中间需要多少时间。

命令是 set_output_delay:

set_output_delay -clock <时钟名> -max <最大延迟(ns)> -min <最小延迟(ns)> [get_ports <数据端口>]

举个例子。FPGA输出数据给DAC,DAC的建立时间要求2ns,保持时间要求1ns,PCB走线延迟0.5ns:

set_output_delay -clock sys_clk -max 2.5 [get_ports dac_data*]
set_output_delay -clock sys_clk -min 1.5 [get_ports dac_data*]

这里 -max 是2+0.5=2.5ns,-min 是1+0.5=1.5ns。注意,输出延迟约束的 -max 对应外部芯片的建立时间要求,-min 对应保持时间要求。

经验之谈: 输出延迟的 -max 和 -min 值,最好留一点余量。比如算出来是2.5ns,我习惯设成2.7ns。这样即使PCB走线有点偏差,也不会出问题。

2.5 知识体系总览

下面这张图,把本章的核心逻辑串起来了。你看一遍,应该能有个整体印象。

时序约束入门知识体系 创建主时钟 create_clock 生成时钟 create_generated_clock 输入延迟约束 set_input_delay 输出延迟约束 set_output_delay 定义时钟源 派生时钟 外部到FPGA FPGA到外部 核心逻辑 所有约束都围绕时钟展开 主时钟是根,生成时钟是枝,输入/输出延迟是叶

2.6 避坑指南

最后,我把自己踩过的坑总结一下,你们遇到了能少走弯路:

  • 时钟没约束全: 所有进入设计的时钟都要约束,包括异步时钟。漏掉一个,工具就不分析那条路径。
  • 生成时钟用错命令: 分频出来的时钟,千万别用 create_clock,要用 create_generated_clock。否则工具会当成两个独立时钟,分析结果完全不对。
  • 输入/输出延迟只给一个值: -max 和 -min 都要给。只给 -max,保持时间分析会偏乐观;只给 -min,建立时间分析会偏乐观。
  • 忘了加余量: 理论计算值直接拿来用,结果实际板子一测,时序差一点点。我建议至少留5%~10%的余量。

好了,这一章就到这里。约束这东西,刚开始觉得麻烦,但用顺手了就会发现——它其实是帮你理清设计思路的好工具。多练练,自然就熟了。

一句话总结: 主时钟是根,生成时钟是枝,输入/输出延迟是叶。根深才能叶茂,约束做扎实了,时序分析才能靠谱。