4、生成时钟约束:create_generated_clock、分频/倍频关系、相位对齐

好,咱们接着聊收发器约束里最绕不开的一个话题——生成时钟。

说白了,create_generated_clock 就是告诉工具:我这个时钟是从哪个源头变过来的。是分频了?倍频了?还是相位挪了?你得说清楚。不然工具算时序的时候,根本不知道你那个 156.25MHz 和 312.5MHz 之间到底啥关系。

4.1 为什么需要生成时钟约束?

收发器内部,PLL 是核心。输入一个参考时钟,PLL 给你吐出好几个不同频率、不同相位的时钟。这些时钟在芯片内部驱动着串化器、解串器、弹性缓冲……

但工具不知道这些时钟是怎么来的。你如果不加约束,它默认认为这些时钟是“独立”的。那跨时钟域的路径它就不给你分析,或者乱分析。我在项目中遇到过,有一次收发器链路误码率很高,查了半天,发现是生成时钟没写对,工具把两个不同相位的时钟当成同步时钟去算了,结果 setup 违例一大堆。

所以,create_generated_clock 不是可选项,是必选项。

4.2 基本语法与分频关系

先看最简单的——分频。比如参考时钟 156.25MHz,PLL 输出一个 78.125MHz 的时钟给逻辑用。

create_generated_clock -name clk_div2 \
    -source [get_ports refclk_p] \
    -divide_by 2 \
    [get_pins u_pll/clk_out]

这里 -divide_by 2 就是告诉工具:输出时钟频率是源时钟的一半。工具会自动算出周期关系。

我个人习惯,-name 一定要起一个有意义的名字。别用默认的 clk_out 这种,后面查起来根本分不清哪个是哪个。

小技巧: 分频关系也可以用 -edges 来指定。比如 -edges {1 3 5} 表示从源时钟的第1个上升沿开始,第3个上升沿结束,第5个上升沿作为下一个周期的开始。但说实话,-divide_by 和 -multiply_by 更直观,我一般优先用它们。

4.3 倍频关系

倍频稍微复杂一点。收发器里的高速时钟经常是参考时钟的整数倍。比如参考 156.25MHz,PLL 输出 2.5GHz 给串化器用。

create_generated_clock -name clk_serdes \
    -source [get_ports refclk_p] \
    -multiply_by 16 \
    [get_pins u_pll/clk_serdes]

这里 -multiply_by 16 表示 156.25MHz × 16 = 2.5GHz。工具会自动算出占空比和相位关系。

嗯,这里要注意:倍频时钟的占空比不一定是 50%。很多 PLL 输出的高速时钟是 50% 占空比,但有些特殊场景下不是。如果你不确定,可以用 -duty_cycle 参数显式指定。

警告: 倍频关系下,源时钟和生成时钟的边沿对齐关系很重要。如果 PLL 内部有相位调整,你需要用 -phase_shift 或者 -invert 来修正。我曾经因为没加相位偏移,导致收发器数据采样点正好落在数据跳变沿上,误码率直接飙到 10^-3。

4.4 相位对齐

相位对齐是收发器约束里最容易出问题的地方。为什么?因为收发器内部有很多时钟域:PMA 域、PCS 域、FPGA 逻辑域……这些时钟之间往往有固定的相位关系。

比如,一个典型的收发器架构:

; 源时钟:156.25MHz
; 生成时钟1:156.25MHz(0° 相位)
; 生成时钟2:156.25MHz(90° 相位)
; 生成时钟3:312.5MHz(0° 相位)

约束写法:

create_generated_clock -name clk_0deg \
    -source [get_ports refclk_p] \
    -multiply_by 1 \
    -phase_shift 0 \
    [get_pins u_pll/clk_0deg]

create_generated_clock -name clk_90deg \
    -source [get_ports refclk_p] \
    -multiply_by 1 \
    -phase_shift 90 \
    [get_pins u_pll/clk_90deg]

create_generated_clock -name clk_2x \
    -source [get_ports refclk_p] \
    -multiply_by 2 \
    -phase_shift 0 \
    [get_pins u_pll/clk_2x]

这里 -phase_shift 的单位是度。90° 相位偏移意味着时钟边沿相对于源时钟延迟了 1/4 周期。

你想想看,如果这些相位关系没写对,工具分析跨时钟域路径时,会以为两个时钟是同步的,但实际上它们的边沿错开了。结果就是时序报告里全是假的违例,或者更糟——真的违例被掩盖了。

4.5 知识体系结构图

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

生成时钟约束核心逻辑 源时钟 (refclk) PLL / MMCM 分频 · 倍频 · 移相 分频关系 -divide_by N 倍频关系 -multiply_by M 相位对齐 -phase_shift deg 生成时钟 (create_generated_clock) -source [get_pins ...] -name clk_gen 时序分析正确 · 误码率降低

4.6 实战中的避坑指南

我总结几个常见问题,都是我在调试收发器时踩过的坑:

  • 源时钟选错: -source 一定要指向生成时钟的“物理源头”,通常是 PLL 的输入引脚。别写成 PLL 的输出,那样就成环了。
  • 相位单位搞混: Vivado 里 -phase_shift 默认单位是度。但有些 IP 核的文档里写的是皮秒。你得换算清楚。360° = 一个周期。
  • 忘记加 -master: 如果同一个源时钟生成多个相关时钟,建议用 -master 指定主时钟。这样工具能正确识别它们之间的相位关系。
  • 分频比不是整数: 收发器里有时会遇到 2.5 分频这种。Vivado 支持 -divide_by 2.5,但你要确认 PLL 是否真的能输出这种非整数分频。
核心要点: 生成时钟约束的本质,是告诉工具“这个时钟是从哪里来的,怎么变过来的”。你描述得越准确,工具分析得越靠谱。收发器这种高速电路,差一个皮秒都可能出问题。

4.7 验证方法

写完约束后,怎么知道对不对?我一般用两个方法:

  1. report_clocks: 在 Vivado 里跑一下,看看生成的时钟周期、波形、源时钟是不是符合预期。
  2. report_timing -clock_edges: 检查关键路径上时钟边沿的对齐关系。如果两个时钟的边沿应该对齐但没对齐,那就是约束写错了。

我记得有一次,我写完约束后 report_clocks 显示周期都对,但 report_timing 里 setup 违例一大堆。后来发现是相位偏移写反了方向。所以,光看周期不够,还得看相位。

好了,生成时钟这块就聊到这儿。下一节咱们会讲如何用 set_clock_groups 处理异步时钟域,那是另一个容易翻车的地方。

个人习惯: 我每次写完生成时钟约束,都会在 report_timing 里挑一条最长的路径,手动算一遍延迟,跟工具报告对比。虽然费时间,但能发现很多隐藏问题。

可可精品资料库