这一篇在干嘛?

上一篇解决了”怎么把流程跑起来”,这一篇解决”怎么让工具理解你的时序意图”。第一部分讲时序约束命令:先学会 get_ports/get_cells 这类对象获取命令(所有约束的地基),再按”时钟约束 → IO 延时约束 → 时序例外约束”的顺序掌握 SDC 核心;第二部分讲设计与时序分析报告命令,教你用 report_timing_summary 等命令定位时序问题。同系列第一篇:TCL 命令手册(1):脚本运行方式与全流程命令

对象获取命令:一切约束的地基

时钟约束:create_clock 与生成时钟

时钟延迟与不确定性

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_portsget_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)和 -duty_cycle(0100)分别调相位和占空比。

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]}]

Timing Wizard 中对生成时钟做重命名操作

常见坑:

  • 忘了约束 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 设 3 后 hold 沿随动,需补 hold 2 保持原 hold 要求

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 审计例外约束的有效范围。