这一篇在干嘛?
你在 TD 里跑完布局布线,时序报告突然蹦出一堆红色 violation——这不是工具坏了,而是它根本不知道你的设计”该跑多快”。时序约束就是把我们脑子里的时序意图翻译成 SDC 文件告诉工具。本篇先建立约束的基本概念,再把 TD 支持的全部 SDC 命令过一遍,每条都带参数和示例。同系列:第 2 篇:STA 原理与典型约束场景。
IO 延迟命令:set_input_delay / set_output_delay
约束为什么不可省
静态时序分析(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 的输入输出信号(注意不是芯片引脚) |
| net | pin 与 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,100(配合-duty_cycle取 0-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_only 是 set_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 周期)。

核心陷阱(务必记住):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 domainmaster clock 及其所有衍生 generated clock 统称一个时钟域,不同时钟域间没有明确相位关系即异步。这个命令影响所有时钟,务必谨慎使用。
通关标准:
- 能独立写出约束 EG4S20 工程的 SDC 骨架:
create_clock定义板级时钟 + PLL 输出、关键 IO 加 min/max input delay、异步域之间set_clock_groups -asynchronous。- 知道
-max管 setup、-min管 hold,双沿约束必须-add_delay,虚拟时钟是”不带 Target 的 create_clock”。- 能说出 false path 和 clock groups 的单方向/双方向区别,以及 multicycle setup 后要补 hold N-1 的原因。
自测 1:为什么生成时钟不能用 create_clock 只写周期,而要用 create_generated_clock?
因为生成时钟由主时钟派生(PLL、分频逻辑、SerDes 等),工具需要知道它与主时钟的转换关系(分频/倍频/移相/反转/占空比)才能推导波形和进行跨域分析。只写周期等于把派生时钟当成了独立主时钟,会丢失与源时钟的相位关系,导致跨时钟域分析出错。
自测 2:set_clock_latency 不加 -source 参数约束的是什么?布线后会怎样?
约束的是网络延迟(FPGA 内部走线延迟)。在布线后的时序分析中,这个值会被实际的布线延迟替代;而带
-source的源端延迟(PCB 部分)不会被替换,因为工具无法测到芯片外部。
自测 3:一条 setup multicycle N 的约束后,hold check 会发生什么变化?怎么修?
hold check 默认使用 setup check 的前一个时钟沿。setup 沿移到第 N 个周期后,hold 沿自动变为第 N-1 个周期,最小延迟要求从 0 变为 N-1 个周期,hold 可能因此报违例。修复方法是补一条
set_multicycle_path N-1 -hold ...(需要时配合-end)把 hold 沿移回原位。
自测 4:异步 FIFO 的读写指针打两拍路径,为什么不用 set_false_path 而推荐 set_max_delay -datapath_only?
false path 会把这些路径完全排除在分析之外,工具不再控制其延迟,打两拍的第一级寄存器之间的走线可能很长,数据到达太慢反而影响亚稳态恢复窗口。
set_max_delay -datapath_only <T>一方面消除了时钟间无意义的相位分析(hold 按 false path 处理),另一方面给路径设定了一个明确的最大延迟上限,让同步器第一级之间的延迟可控。
自测 5:get_ports 和 get_pins 的区别是什么?create_clock 的 Target 用哪个?
get_ports 只返回顶层模块声明的输入/输出/双向端口(真实芯片管脚);get_pins 返回设计中 cell 的管脚,配合层级路径可以定位内部单元(如 PLL 输出、SerDes 恢复时钟)。外部管脚进来的主时钟用 get_ports,FPGA 内部硬核输出的时钟用 get_pins。