这一篇在干嘛?

你在 TD 里跑完布局布线,时序报告突然蹦出一堆红色 violation——这不是工具坏了,而是它根本不知道你的设计”该跑多快”。时序约束就是把我们脑子里的时序意图翻译成 SDC 文件告诉工具。本篇先建立约束的基本概念,再把 TD 支持的全部 SDC 命令过一遍,每条都带参数和示例。同系列:第 2 篇:STA 原理与典型约束场景

约束为什么不可省

11 个基本概念

对象获取命令:约束的”原材料”

时钟定义命令:create_clock 与朋友们

IO 延迟命令:set_input_delay / set_output_delay

例外约束命令:max/min、多周期、伪路径、时钟组

冷门但有用的辅助命令

约束为什么不可省

静态时序分析(STA,Static Timing Analysis)的目标是验证 FPGA 加载位流之后,所有数据路径都能在时钟节拍下被正确采样。没有约束时,TD 工具不知道哪些管脚是时钟、跑多少 MHz,自然无法分析,甚至综合和布局布线阶段也不会为这些路径做优化。

SWUG200 手册把约束分成三大类,正好对应第 2 篇的三大场景:

  • 时钟约束:定义时钟的频率、波形、来源,是所有分析的地基;
  • IO 约束:描述芯片外部(PCB 走线、对方芯片)的延迟,即 input/output delay;
  • 例外约束:对工具的默认分析规则做”豁免”或”放宽”,比如多周期路径、伪路径、时钟组。

一份不写例外约束的 SDC 往往过于悲观(工具按最坏情况报违例),而一份乱写例外约束的 SDC 会漏掉真问题——所以既要会写,也要懂原理。

11 个基本概念

手册第 2 章给出了 11 个核心概念,把它们读懂,后面所有命令都好理解:

1. 主时钟(Primary Clock):通过外部输入引脚进入 FPGA 的时钟。注意”源点”的概念——主时钟定义在管脚上,TD 综合后会给它一个网表名字,所有该时钟的扇出都从这个点开始时序分析,这个点就是时钟延迟和不确定性计算的时间零点。主时钟的对象也可以是 FPGA 内部硬核单元的输出管脚,比如 SerDes 恢复时钟、内部振荡器时钟。

2. 虚拟时钟(Virtual Clock):物理上没有连接到 FPGA 任何网表对象的时钟。典型用途是给 IO 延迟约束当参考:外部芯片的时钟在 FPGA 里并不存在,但分析输入/输出延迟时又需要它,于是用 create_clock 不带 Target 只定义一个”影子时钟”。

3. 生成时钟(Generated Clock):FPGA 内部由 PLL、用户逻辑分频器或硬核时钟单元从主时钟派生出来的时钟。约束时不能只说它的周期,而要描述它与主时钟的转换关系,包括:整数分频、整数倍频、小数倍频(一般由 PLL 完成)、移相、波形反转、占空比调整。

4. 时钟组(Clock Groups):把时钟按时钟域分组,工具默认分析所有时钟之间的路径,但分了组之后就不再分析组与组之间的路径。这是处理异步时钟域最省事的方式。

5. 时钟延迟(Clock Latency):时钟从源到目的的延迟,分两部分——源端延迟(FPGA 外部 PCB 上的传输延迟,需要用 set_clock_latency 手工约束)和网络延迟(FPGA 内部走线延迟,工具自动计算,布线后得到精确值)。

6. 时钟不确定性(Clock Uncertainty):用 set_clock_uncertainty 为时钟的偏移和抖动建模,相当于给时序分析人为增加裕量。注意主时钟的不确定性不会传播到下游生成时钟。

7. 输入延迟(Input Delay):数据从外部芯片经 PCB 传播到 FPGA 输入引脚,相对参考时钟沿的相位差。它取决于发送时钟沿和数据的相对相位,是捕获”来自外部的时序隐患”的关键参数。

8. 输出延迟(Output Delay):数据从 FPGA 输出引脚经 PCB 到下游设备的延迟。数值可正可负,正负只表示相对时钟沿的位移方向。

9. 多周期路径(Multicycle Paths):默认分析要求组合逻辑延迟在一个时钟周期内到达(单周期分析),但有些路径功能上允许多个周期才稳定——典型如带时钟使能的慢速数据通路,这类路径应约束为 multicycle path,避免被过度约束。

10. 无效路径(False Paths):不需要工具分析的路径。两种视角:逻辑上与电路正常工作无关(测试逻辑、静态逻辑);时序上无真实逻辑关系(如已经过双重同步器的异步时钟域交叉点)。典型例子还包括:上电只写一次的寄存器、复位或测试逻辑、异步 DRAM 读写时钟之间的路径。实现方式有两种:set_false_path(针对特定节点)和 set_clock_groups(针对时钟域)。

11. 最小/最大延迟(Min/Max Delays):用约束值覆盖路径的默认要求——max delay 覆盖默认 setup(recovery)值,min delay 覆盖默认 hold(removal)值。

对象获取命令:约束的”原材料”

所有约束命令都需要”指定对象”,而指定对象靠的是这一族命令。先搞清五种节点的区别:

节点含义
cell代码中实例化的模块(inst),或综合后网表中的 LUT、RAM、DSP、PLL 等
reg模块中的寄存器
pin每个 cell 的输入输出信号(注意不是芯片引脚)
netpin 与 pin 之间的连线
port顶层模块声明的输入输出端口,即与 FPGA 外部通信的管脚

TD 支持的对象获取命令汇总:

命令描述
all_clocks返回设计中的所有时钟
all_inputs返回设计中的所有输入和双向端口
all_outputs返回设计中的所有输出和双向端口
all_registers返回设计中的所有寄存器
get_ports返回用户指定的输入、输出和双向端口
get_cells返回设计中的指定单元实例
get_nets返回设计中的指定线网
get_regs返回设计中的指定寄存器
get_pins返回端口或单元的指定引脚
get_clocks返回已定义的时钟集合

常用语法要点(-o 表示返回转译后的名字,TD5.6 之后为必选项):

all_clocks -o
all_inputs -o
get_ports {A B C}
get_cells -hierarchical -nocase reg*
get_nets -o reg*
get_regs -o -nocase reg
get_pins -o *.*reg*
get_clocks -o -include_generated_clocks *

几个容易踩的点:

常见坑:

  • get_ports 只搜顶层模块管脚(真实 IO),get_pins 能搜到设计内部所有 cell 的管脚,别混用;
  • get_cells/get_nets 默认模式要求路径完全匹配层级地址(TD 用 / 分隔层级,如 B/Z),加 -hier 才是递归搜索;
  • 除了与顶层 IO 相连、与寄存器 Q 端相连的 net,其他 net 可能在综合优化中消失或改名,不推荐用普通 wire 名搜 net;
  • get_clocks-v 是反选(选 filter 之外的所有时钟),-include_generated_clocks 会把主时钟的所有生成时钟一并返回。

时钟定义命令:create_clock 与朋友们

create_clock —— 定义主时钟

命令参数:

create_clock -name {<clock name>} -period <time ns> \
    -waveform {<rise time ns> <fall time ns>} [<Target>] [-add]

参数释义:

参数说明
-name时钟名字,用于后续时序分析和其他约束命令,不能省略
-period时钟周期,单位 ns,正值,保留三位小数
-waveform第一个上升沿与第一个下降沿的时间点,可指定相位或占空比;省略则默认相位 0、占空比 50%
[Target]时钟源点,一般是 I/O 管脚(get_ports)或硬核时钟管脚(get_pins)
-add对同一对象多次约束(不同频率/相位/占空比),一般用于时钟 MUX;不加则后一次覆盖前一次

命令示例(覆盖手册全部 9 种情形):

# 1. 标准示例:25MHz
create_clock -name clk_25m -period 40.000 -waveform {0.000 20.000} [get_ports {clk_25m}]
# 2. 省略 -waveform(默认 50% 占空比、0 相位)
create_clock -name clk_25m -period 40.000 [get_ports {clk_25m}]
# 3. 相位偏移 5ns
create_clock -name clk_50m -period 20.000 -waveform {5.000 15.000} [get_ports {clk_50m}]
# 4. 占空比 30%
create_clock -name clk_100m -period 10.000 -waveform {0.000 3.000} [get_ports {clk_100m}]
# 5~7. -add 用于时钟 MUX:同一管脚多个时钟共存
create_clock -name clk_25m_add -period 50.000 -waveform {0.000 25.000} [get_ports {clk_25m}] -add
# 8. 虚拟时钟:不带 Target
create_clock -name clk_virtual -period 10.000 -waveform {0.000 5.000}
# 9. 硬核 serdes 恢复时钟:源点是 get_pins
create_clock -name clk_rserdes_1 -period 6.400 -waveform {0.000 3.200} \
    [get_pins {u_serdes_top/u_dual_hss/serdes_dual_inst.10_rx_out2clk}]

create_generated_clock —— 定义生成时钟

命令参数:

create_generated_clock -name <clock name> -source [<source>] \
    [-master_clock <clock name>] \
    { -divide_by <factor> | -multiply_by <factor> | -edges {<e0> <e1> <e2>} } \
    [-edge_shift {<s0> <s1> <s2>}] [-invert] [-phase <phase_num>] \
    [-duty_cycle <duty_cycle_num>] [<Target>] [-add]

关键规则:

  • -source 指定生成时钟的驱动时钟源点,-master_clock 指定该源点上的驱动时钟(同一源点可能被多个时钟驱动时必须写明);
  • -divide_by / -multiply_by / -edges 三者有且只能用一个
  • -edges {e0 e1 e2} 描述沿对应关系:驱动时钟的第 e0 沿 → 生成时钟第 1 个上升沿,第 e1 沿 → 第 1 个下降沿,第 e2 沿 → 第 2 个上升沿;edge 必须为整数且 e0 ≥ 1;
  • -edge_shift 仅在 -edges 恰好三个整数时有效,单位 ns,正右负左;
  • -phase 取 0360,-duty_cycle 取 0100(配合 -multiply_by 调占空比)。

示例:

# 分频 + 移相
create_generated_clock -name pll_gen1 -source [get_pins {u_pll/pll_inst.clkc[0]}] \
    -divide_by 2 -phase 180 [get_nets {u_buttons_led0/clk_25m}]
# 倍频 + 占空比
create_generated_clock -name pll_gen2 -source [get_pins {u_pll/pll_inst.clkc[1]}] \
    -multiply_by 2 -duty_cycle 0.4 [get_nets {u_buttons_led1/clk_25m}]
# 沿对应关系 + 偏移
create_generated_clock -name clk_50m -source [get_ports {clk_200m}] \
    -master_clock [get_clocks {clk_200m}] \
    -edges {1 5 9} -edge_shift {-1 1 -1} [get_nets {clk_50m}]

derive_clocks / rename_clock —— 自动约束与改名

derive_clocks 让 TD 自动约束时钟单元(PLL、LCLK、DBUF、DBUF_HXT 等时钟缓冲器)的输出,生成的时钟名为”单元名.输出接口名”。注意参考时钟仍需用户自己先约束好。

derive_clocks -instance [get_cells {u_pll/pll_inst}]   # 只约束指定单元
derive_clocks                                           # 约束所有时钟单元

rename_clock 依赖 derive_clocks,把自动生成的名字改成可读的名字(也可以直接在 TD 的 Timing Wizard 里操作):

derive_clocks
rename_clock -name {pll_1} [get_clocks {u_pll_clk/pll_inst.clkc[0]}]
rename_clock -name {pll_2} [get_clocks {u_pll_clk/pll_inst.clkc[1]}]
rename_clock -name {pll_3} [get_clocks {u_pll_clk/pll_inst.clkc[2]}]

set_clock_latency / set_clock_uncertainty

set_clock_latency 设定时钟延迟的最大/最小值:

set_clock_latency -late  -source 1.2 [get_clocks {clk_25m}]  # slow corner 最大源延迟
set_clock_latency -early -source 0.9 [get_clocks {clk_25m}]  # fast corner 最小源延迟
set_clock_latency -source 1 [get_clocks {clk_50m}]           # 不区分最值
set_clock_latency 2 [get_clocks {clk_100m}]                  # 不带 -source = 网络延迟
set_clock_latency -source 1 [get_clocks {pll_1 pll_2 pll_3}] # 多时钟一次约束
set_clock_latency 1.2 -rise [get_clocks CLK1]                # 上升沿延迟
set_clock_latency 0.9 -fall [get_clocks CLK1]                # 下降沿延迟

要点:-source 表示源端延迟(PCB 部分);不加 -source 则是网络延迟,且布线后会被实际布线延迟替代。target 为 pin 且被多个时钟驱动时,用 -clock 指定作用于哪个时钟。

set_clock_uncertainty 对时钟偏移和抖动建模:

set_clock_uncertainty -setup 1   [get_clocks {pll_2}]              # 只影响 setup
set_clock_uncertainty -hold 1.1  [get_clocks {pll_2}]              # 只影响 hold
set_clock_uncertainty 1.3        [get_clocks {pll_3}]              # 两者同时生效
set_clock_uncertainty 1.3        [get_clocks {pll_1 pll_2 pll_3}]  # 多时钟
set_clock_uncertainty 1.3        [all_clocks]                      # 所有时钟

原理上:setup uncertainty 会减少 setup 的 required time(要求数据更早到达),hold uncertainty 会增加 hold 的 required time(要求数据更晚撤离)。目前仅支持对单个时钟本身设置,不支持跨时钟域的 uncertainty 定义。

IO 延迟命令:set_input_delay / set_output_delay

set_input_delay 定义外部信号到达 FPGA 输入端口所需的延迟:

set_input_delay -clock <clock name> {-min | -max} <delay value> \
    [-add_delay] [-clock_fall] [-source_latency_included] \
    [-network_latency_included] [-reference_pin] [<Target>]
参数说明
-clock输入延迟对应的时钟(可以是虚拟时钟)
-max最大输入延迟,用于 setup 分析
-min最小输入延迟,用于 hold 分析
-add_delay同一端口多条约束共存(工具计算多个 delay 值);不加则后者覆盖前者
-clock_fall以下降沿为参考,须与 -clock 同用
-source_latency_included / -network_latency_included分析时从 input delay 中减去相应时钟延迟
-reference_pin为延迟添加时钟树上某节点作为参考点

示例:

set_input_delay -clock [get_clocks clk_virtual] -min 2.000 \
    [get_ports {{button[0]} {button[1]}}]
set_input_delay -clock [get_clocks clk_virtual] -max 8.000 \
    [get_ports {{button[0]} {button[1]}}]
# DDR 类接口:上升沿 + 下降沿各一组,必须加 -add_delay
set_input_delay -max 0.8 -clock [get_clocks virtual_clk_d] -add_delay [get_ports {d_rgmii_rxdv}]
set_input_delay -min -0.6 -clock [get_clocks virtual_clk_d] -add_delay [get_ports {d_rgmii_rxdv}]
set_input_delay -max 0.8 -clock_fall -clock [get_clocks virtual_clk_d] -add_delay [get_ports {d_rgmii_rxdv}]
set_input_delay -min -0.6 -clock_fall -clock [get_clocks virtual_clk_d] -add_delay [get_ports {d_rgmii_rxdv}]

set_output_delay 参数与 input 版完全对称,定义信号从 FPGA 输出端口到达外部寄存器所需的时间。系统同步场景下,launch clk 是 FPGA 内部发送时钟,capture clk 是外部锁存时钟(没有就用虚拟时钟);若外部 capture clk 也来自 FPGA,工具会自动考虑其内部延迟。

常见坑:

  • -max 对应 setup、-min 对应 hold——很多初学者背反了。记住”延迟越大,留给内部逻辑的时间越少,越难满足建立”;
  • 同一端口写双沿采样约束时,第二条开始必须加 -add_delay,否则会被覆盖只剩一组;
  • 系统同步下 input delay 的 launch clk 是外部器件的发送时钟(FPGA 里不存在,用虚拟时钟),capture clk 才是 FPGA 内部采样时钟,方向别搞反。

例外约束命令:max/min、多周期、伪路径、时钟组

set_max_delay / set_min_delay

覆盖路径默认的最大/最小允许延时:

set_max_delay -from [<source node>] [-rise_from] [-fall_from] \
    [-through [<node>]] -to [<target node>] [-rise_to] [-fall_to] \
    <Delay value> [-datapath_only]

示例:

# 纯组合 IO 路径:x/y 端口进、sum 端口出
set_max_delay -from [get_ports {x y}] -to [get_ports {sum}] 7
set_min_delay -from [get_ports {x y}] -to [get_ports {sum}] 3
# 跨时钟域路径
set_max_delay -from [get_clocks {clk0}] -to [get_clocks {clk1}] 4
set_min_delay -from [get_clocks {clk0}] -to [get_clocks {clk1}] 2.5

使用规则:-from 后应是时序路径起点(寄存器时钟管脚或 input port),-to 后应是寄存器的非时钟输入端口或 output port;-from 对象为时钟管脚时约束覆盖所有被该时钟驱动的路径;对象为 cell 时覆盖该 cell 内所有寄存器;-through 覆盖经过该节点的所有路径。

-datapath_onlyset_max_delay 独有选项:消除时钟 jitter/skew/pessimism 计算,只考虑第一个触发器的 Clock-to-Q、触发器间连线延迟和第二级触发器的建立时间;配合使用时 hold 分析自动按 false path 处理。异步 FIFO、打两拍跨时钟域约束就靠它(第 2 篇详解)。

关于 I/O 约束用哪组命令,手册给出明确结论:只有从 FPGA 管脚进出、且不经过任何时序元件的纯组合逻辑路径才用 set_max_delay/set_min_delay 约束,其余 I/O 路径都应使用 set_input_delay/set_output_delay。对 I/O 完全不加约束的后果是:综合实现不考虑 I/O 时序,时序报告里也根本不会出现这些路径。

set_multicycle_path

set_multicycle_path {-setup | -hold} {-start | -end} \
    -from [<source node>] -to [<target node>] <multiplier value>
  • -setup:建立计算使用指定的周期倍数;改 setup 的 multiplier 会连带影响默认 hold 检查
  • -start/-end:指定周期数相对 launch clock 还是 capture clock。默认 setup 是 -end(capture clock 周期,向后移),hold 是 -start(launch clock 周期)。

set_multicycle_path 约束示意图:setup 沿后移后,hold 沿会自动跟着移动

核心陷阱(务必记住):hold check 默认取 setup check 的前一个时钟沿。当把 setup 约束为 N 周期后,hold 沿自动变成第 N-1 个沿,最小延迟要求从 0 变成 N-1 个周期——必须补一条对 hold 的 N-1 周期约束才能维持原本的保持要求:

set_multicycle_path -setup -from [get_regs {rx_a_1d.q}] -to [get_regs {rx_a_2d.d}] 3
set_multicycle_path -hold  -end -from [get_regs {rx_a_1d.q}] -to [get_regs {rx_a_2d.d}] 2

-from/-to 尽量用 clock 对象:约束会覆盖所有由 from clock 驱动的寄存器/端口到 to clock 驱动的寄存器/端口之间的路径,比逐个选节点高效得多。

set_false_path

set_false_path -from [<source node>] [-through [<node>]] -to [<target node>] [-setup | -hold]
  • 不指定 -setup/-hold 时对 setup 和 hold 同时生效;
  • 用 clock 作对象时覆盖两个时钟域之间全部路径,比选节点高效;
  • 注意与 set_clock_groups 的区别:set_false_path 只禁止单方向的时序分析,而时钟组约束两个方向都禁止。
set_false_path -from [get_clocks clk_25m] -to [get_clocks clk_25m_add]
set_false_path -from [get_regs {rx_b_1d}] -to [get_regs {tx_b2c}]
# 只约束指定沿的组合
set_false_path -rise_from [get_clocks {rgmii_rxc}] -fall_to [get_clocks {sdr_clk}]
set_false_path -rise_from [get_clocks {rgmii_rxc}] -rise_to [get_clocks {sdr_clk}]

set_clock_groups

set_clock_groups -name [<group_name>] {-exclusive | -asynchronous} \
    -group [<clock name>] [-group [<clock name>] ...]
  • -exclusive:互斥时钟,不会同时有效(典型如时钟 MUX 的两个输入);
  • -asynchronous:异步时钟,源完全不同;
  • 同一条指令内只有一个 -group 时,该组与其他所有时钟互斥;有多个 -group 时,各组之间互斥;
  • 设计中至少要给出两个组,约束才有效。
create_clock -name CLK2 -period 10.0 [get_ports CLK2]
create_clock -name CLK3 -period 5.0  [get_ports CLK3]
set_clock_groups -exclusive -group CLK2 -group CLK3

冷门但有用的辅助命令

set_clock_sense:让指定时钟从某个网表对象开始停止传播:

set_clock_sense -stop_propagation -clocks [get_clocks {clk_1}] [get_pins {rx_a_mux.o}]

set_clock_route_enforce:强制指定时钟线网只走专用全局时钟互连。多 bank 源同步场景建议使用,可保证时钟 skew 较小:

set_clock_route_enforce {clk_1_net}

set_clock_route:反过来,指定时钟线网不走全局时钟网络(设计中时钟数目超过器件限制时用):

set_clock_route {clk_1_net}

常见坑:

  • 这两条命令互斥,不能同时使用;且只要手动指定了一条,就必须把所有不走全局时钟网络的时钟一一指定;
  • 强制走/不走时钟网络都可能导致布局布线失败(TD 会报 error);代码里已显式使用 BUFG 等时钟资源时 set_clock_route 无效。

set_cross_domain_timing:一键忽略所有异步/跨时钟时序关系,可选 all(默认,分析任意两时钟)/ self(只分析同时钟内部)/ domain(只分析同时钟域内部):

set_cross_domain_timing domain

master clock 及其所有衍生 generated clock 统称一个时钟域,不同时钟域间没有明确相位关系即异步。这个命令影响所有时钟,务必谨慎使用

通关标准:

  1. 能独立写出约束 EG4S20 工程的 SDC 骨架:create_clock 定义板级时钟 + PLL 输出、关键 IO 加 min/max input delay、异步域之间 set_clock_groups -asynchronous
  2. 知道 -max 管 setup、-min 管 hold,双沿约束必须 -add_delay,虚拟时钟是”不带 Target 的 create_clock”。
  3. 能说出 false path 和 clock groups 的单方向/双方向区别,以及 multicycle setup 后要补 hold N-1 的原因。