4. 生成时钟约束:create_generated_clock命令、分频与倍频时钟约束、相位关系

时钟,是FPGA设计的脉搏。主时钟约束好了,只是第一步。

真正让工程师头疼的,往往是那些由主时钟衍生出来的时钟——分频时钟、倍频时钟、还有各种带相位的时钟。说白了,就是create_generated_clock这个命令要解决的问题。

我个人习惯把衍生时钟叫做「子时钟」。它们跟主时钟之间,有明确的数学关系。你想想看,如果这个关系没告诉工具,时序分析就会乱套。

4.1 为什么要约束衍生时钟?

很多新手会问:PLL输出都连到全局时钟网络了,工具难道不能自动识别吗?

嗯,这里要注意。工具确实能识别PLL的输出频率,但它不知道你期望的相位关系是什么。举个例子,你用PLL生成了一个90度相移的时钟,工具默认按0度去分析,结果就是时序报告全是假的。

我在项目中遇到过这样的情况:一个DDR接口,数据在时钟的上升沿和下降沿都采样。我忘了约束PLL输出的90度相移时钟,结果后仿真一直报错。查了两天才发现,是衍生时钟的相位关系没告诉工具。

⚠️ 重要提醒: 主时钟用 create_clock,衍生时钟用 create_generated_clock。这两个命令不能混用。混用的后果是工具会认为两个时钟完全独立,跨时钟域分析会出问题。

4.2 create_generated_clock 命令详解

这个命令的语法,其实不复杂。核心参数就几个:

create_generated_clock -name <时钟名> \
    -source <主时钟的源端> \
    -divide_by <分频系数> \
    -multiply_by <倍频系数> \
    -edges <边沿列表> \
    -edge_shift <边沿偏移> \
    -invert \
    -combinational \
    [get_pins <输出引脚>]

我来拆解一下每个参数的实际用途:

参数 作用 我的使用建议
-name 给衍生时钟起个名字 命名要有规律,比如 clk_50M、clk_100M_phase90
-source 指定主时钟的源端 必须是主时钟定义时的那个引脚或端口
-divide_by 分频系数 整数分频最常用,比如 /2、/4
-multiply_by 倍频系数 PLL输出常用,比如 *2、*4
-edges 自定义边沿序列 处理非50%占空比时钟时必用
-edge_shift 边沿偏移量 相位调整时用,单位是ps

4.3 分频时钟约束实战

分频时钟,是衍生时钟里最常见的。比如你用寄存器做了一个二分频:

// Verilog代码:简单的二分频
reg clk_div2;
always @(posedge clk_100M or negedge rst_n) begin
    if (!rst_n)
        clk_div2 <= 1'b0;
    else
        clk_div2 <= ~clk_div2;
end

对应的约束怎么写?

# 先定义主时钟
create_clock -name clk_100M -period 10.000 [get_ports clk_in]

# 再定义衍生时钟
create_generated_clock -name clk_50M \
    -source [get_ports clk_in] \
    -divide_by 2 \
    [get_pins reg_clk_div2/Q]

这里有个细节:-source指向的是主时钟的源端,也就是clk_in这个端口,而不是寄存器的时钟引脚。我曾经见过有人写成-source [get_pins reg_clk_div2/C],这样工具会找不到主时钟关系。

💡 个人经验: 对于简单的整数分频,用 -divide_by 就够了。但如果你的分频电路有使能信号,或者不是简单的2的幂次分频,建议用 -edges 来精确描述边沿位置。

4.4 倍频时钟约束实战

倍频时钟,通常来自PLL或MMCM。比如你用PLL把100MHz倍频到200MHz:

# PLL输出约束
create_generated_clock -name clk_200M \
    -source [get_pins pll_inst/CLKIN] \
    -multiply_by 2 \
    [get_pins pll_inst/CLKOUT0]

但PLL往往不止一个输出。我见过一个项目,PLL有4个输出,频率分别是100M、200M、300M、400M。每个都要单独约束。而且要注意,PLL的-source要指向PLL的输入时钟引脚,不是PLL本身。

为什么会这样?因为工具需要知道衍生时钟和主时钟之间的相位关系。PLL内部有锁相环,输出和输入之间有固定的相位关系。你指定了源端,工具才能计算。

4.5 相位关系约束

相位约束,是衍生时钟里最容易出错的地方。比如你有一个0度相位的时钟和一个90度相位的时钟:

# 0度相位时钟
create_generated_clock -name clk_0deg \
    -source [get_pins pll_inst/CLKIN] \
    -multiply_by 2 \
    [get_pins pll_inst/CLKOUT0]

# 90度相位时钟
create_generated_clock -name clk_90deg \
    -source [get_pins pll_inst/CLKIN] \
    -multiply_by 2 \
    -edge_shift 2500 \
    [get_pins pll_inst/CLKOUT1]

这里-edge_shift 2500表示偏移2500ps。200MHz时钟周期是5000ps,90度就是1250ps?不对,等等。

200MHz周期是5ns,90度相移是1.25ns,也就是1250ps。我上面写2500ps是错的。你看,我自己写的时候都会搞混。所以我的习惯是:先算清楚周期,再算相移量,最后写约束

⚠️ 避坑指南: 我曾经在DDR3接口上犯过一个错误。PLL输出90度相移时钟,我用了 -edge_shift 但单位写错了。工具按ps处理,我按ns写,结果差了1000倍。从那以后,我每次写 -edge_shift 都会在注释里标明单位。

4.6 复杂边沿关系的处理

有些时钟不是简单的分频或倍频。比如一个三分频时钟,占空比50%:

# 三分频,占空比50%
create_generated_clock -name clk_div3 \
    -source [get_ports clk_in] \
    -edges {1 3 5} \
    [get_pins reg_div3/Q]

-edges {1 3 5}是什么意思?它表示:衍生时钟的第一个上升沿对应主时钟的第1个边沿,第一个下降沿对应第3个边沿,第二个上升沿对应第5个边沿。说白了,就是每3个主时钟边沿产生一个衍生时钟边沿。

如果你需要偏移,可以加上-edge_shift

# 三分频,占空比50%,偏移500ps
create_generated_clock -name clk_div3_shift \
    -source [get_ports clk_in] \
    -edges {1 3 5} \
    -edge_shift {500 500 500} \
    [get_pins reg_div3/Q]

这里-edge_shift后面跟了三个值,分别对应三个边沿的偏移量。如果只写一个值,三个边沿都偏移同样的量。

4.7 知识体系总览

下面这张图,是我自己总结的衍生时钟约束知识体系。每次做新项目,我都会对照着检查一遍:

衍生时钟约束知识体系 主时钟 create_clock 衍生时钟 create_generated_clock 分频 -divide_by 倍频 -multiply_by 相位 -edge_shift 整数分频 / 小数分频 PLL / MMCM 输出 0°/90°/180°/270° 关键:-source 必须指向主时钟源端 边沿序列 -edges 用于非50%占空比或非整数分频

4.8 常见错误与避坑指南

做衍生时钟约束,有几个坑我几乎每次培训都会提到:

  • 源端指向错误-source要指向主时钟的源端,不是衍生时钟的源端。我见过有人指向PLL的输出,这是错的。
  • 相位计算错误-edge_shift的单位是ps,不是ns。1ns = 1000ps,别搞混。
  • 忘记约束所有输出:PLL可能有多个输出,每个都要单独约束。漏掉一个,那个时钟域就是未约束状态。
  • 分频系数写反:二分频是-divide_by 2,不是-divide_by 1。这个看起来简单,但我在代码评审时见过好几次。

📌 核心要点:

  • 衍生时钟必须基于主时钟定义,不能独立存在
  • 分频用 -divide_by,倍频用 -multiply_by
  • 相位调整用 -edge_shift,单位是ps
  • 复杂边沿关系用 -edges 精确描述
  • 约束完成后,用 report_clock_interaction 检查时钟关系

好了,关于衍生时钟约束,核心内容就这些。你写约束的时候,记住一句话:告诉工具,你的衍生时钟是从哪里来的,跟主时钟是什么关系。做到这一点,时序分析就不会跑偏。


公众号:蓝海资料掘金营,微信deep3321