第4章 时钟定义:create_clock命令深度解析

各位同学,今天我们来啃一块硬骨头——create_clock命令。说实话,我刚入行那会儿,以为这就是个声明时钟的命令,随便写几行就完事了。直到有一次,我定义了一个时钟周期少打了一个0,结果整个芯片的时序报告全是红的……

好,咱们正式开始。

4.1 命令的基本语法

先看最基础的样子,这条命令也是我们每天都要打的代码:

create_clock -name <clock_name> -period <period_value> [-waveform <edge_list>] [source_objects]

我习惯把参数拆成三部分来记:

  • -name:时钟的名字,你给它取个代号,比如clk_sys、clk_ddr
  • -period:时钟周期,单位是ns,比如10ns对应100MHz
  • -waveform:波形定义,默认是上升沿在0,下降沿在period/2
小提示: -name不是必须的。如果你不写,工具会自动用source_objects的名字当时钟名。但我建议你每次都显式写-name,因为后面sdc脚本里引用时钟名的时候,一眼就能看出是哪个时钟。

4.2 时钟周期 -period

周期就是时钟信号的重复时间。100MHz的时钟,周期就是10ns。这个不难理解,但有一个坑——单位问题。

我个人习惯在SDC文件开头加一句set_units -time ns,这样所有时间单位就统一为纳秒了。如果你忘了写,默认也是ns。

看几个例子:

# 100MHz时钟
create_clock -name clk_100m -period 10 [get_ports clk_in]

# 50MHz时钟
create_clock -name clk_50m -period 20 [get_ports clk_in]

# 25MHz时钟
create_clock -name clk_25m -period 40 [get_ports clk_in]

你想想看,如果我不说单位,你敢直接用吗?我见过有人把50MHz写成period 20,然后工具报错,查半天才发现是单位理解错了。

4.3 波形定义 -waveform

默认波形是上升沿在0ns,下降沿在period/2。对于占空比50%的时钟,这个默认值就够了。

但如果你遇到非50%占空比的时钟怎么办?比如DDR接口,经常需要调整占空比。这时候就要用-waveform参数了。

# 占空比30%,上升沿在0ns,下降沿在3ns(10ns周期)
create_clock -name clk_ddr -period 10 -waveform {0 3} [get_ports ddr_clk]

# 占空比70%,上升沿在0ns,下降沿在7ns
create_clock -name clk_70 -period 10 -waveform {0 7} [get_ports clk_in]

这里的-waveform列表,{0 3}表示第一个上升沿在0ns,第一个下降沿在3ns。工具会自动推算出后面的边沿位置。

注意:波形列表的元素个数必须是偶数。我曾经犯过只写一个边沿的错误,结果工具把后面的边沿全算错了,我怎么分析都找不到原因。

另一个问题是:如果上升沿不在0怎么办?

# 上升沿在2ns,下降沿在7ns的10ns时钟
create_clock -name clk_shift -period 10 -waveform {2 7} [get_ports clk_in]

工具会认定第一个上升沿是2ns,然后推断下一个上升沿在2+10=12ns。这就是波形平移的效果。

4.4 时钟源对象

源对象就是时钟信号从哪里来。通常是端口或引脚。你不能把一个组合逻辑的输出直接当时钟源(至少应该用寄存器输出)。

# 从端口进来
create_clock -name sys_clk -period 10 [get_ports sys_clk_pad]

# 从PLL输出引脚进来
create_clock -name pll_clk -period 5 [get_pins u_pll/clk_out]

# 从寄存器Q端进来(分频时钟)
create_clock -name div_clk -period 20 [get_pins u_div/clk_div_reg/Q]

这里有个细节:get_ports和get_pins的区别。我刚开始总搞混,直到有一次我把PLL的输出引脚用get_ports去抓,结果返回空指针,工具报错说找不到对象。

命令 对象类型 典型应用场景
get_ports 端口(port) 芯片输入时钟引脚、输出时钟引脚
get_pins 引脚(pin) PLL输出、分频器输出、内部模块时钟输出

4.5 多个时钟定义

实际项目里很少只有一个时钟。你可能会在同一个端口定义多个时钟,比如测试用的慢时钟和功能用的快时钟。

# 在同一个端口定义两个时钟
create_clock -name clk_sys -period 10 -waveform {0 5} [get_ports clk_in]
create_clock -name clk_test -period 100 -waveform {0 50} [get_ports clk_in] -add

注意那个-add参数。如果没有它,第二个create_clock会覆盖第一个。我刚学的时候就被这个坑过——仿真里加了一个测试时钟,结果原来的功能时钟没了。

为什么会这样?因为工具默认每个端口或引脚只能有一个主时钟。如果你想放多个,必须用-add告诉工具:这是额外加的。

4.6 虚拟时钟

虚拟时钟是个好东西,但它不是物理存在的信号。它只用于约束输入输出延迟。比如你的芯片要和外部器件通信,外部器件的时钟不是从你芯片内部产生的,这时候就需要虚拟时钟。

# 定义一个虚拟时钟(不绑定任何端口或引脚)
create_clock -name vclk -period 10 -waveform {0 5}

# 然后用它约束输入延迟
set_input_delay -clock vclk -max 3.5 [get_ports data_in]
set_output_delay -clock vclk -min 1.0 [get_ports data_out]

虚拟时钟不需要source_objects。它就是工具脑补的一个理想时钟源,只用于计算时序裕量。

核心要点:虚拟时钟常用于约束I/O接口。如果没有它,工具不知道你的数据相对于哪个时钟对齐。

4.7 案例:一个真实的时钟定义场景

我之前做的一个FPGA项目,芯片内部有3个时钟域:

  • 主频200MHz(PLL产生)
  • 慢速SPI接口时钟50MHz(从外部引脚进来)
  • 调试时钟100MHz(也是PLL产生,但占空比25%)

我当时写的SDC片段是这样:

# 1. 外部输入时钟
create_clock -name ext_clk -period 20 [get_ports sys_clk_p]

# 2. PLL输出200MHz(上升沿在0,下降沿在2.5ns)
create_clock -name pll_fast -period 5 -waveform {0 2.5} [get_pins u_pll/clk_out]

# 3. SPI接口时钟
create_clock -name spi_clk -period 20 [get_ports spi_clk_p]

# 4. 调试时钟100MHz,占空比25%
create_clock -name dbg_clk -period 10 -waveform {0 2.5} [get_pins u_pll/dbg_clk]

# 5. 在同一个PLL输出再加一个测试时钟(周期100ns)
create_clock -name test_clk -period 100 [get_pins u_pll/clk_out] -add

你留意到没有?第5行用了-add,在同一个PLL输出定义了test_clk。这个test_clk是用于低速测试模式的,频率比pll_fast慢很多。通过这种方式,同一个管脚可以切换不同模式。

4.8 避坑经验总结

我做了这么多年,踩过不少坑,挑几个最典型的说说:

  • 周期写错:10ns周期写成10.0ns没问题,但10.0和10ns在工具里是一样的。不过有人会把10MHz写成period 10,结果变成了100MHz。记住:period = 1000 / F(MHz)
  • 边沿顺序搞反:波形{0 3}是上升沿在0,下降沿在3。有人写成{3 0},工具会报错或者产生奇怪的波形
  • 忘记-add:多个时钟在同一端口时,不加-add会被覆盖
  • 虚拟时钟不绑定端口:这个不是错误,但你要记住它只用于约束,不是真实信号
避坑指南:我曾经把虚拟时钟误用了get_ports,结果工具报找不到端口。实际上虚拟时钟不需要source_objects。如果你发现create_clock后面跟着get_ports或get_pins,那它一定不是虚拟时钟。

4.9 本章知识图谱

下面这张图概括了create_clock命令的整个知识结构:

create_clock 命令核心知识体系 create_clock 关键参数 -name 时钟名 -period 周期 -waveform 波形 源对象 端口 get_ports 引脚 get_pins 虚拟时钟(无源对象) 常见场景 非50%占空比 同一端口多时钟(-add) I/O约束用虚拟时钟 核心原则:一个周期内边沿数必须为偶数,且第一个边沿是上升沿 避坑:多个时钟同一源对象必须加 -add

上面的图把create_clock的核心要素都串起来了。你可以先从中心节点开始,然后向三个分支扩展:关键参数决定时钟怎么定义,源对象决定时钟从哪里来,常见场景解决实际项目中遇到的问题。

好,今天的课就这些。大家把create_clock命令练熟,后面我们还会反复用到它。