4. SDC 约束编写实战:时钟定义、输入输出延迟、虚假路径与多周期路径约束

SDC 约束,说白了就是告诉工具你的芯片想怎么跑。我见过不少新手,一上来就急着写约束,结果跑出来的时序报告根本没法看。其实约束写得好不好,直接决定了你的芯片能不能按时收敛。

这一章,咱们就聊聊 SDC 约束里最核心的几个部分:时钟怎么定义、输入输出延迟怎么给、哪些路径该设 false path、哪些路径该设 multicycle。这些都是我平时做项目天天要打交道的东西。

4.1 时钟定义——芯片的“心跳”

时钟是时序分析的基准。你想想看,没有时钟,工具根本不知道信号什么时候该到、什么时候该走。所以写 SDC 的第一步,就是把时钟定义清楚。

4.1.1 主时钟(create_clock)

主时钟通常来自芯片的输入引脚,或者 PLL 的输出。我个人习惯,先确认时钟源是什么,再写约束。

# 定义一个 100MHz 的主时钟,占空比 50%
create_clock -name clk_sys -period 10.0 [get_ports clk_in]

这里有几个关键参数:

  • -name:时钟名字,方便后面引用
  • -period:周期,单位 ns
  • [get_ports]:时钟源端口
小技巧:如果时钟有抖动或不确定性,可以用 -jitter 参数。不过我更推荐在时序分析时统一设置 uncertainty,后面会讲到。

4.1.2 生成时钟(create_generated_clock)

芯片内部经常有分频、倍频或门控时钟。这些时钟不能直接用 create_clock,得用 create_generated_clock 来定义它们和主时钟的关系。

# 定义一个 2 分频时钟
create_generated_clock -name clk_div2 -source [get_ports clk_in] \
    -divide_by 2 [get_pins u_div/clk_out]

我在项目中遇到过一个问题:有人把分频时钟直接当主时钟定义,结果工具把两个时钟当成异步的,该分析的路径没分析到,不该分析的反而分析了。嗯,这个坑踩过一次就记住了。

4.1.3 时钟分组与不确定性

不同时钟域之间的路径,需要明确告诉工具它们是同步还是异步的。同步时钟域用 set_clock_groups -physically_exclusive 或 -logically_exclusive,异步的用 -asynchronous。

# 异步时钟分组
set_clock_groups -asynchronous -group [get_clocks clk_a] -group [get_clocks clk_b]

时钟不确定性(uncertainty)也很关键。它包含了时钟抖动、PLL 偏差等因素。我一般这样设:

# 设置时钟不确定性
set_clock_uncertainty -setup 0.2 [get_clocks clk_sys]
set_clock_uncertainty -hold 0.05 [get_clocks clk_sys]
注意:setup 和 hold 的 uncertainty 通常不一样。setup 留的余量要大一些,因为时钟抖动对 setup 影响更大。hold 的 uncertainty 可以小一点,但也不能省。

4.2 输入输出延迟——芯片与外界的“握手”

芯片不是孤立存在的。它要和外部器件通信,比如 DDR 颗粒、ADC、DAC 等。这时候就需要定义输入输出延迟,告诉工具信号从芯片引脚到内部寄存器,或者从内部寄存器到芯片引脚,花了多少时间。

4.2.1 输入延迟(set_input_delay)

输入延迟指的是外部信号从发送端到芯片输入引脚的时间。这个时间包括外部器件的输出延迟、PCB 走线延迟等。

# 定义输入延迟,相对于时钟的上升沿
set_input_delay -clock clk_sys -max 2.5 [get_ports data_in]
set_input_delay -clock clk_sys -min 1.0 [get_ports data_in]

这里 -max 和 -min 分别对应最坏和最好情况。我习惯先看外部器件的 datasheet,把它的输出延迟范围找出来,再加上 PCB 走线的估计值。

4.2.2 输出延迟(set_output_delay)

输出延迟是芯片输出信号到外部器件接收端的时间。它包含了 PCB 走线延迟和外部器件的建立/保持时间要求。

# 定义输出延迟
set_output_delay -clock clk_sys -max 3.0 [get_ports data_out]
set_output_delay -clock clk_sys -min 0.5 [get_ports data_out]
经验之谈:输入输出延迟的约束,宁紧勿松。我曾经因为把输出延迟设得太宽松,结果芯片在高温下输出时序不满足,差点导致流片失败。从那以后,我每次都会留 10%-20% 的余量。

4.3 虚假路径(False Path)——别让工具白费力气

芯片里有些路径,工具根本不需要分析。比如跨时钟域的同步器路径、复位信号路径、测试模式下的路径等。这些路径如果不去掉,工具会花大量时间优化它们,反而耽误了真正关键的路径。

4.3.1 什么时候设 False Path

  • 跨时钟域的同步器路径:已经用两级或三级触发器同步了,不需要 STA 分析
  • 复位信号路径:复位信号通常异步,不需要时序约束
  • 测试模式下的路径:比如 scan 模式下的路径,只在测试时用
  • 静态配置信号:芯片上电后就不变的信号
# 设置跨时钟域路径为 false path
set_false_path -from [get_clocks clk_a] -to [get_clocks clk_b]

# 设置复位路径为 false path
set_false_path -to [get_pins *rst_n_reg/D]
警告:False path 不能乱设。我见过有人把整个模块的路径都设成 false path,结果芯片功能正常但时序完全没收敛。记住,只有你确定不需要分析的路径才设 false path。

4.4 多周期路径(Multicycle Path)——给慢信号多一点时间

有些路径不需要在一个时钟周期内完成。比如从慢速外设读取数据,或者某些计算路径需要多个周期才能稳定。这时候就要用多周期约束,告诉工具放宽检查条件。

4.4.1 什么时候用 Multicycle

  • 慢速外设接口:比如 SPI、I2C 等,数据可能多个周期才变化一次
  • 长计算路径:比如乘法器、除法器,可能需要多个周期才能出结果
  • 使能信号控制的路径:数据只在使能信号有效时才变化
# 设置 setup 检查为 2 个周期
set_multicycle_path -setup 2 -from [get_pins u_mul/a_reg/CK] -to [get_pins u_mul/b_reg/D]

# 对应的 hold 检查也要调整
set_multicycle_path -hold 1 -from [get_pins u_mul/a_reg/CK] -to [get_pins u_mul/b_reg/D]

这里有个容易搞混的地方:setup 设成 2,hold 要设成 1。为什么?因为 hold 检查默认比 setup 早一个周期。如果你只设 setup 不设 hold,工具会认为 hold 也要放宽,结果可能把 hold 检查放得太松。

核心要点:Multicycle 约束一定要 setup 和 hold 一起设。我习惯先设 setup,再设 hold,两个都检查一遍。

4.5 知识结构总览

下面这张图把本章的核心知识点串起来了。你可以把它当成一个检查清单,写 SDC 的时候对照着看。

SDC 约束核心知识结构 SDC 约束 时钟定义 输入输出延迟 虚假路径 多周期路径 主时钟 生成时钟 输入延迟 输出延迟 跨时钟域 复位/测试 setup 调整 hold 调整 提示:写 SDC 时,按“时钟 → 延迟 → 例外”的顺序来,不容易漏掉关键约束 每个约束写完后,用 report_timing 检查一下效果

4.6 实战案例:一个简单的 SDC 文件

说了这么多,咱们来写一个完整的 SDC 文件。假设有一个芯片,包含一个 100MHz 的主时钟、一个 50MHz 的分频时钟、一个 SPI 接口和一个乘法器模块。

# ============================================
# 时钟定义
# ============================================
create_clock -name clk_sys -period 10.0 [get_ports clk_in]
create_generated_clock -name clk_spi -source [get_ports clk_in] \
    -divide_by 2 [get_pins u_div/clk_out]

# ============================================
# 时钟分组
# ============================================
set_clock_groups -asynchronous -group [get_clocks clk_sys] \
    -group [get_clocks clk_spi]

# ============================================
# 时钟不确定性
# ============================================
set_clock_uncertainty -setup 0.2 [get_clocks clk_sys]
set_clock_uncertainty -hold 0.05 [get_clocks clk_sys]

# ============================================
# 输入输出延迟
# ============================================
# SPI 输入
set_input_delay -clock clk_spi -max 5.0 [get_ports spi_mosi]
set_input_delay -clock clk_spi -min 2.0 [get_ports spi_mosi]

# SPI 输出
set_output_delay -clock clk_spi -max 4.0 [get_ports spi_miso]
set_output_delay -clock clk_spi -min 1.0 [get_ports spi_miso]

# ============================================
# 虚假路径
# ============================================
# 跨时钟域路径
set_false_path -from [get_clocks clk_sys] -to [get_clocks clk_spi]
set_false_path -from [get_clocks clk_spi] -to [get_clocks clk_sys]

# 复位路径
set_false_path -to [get_pins *rst_n_reg/D]

# ============================================
# 多周期路径
# ============================================
# 乘法器路径,需要 2 个周期
set_multicycle_path -setup 2 -from [get_pins u_mul/a_reg/CK] \
    -to [get_pins u_mul/result_reg/D]
set_multicycle_path -hold 1 -from [get_pins u_mul/a_reg/CK] \
    -to [get_pins u_mul/result_reg/D]
总结:写 SDC 约束,说白了就是三个步骤:先定义时钟,再定义输入输出延迟,最后处理例外路径。每一步都要想清楚为什么这么写。我每次写完 SDC,都会跑一遍 report_timing 和 report_clock,确认约束生效了再往下走。