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 参数显式指定。
-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 知识体系结构图
下面这张图总结了生成时钟约束的核心逻辑:
4.6 实战中的避坑指南
我总结几个常见问题,都是我在调试收发器时踩过的坑:
- 源时钟选错:
-source一定要指向生成时钟的“物理源头”,通常是 PLL 的输入引脚。别写成 PLL 的输出,那样就成环了。 - 相位单位搞混: Vivado 里
-phase_shift默认单位是度。但有些 IP 核的文档里写的是皮秒。你得换算清楚。360° = 一个周期。 - 忘记加 -master: 如果同一个源时钟生成多个相关时钟,建议用
-master指定主时钟。这样工具能正确识别它们之间的相位关系。 - 分频比不是整数: 收发器里有时会遇到 2.5 分频这种。Vivado 支持
-divide_by 2.5,但你要确认 PLL 是否真的能输出这种非整数分频。
4.7 验证方法
写完约束后,怎么知道对不对?我一般用两个方法:
- report_clocks: 在 Vivado 里跑一下,看看生成的时钟周期、波形、源时钟是不是符合预期。
- report_timing -clock_edges: 检查关键路径上时钟边沿的对齐关系。如果两个时钟的边沿应该对齐但没对齐,那就是约束写错了。
我记得有一次,我写完约束后 report_clocks 显示周期都对,但 report_timing 里 setup 违例一大堆。后来发现是相位偏移写反了方向。所以,光看周期不够,还得看相位。
好了,生成时钟这块就聊到这儿。下一节咱们会讲如何用 set_clock_groups 处理异步时钟域,那是另一个容易翻车的地方。
report_timing 里挑一条最长的路径,手动算一遍延迟,跟工具报告对比。虽然费时间,但能发现很多隐藏问题。
可可精品资料库