这一篇在干嘛?
上一篇解决了”怎么把流程跑起来”,这一篇解决”怎么让工具理解你的时序意图”。第一部分讲时序约束命令:先学会
get_ports/get_cells这类对象获取命令(所有约束的地基),再按”时钟约束 → IO 延时约束 → 时序例外约束”的顺序掌握 SDC 核心;第二部分讲设计与时序分析报告命令,教你用report_timing_summary等命令定位时序问题。同系列第一篇:TCL 命令手册(1):脚本运行方式与全流程命令。
IO 延时约束:set_input_delay 与 set_output_delay
对象获取命令:一切约束的地基
写 SDC 约束时,你几乎永远在回答同一个问题:“约束谁?“。答案由对象获取命令给出——它们按名字规则筛选设计中的对象,返回的集合用方括号 [...] 嵌进约束命令里。先记住这张命令表(表 4-3):
| 命令 | 描述 |
|---|---|
| all_clocks | 返回设计中的所有时钟 |
| all_inputs | 返回设计中的所有输入和双向端口 |
| all_outputs | 返回设计中的所有输出和双向端口 |
| all_registers | 返回设计中的所有寄存器 |
| get_ports | 返回设计中的用户指定的输入、输出和双向端口 |
| get_cells | 返回设计中的指定单元实例 |
| get_nets | 返回设计中的指定线网 |
| get_regs | 返回设计中的指定寄存器 |
| get_pins | 返回设计中的端口或单元的指定引脚 |
| get_clocks | 返回设计中用户指定的时钟 |
理解这些命令前,先分清五种节点的含义:
- cell:代码中实例化的模块(inst),或综合后网表中的 LUT、RAM、DSP、PLL 等;
- reg:模块中的寄存器;
- pin:每个 cell 的输入输出信号引脚(注意不是芯片的物理引脚);
- net:pin 与 pin 之间的连线;
- port:顶层模块声明的输入输出端口,即 FPGA 与外界通信的物理管脚。
get_ports 和 get_pins 的区别是高频考点:get_ports 只搜顶层模块的管脚(设计对外的真实 IO),get_pins 能搜到设计中所有单元的引脚。比如约束外部晶振输入时钟用 [get_ports sys_clk],而约束 PLL 输出引脚就要用 [get_pins u_pll/pll_inst.clkc[0]]。
所有 get 类命令共享一组选项:-nocase(不区分大小写)、-nowarn(没匹配到也不告警)、-hierarchical/-hier(跨层级递归搜索,TD 用 / 表示层级,如 B/Z)、-o(返回转译后的可读名字)、<filter>(支持 * 通配符的匹配串,多字段用大括号 {A* B* C*})。
TD 5.6 之后的重大变化:get/all 类命令不加 -o 选项将返回用户不可读的 uid 码型字符串,所以新版本请养成”命令必带 -o”的习惯。
常用示例(直接可在 Console 里试):
all_clocks -o # 列出所有时钟
all_inputs -o # 列出所有输入/双向端口
get_ports * # 列出所有顶层端口
get_cells -o -nocase reg # 不区分大小写找名为 reg 的 cell
get_cells -o reg* # 收集名字以 reg 开头的所有 cell
get_pins -o -nocase *.reg # 找名字含 reg 的 pin
get_clocks -o * # 列出 SDC 中已定义的所有时钟
get_clocks -include_generated_clocks -o sys_clk # 连生成时钟一起返回
关于 get_nets 还有个实用细节:TD 中 net 命名分三类,按优先级排——(1) 与输入输出 port 相连的 net,随 port 命名;(2) 与寄存器 Q 端相连的 net,随寄存器名命名(有层级结构);(3) 用户在 Verilog 里定义的 wire。第 (2) 类之外的 net 大多会在综合优化中消失或改名,所以不要用 wire 名去搜 net,用寄存器名和端口名最可靠。get_regs 的查找规则与 get_nets 一致——工具先找到寄存器 Q 端的 net,再反查 reg 单元。
常见坑:
- TD 5.6+ 不带
-o的 get 命令返回的是不可读的 uid 字符串,约束看起来”生效了”其实对象是乱码。- 用被优化掉的 wire 名搜 net 会得到空集,改用寄存器名。
-through类选项的对象只能是 pins 或 nets,写 cells 会报错。
时钟约束:create_clock 与生成时钟
时序约束的第一句话永远是”告诉工具时钟长什么样”。
create_clock —— 定义主时钟,参数格式:
create_clock -name <clock_name> -period <time ns> [-waveform {<rise ns> <fall ns>}] [<Target>] [-add]
-name不能省略,时钟名会用于后续所有时序命令;-period单位 ns,正数,保留三位小数;-waveform指定第一个上升沿/下降沿时间点,可表达不同相位或占空比;省略则默认 0ns 上升沿、50% 占空比;[<Target>]指定时钟源点,一般是 I/O 管脚(配get_ports)或片内硬核时钟引脚(配get_pins);-add对同一对象追加第二套约束(不同频率/相位/占空比),用于时钟 MUX 场景;不加-add时后写的约束会覆盖前面的。
典型示例:
# 标准:25MHz,40ns 周期
create_clock -name clk_25m -period 40.000 -waveform {0.000 20.000} [get_ports {clk_25m}]
# 省略 waveform(默认 50% 占空比)
create_clock -name clk_25m -period 40.000 [get_ports {clk_25m}]
# 相位移:上升沿在 5ns
create_clock -name clk_50m -period 20.000 -waveform {5.000 15.000} [get_ports {clk_50m}]
# 占空比 30%:10ns 周期,3ns 处下降
create_clock -name clk_100m -period 10.000 -waveform {0.000 3.000} [get_ports {clk_100m}]
# 同一管脚追加第二套约束(时钟 MUX)
create_clock -name clk_25m_add -period 50.000 -waveform {0.000 25.000} [get_ports {clk_25m}] -add
# 虚拟时钟:不挂在任何物理节点上,常用于 IO 约束的参考
create_clock -name clk_virtual -period 10.000 -waveform {0.000 5.000}
# 片内硬核 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 —— 描述生成时钟(PLL 输出、分频/倍频时钟)与主时钟的关系:
create_generated_clock -name <name> -source <source_pin> [-master_clock <clock>] [-divide_by <factor> | -multiply_by <factor> | -edges {e0 e1 e2}] [-invert] [<Target>]
三个核心选项 -divide_by/-multiply_by/-edges 三者有且只能用一个。示例:
# 二分频,相位 180°
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}]
# 二倍频,占空比 40%
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}]
# 用 -edges 精确描述沿对应关系:主时钟第 1/5/9 个沿 → 生成时钟的第 1 个上升沿/第 1 个下降沿/第 2 个上升沿
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}]
-edge_shift(单位 ns,正右负左)只能在 -edges 恰好给三个整数时使用;-phase(0360)和 100)分别调相位和占空比。-duty_cycle(0
derive_clocks —— 懒人福音:让工具自动为时钟单元(PLL、LCLK、DBUF、DBUF_HXT 等时钟缓冲器)推导生成时钟,生成时钟名为”单元名.输出接口名”:
derive_clocks -instance [get_cells {u_pll/pll_inst}] # 只约束指定 PLL
derive_clocks # 自动约束所有时钟单元
注意:时钟单元的参考时钟(输入主时钟)仍需你自己用 create_clock 约束。
自动生成的时钟名不好看?用 rename_clock 重命名(需在 derive_clocks 之后):
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]}]

常见坑:
- 忘了约束 PLL 的参考时钟就 derive_clocks,生成时钟的频率全是错的。
- 对同一管脚定义两个不同频率的时钟却忘加
-add,第二套约束悄悄覆盖了第一套。-divide_by、-multiply_by、-edges混用直接报错,三选一。
时钟延迟与不确定性
这两个命令用来给时钟”加保险”,让分析结果更接近真实硬件。
set_clock_latency —— 设定时钟延迟的最大/最小值。分两种:-source 指源延迟(时钟源之前的延迟);不加 -source 是网络延迟,布线后的时序分析中会被实际布线延迟替代:
set_clock_latency -late -source 1.2 [get_clocks {clk_25m}] # 源延迟最大值
set_clock_latency -early -source 0.9 [get_clocks {clk_25m}] # 源延迟最小值
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] # 下降沿延迟
选项族还包括 -min(fast corner 分析)、-max(slow corner 分析)、-clock(target 为 pin 且被多时钟驱动时指定用哪个时钟)。
set_clock_uncertainty —— 对时钟偏移和抖动建模。-setup 会在 setup 分析时减少 required time,-hold 会在 hold 分析时增加 required time;都不写则两种检查同时生效:
set_clock_uncertainty -setup 1 [get_clocks {pll_2}]
set_clock_uncertainty -hold 1.1 [get_clocks {pll_2}]
set_clock_uncertainty 1.3 [all_clocks] # 给所有时钟统一留 1.3ns 余量
注意目前只支持对单个时钟本身设置,不支持跨时钟域的 uncertainty。
IO 延时约束:set_input_delay 与 set_output_delay
FPGA 外部也有电路:上游芯片送数据进来,下游芯片收数据出去。IO 延时约束就是告诉工具”芯片外占了多少时间预算”,让内部时序分析把外部因素算进去。不加 IO 约束的后果:综合实现完全不考虑 IO 时序,时序报告里也没有这些路径——你会在毫不知情的情况下背上时序隐患。
set_input_delay —— 定义外部信号到达 FPGA 输入端口的延时:
set_input_delay -clock <clock> [-min|-max] [-add_delay] [-rise|-fall] [-clock_fall] <delay> [<Target>]
-max用于 setup 分析,-min用于 hold 分析;-add_delay:同一端口设多条延时约束时累加计算;不加则后写覆盖前写;-clock_fall:以时钟下降沿为参考(DDR 类接口常用),须与-clock同用;-source_latency_included/-network_latency_included:声明延时值中已包含时钟源/网络延迟,分析时会扣除。
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]}}]
# 同一端口多套约束(如双沿采样)
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_output_delay —— 定义信号从 FPGA 输出端口到达外部寄存器所需的时间,参数与 input 版完全对称。它背后的场景是:内部寄存器(launch clk)送出数据、外部寄存器(capture clk)锁存,setup 或 hold 不满足就会采错样。手册特别提醒区分系统同步与源同步:系统同步下 capture clk 若来自 FPGA 外部而板上看不到,可以建一个虚拟时钟做参考;若 capture clk 也由 FPGA 输出,软件会自动考虑它在片内的延时。
set_output_delay -clock [get_clocks clk_virtual] -min 2.000 [get_ports {{button[0]} {button[1]}}]
set_output_delay -clock output_b_clk -max 1.0 [get_ports {b_rgmii_txdv}] -add_delay
set_output_delay -clock output_b_clk -clock_fall -min -0.8 [get_ports {b_rgmii_txdv}] -add_delay
通关标准:
- 能给自己的设计写齐四句话:create_clock + set_input_delay + set_output_delay + 一条例外约束。
- 能解释为什么按钮输入可以不设 input delay,而 DDR 数据线必须设。
- 能用 check_timing 报告找出漏约束的时钟和端口。
路径延时与时序例外
这一组命令直接对”路径”下约束,分两类:给纯组合路径定延时上限/下限(set_max/min_delay),以及告诉工具”某些路径不用管”的时序例外(multicycle、false path、clock groups)。
set_max_delay / set_min_delay —— 指定路径的最大/最小允许延时:
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/-through/-to 描述,-rise_from/-fall_to 等变体可细化到具体沿。set_max_delay 独有 -datapath_only 选项:忽略时钟 jitter/skew/pessimism,只算第一级触发器 CtoQ 延迟 + 连线延迟 + 第二级建立时间,此时 hold 分析自动按 false path 处理——跨时钟域打两拍同步器时常用。
关键的使用边界:SDC 中 I/O 约束有 set_input/output_delay 和 set_max/min_delay 两套,只有”从管脚进来到出去全程不经过任何时序元件”的纯组合路径才用 set_max/min_delay,其余 IO 路径都应交给 input/output delay 约束。
set_multicycle_path —— 放宽多周期路径的检查。默认工具要求组合逻辑在一个周期内到达;如果设计里数据允许 N 个周期才到(比如使能信号门控的数据通路),就该声明成多周期路径:
set_multicycle_path -setup/-hold [-start|-end] -from <起点> -to <终点> <周期数>
最容易踩的坑在这里:setup 的 multicycle 设了 N 之后,hold check 会自动跟着把 capture 沿移到 N 的前一个沿,相当于 hold 要求也悄悄变成了 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
-start/-end 指定周期数相对哪个时钟计算:setup 默认 -end(capture clock),hold 默认 -start(launch clock);跨时钟域路径要显式指明。另外尽量用 clock 而不是具体节点做 -from/-to 对象——一次覆盖所有相关路径,效率更高。
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}] # 细化到沿
-setup/-hold 可让 false path 只在 setup/recovery 或 hold/removal 检查中生效;不写则两者同时生效。
常见坑:
- multicycle 只写 setup 不补 hold,hold 检查被悄悄放宽到 N-1 周期,硬件可能真的挂。
- false_path 滥用:把还没做同步处理的跨时钟域路径设成 false path,只是把时序违例藏起来了,板子上照样出问题。先有同步设计,再有 false path。
- 对经过时序元件的 IO 路径用 set_max_delay,工具不会按你预期约束,应改用 input/output delay。
时钟分组与布线控制
set_clock_groups —— 按组禁止跨时钟组的时序分析,比一条条写 false_path 高效得多:
set_clock_groups [-name <group>] [-exclusive | -asynchronous] -group <clocks> [-group <clocks> ...]
规则:-exclusive 表示组内时钟互斥(不同时有效);-asynchronous 表示异步时钟(完全不同的时钟源)。只有一条 -group 时,该组与其他所有时钟互斥;多条 -group 时各组之间互斥。至少要两个组约束才生效,且它禁止的是组与组之间两个方向的时序分析(false_path 只禁单方向):
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_cross_domain_timing —— 一刀切控制所有跨时钟分析,三选一:
set_cross_domain_timing all # 默认:分析任意两时钟间时序
set_cross_domain_timing self # 只分析同一时钟内部,跨时钟全部忽略
set_cross_domain_timing domain # 只分析同一时钟域内部(master clock 及其衍生 generated clock 为一个域)
影响所有时钟,谨慎使用。
set_clock_route_enforce / set_clock_route —— 手动控制某时钟线网走不走专用全局时钟互连:
set_clock_route_enforce {clk_1_net} # 强制只走全局时钟网络(多 bank 源同步保 skew 小)
set_clock_route {clk_1_net} # 强制不走全局时钟网络(时钟数超器件限制时腾资源)
两者互斥只能用其一。注意:指定走时钟网络可能导致布局布线失败(TD 会报 error);用 set_clock_route 手动指定了任何一个时钟不走全局网络,就必须把所有不走全局网络的时钟一一指定;代码里显式用了 BUFG 等时钟资源时该命令无效。
设计分析报告命令
跑完(或跑到中途)之后,用这一组命令给设计”做体检”。所有报告命令都支持 -file 输出到文件,不写就打印到 Console。
report_area —— 面积报告,看资源用了多少:-verbose 更详细,-io_info 输出详细 IO 信息。
report_logic_level —— 逻辑级数报告(逻辑级数越多路径越难满足时序):输出内容较多,-file 为必选项;-worst_n <int> 只看 setup slack 最差的 N 条路径的逻辑级数与时序信息。
report_hfn —— 高扇出 net(high fanout net)分析,扇出大的信号线延迟高,是时序杀手:
report_hfn gate # 按 fanout 降序打印前 20 条 net 摘要
report_hfn place -net {rst} -timing # 指定 net 的详细信息 + slack/net delay
report_hfn route -min 100 -timing # 只看 fanout ≥100 的
mode 取 rtl/gate/place/route 表示当前阶段;-limit 改打印条数。
report_analysis —— 关键路径与拥塞分析。mode 取 basic/manhattan/final 对应 gate/place/route 三阶段;-timing 与 -congestion 至少用一个:
report_analysis final -congestion -file ./congestion_analysis.rpt
-max_path_num(默认 10)、-path_num_per_ep(每个 endpoint 默认 3)、-slack_threshold(单位 ps,默认 -1 即只分析 slack 为负的路径)、-full(final 模式下同时打印 manhattan 模式的 slack/delay 便于对比)。
report_cells / report_nets —— 查看指定 cell/net 的细节:
report_cells {o_dat_reg[1]} -connection -mode manhattan -timing -hold
report_nets {datgen_din[0]} -connection -mode manhattan -timing -hold
-connection 打印各 pin 连接信息,-pin_loc 加位置信息,-timing 配 -mode 打印 pin 级 slack(默认 setup,加 -hold 看 hold)。名字支持通配符,也可用 get_cells/get_nets 获取。
report_qor —— 一站式质量报告 + 自动优化建议,每个阶段跑完都值得看一眼:
report_qor -step gate -file gate.qor # -step 必选:gate/place/route
好消息:脚本里用 optimize_gate/pack、place、route 命令时会自动调用 report_qor 生成报告,工程模式下输出路径与 GUI 工程一致(<project_name>_Runs/syn_*/gate.qor 等),非工程模式生成在当前目录。
时序分析报告命令
时序收敛的闭环靠这一组命令:看全局 → 钻路径 → 查时钟 → 审约束。
report_timing_summary —— 全设计时序总览,最重要的一条:
report_timing_summary -file route.timing
-ep_num <int> 展示路径终点数(默认 10),-path_num <int> 每个终点展示的路径数(默认 1),-viol_only 只看违例(slack 为负)路径,-include_ignored 把被 false_path/clock_groups 排除的路径也列出来,-no_header 不打印版本头。
report_timing_path —— 钻取指定路径的逐级延时明细,-from/-through/-to 定位:
report_timing_path -file a2b.timing -from [get_regs {datgen_din[0]}]
report_clock_intersection —— 跨时钟域路径的约束健康度与风险报告:如果你有跨时钟域路径既没被约束也没被 false_path/clock_groups 豁免,这里会现形。竞赛调试异步接口时的第一排查工具。
report_clock_summary —— 打印时钟树结构,-clocks {c1 c2} 可只看部分时钟:
report_clock_summary -file clk_smry.rpt
report_timing_exception / report_timing_ignored —— 审计你的例外约束:前者打印 set_max_delay/set_multicycle_path 覆盖的路径(含被 false_path 覆盖的),后者打印 set_false_path/set_clock_groups 排除掉的路径。两者对照着看,能发现”例外约束没盖住你以为盖住的路径”或”盖住了不该盖的路径”。
update_timing —— 更新时序数据库:import_db 后第一次做 STA 前、或网表变更后必须先跑:
update_timing -mode manhattan # basic/manhattan/final 对应 gate/place/route
update_timing -mode final -check_corner slow # 只分析 slow corner
check_timing —— 检查 SDC 健康程度(漏约束的时钟、未约束的端口等),写完约束先跑它再跑流程:
check_timing -verbose
通关标准:
- 能按”check_timing → update_timing → report_timing_summary → report_timing_path”的顺序完成一次时序收敛排查,并说出每步在找什么。
- 能用 report_clock_intersection 发现漏处理的跨时钟域路径,并用 set_clock_groups 或 set_false_path 妥善处理。
- 能用 report_timing_exception 与 report_timing_ignored 审计例外约束的有效范围。
自测:
get_ports和get_pins都能"找到管脚",什么时候必须用哪个?约束 FPGA 对外的物理引脚(如晶振输入 sys_clk)必须用
get_ports——它只搜顶层端口;引用设计内部单元的引脚(如 PLL 输出u_pll/pll_inst.clkc[0]、寄存器的 .q/.d)必须用get_pins——它能跨层级搜到所有单元引脚。
自测:为什么 set_multicycle_path 设置了 setup N 之后通常还要补一条 hold N-1?
hold check 默认使用 setup capture 沿的前一个时钟沿。setup 设为 N 后,hold 的 capture 沿自动移到第 N-1 个沿,最小延迟要求从 0 变成 N-1 个周期,工具会去修根本不需要修的 hold。补一条同路径的
set_multicycle_path -hold ... N-1才能把 hold 要求恢复原状。
自测:set_false_path 和 set_clock_groups 有什么区别?
false_path 只禁止指定方向(-from 到 -to)的时序分析;clock_groups 禁止组与组之间双向的时序分析,且一条命令可覆盖成片时钟,效率更高。另外 clock_groups 至少要有两个组才生效。
自测:import_db 后直接 report_timing_summary 为什么可能报错或结果不对?
时序数据库还没建立/已过期。import_db 后第一次做 STA 或网表变更后,必须先执行
update_timing(并用-mode指明当前是 gate/place/route 哪个阶段)刷新时序数据,再做报告。
自测:虚拟时钟是什么?什么时候需要它?
create_clock -name clk_virtual -period 10不挂在任何物理端口上。当 IO 约束需要一个参考时钟、而该时钟在 FPGA 内部并不存在(如系统同步输出接口中,外部芯片的锁存时钟由板上级晶振提供)时,就建一个虚拟时钟作为 set_input/set_output_delay 的-clock参考。