时序约束基础:时钟
时钟,是数字设计的脉搏。做时序约束,说白了就是先把时钟这颗“心脏”给定义清楚。很多新手觉得 create_clock 不就是写个周期嘛?但实际项目中,时钟树一复杂,坑就来了。
时钟从哪里来?
先记一个原则:所有时序路径,都必须有一个已知的时钟起点。在设计中,时钟的来源主要有两种:
- 外部输入:晶振、时钟发生器,通过管脚进来。
- 内部生成:PLL、MMCM,从已有的时钟分频或倍频得到。
对于外部时钟,我们给它套上 create_clock。对于内部生成的时钟,我们用 create_generated_clock 来描述它和源时钟的关系。
create_clock 只定义时钟的物理波形,不改变设计的功能。
核心命令:create_clock
这个命令,是时序约束的基石。它的基本语法就像这样:
create_clock -name clk_sys \
-period 10.000 \
[get_ports clk_in]
-period 10.000 含义是时钟周期为 10ns,对应的频率就是 100MHz。get_ports clk_in 是指出这个时钟是从哪个顶层管脚进来的。
我刚开始做设计时,总觉得周期给得越紧,工具优化越卖力。结果有一次,我随手写了个 -period 8.6(约116MHz),但板级晶振其实是 100MHz。综合工具为了“满足”这个不存在的快时钟,白白塞了很多冗余逻辑进去,功耗和面积都超标。从那以后,我每次都先跟硬件工程师对一遍晶振型号。
时钟周期和占空比
这两个参数,直接决定了时间窗口的长短。
- 时钟周期:一个完整上升沿到下一个上升沿的时间。它定义了逻辑必须在多快的时间内完成计算。
- 占空比:高电平持续时间占整个周期的比例。默认是 50%,也就是高、低电平各占一半。
万一你的时钟不是 50% 占空比怎么办?create_clock
create_clock -name clk_ddr \
-period 5.000 \
-waveform {0.000 2.500}
-waveform 的第一个数是上升沿时间,第二个数是下降沿时间。上面这个例子,上升沿在 0ns,下降沿在 2.5ns(周期的一半),占空比就是 50%。
如果改成 {0.000 1.800},那么高电平 1.8ns,低电平 3.2ns,占空比 36%。
时钟源:谁才是真正的「根」?
当设计中存在多个时钟域,或者有多个同频不同相的时钟时,必须清楚它们的源点在哪里。先看一段 SDC:
create_clock -name clk_a -period 10.000 [get_ports clk_in_a]
create_clock -name clk_b -period 10.000 [get_ports clk_in_b]
假设 clk_in_a 和 clk_in_b 来自同一块晶振的两个输出管脚。在理想世界里,它们完全同频同相。但在实际布局布线中,管脚到触发器时钟端的路径长度不同,会产生不同的时钟插入延迟(Clock Latency)。你不能默认它们天生对齐,必须在约束中明确它们的关系。
我曾经吃过一个亏:板上有两个 PLL,输出时钟通过专用时钟网络送到 FPGA 的两个 BANK。我以为它们同频且相位对齐,就没做跨时钟域同步。结果上板测试,数据隔三差五就错一次。后来用示波器一抓,两个时钟的相位差随着温度和电压在漂移。嗯,硬件没说,我也没问,这个锅我背了。
ODDR、BUFGMUX、BUFIO 等原语转出来的,它的“源”仍然是原始的 create_clock 点。不要重复创建,要用 create_generated_clock 来描述它的生成逻辑。
生成时钟:create_generated_clock
当内部逻辑对时钟做了分频、倍频、相移或反相后,衍生出的新时钟,就叫生成时钟。如果你只定义源时钟,不定义生成时钟,STA 工具就不知道新时钟的存在,导致很多路径不被检查。
来看一个典型的毫米波(MMCM)输出:
create_clock -name clk_100m -period 10.000 [get_ports ref_clock]
create_generated_clock -name clk_200m \
-source [get_ports ref_clock] \
-multiply_by 2 \
-divide_by 1 \
[get_pins mmcm_inst/clk_out1]
create_generated_clock -name clk_div2 \
-source [get_pins mmcm_inst/clk_out1] \
-divide_by 2 \
[get_pins some_ff/Q]
看到没?第一个生成时钟的 -source 是 ref_clock 管脚,第二个生成时钟的 -source 是 mmcm_inst/clk_out1。这串链条,必须完整。
还有一种特殊情况:时钟频率不变,只做移相。比如读取源同步数据时,需要把采样时钟偏移 90°:
create_generated_clock -name clk_shift_90 \
-source [get_pins pll/clk_0] \
-edges {1 2 3} \
-edge_shift {0.000 1.250 2.500} \
[get_pins pll/clk_90]
-edges 表示波形编号(1:上升沿、2:下降沿、3:下一个上升沿),-edge_shift 就是针对每个沿的偏移量。这个写法,我当初看手册时也晕,后来对着波形图比划了两次就明白了——说白了就是手动把整个周期切了一刀。
-source 选哪个点,直接决定了跨时钟域路径的分析范围。选错了,工具要么漏报,要么把不相关的路径也算进来。我会把 get_pins 的路径打印出来,跟 RTL 里的一一对照。
为了把今天这几个概念串起来,我画了一张图,把主时钟、生成时钟和它们之间的关系讲明白:
最后,我想强调一点: