2、SDC约束基础:SDC文件结构、时钟定义、生成时钟与时钟组

各位同学,咱们今天聊聊SDC约束。说实话,我刚入行那会儿,觉得SDC就是个麻烦事——不就是告诉工具时钟跑多快嘛?后来吃过亏才明白,约束写不好,后端跑出来的结果全是坑。

SDC,全称Synopsys Design Constraints,是业界通用的时序约束格式。Siemens EDA的工具也完全支持它。你想想看,一个芯片里成百上千条路径,工具怎么知道哪些路径重要?全靠SDC告诉它。

2.1 SDC文件结构

一个规范的SDC文件,我习惯把它分成三大块:

  • 时钟定义:告诉工具时钟长什么样——频率、占空比、抖动
  • IO约束:输入输出端口相对于时钟的时序关系
  • 例外约束:false path、multicycle path这些特殊路径

我个人习惯把SDC文件按模块拆分,顶层只做时钟定义和IO约束,子模块的例外约束放在各自目录下。这样后期调试时,找问题快得多。

小技巧: 我建议每个SDC文件开头都加个注释块,写明版本号、修改日期、修改人。别嫌麻烦,项目后期你就知道这有多重要了。

2.2 时钟定义:create_clock

时钟是时序分析的基准。没有时钟,工具根本不知道什么时候该采样数据。

最基本的时钟定义长这样:

create_clock -name clk_sys -period 10.0 [get_ports clk_in]

这条命令的意思是:在端口clk_in上创建一个名为clk_sys的时钟,周期10ns,也就是100MHz。

嗯,这里要注意——-name参数不是必须的,但我强烈建议加上。为什么?因为工具会自动生成一个名字,那名字又长又难记,你后面写约束时根本不想用它。

时钟还可以定义占空比和边沿:

create_clock -name clk_ddr -period 5.0 \
  -waveform {0.0 2.5} [get_ports clk_ddr_in]

-waveform后面跟两个值:上升沿时间和下降沿时间。上面这个例子,上升沿在0ns,下降沿在2.5ns,占空比50%。

重要: 时钟定义时,-waveform的默认值是{0 period/2},也就是50%占空比。如果你的时钟不是50%,一定要显式指定。

我在项目中遇到过一件事:有个同事定义时钟时没写-name,结果工具自动生成了clk_in作为时钟名。后来他写生成时钟时引用了这个名字,但端口名和时钟名混在一起,查了半天才找到问题。从那以后,我每个时钟都手动命名。

2.3 生成时钟:create_generated_clock

生成时钟,说白了就是由主时钟分频、倍频或相移得到的时钟。比如PLL输出的时钟,或者寄存器分频后的时钟。

基本语法:

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

这条命令的意思是:在u_divider/Q这个引脚上,创建一个名为clk_div2的生成时钟,它由clk_in二分频得到。

常用的参数有:

参数 说明 示例
-divide_by 分频系数 -divide_by 2
-multiply_by 倍频系数 -multiply_by 2
-invert 反相 -invert
-edges 自定义边沿 -edges {1 3 5}
-edge_shift 边沿偏移 -edge_shift {0 0.5 0}

这里有个坑,我曾经踩过——-source参数指定的是主时钟的源点,不是主时钟的名字。很多人写成-source [get_clocks clk_in],这是错的。应该写成-source [get_ports clk_in]或者-source [get_pins ...]

注意: 生成时钟的-source必须指向物理节点(端口或引脚),不能指向时钟对象。这是新手最容易犯的错误之一。

再举个复杂点的例子——DDR时钟,需要产生相移90度的时钟:

create_generated_clock -name clk_ddr_90 \
  -source [get_ports clk_in] \
  -edges {1 2 3} \
  -edge_shift {0 1.25 0} \
  [get_pins u_pll/clk_out]

这个例子中,-edges {1 2 3}表示继承主时钟的第1、2、3个边沿,-edge_shift给第二个边沿偏移了1.25ns,也就是90度相移。

2.4 时钟组:set_clock_groups

时钟组的作用,说白了就是告诉工具:这些时钟之间不需要做时序分析。最常见的场景是异步时钟域。

基本语法:

set_clock_groups -asynchronous \
  -group [get_clocks clk_a] \
  -group [get_clocks clk_b]

这条命令的意思是:clk_a和clk_b是异步时钟,它们之间的路径不需要分析。

除了-asynchronous,还有-physically_exclusive-logically_exclusive

  • -asynchronous:异步时钟,频率和相位都没关系
  • -physically_exclusive:物理上互斥,比如两个时钟不会同时出现在芯片上
  • -logically_exclusive:逻辑上互斥,比如MUX选择的两个时钟

我个人习惯,对于真正的异步时钟域,一定用-asynchronous。对于MUX选择的时钟,用-logically_exclusive。别混用,否则工具可能会漏掉一些该分析的路径。

关键点: 同一个时钟不能出现在多个-group中。如果你想把多个时钟放在一组,用空格隔开就行:
set_clock_groups -asynchronous -group {clk_a clk_b} -group {clk_c}

我曾经在一个项目里看到有人这么写:

set_clock_groups -asynchronous \
  -group [get_clocks clk_a] \
  -group [get_clocks clk_a]

同一个时钟出现在两个组里,工具直接报错。嗯,这种低级错误其实很容易犯,特别是项目后期时钟定义改来改去的时候。

2.5 实战建议

最后,我给大家几个实战中的小建议:

  1. 时钟命名要规范:我习惯用clk_前缀,后面跟功能名和频率,比如clk_core_500m
  2. 生成时钟一定要加-master吗? 不一定。但如果你有多个主时钟到达同一个生成点,就必须用-master指定用哪个
  3. 时钟组要写注释:说明为什么这些时钟是异步的,方便后人理解
  4. 写完约束后跑一下report_clock:看看时钟定义是否符合预期,这一步能发现很多低级错误

好了,SDC约束基础就讲到这里。下一章咱们聊聊IO约束和时序例外,那才是真正考验功力的地方。