生成时钟约束:分频、倍频、相移与级联实战

时钟约束,说白了就是告诉工具:我这个时钟长什么样。但生成时钟(Generated Clock)就没那么简单了。它是由主时钟经过PLL、MMCM或者简单的组合逻辑派生出来的。你想想看,如果工具连时钟的源头都搞不清楚,时序分析怎么可能准?

我个人习惯,拿到一个设计,先找主时钟,再找生成时钟。主时钟是根,生成时钟是枝叶。根扎不稳,枝叶就别想长好。

分频时钟的约束

分频时钟最常见。比如你用计数器对100MHz做了个2分频,得到50MHz。怎么约束?

create_generated_clock -name clk_div2 \
    -source [get_ports clk_in] \
    -divide_by 2 \
    [get_pins reg_div/Q]

这里有个坑:-source 到底指什么?是源时钟的端口,还是源时钟经过的某个引脚?我刚开始也搞混过。记住一句话:source 是生成时钟的参考起点,通常是主时钟的根节点

我曾经踩过的坑: 有一次我把 source 指向了分频寄存器的时钟引脚(CK),结果工具报了一堆莫名其妙的时序违例。后来才发现,source 应该指向主时钟的源端口,而不是中间节点。工具需要知道生成时钟和主时钟之间的相位关系,你给它一个中间节点,它算出来的相位就是错的。

如果是偶数分频,用 -divide_by 就行。奇数分频呢?比如3分频,占空比可能不是50%。这时候要加 -duty_cycle 参数:

create_generated_clock -name clk_div3 \
    -source [get_ports clk_in] \
    -divide_by 3 \
    -duty_cycle 33.33 \
    [get_pins reg_div3/Q]

倍频时钟的约束

倍频通常靠PLL或MMCM实现。这类器件内部已经帮你算好了,你只需要告诉工具:这个时钟是从哪个PLL输出来的。

create_generated_clock -name clk_200m \
    -source [get_pins mmcm_inst/CLKIN1] \
    -multiply_by 2 \
    [get_pins mmcm_inst/CLKOUT0]

嗯,这里要注意:-source 指向的是PLL的输入时钟引脚,不是主时钟的端口。为什么?因为PLL内部有相位调整,工具需要知道输入到输出的精确关系。

我的小技巧: 用Xilinx的IP核生成PLL时,Vivado会自动帮你把生成时钟约束写好。但别完全依赖它。我遇到过几次自动生成的约束里 -source 指向不对,导致跨时钟域分析出问题。所以每次生成IP后,我都会手动检查一下约束文件。

相移时钟的约束

相移时钟在DDR接口里用得最多。比如DDR需要90度相移的时钟来采样数据。约束方法很简单:

create_generated_clock -name clk_90shift \
    -source [get_pins mmcm_inst/CLKIN1] \
    -multiply_by 1 \
    -phase_shift 90 \
    [get_pins mmcm_inst/CLKOUT1]

关键参数是 -phase_shift,单位是度。但注意,有些工具里 -phase_shift 的单位是皮秒,要看清楚文档。

我做过一个DDR3项目,需要0度、90度、180度、270度四相时钟。一开始我偷懒,只约束了0度和90度,结果后仿真时数据采样总出错。后来老老实实把四个相位全约束上,问题就解决了。工具不知道你没约束的时钟长什么样,它就会用最乐观的方式去分析,这很危险。

生成时钟级联问题

这是DDR接口项目里最容易出问题的地方。什么叫级联?就是一个生成时钟的源,本身也是另一个生成时钟。

举个例子:主时钟100MHz -> PLL倍频到200MHz -> 再用寄存器对200MHz做2分频得到100MHz。这个最终的100MHz就是级联生成时钟。

约束方法有两种:

  1. 直接级联约束:source指向中间生成时钟的输出
  2. 参考主时钟约束:source直接指向主时钟,用 -divide_by-multiply_by 组合算出最终频率

我个人强烈推荐第二种。为什么?因为第一种方式,工具需要追踪两级生成时钟的关系,容易产生歧义。我曾经在一个项目里用了第一种方式,结果STA报告里出现了负的setup slack,但实际硬件跑起来没问题。查了两天才发现,是级联约束导致工具算错了时钟沿对齐。

// 推荐做法:直接参考主时钟
create_generated_clock -name clk_final \
    -source [get_ports clk_100m] \
    -multiply_by 2 \
    -divide_by 2 \
    [get_pins reg_div/Q]

// 不推荐做法:级联参考
create_generated_clock -name clk_final \
    -source [get_pins pll_inst/CLKOUT0] \
    -divide_by 2 \
    [get_pins reg_div/Q]

核心原则: 生成时钟的 source 尽量指向主时钟,不要指向另一个生成时钟。除非你非常清楚自己在做什么,并且验证过时序分析结果正确。

DDR接口中的实战案例

去年我做的一个DDR4项目,遇到了一个典型的级联问题。架构是这样的:

  • 主时钟 100MHz
  • PLL 输出 400MHz(DDR时钟)
  • PLL 输出 200MHz(控制器时钟)
  • 用 400MHz 再生成一个 200MHz 的相移时钟(用于DQ采样)

问题出在哪?第三个时钟的 source 我指向了 PLL 的 400MHz 输出。结果工具在做跨时钟域分析时,认为 200MHz 控制器时钟和 200MHz 采样时钟是异步的——明明它们同源!

解决方案:把第三个时钟的 source 改为主时钟 100MHz,用 -multiply_by 4 -divide_by 2 -phase_shift 90 来描述。这样工具就知道它们同源,会自动做相位分析。

注意: 如果你用了 -phase_shift,一定要确保 source 的时钟周期是已知的。否则工具无法计算相移对应的实际延迟。我见过有人把 source 指向一个没有约束的时钟,然后 phase_shift 设了90度,工具直接忽略了这个参数。

知识体系总结

下面这张图,是我做这个章节时画的。它把生成时钟约束的核心逻辑串起来了:

生成时钟约束知识体系 主时钟 (Primary Clock) 通过 PLL / MMCM / 组合逻辑 派生 分频时钟 -divide_by / -duty_cycle 倍频时钟 -multiply_by 相移时钟 -phase_shift 生成时钟级联 source 尽量指向主时钟,避免级联 DDR接口:多相位时钟 + 级联约束

这张图你看懂了吗?从主时钟出发,经过分频、倍频、相移三种方式,最终都可能遇到级联问题。而DDR接口,恰恰是这三种方式混合使用最多的地方。

最后分享一个经验: 写完生成时钟约束后,用 report_clock_interaction 命令检查一下。如果发现两个明明同源的时钟显示为异步,那八成是约束写错了。我每次做完约束,都会跑这个命令看一眼,能省下不少调试时间。


公众号:蓝海资料掘金营,微信deep3321