3. 生成时钟约束:create_generated_clock 命令详解、分频时钟、倍频时钟、相移时钟

好,咱们进入第三章。这一章我打算把 create_generated_clock 这个命令彻底讲透。说实话,这个命令是时序约束里最容易出幺蛾子的地方之一。我自己刚入行那会儿,就因为分频时钟的相位关系没搞对,导致后仿出现了一堆 hold 违例,查了整整两天才找到原因。

生成时钟,说白了就是从一个已有的主时钟(master clock)衍生出来的时钟。比如你有一个 100MHz 的输入时钟,经过 PLL 或者分频器之后,得到了 50MHz、200MHz,或者相位偏移了 90 度的时钟。这些统统叫生成时钟。

3.1 create_generated_clock 的基本语法

先看命令长什么样:

create_generated_clock -name <clock_name> \
    -source <master_clock_pin> \
    -divide_by <factor> \
    -multiply_by <factor> \
    -edges <edge_list> \
    -edge_shift <shift_list> \
    -invert \
    -combinational \
    [get_pins <target_pin>]

嗯,参数不少。但别怕,核心就三个场景:分频、倍频、相移。咱们一个一个来。

3.2 分频时钟约束

分频是最常见的场景。比如你有一个 100MHz 的主时钟,想生成一个 50MHz 的二分频时钟。

create_clock -name clk -period 10 [get_ports clk_in]
create_generated_clock -name clk_div2 \
    -source [get_ports clk_in] \
    -divide_by 2 \
    [get_pins divider/Q]

这里要注意,-source 指定的是主时钟的源点,而不是生成时钟的源点。我见过不少新手把 -source 写成生成时钟的 pin,结果工具直接报错。

关键点: -source 必须指向主时钟定义的那个 pin 或 port,而不是生成时钟的驱动 pin。

如果你有多个分频级联,比如先二分频再四分频,那就要注意了:

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

create_generated_clock -name clk_div4 \
    -source [get_pins div1/Q] \
    -divide_by 2 \
    [get_pins div2/Q]

第二个生成时钟的 -source 指向了第一个分频器的输出。这样工具才能正确推导出时钟之间的相位关系。

我的经验: 分频时钟的 -divide_by 和 -multiply_by 参数,工具会自动计算占空比。默认是 50% 占空比。如果你的分频器不是 50% 占空比,那就得用 -edges 参数手动指定边沿了。

3.3 倍频时钟约束

倍频通常由 PLL 产生。比如 PLL 把 100MHz 倍频到 400MHz:

create_generated_clock -name clk_pll \
    -source [get_ports clk_in] \
    -multiply_by 4 \
    [get_pins pll/clk_out]

这里有个坑——PLL 的输出和输入之间通常有固定的相位关系。但如果你不指定 -edges,工具会默认使用主时钟的第一个上升沿作为参考。这在某些 PLL 模型里会导致时序分析出错。

我曾经踩过的坑: 有一次做项目,PLL 输出时钟比输入时钟延迟了 2ns。我没加 -edge_shift,结果工具认为两个时钟的上升沿是对齐的。后仿发现数据采样窗口完全错位。从那以后,我只要用 PLL,一定会确认相位偏移参数。

正确的做法是加上边沿偏移:

create_generated_clock -name clk_pll \
    -source [get_ports clk_in] \
    -multiply_by 4 \
    -edges {1 3 5} \
    -edge_shift {0.5 0.5 0.5} \
    [get_pins pll/clk_out]

-edges 指定了生成时钟的边沿对应主时钟的第几个边沿。-edge_shift 则是在此基础上再加一个偏移量。

3.4 相移时钟约束

相移时钟在 DDR 接口、双沿采样等场景很常见。比如你想生成一个相位偏移 90 度的时钟:

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

这里 -edge_shift 的单位是 ns。如果主时钟周期是 10ns,偏移 2.5ns 就是 90 度。

你想想看,为什么不用 -divide_by 和 -multiply_by 来实现相移?因为这两个参数只能改变频率,不能改变相位。相移必须用 -edges 配合 -edge_shift。

3.5 组合逻辑生成的时钟

还有一种特殊情况——时钟经过组合逻辑后产生。比如一个与门:

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

-combinational 告诉工具:这个生成时钟和主时钟的频率一样,只是经过了组合逻辑。工具会计算组合逻辑的延迟,并体现在时钟路径上。

我个人习惯: 尽量少用组合逻辑产生时钟。门控时钟最好用专门的时钟门控单元(ICG)。组合逻辑产生的时钟容易有毛刺,而且 STA 分析也更复杂。

3.6 生成时钟的波形推导

工具是怎么推导生成时钟波形的?说白了,就是根据主时钟的波形,结合你给的参数,算出每个边沿的位置。

举个例子。主时钟周期 10ns,上升沿在 0ns,下降沿在 5ns。你定义了一个二分频时钟:

  • 工具会取主时钟的第 1 个上升沿(0ns)作为生成时钟的第 1 个上升沿
  • 取主时钟的第 3 个上升沿(10ns)作为生成时钟的第 1 个下降沿
  • 取主时钟的第 5 个上升沿(20ns)作为生成时钟的第 2 个上升沿

所以生成时钟的周期是 20ns,占空比 50%。

如果你用 -edges 手动指定,比如 -edges {1 2 3}:

  • 第 1 个边沿:主时钟第 1 个上升沿(0ns)
  • 第 2 个边沿:主时钟第 2 个上升沿(10ns)
  • 第 3 个边沿:主时钟第 3 个上升沿(20ns)

这样生成时钟的周期还是 20ns,但占空比变成了 10ns 高电平、10ns 低电平?不对,这里要注意:-edges 的边沿类型是交替的。第 1 个是上升沿,第 2 个是下降沿,第 3 个是上升沿。所以实际波形是:0ns 上升,10ns 下降,20ns 上升。占空比 50%。

3.7 常见错误与避坑指南

我总结几个常见的错误,你写约束的时候可以对照检查:

错误类型 现象 解决方法
-source 指向错误 工具报找不到主时钟 确认 -source 指向主时钟定义的 pin/port
分频比写反 生成时钟频率不对 分频用 -divide_by,倍频用 -multiply_by
相位偏移未指定 时序分析结果偏乐观 PLL 输出务必加 -edge_shift
组合逻辑时钟漏约束 时钟路径延迟未计算 加 -combinational 参数

我曾经犯过的错: 有一次做多时钟域设计,两个生成时钟来自同一个主时钟,但分频比不同。我忘了给其中一个加 -master_clock 参数(虽然这个参数现在不常用),结果工具把两个时钟当成异步时钟处理,跨时钟域的路径全没检查。嗯,从那以后我每次都会用 report_clock_interaction 确认时钟关系。

3.8 本章知识体系

下面这张图总结了生成时钟约束的核心逻辑:

主时钟 (Master Clock) create_generated_clock -source -divide_by -multiply_by -edges -edge_shift -combinational 分频时钟 -divide_by N 倍频时钟 -multiply_by N 相移时钟 -edges + -edge_shift ⚠️ 常见陷阱 • -source 必须指向主时钟定义点,而非生成时钟驱动 pin • PLL 输出务必确认相位偏移,使用 -edge_shift 参数 • 组合逻辑时钟加 -combinational,避免毛刺风险 核心原则 生成时钟的波形 = 主时钟波形 × 分频/倍频系数 + 相位偏移 工具自动推导边沿位置,但复杂场景需手动指定 -edges

嗯,生成时钟的约束说复杂也复杂,说简单也简单。核心就是搞清楚三个问题:频率变不变?相位变不变?边沿怎么对齐?把这三点想明白了,create_generated_clock 就不会用错。

我个人建议,写完约束之后,一定要用 report_clock -skew 和 report_timing 验证一下。看看时钟波形是不是你想要的,时序路径是不是都覆盖到了。别等到后仿才发现问题,那时候改起来就麻烦了。