第四章:生成时钟约束——generate_clocks的使用场景、分频与倍频时钟的约束方法、主时钟与生成时钟的关系

各位同学,今天我们来聊聊生成时钟。说实话,这个知识点在时序约束里属于「看着简单,用起来容易翻车」的类型。我自己刚入行那会儿,就因为生成时钟没写对,导致后仿时序一片红,差点没赶上流片节点。

生成时钟,说白了就是由主时钟通过分频、倍频或者相位调整派生出来的时钟。你想想看,一个芯片里不可能只有一个时钟频率——CPU跑800MHz,外设可能只需要100MHz,这些低速时钟怎么来的?要么用PLL倍频分频,要么用寄存器做分频。而我们要做的,就是用create_generated_clock这个命令,把这些派生关系告诉工具。

4.1 什么时候用generate_clocks?

我个人的习惯是:只要时钟不是直接从PLL输出端取出来的,而是经过了一些逻辑处理(比如分频器、门控单元、多路选择器),那就得用生成时钟。具体来说,常见场景有这几个:

  • 分频时钟:用寄存器做2分频、4分频,或者用计数器产生任意分频比
  • 倍频时钟:PLL内部已经做了倍频,但输出端经过了一些缓冲或分频网络
  • 门控时钟:时钟经过与门或或门后产生的使能时钟
  • 多路选择时钟:多个时钟源通过MUX选择后输出的时钟
  • 相位调整时钟:比如DDR接口中需要的90度相移时钟

核心原则:只要时钟路径上存在非缓冲逻辑(寄存器、组合逻辑、MUX等),就必须用create_generated_clock来定义。否则工具会认为这些节点是普通数据路径,不会做时钟树分析。

我记得有一次,一个同事把分频时钟直接当主时钟约束了——也就是用了create_clock而不是create_generated_clock。结果呢?工具把分频器和主时钟之间的路径当成了异步路径,完全没做时序检查。嗯,这种坑踩过一次就记住了。

4.2 分频时钟的约束方法

分频时钟是最常见的生成时钟场景。我们来看一个2分频的例子:

// RTL代码:简单的2分频器
reg clk_div2;
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        clk_div2 <= 1'b0;
    else
        clk_div2 <= ~clk_div2;
end

对应的约束写法:

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

这里有几个关键点:

  • -source指定的是主时钟的源点,也就是create_clock定义的那个点
  • 目标引脚是分频寄存器的输出端Q,不是时钟输入端
  • -divide_by 2告诉工具分频比是2

对于奇数分频呢?比如3分频:

create_generated_clock -name clk_div3 -source [get_ports clk] \
    -edges {1 3 5} [get_pins u_div/clk_div3_reg/Q]

-edges参数指定了生成时钟的上升沿对应主时钟的第几个边沿。这里{1 3 5}表示:生成时钟的第一个上升沿对应主时钟的第1个上升沿,第二个上升沿对应第3个,第三个对应第5个。说白了就是每3个主时钟周期产生一个生成时钟周期。

小技巧:对于50%占空比的奇数分频,需要用-edges配合-edge_shift来做相位调整。我曾经在项目中做过一个5分频时钟,折腾了半天才发现占空比不对,最后用-edge_shift {0 2.5 0}才搞定。

4.3 倍频时钟的约束方法

倍频时钟通常由PLL产生。但有时候PLL输出经过了一些缓冲或分频网络,这时候就需要用生成时钟来描述。

举个例子,PLL输出2倍频时钟:

create_clock -name clk_ref -period 20 [get_ports clk_ref]
create_generated_clock -name clk_pll -source [get_ports clk_ref] \
    -multiply_by 2 [get_pins pll_instance/clk_out]

这里-multiply_by 2表示倍频。工具会自动计算生成时钟的周期:20ns ÷ 2 = 10ns。

如果既有倍频又有分频呢?比如PLL内部先倍频再分频:

create_generated_clock -name clk_pll_out -source [get_ports clk_ref] \
    -multiply_by 4 -divide_by 3 [get_pins pll_instance/clk_out]

周期计算:20ns × 3 ÷ 4 = 15ns。嗯,这个顺序要注意——先乘后除。

4.4 主时钟与生成时钟的关系

这个关系其实很简单,但很多人容易搞混。我直接说结论:

  • 主时钟是源头,通常定义在芯片的输入端口或PLL的输出端
  • 生成时钟是从主时钟派生出来的,两者有明确的时序关系
  • 工具会自动分析主时钟和生成时钟之间的路径,做跨时钟域检查

举个例子:

create_clock -name sys_clk -period 10 [get_ports sys_clk]
create_generated_clock -name clk_half -source [get_ports sys_clk] \
    -divide_by 2 [get_pins u_div/q_reg/Q]

这里sys_clk是主时钟,clk_half是生成时钟。工具知道:

  1. 两个时钟是同源的
  2. clk_half的每个周期对应sys_clk的两个周期
  3. 两个时钟之间的路径需要做同步检查

注意:如果生成时钟的源点不是主时钟的定义点,而是中间某个节点,工具可能无法正确分析时序关系。我曾经见过一个案例,生成时钟的source写成了分频器的输出,结果工具认为这是两个独立的时钟域,该检查的路径全漏了。

还有一个容易忽略的点:生成时钟的波形。默认情况下,工具假设生成时钟的占空比是50%。如果你的分频电路不是50%占空比,需要用-waveform参数手动指定:

create_generated_clock -name clk_odd -source [get_ports clk] \
    -edges {1 2 4} -waveform {0 5 10} [get_pins u_div/q_reg/Q]

这里-waveform {0 5 10}表示:第一个上升沿在0ns,下降沿在5ns,第二个上升沿在10ns。说白了就是手动画波形。

4.5 实战中的避坑指南

最后分享几个我踩过的坑:

  • 源点选择错误:生成时钟的-source一定要指向主时钟的定义点,而不是时钟树的末端。我曾经把source写成了PLL的输入时钟引脚,结果工具报了一堆unconstrained path。
  • 分频器输出引脚搞错:生成时钟的目标引脚是分频寄存器的Q端,不是时钟输入端。这个错误很隐蔽,因为综合工具可能不会报错,但后仿时序会出问题。
  • 忘记约束门控时钟:如果时钟经过了一个与门,一定要用生成时钟来描述。否则工具会把与门输出当成普通信号,不做时钟树。
  • 多级生成时钟:如果生成时钟又作为源产生了另一个生成时钟,要确保每一级都正确约束。我建议用report_generated_clock命令检查层级关系。

好了,这一章的内容就到这里。生成时钟看似简单,但实际项目中出问题最多的往往就是这里。下一章我们会讲如何约束异步时钟域,到时候会用到今天学的知识。