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 本章知识体系
下面这张图总结了生成时钟约束的核心逻辑:
嗯,生成时钟的约束说复杂也复杂,说简单也简单。核心就是搞清楚三个问题:频率变不变?相位变不变?边沿怎么对齐?把这三点想明白了,create_generated_clock 就不会用错。
我个人建议,写完约束之后,一定要用 report_clock -skew 和 report_timing 验证一下。看看时钟波形是不是你想要的,时序路径是不是都覆盖到了。别等到后仿才发现问题,那时候改起来就麻烦了。