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 知识体系图

下面这张图,把主时钟约束的核心逻辑串起来了。建议你保存下来,写约束时对照着看。

主时钟约束知识体系 create_clock 命令参数详解 -name:时钟命名规范 -period:周期定义 -waveform:波形定制 -add:多时钟叠加 周期与占空比 周期 = 1 / 频率 默认占空比 50% 自定义 waveform 高电平/低电平时间 多时钟域定义 每个时钟独立约束 同一端口多时钟 -add 参数用法 跨时钟域注意 虚拟时钟 无物理源对象 用于约束输入/输出延迟 实际应用场景 外部芯片接口时序 ADC/DAC/PHY等

4.6 避坑指南

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

  • 时钟命名别重复:同一个设计中,时钟名必须唯一。我见过有人把两个不同频率的时钟都叫“clk”,结果工具报错。
  • 周期单位是ns:-period 10 是10ns,不是10MHz。新手常犯的错误。
  • 虚拟时钟不需要源对象:如果你在虚拟时钟后面加了 [get_ports ...],那就不是虚拟时钟了。
  • 多时钟域别忘了CDC约束:定义了多个时钟,不等于跨时钟域就安全了。还需要 set_clock_groups 或 set_false_path 来配合。

我的建议:写约束文件时,把每个时钟的定义放在一起,加上注释。比如“// 系统时钟 50MHz from晶振 U1”。这样后期维护时,一眼就能看懂。

好了,主时钟约束的核心内容就这些。记住一句话:时钟约束是时序分析的基石。这块搞不明白,后面的所有分析都是空中楼阁。

可可精品资料库