4. 生成时钟约束:create_generated_clock、分频与倍频时钟、时钟相位关系

各位同学,今天我们来聊聊生成时钟。说实话,这个知识点在时序约束里属于「看着简单,用起来容易翻车」的类型。我自己刚入行那会儿,就因为一个分频时钟的相位关系没搞对,导致后仿出现了一堆hold违例,查了整整两天才找到根因。

生成时钟,说白了就是由主时钟通过分频、倍频或者相位调整派生出来的时钟。在数字芯片里,PLL、DLL、分频器、门控时钟,这些都是生成时钟的常见来源。SDC里用create_generated_clock来定义它们。

4.1 基本语法与核心参数

先看一个最基础的例子。假设你有一个100MHz的主时钟,经过一个二分频寄存器,得到50MHz的时钟:

create_clock -name clk_sys -period 10.0 [get_ports clk_in]
create_generated_clock -name clk_div2 \
    -source [get_ports clk_in] \
    -divide_by 2 \
    [get_pins u_divider/Q]

这里有几个关键参数,我挨个说一下:

  • -source:指定源时钟的引脚或端口。注意,不是时钟名,是物理节点。
  • -divide_by / -multiply_by:分频或倍频系数。整数倍,不支持小数。
  • -edges:显式指定生成时钟的边沿位置。这个参数很强大,但容易用错。
  • -edge_shift:在-edges基础上做偏移,单位是ns。
  • -invert:直接取反,相当于180度相移。
我的个人习惯:能用-divide_by和-multiply_by搞定的,就别用-edges。后者虽然灵活,但可读性差,团队协作时容易产生误解。

4.2 分频时钟的相位关系

分频时钟的相位,取决于你从哪个边沿开始计数。举个例子,一个二分频时钟,如果源时钟的上升沿触发分频器,那么生成时钟的上升沿与源时钟的上升沿对齐。但如果是下降沿触发呢?相位就差了半个周期。

嗯,这里要注意:工具默认认为生成时钟与源时钟的边沿对齐。如果你的设计不是这样,必须显式指定。

我曾经遇到过一个案例:设计里用了一个二分频器,但分频器的输出又经过了一个反相器。结果STA工具默认认为生成时钟的上升沿在0ns,实际上它在5ns(对于10ns周期的源时钟)。这导致setup分析时,工具少算了半个周期的裕量。后来我在约束里加上了-invert才搞定。

4.3 倍频时钟与PLL

倍频时钟通常由PLL产生。PLL的输出与输入之间,不仅有频率关系,还有相位关系。很多PLL支持相位可调,比如0度、45度、90度等。

create_generated_clock -name clk_200m \
    -source [get_pins pll/ref_clk] \
    -multiply_by 2 \
    [get_pins pll/out_clk]

但PLL的输出与输入之间,往往有一个固定的相位偏移。这个偏移由PLL的锁定特性决定,可能是0,也可能是某个固定值。我建议你在约束里加上-edge_shift来补偿这个偏移,除非你确认PLL的输出与输入是严格对齐的。

避坑指南:PLL的锁定时间、抖动、以及输出时钟的占空比,这些STA工具不会自动考虑。你需要通过set_clock_uncertainty来额外设置抖动和裕量。我曾经见过一个设计,PLL输出时钟的抖动达到了±150ps,但约束里只设了±50ps,结果流片回来高频工况下频繁出错。

4.4 多级生成时钟

有时候,生成时钟还可以作为另一个生成时钟的源。比如,先二分频,再四分频:

create_generated_clock -name clk_div2 \
    -source [get_ports clk_in] -divide_by 2 [get_pins div2/Q]
create_generated_clock -name clk_div4 \
    -source [get_pins div2/Q] -divide_by 2 [get_pins div4/Q]

这里有个细节:第二个生成时钟的-source是div2/Q,而不是clk_in。如果你写成了clk_in,工具会认为clk_div4直接由clk_in派生,分频系数变成4。结果虽然一样,但相位关系可能不对。你想想看,如果div2/Q的相位相对于clk_in有延迟,那么clk_div4的相位就应该基于div2/Q来计算,而不是clk_in。

4.5 时钟相位关系的可视化

为了让大家更直观地理解,我画了一张图。这张图展示了主时钟、二分频时钟(上升沿触发)、以及四分频时钟之间的相位关系。

生成时钟相位关系示意图 clk_in 0 5 10 15 20 25 30 35 40 45 50 55 60 clk_div2 clk_div4 相位关系: ● clk_div2 上升沿与 clk_in 对齐 ● clk_div4 上升沿与 clk_in 对齐 ● 若分频器由下降沿触发,则相位偏移半个周期 clk_in (100MHz) clk_div2 (50MHz) clk_div4 (25MHz)

从图上可以清楚看到,二分频时钟的周期是20ns,四分频是40ns。它们的上升沿都与主时钟的上升沿对齐——这是最理想的情况。实际设计中,由于分频器本身的延迟,这个对齐会有偏差,需要通过set_clock_latency来补偿。

4.6 实战中的常见问题

我总结几个实际项目中容易踩的坑:

  1. 源时钟指定错误:-source应该指向时钟树的根节点,而不是时钟的终点。如果你指向了寄存器时钟引脚,工具会报warning,但结果可能不对。
  2. 分频系数与边沿不匹配:比如你写-divide_by 2,但实际电路是下降沿触发分频。这时候生成时钟的相位会差半个周期,必须用-edges或-invert来修正。
  3. 多级生成时钟的-source链:每一级生成时钟的-source都应该指向上一级生成时钟的物理节点,而不是最顶层的主时钟。否则相位关系会乱。
  4. PLL输出时钟的相位偏移:很多PLL在锁定后,输出与输入之间有一个固定的相位偏移。这个偏移在约束里必须体现,否则跨时钟域分析会出错。
核心要点:生成时钟约束的本质,是告诉STA工具「这个时钟的边沿在哪里」。频率可以算,但相位必须精确。相位错了,setup和hold分析全都会偏。

4.7 检查与验证

写完约束后,我建议你用以下命令检查:

report_clock -generated
report_clock_timing -type summary
check_timing -verbose

report_clock -generated会列出所有生成时钟及其源时钟、分频/倍频系数、边沿信息。仔细核对每个生成时钟的边沿时间,确保与你的设计意图一致。

check_timing -verbose会报告所有未约束的路径、悬空的时钟、以及可能的约束冲突。我每次跑完约束都会先跑这个命令,确保没有遗漏。

好了,关于生成时钟的约束,核心就是这些。记住:频率可以算,相位必须量。下一节我们聊聊如何约束异步时钟域,那又是一个容易翻车的地方。

一个小技巧:如果你不确定生成时钟的相位,可以在仿真里把主时钟和生成时钟同时拉出来看波形。用眼睛确认一下边沿对齐关系,再写到约束里。虽然土,但管用。

可可精品资料库