4. 生成时钟约束:create_generated_clock 命令与分频倍频实战
时钟树综合做完之后,我们面对的往往不是单一的主时钟。芯片内部会有大量的分频时钟、倍频时钟,甚至门控时钟。这时候,create_generated_clock 就派上用场了。
说实话,我见过不少新手直接把分频后的信号当成主时钟来约束。结果呢?时序分析报告里一堆莫名其妙的路径,查半天才发现是时钟关系没定义对。嗯,咱们今天就把这块彻底讲透。
4.1 为什么需要生成时钟?
主时钟(create_clock)定义的是芯片输入端口或 PLL 参考端的时钟。而内部经过分频器、倍频器、门控单元产生的时钟,都属于生成时钟。
你想想看,如果我把一个 100MHz 的时钟通过寄存器二分频,得到 50MHz 的信号。这个 50MHz 的信号和源时钟之间是什么关系?
- 频率关系:50MHz = 100MHz / 2
- 相位关系:分频后的上升沿对齐源时钟的某个边沿
- 波形关系:占空比可能从 50% 变成 50%,也可能变成其他值
这些信息,create_generated_clock 都能精确描述。工具拿到这些信息后,才能正确分析跨时钟域的路径。
核心原则:每个由内部逻辑产生的时钟信号,只要它被用作时序单元的时钟端,就必须用 create_generated_clock 定义。
4.2 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>]
参数看着多,但常用的就几个。我给大家拆开讲。
4.2.1 分频约束:-divide_by
最简单的场景——二分频:
create_clock -name clk_src -period 10 [get_ports clk_in]
create_generated_clock -name clk_div2 \
-source [get_ports clk_in] \
-divide_by 2 \
[get_pins div_reg/Q]
这里有个细节我提醒一下:-source 指定的是主时钟的源点,不是分频器的输入引脚。很多初学者写成分频器的时钟端,结果工具报 warning 说找不到主时钟。
我在项目中遇到过这种情况:一个 8 分频的计数器,用了三级寄存器级联。如果只定义最后一级的输出为生成时钟,中间级的时序路径会被工具忽略。正确的做法是——每一级分频输出都定义生成时钟,或者用 -master_clock 明确指定主时钟。
4.2.2 倍频约束:-multiply_by
倍频通常由 PLL 或 DLL 产生。比如 PLL 输出 400MHz,参考时钟是 100MHz:
create_clock -name ref_clk -period 10 [get_ports ref_clk_in]
create_generated_clock -name pll_clk \
-source [get_ports ref_clk_in] \
-multiply_by 4 \
[get_pins pll_instance/clk_out]
注意,倍频时钟的 -source 仍然是参考时钟的源点,不是 PLL 的输入引脚。工具会自动计算 PLL 内部的延迟。
个人习惯:我通常会在 -source 后面同时指定主时钟名和引脚,比如 -source [get_ports ref_clk_in] -master_clock ref_clk。这样更明确,避免工具选错主时钟。
4.2.3 边沿指定:-edges 与 -edge_shift
当分频或倍频的占空比不是 50%,或者边沿位置有偏移时,-divide_by 和 -multiply_by 就不够用了。这时候要用 -edges。
-edges 接受三个值,分别对应生成时钟的第一个上升沿、下降沿、第二个上升沿在主时钟边沿列表中的位置。
举个例子,一个三分频电路,占空比 33.3%:
# 主时钟边沿:0ns(上升), 5ns(下降), 10ns(上升), 15ns(下降)...
# 生成时钟:0ns(上升), 15ns(下降), 30ns(上升)
create_generated_clock -name clk_div3 \
-source [get_ports clk_in] \
-edges {1 4 7} \
[get_pins div3_reg/Q]
这里 {1 4 7} 表示:取主时钟的第 1 个上升沿、第 4 个下降沿、第 7 个上升沿。工具会自动计算周期和占空比。
如果边沿有偏移,比如延迟了 1ns,就用 -edge_shift:
create_generated_clock -name clk_div3_shift \
-source [get_ports clk_in] \
-edges {1 4 7} \
-edge_shift {1 1 1} \
[get_pins div3_reg/Q]
4.3 分频与倍频的典型场景
咱们用表格总结一下常见场景和对应的约束写法:
| 场景 | 主时钟频率 | 生成时钟频率 | 约束写法 |
|---|---|---|---|
| 二分频 | 100 MHz | 50 MHz | -divide_by 2 |
| 四分频 | 100 MHz | 25 MHz | -divide_by 4 |
| PLL 倍频 | 100 MHz | 400 MHz | -multiply_by 4 |
| 奇数分频(3分频) | 100 MHz | 33.33 MHz | -edges {1 4 7} |
| 带相移的分频 | 100 MHz | 50 MHz 延迟 2ns | -divide_by 2 -edge_shift {2 2 2} |
我曾经踩过的坑:在定义奇数分频时,直接用 -divide_by 3。工具不会报错,但默认生成的占空比是 50%,而实际电路输出可能是 33.3% 或 66.7%。这会导致 setup/hold 分析偏差。所以奇数分频一定要用 -edges 精确指定边沿。
4.4 相位对齐与边沿对齐
这两个概念容易混淆。我用自己的理解给大家说清楚。
相位对齐:生成时钟的某个边沿和主时钟的某个边沿在时间上对齐(不考虑时钟树延迟)。比如二分频的上升沿和主时钟的上升沿对齐。
边沿对齐:生成时钟的边沿和主时钟的边沿在时序分析时被视为同一时刻。工具会认为这两个时钟域之间的路径是同步的,不需要做异步处理。
说白了,相位对齐是物理上的,边沿对齐是分析上的。工具默认生成时钟和主时钟是边沿对齐的,除非你用 -edge_shift 或 -invert 改变了关系。
来看一个反相时钟的例子:
create_generated_clock -name clk_inv \
-source [get_ports clk_in] \
-invert \
[get_pins inv_gate/Y]
这个反相时钟的上升沿对齐主时钟的下降沿。工具会认为它们之间是同步关系,但 setup/hold 的检查窗口会偏移半个周期。
4.5 知识体系结构图
下面这张图展示了生成时钟约束的完整知识框架:
4.6 实战避坑指南
最后,我把自己这些年踩过的坑总结一下:
- 源时钟选错:
-source必须指向主时钟的源点(通常是端口或 PLL 输出),不是分频器的时钟输入引脚。 - 多级分频漏定义:级联分频时,每一级输出都要定义生成时钟,否则中间路径会被工具忽略。
- 奇数分频用 -divide_by:奇数分频必须用
-edges,否则占空比和边沿位置会算错。 - 忽略 -master_clock:当有多个主时钟时,不指定
-master_clock可能导致工具选错源时钟。 - 门控时钟当生成时钟:门控时钟(clock gating)通常用
set_clock_gating_check约束,不是create_generated_clock。
我的检查习惯:写完约束后,用 report_clock -generated 查看所有生成时钟的源、分频比、边沿关系。如果发现某个生成时钟的 source 是内部节点而不是端口,多半是写错了。