时钟约束入门:从一条命令说起

时钟,是数字芯片的“心跳”。

我刚入行那会儿,总觉得时序约束是后端工程师的事。直到有一次,我写了一个简单的 SPI 接口,仿真跑得飞起,结果流片回来死活读不到数据。查了三天,最后发现——我根本没告诉工具时钟长什么样。从那以后,我养成了一个习惯:写 RTL 之前,先把时钟约束写好。

这一章,我们就从最基础的 create_clock 命令开始,把时钟约束的门摸清楚。

1. create_clock 命令详解

说白了,create_clock 就是告诉 STA 工具:“嘿,这里有个时钟,它的周期是多少,什么时候跳变。” 没有这条命令,工具连你的设计跑多快都不知道。

基本语法长这样:

create_clock -name <时钟名> -period <周期> [ -waveform <上升沿 下降沿> ] [ -add ] [ <端口/引脚列表> ]

我来拆开讲讲每个参数。

1.1 -name:给时钟起个名字

名字最好有意义。我见过有人用 clk1、clk2,结果三个月后自己都忘了哪个是哪个。我个人习惯用 sys_clk、mem_clk、pcie_refclk 这种命名方式,一看就知道用途。

1.2 -period:时钟周期

单位是纳秒(ns)。比如 100MHz 的时钟,周期就是 10ns。

这里有个坑:周期必须大于 0。你可能觉得这是废话,但我真见过有人写 -period 0,结果工具直接报错。嗯,那哥们后来被项目经理叫去喝茶了。

1.3 -waveform:定义波形

这个参数指定时钟的上升沿和下降沿位置。默认情况下,工具认为时钟在 0ns 上升、在周期的一半下降——也就是 50% 占空比。

举个例子:

# 100MHz 时钟,50% 占空比
create_clock -name sys_clk -period 10 [get_ports clk_in]

# 显式指定波形:0ns 上升,5ns 下降
create_clock -name sys_clk -period 10 -waveform {0 5} [get_ports clk_in]

这两种写法效果完全一样。但如果你要定义非 50% 占空比的时钟,就必须用 -waveform 了。

小技巧: 我习惯把 -waveform 写出来,哪怕只是默认值。为什么?因为半年后你回来看脚本,一眼就能确认占空比,不用猜。

1.4 -add:叠加多个时钟

这个参数容易被忽略。如果你在同一个端口上定义多个时钟,不加 -add,后面的会覆盖前面的。

比如:

# 错误:第二个会覆盖第一个
create_clock -name clk_a -period 10 [get_ports clk]
create_clock -name clk_b -period 20 [get_ports clk]  # clk_a 没了!

# 正确:用 -add 保留两个时钟
create_clock -name clk_a -period 10 [get_ports clk]
create_clock -name clk_b -period 20 [get_ports clk] -add

我在一个多协议芯片项目中就吃过这个亏。当时想给同一个引脚定义两种时钟模式,忘了加 -add,结果综合出来的电路少了一半的时序路径。排查了两天才发现是约束脚本的问题。

2. 时钟周期与占空比

时钟周期决定了芯片能跑多快。占空比则影响时序分析的精度。

2.1 周期:性能的标尺

周期越短,频率越高,性能越好。但代价是时序收敛更难。

频率 周期 典型应用
1 MHz 1000 ns 低速控制接口
100 MHz 10 ns 通用 SoC 总线
1 GHz 1 ns 高速 SerDes
5 GHz 0.2 ns 超算互联

你想想看,一个 1GHz 的时钟,周期只有 1ns。扣除时钟偏斜、建立时间、传输延迟,留给逻辑门的有效时间可能只有 0.3ns。这就是为什么高频设计那么难。

2.2 占空比:不只是 50%

大多数设计用 50% 占空比。但有些场景需要非对称时钟:

  • DDR 接口:数据在时钟的上升沿和下降沿都采样,占空比偏差会直接影响时序裕量。
  • 脉冲锁存器:需要很窄的高电平脉冲,占空比可能只有 10%。
  • 时钟门控:为了降低功耗,有时会故意调整占空比。

定义非 50% 占空比的时钟:

# 100MHz 时钟,30% 占空比(高电平 3ns,低电平 7ns)
create_clock -name ddr_clk -period 10 -waveform {0 3} [get_ports ddr_ck]
注意: 占空比偏差(duty cycle distortion)是真实存在的。我曾经在一个 28nm 项目里,PLL 输出的时钟占空比只有 48%,但约束里写的是 50%。结果后仿发现 setup 和 hold 都差一点点。最后不得不把占空比改成 48% 重新跑 STA。

3. 时钟组的概念

一个复杂芯片里,往往有几十个时钟。它们之间有些是同步的,有些是异步的。如果不告诉工具哪些时钟属于同一组,工具会默认所有时钟都是同步的——然后疯狂分析跨时钟域路径,产生大量假路径,浪费运行时间。

3.1 什么是时钟组

时钟组,就是把功能相关、频率成整数倍关系的时钟归到一起。工具会分析组内时钟之间的路径,而忽略组间路径(除非你另外约束)。

常见的分组方式:

  • 同源时钟:由同一个 PLL 分频出来的时钟,比如 200MHz 和 100MHz。
  • 同频时钟:频率相同但来源不同的时钟,比如两个独立晶振。
  • 异步时钟:频率无关、相位无关的时钟,必须分到不同组。

3.2 用 set_clock_groups 分组

命令格式:

set_clock_groups -name <组名> -group {时钟列表} [-group {时钟列表} ...]

举个例子:

# 定义两个时钟组
set_clock_groups -name cpu_group \
  -group {cpu_clk cpu_clk_div2} \
  -group {mem_clk mem_clk_div4}

# 定义异步时钟组(不分析跨组路径)
set_clock_groups -name async_group \
  -group {uart_clk} \
  -group {pcie_clk} \
  -asynchronous

这里 -asynchronous 是关键。加了它,工具就知道这两个组之间的路径不需要做时序分析——除非你显式用 set_false_path 或 set_max_delay 去约束。

3.3 我踩过的坑

有一次,我把 CPU 总线的两个时钟分到了不同组,结果工具没分析它们之间的路径。流片回来,CPU 在特定频率下偶尔读错数据。查了两个月,最后发现是时钟组设错了。

从那以后,我定了个规矩:时钟组定义必须经过团队 review。一个人写脚本,另一个人检查,谁都不许偷懒。

4. 知识结构总览

下面这张图,把本章的核心知识点串起来了:

时钟约束入门知识结构 时钟约束入门知识结构 create_clock 命令参数详解 -name / -period / -waveform / -add 周期与占空比 频率 ↔ 周期 / 占空比偏差 时钟组概念 set_clock_groups / 异步分组 核心原则:先定义时钟,再分组,最后做时序分析 本章要点 • create_clock 是时序约束的起点,必须掌握每个参数的含义 • 周期决定性能上限,占空比影响分析精度,两者都要准确 • 时钟组管理跨时钟域路径,分组错误会导致假路径或漏分析

5. 实战建议

说了这么多,最后给几条实在的建议:

  1. 写约束脚本时,把 create_clock 放在最前面。我习惯在 RTL 代码的顶层文件里加注释,标注所有输入时钟的预期频率,然后同步更新约束脚本。
  2. 每个时钟都加 -waveform,哪怕只是默认值。这样脚本自文档化,别人(包括未来的你)一看就懂。
  3. 时钟组定义要尽早做。不要等到 STA 报了几千条 violation 才想起来分组。我一般在项目第二周就把时钟组定下来。
  4. 用 report_clock 验证。写完约束后,跑一下 report_clock,确认所有时钟的周期、波形、分组都符合预期。这一步花不了 5 分钟,但能省下后面几天的排查时间。

一句话总结: 时钟约束是时序分析的基石。create_clock 定义时钟的“心跳”,时钟组管理不同“心跳”之间的关系。这两件事做好了,后面的 STA 工作就顺了。