4. 主时钟约束
时钟,是FPGA设计的“心跳”。
没有时钟约束,时序分析工具就是个瞎子。我见过太多新手,代码写得挺漂亮,结果一跑时序全是红的。为什么?因为没告诉工具时钟长什么样。
今天咱们就把主时钟约束这件事聊透。
4.1 create_clock 命令详解
先看最核心的命令:create_clock。它的语法其实不复杂,但参数的含义你得吃透。
create_clock -name <时钟名> -period <周期> [ -waveform <上升沿/下降沿时间> ] [ -add ] [ <源对象> ]
我来拆开讲。
- -name:给时钟起个名字。我习惯用“clk_xxx”的格式,比如
clk_sys、clk_ddr。名字别乱起,后面调试时你会感谢自己的。 - -period:时钟周期,单位是ns。比如100MHz的时钟,周期就是10ns。
- -waveform:定义上升沿和下降沿的位置。默认是50%占空比,第一个上升沿在0ns,下降沿在周期的一半。
- -add:给同一个端口添加多个时钟定义。这个后面会细说。
- 源对象:时钟从哪个端口或引脚进来。可以是
[get_ports clk_in]或[get_pins ...]。
重要提醒:create_clock 定义的是主时钟(primary clock),也就是从FPGA外部进来的时钟。内部PLL产生的时钟,用 create_generated_clock 来定义,别搞混了。
举个例子。假设板子上有个50MHz的晶振,连接到FPGA的 clk_in 引脚。约束这么写:
create_clock -name clk_50m -period 20.0 [get_ports clk_in]
这就完事了?嗯,对于最简单的场景,够了。但实际项目中,往往没这么简单。
4.2 时钟周期与占空比
时钟周期,就是时钟信号重复一次的时间。频率的倒数。
占空比呢?就是高电平时间占整个周期的比例。默认是50%,也就是高电平一半、低电平一半。
但有些芯片输出的时钟,占空比不是50%。比如DDR接口的时钟,有时候是45%/55%。这时候就得手动指定 -waveform。
# 周期10ns,占空比60%(高电平6ns,低电平4ns)
create_clock -name clk_ddr -period 10.0 -waveform {0 6} [get_ports ddr_clk]
-waveform 后面跟两个值:第一个是上升沿时间,第二个是下降沿时间。单位是ns。
我的习惯:如果时钟占空比不是50%,我一般会在注释里写明原因。比如“DDR PHY要求60%占空比”。这样半年后回来看代码,不至于一脸懵。
我曾经在一个项目中,遇到一个时钟占空比是40%的。当时没注意,直接用默认的50%去约束。结果时序分析报告里一堆hold violation。查了半天才发现是占空比搞错了。嗯,从那以后,我每次都会确认时钟的占空比。
4.3 多时钟域定义
现在的FPGA设计,很少只有一个时钟。一个典型项目里,可能有系统时钟、DDR时钟、以太网时钟、PCIe时钟……每个时钟域都有自己的约束。
多时钟域定义,说白了就是给每个时钟都写一条 create_clock。
# 系统时钟 50MHz
create_clock -name clk_sys -period 20.0 [get_ports sys_clk]
# 以太网时钟 125MHz
create_clock -name clk_eth -period 8.0 [get_ports eth_clk]
# DDR时钟 200MHz
create_clock -name clk_ddr -period 5.0 [get_ports ddr_clk]
但有个坑要注意:同一个端口上,能不能定义多个时钟?
能。用 -add 参数。
# 同一个端口上定义两个时钟
create_clock -name clk_a -period 10.0 [get_ports clk_in]
create_clock -name clk_b -period 5.0 [get_ports clk_in] -add
这有什么用?比如有些芯片支持多种工作模式,不同模式下时钟频率不同。你可以在同一个端口上定义多个时钟,工具会分别分析每种模式下的时序。
注意:不加 -add 的话,第二条 create_clock 会覆盖第一条。我见过有人因为这个,折腾了一整天没找到问题。
多时钟域之间,还需要考虑跨时钟域(CDC)的问题。这个后面会专门讲,今天先记住:每个时钟域都要有明确的约束。
4.4 虚拟时钟的概念
虚拟时钟,是很多新手搞不懂的概念。说白了,它就是一个“不存在于FPGA内部”的时钟。
它不连接到任何寄存器,不驱动任何逻辑。那为什么要定义它?
为了约束输入输出延迟。
你想想看,芯片外面的信号,是跟着外部时钟走的。这个外部时钟可能没进FPGA,或者进了但没用来驱动内部逻辑。但时序分析时,工具需要知道这个外部时钟的周期,才能计算输入输出路径的时序裕量。
这时候,虚拟时钟就派上用场了。
# 定义虚拟时钟,代表外部芯片的时钟
create_clock -name vclk_ext -period 10.0
# 用虚拟时钟约束输入延迟
set_input_delay -clock vclk_ext -max 2.0 [get_ports data_in]
set_input_delay -clock vclk_ext -min 1.0 [get_ports data_in]
注意,create_clock 后面没有指定源对象。这就是虚拟时钟和普通时钟的唯一区别。
核心要点:虚拟时钟只定义周期和波形,不绑定到任何物理引脚或网络。它纯粹是一个“参考时钟”,用于约束外部接口的时序。
我做过一个项目,FPGA和外部ADC通信。ADC的采样时钟是100MHz,但这个时钟没有连到FPGA。FPGA只接收ADC的数据和同步信号。这时候,我就定义了一个虚拟时钟,周期10ns,然后用它来约束输入延迟。效果很好。
4.5 知识体系图
下面这张图,把主时钟约束的核心逻辑串起来了。建议你保存下来,写约束时对照着看。
4.6 避坑指南
最后,分享几个我踩过的坑。
- 时钟命名别重复:同一个设计中,时钟名必须唯一。我见过有人把两个不同频率的时钟都叫“clk”,结果工具报错。
- 周期单位是ns:
-period 10是10ns,不是10MHz。新手常犯的错误。 - 虚拟时钟不需要源对象:如果你在虚拟时钟后面加了
[get_ports ...],那就不是虚拟时钟了。 - 多时钟域别忘了CDC约束:定义了多个时钟,不等于跨时钟域就安全了。还需要
set_clock_groups或set_false_path来配合。
我的建议:写约束文件时,把每个时钟的定义放在一起,加上注释。比如“// 系统时钟 50MHz from晶振 U1”。这样后期维护时,一眼就能看懂。
好了,主时钟约束的核心内容就这些。记住一句话:时钟约束是时序分析的基石。这块搞不明白,后面的所有分析都是空中楼阁。
可可精品资料库