4、生成时钟:create_generated_clock,分频与倍频时钟的约束方法

好,咱们接着聊时钟约束。前面讲了主时钟怎么创建,但芯片里不可能只有一个时钟源。PLL锁相环、分频器、门控时钟……这些衍生出来的时钟,才是真正驱动你逻辑的“主力军”。

说白了,create_generated_clock 就是用来定义这些“子时钟”的。它告诉工具:这个时钟是从哪个主时钟变过来的,变的方式是分频、倍频,还是相移。

4.1 为什么不能直接用 create_clock?

我见过不少新手,直接在分频输出端口上又写一个 create_clock。嗯,这样工具也能跑,但时序分析会出大问题。

为什么?因为 create_clock 定义的是一个“独立”的时钟源。工具会认为这个时钟和主时钟之间没有任何关系。但实际电路里,分频时钟和主时钟是同步的,它们的边沿有确定的相位关系。

如果你用了两个 create_clock,工具在做跨时钟域分析时,会保守地认为这两个时钟完全异步,从而插入大量不必要的同步器,或者报出虚假的时序违例。

记住:只要时钟是同一个源头衍生出来的,就必须用 create_generated_clock

4.2 基本语法与参数

先看个最简单的例子。假设你有一个主时钟 clk_in 是 100MHz,经过一个二分频寄存器后得到 clk_div2

create_clock -name clk_in -period 10 [get_ports clk_in]

create_generated_clock -name clk_div2 \
    -source [get_ports clk_in] \
    -divide_by 2 \
    [get_pins reg_div/Q]

这里几个关键参数:

  • -source:指定源时钟的端口或引脚。注意,这里要填主时钟定义的那个点。
  • -divide_by-multiply_by:分频或倍频系数。
  • -edges:更灵活的方式,直接指定生成时钟的边沿相对于源时钟的边沿位置。
  • -edge_shift:在 -edges 基础上做偏移。
  • -invert:反相,相当于 50% 占空比下的二分频取反。

我个人习惯:能用 -divide_by-multiply_by 解决的,尽量不用 -edges。后者虽然灵活,但写起来容易出错,尤其是边沿编号搞混的时候。

4.3 分频时钟的约束

分频是最常见的场景。整数分频、偶数分频、奇数分频,写法上略有不同。

4.3.1 偶数分频

比如四分频,占空比 50%:

create_generated_clock -name clk_div4 \
    -source [get_ports clk_in] \
    -divide_by 4 \
    [get_pins reg_div4/Q]

工具会自动计算边沿位置。源时钟周期 10ns,四分频后周期 40ns,上升沿在 0ns、40ns、80ns……

4.3.2 奇数分频

奇数分频稍微麻烦点,因为占空比往往不是 50%。比如三分频,占空比 1/3 或 2/3。

这时候用 -edges 更直观:

create_generated_clock -name clk_div3 \
    -source [get_ports clk_in] \
    -edges {1 3 4} \
    [get_pins reg_div3/Q]

这里 {1 3 4} 是什么意思?

  • 第一个边沿:源时钟的第 1 个边沿(上升沿)
  • 第二个边沿:源时钟的第 3 个边沿(下降沿)
  • 第三个边沿:源时钟的第 4 个边沿(上升沿)

这样生成的就是一个周期为 3 个源时钟周期、高电平占 1 个周期、低电平占 2 个周期的波形。

避坑指南:我曾经在写奇数分频时,把边沿编号数错了。源时钟的边沿编号是从 1 开始,上升沿为奇数,下降沿为偶数。第 1 个边沿是上升沿,第 2 个是下降沿,依此类推。这个顺序千万别搞反。

4.4 倍频时钟的约束

倍频通常由 PLL 产生。比如 PLL 输出 500MHz,源时钟 100MHz,5 倍频。

create_generated_clock -name clk_pll \
    -source [get_ports clk_in] \
    -multiply_by 5 \
    [get_pins pll_instance/clk_out]

这里有个细节:PLL 的输出引脚通常不是寄存器输出,而是模拟模块的输出。工具需要知道这个时钟的源头是哪个时钟。

如果 PLL 有多个输出,比如 500MHz 和 200MHz,那就分别定义:

create_generated_clock -name clk_500m \
    -source [get_ports clk_in] \
    -multiply_by 5 \
    [get_pins pll/clk_out1]

create_generated_clock -name clk_200m \
    -source [get_ports clk_in] \
    -multiply_by 2 \
    [get_pins pll/clk_out2]

4.5 相移与占空比调整

有时候我们需要一个相移 90 度的时钟。可以用 -edge_shift

create_generated_clock -name clk_shift90 \
    -source [get_ports clk_in] \
    -edges {1 2} \
    -edge_shift {2.5 2.5} \
    [get_pins reg_shift/Q]

这里 -edge_shift {2.5 2.5} 表示把两个边沿都往后移 2.5ns。如果源时钟周期是 10ns,那这个生成时钟的上升沿就在 2.5ns 处,下降沿在 7.5ns 处。

嗯,这个功能在 DDR 接口中很常用。数据在时钟的中间变化,需要把采样时钟偏移半个周期。

4.6 组合逻辑生成的时钟

还有一种情况:时钟不是从寄存器出来的,而是从组合逻辑出来的。比如一个与门、或门。

这时候要加 -combinational 选项:

create_generated_clock -name gated_clk \
    -source [get_ports clk_in] \
    -combinational \
    [get_pins and_gate/Y]

工具会认为这个生成时钟的波形和源时钟完全一样,只是经过了组合逻辑延迟。

注意:组合逻辑生成的时钟,在时序分析中要特别小心。组合逻辑的延迟会引入毛刺,而且 PVT 变化下延迟不稳定。我建议尽量用寄存器或 PLL 产生时钟,少用组合逻辑。

4.7 多个生成时钟的级联

有时候生成时钟的源不是主时钟,而是另一个生成时钟。比如先二分频,再三分频。

工具是支持这种级联的:

create_generated_clock -name clk_div2 \
    -source [get_ports clk_in] \
    -divide_by 2 \
    [get_pins reg_div2/Q]

create_generated_clock -name clk_div6 \
    -source [get_pins reg_div2/Q] \
    -divide_by 3 \
    [get_pins reg_div6/Q]

工具会自动推导出 clk_div6 相对于主时钟 clk_in 的关系。你不需要手动计算 2×3=6。

4.8 验证生成时钟

约束写完了,怎么知道对不对?

在工具里用 report_clock 命令查看:

report_clock -generated

你会看到每个生成时钟的源时钟、分频/倍频系数、边沿位置、占空比等信息。

我习惯再跑一个 check_timing,看看有没有未约束的生成时钟,或者源时钟找不到的情况。

总结一下我的经验

  • 能用 -divide_by / -multiply_by 的,优先用
  • 奇数分频或特殊占空比,用 -edges
  • PLL 输出一定要指定正确的源时钟引脚
  • 组合逻辑生成时钟,能避免就避免
  • 写完约束后,用 report_clockcheck_timing 验证

生成时钟的约束,说白了就是告诉工具:这个时钟是从哪来的,怎么变的。你描述得越准确,工具分析得就越精准。下次咱们聊聊如何约束输入输出延迟,那是连接芯片内部和外部的桥梁。