第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
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。它就是工具脑补的一个理想时钟源,只用于计算时序裕量。
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命令练熟,后面我们还会反复用到它。