2. SDC约束基础:语法、常用命令与文件组织

SDC,全称Synopsys Design Constraints,是FPGA时序约束的行业标准格式。说白了,它就是一份告诉工具“你的设计要在什么条件下工作”的说明书。

我刚入行那会儿,觉得SDC就是个麻烦事。后来有一次,一个跑得好好的设计,换了个器件型号就崩了。查了三天,最后发现是约束文件里少写了一条时钟定义。从那以后,我再也不敢小看SDC了。

2.1 SDC语法基础

SDC的语法其实很简单。它基于Tcl(Tool Command Language),所以每条命令都是一个Tcl函数调用。基本格式就两种:

# 带参数的命令
命令名 -选项1 值1 -选项2 值2 对象

# 不带参数的命令
命令名 对象

举个例子:

create_clock -name sys_clk -period 10.000 [get_ports clk_in]

这条命令的意思是:创建一个名为sys_clk的时钟,周期10ns(对应100MHz),源端口是clk_in。

小技巧:SDC中的注释用#开头。我习惯在每个约束块前面加一段注释,说明这个约束是干什么的。半年后你自己回来看,会感谢这个习惯的。

2.2 常用命令概览

SDC命令很多,但日常用的也就那么十来条。我把它们分成三类:

类别 命令 作用
时钟约束 create_clock 定义主时钟
时钟约束 create_generated_clock 定义衍生时钟(PLL输出等)
时钟约束 set_clock_uncertainty 设置时钟抖动和裕量
I/O约束 set_input_delay 定义输入信号相对于时钟的延迟
I/O约束 set_output_delay 定义输出信号相对于时钟的延迟
I/O约束 set_input_transition 设置输入信号的转换时间
I/O约束 set_output_load 设置输出端口的负载电容
例外约束 set_false_path 标记不需要时序分析的路径
例外约束 set_multicycle_path 定义跨多个时钟周期的路径
例外约束 set_max_delay / set_min_delay 自定义路径延迟上限/下限

2.2.1 create_clock — 一切约束的起点

没有时钟,时序分析就无从谈起。create_clock是最基础的命令。它的核心参数就几个:

create_clock -name <时钟名> -period <周期(ns)> \
             [ -waveform {上升沿 下降沿} ] \
             [ -add ] [get_ports <端口名>]

注意-waveform参数。默认情况下,时钟的上升沿在0ns,下降沿在period/2。如果你的时钟是占空比50%的方波,可以省略这个参数。但如果是非50%占空比,或者有相位偏移,就必须显式指定。

避坑:我曾经在一个设计中,用PLL生成了一个90度相移的时钟。我偷懒没写-waveform,结果工具默认认为上升沿在0ns。整个设计的时序分析全错了。后来花了整整一天才找到原因。所以,只要时钟波形不是标准的50%占空比方波,就老老实实写-waveform。

2.2.2 set_input_delay / set_output_delay — 连接片外世界

这两个命令是FPGA与外部器件通信的关键。它们告诉工具:数据相对于时钟,在芯片引脚上是什么时候有效的。

# 输入延迟:外部器件发送数据到FPGA引脚的时间
set_input_delay -clock clk -max 2.5 [get_ports data_in]
set_input_delay -clock clk -min 1.0 [get_ports data_in]

# 输出延迟:FPGA输出数据到外部器件被采样的时间
set_output_delay -clock clk -max 3.0 [get_ports data_out]
set_output_delay -clock clk -min 0.5 [get_ports data_out]

这里-max和-min分别对应最坏情况和最好情况。工具会用-max检查建立时间,用-min检查保持时间。

经验之谈:很多新手搞不清set_input_delay和set_output_delay的方向。我教你一个记忆方法:站在FPGA内部看引脚。
set_input_delay:数据从外部进来,已经走了一段路才到FPGA引脚,所以是“延迟”。
set_output_delay:数据从FPGA出去,还要走一段路才能被外部器件采到,所以也是“延迟”。
说白了,这两个值描述的都是“FPGA引脚之外”的延迟。

2.2.3 set_false_path / set_multicycle_path — 处理特殊路径

不是所有路径都需要在一个时钟周期内完成。比如跨时钟域的同步器路径、复位信号路径,这些就可以标记为false_path。

# 跨时钟域路径,不需要时序分析
set_false_path -from [get_clocks clk_a] -to [get_clocks clk_b]

# 复位信号,不需要时序分析
set_false_path -to [get_pins *rst_n_reg*]

而set_multicycle_path用于那些需要多个周期才能完成的路径。比如一个计数器,每4个时钟周期才更新一次输出:

set_multicycle_path -setup 4 -from [get_pins cnt_reg*] -to [get_pins out_reg*]
set_multicycle_path -hold 3 -from [get_pins cnt_reg*] -to [get_pins out_reg*]
注意:使用set_multicycle_path时,-hold的值通常是-setup减1。这是很多新手容易忽略的。我曾经见过一个设计,只写了-setup 4没写-hold,结果保持时间检查全错了。

2.3 SDC文件的组织与结构

一个完整的SDC文件,不是把命令随便堆在一起就行。我见过最乱的SDC文件,2000多行,没有注释,没有分组,改一个参数要找半天。

我个人习惯把SDC文件分成几个逻辑块:

  1. 时钟定义块 — 所有create_clock和create_generated_clock
  2. 时钟特性块 — set_clock_uncertainty、set_clock_latency等
  3. I/O约束块 — 所有set_input_delay、set_output_delay等
  4. 例外约束块 — set_false_path、set_multicycle_path等
  5. 其他约束块 — set_max_fanout、set_max_transition等

每个块之间用空行和注释分隔。举个例子:

# ============================================================
# 1. 时钟定义
# ============================================================

# 主时钟:100MHz,来自板级晶振
create_clock -name sys_clk -period 10.000 [get_ports clk_in]

# PLL输出:200MHz,0度相移
create_generated_clock -name pll_clk_200m \
    -source [get_ports clk_in] \
    -multiply_by 20 -divide_by 1 \
    [get_pins pll_inst/clk_out]

# PLL输出:100MHz,90度相移
create_generated_clock -name pll_clk_100m_90 \
    -source [get_ports clk_in] \
    -multiply_by 10 -divide_by 1 \
    -phase 90 \
    [get_pins pll_inst/clk_out_90]

# ============================================================
# 2. 时钟特性
# ============================================================

set_clock_uncertainty -setup 0.200 [get_clocks sys_clk]
set_clock_uncertainty -hold  0.050 [get_clocks sys_clk]

# ============================================================
# 3. I/O约束
# ============================================================

# 输入:ADC数据,相对于sys_clk
set_input_delay -clock sys_clk -max 2.0 [get_ports adc_data*]
set_input_delay -clock sys_clk -min 0.5 [get_ports adc_data*]

# 输出:DAC数据,相对于pll_clk_200m
set_output_delay -clock pll_clk_200m -max 1.5 [get_ports dac_data*]
set_output_delay -clock pll_clk_200m -min 0.3 [get_ports dac_data*]

# ============================================================
# 4. 例外约束
# ============================================================

# 跨时钟域:sys_clk -> pll_clk_200m
set_false_path -from [get_clocks sys_clk] -to [get_clocks pll_clk_200m]

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

你想想看,这样组织之后,是不是一目了然?改时钟频率,去第一个块;调I/O时序,去第三个块。维护成本大大降低。

核心原则:SDC文件是写给工具看的,但更是写给人看的。一个结构清晰、注释完整的SDC文件,能让你和你的同事少掉很多头发。

2.4 SDC知识体系总览

为了让你对SDC有一个整体的认识,我画了一张图:

SDC约束知识体系 SDC约束文件 时钟约束 I/O约束 例外约束 create_clock create_generated_clock set_clock_uncertainty set_input_delay set_output_delay set_input_transition set_false_path set_multicycle_path set_max/min_delay SDC文件组织:按逻辑块分组 + 注释分隔 最佳实践 1. 每个约束块加注释 2. 使用通配符时注意范围 3. 运行前检查语法 掌握这些,你就掌握了SDC的80%

这张图把SDC的核心内容串起来了。从顶层文件,到三大类约束,再到具体命令,最后落到文件组织和最佳实践。你每次写SDC的时候,可以对照这张图,看看自己有没有遗漏什么。

总结一下:SDC约束不是写完了就完事的。它是一个需要持续维护的文档。我见过最好的团队,会把SDC文件纳入版本管理,每次修改都走评审流程。因为一个错误的约束,可能导致整个设计的时序崩溃,甚至流片失败。嗯,这绝不是危言耸听。