Vivado时序约束有哪些关键点?

文章来源:FPGA入门到精通

时序不过,很多人第一反应是改逻辑、降频率、加流水线。

但很多时候问题根本不在设计本身,而是 XDC 约束文件写得有问题。约束写对了,Vivado 才知道你要多快、哪些路径不用管、时钟之间是什么关系。

下面几个点,都是实际项目中踩出来的经验。

第一个,create_clock 写对,后面约束才有意义。

create_clock 是所有时序约束的地基。

语法本身不复杂,但有几个坑很多人踩过。

最常见的是忘写 -name 参数,Vivado 会自动分配一个名字,后面引用的时候对不上。

还有一种错误是周期值写反,比如 100MHz 的时钟周期是 10ns,有人写成 100。

# 主时钟约束
create_clock -period 10.000 -name sys_clk [get_ports clk]

# 带占空比的时钟
create_clock -period 8.000 -name clk_125m [get_ports clk_125m] -waveform {0.000 4.000}

衍生时钟也要单独约束。

MMCM 或 PLL 输出的时钟,如果你用的是纯 RTL 代码实例化而不是 Block Design IP Integrator 自动连线,Vivado 不会自动生成衍生时钟约束。

这种情况漏写了 create_generated_clock,时序分析会把衍生时钟当成和主时钟毫无关系的独立时钟,该切的时钟域没切,不该分析的路径反而分析了一大堆。

# MMCM 衍生时钟
create_generated_clock -name clk_div2 -source [get_pins mmcm_0/CLKIN1] \
  -divide_by 2 [get_pins mmcm_0/CLKOUT1]

-source 指向主时钟引脚,-divide_by 或 -multiply_by 说明分频倍频关系。漏写衍生时钟约束,报告里的违例路径会让你怀疑人生。

第二个,异步时钟不切,时序报告全是违例。

Vivado 默认会分析所有时钟两两之间的路径。

如果板子上有两个异步时钟,比如 100MHz 系统时钟和 125MHz 以太网时钟,不显式切断它们的时序关系,Vivado 会按同步时钟去分析跨域路径,出一堆不可能通过的违例。

切断异步时钟有两种方式。set_false_path 切断特定路径,set_clock_groups 切断整组时钟之间的关系。

大部分情况用 set_clock_groups 更干净,把所有相关的异步时钟分别放到不同组里。

# 切断两组异步时钟
set_clock_groups -asynchronous \
  -group [get_clocks -include_generated_clocks clk_100m] \
  -group [get_clocks -include_generated_clocks clk_125m]

有个细节很容易漏,include_generated_clocks。

如果不加这个选项,MMCM 衍生出来的时钟不会被自动归入组内,跨域路径还是会被分析。

如果两个时钟是 MUX 切换的互斥关系,用 -physically_exclusive 代替 -asynchronous,Vivado 会在时序分析中考虑 MUX 选择,路径约束更精确。

第三个,IO 延迟约束很多人只写了 Max 忘了 Min。

set_input_delay 和 set_output_delay 是约束 FPGA 引脚和外部芯片之间数据延迟的。

很多人只写了 -max 参数,漏了 -min。

-max 对应建立时间的分析,-min 对应保持时间的分析。

只写 Max 不写 Min,Hold Time 违例排查半天,最后发现是约束没写全。

# 完整的 IO 延迟约束
set_input_delay -clock sys_clk -max 2.5 [get_ports data_in]
set_input_delay -clock sys_clk -min 0.8 [get_ports data_in]

set_output_delay -clock sys_clk -max 3.0 [get_ports data_out]
set_output_delay -clock sys_clk -min 1.0 [get_ports data_out]

如果外部芯片的时钟和 FPGA 的时钟不是同一个晶振,用 Virtual Clock 作为参考。

先定义一个虚拟时钟,再用它来约束 IO 延迟,避免板级延迟被算进 FPGA 内部路径。

create_clock -period 10.000 -name virt_clk
set_input_delay -clock virt_clk -max 2.5 [get_ports data_in]

第四个,XDC 文件里的执行顺序是个隐形坑。

XDC 指令从上往下读,后面的覆盖前面的。

描述越精确的约束优先级越高,比如精确到端口的 set_false_path 会覆盖针对整个时钟的约束。

但很多人没意识到的是,物理约束如果写在时序约束前面,可能会被后面的时序约束覆盖。

推荐的排列顺序是,时序约束在前,物理约束在后。

时序约束内部也按优先级排:先 create_clock 定义时钟,再写时序例外如 set_false_path,最后写 set_max_delay 之类的路径约束。

# 推荐顺序
# 1. 时钟定义
create_clock -period 10.000 -name sys_clk [get_ports clk]
create_generated_clock -name clk_div2 ...

# 2. 时钟组
set_clock_groups -asynchronous -group ... -group ...

# 3. IO 延迟
set_input_delay -clock sys_clk ...

# 4. 时序例外
set_false_path -from [get_clocks clk_100m] -to [get_clocks clk_125m]

# 5. 物理约束
set_property PACKAGE_PIN ... [get_ports ...]

还有一个 Tcl 语法坑,换行符反斜杠后面不能有空格。\ 后面哪怕只有一个空格,Vivado 会把下一行当成独立命令,约束直接失效,而且不会报错。

第五个,约束写完先跑综合验证,别急着进布局布线。

约束写完后,先跑一次综合,打开 Synthesized Design,用 Tcl 命令检查约束质量。

这一步不费什么时间,但能提前发现约束层面的漏洞,省得跑完一整个布局布线才发现时序违例是约束没写对。

check_timing 是第一个要跑的,它会列出所有未约束的时钟和端口。如果看到 Unconstrained Clocks 下面有时钟,说明 create_clock 漏了。

# 综合后验证约束
check_timing -verbose
report_clock_interaction
report_timing_summary -max_paths 10 -delay_type max

report_clock_interaction 用来检查时钟组关系是否正确建立。

如果异步时钟之间的交叉单元格还是红色,说明 set_clock_groups 没生效。

report_timing_summary 看关键路径的裕量,如果 Worst Negative Slack 已经是负数,后面跑布局布线大概率过不了,不如先修约束再编译。

约束不是一次性写完就完事。项目迭代中时钟域会变多,IP 核会换,约束得跟着走。养成写完约束先跑 check_timing 的习惯,能省掉后面不少排查时间。