3. Vivado时序约束入门:create_clock、set_input_delay、set_output_delay、时序约束文件(.xdc)编写规范

时序约束,说白了就是告诉工具你的设计要在什么条件下工作。很多新手觉得这步可有可无,直接跑综合实现就完事了。我刚开始做FPGA时也这么想,结果板子调了三天,信号就是不稳定。后来才发现,没加约束的时序分析结果根本不可信。

这一章,我们就从最基础的三个命令入手,把时序约束的底子打牢。

3.1 时序约束的核心逻辑

在动手写约束之前,你得先明白一件事:时序约束的本质是定义数据在寄存器之间传输的时间窗口。Vivado根据你定义的时钟和IO延迟,去检查每个路径的建立时间和保持时间是否满足。

我个人习惯把时序约束分成三类:

  • 时钟约束:定义时钟的周期、波形、占空比
  • IO约束:定义芯片引脚与外部器件的时序关系
  • 例外约束:定义不需要检查的路径或放宽的时序要求

这一章我们重点讲前两类,第三类后面章节会展开。

核心原则:约束越完整,时序分析结果越接近真实硬件行为。缺约束的路径,Vivado会按理想情况分析,实际跑起来大概率出问题。

下面这张图展示了时序约束在FPGA设计流程中的位置和作用:

时序约束在FPGA设计流程中的位置 RTL设计输入 时序约束(.xdc) 综合 (Synthesis) 实现 (Implementation) 时序分析 (STA) 约束驱动优化 时序不满足时,修改约束或RTL重新迭代 约束文件与RTL 同步输入到综合

3.2 create_clock:定义你的时钟

create_clock是最基础的约束命令。没有它,Vivado不知道你的设计跑多快,时序分析也就无从谈起。

3.2.1 基本语法

create_clock -name <时钟名> -period <周期(ns)> [ -waveform {上升沿 下降沿} ] [get_ports <端口名>]

举个例子,一个100MHz的时钟,占空比50%:

create_clock -name sys_clk -period 10.000 [get_ports clk]

这里-period 10.000对应100MHz,单位是纳秒。Vivado默认上升沿在0ns,下降沿在周期的一半,也就是5ns。

如果你需要自定义波形,比如占空比60%:

create_clock -name custom_clk -period 10.000 -waveform {0.000 6.000} [get_ports clk]

小技巧:我习惯在时钟名里带上频率信息,比如clk_100m、clk_50m。这样看报告时一眼就能认出是哪个时钟,不用去翻定义。

3.2.2 多时钟与虚拟时钟

一个设计里通常有多个时钟。每个时钟都要单独定义:

create_clock -name clk_a -period 10.000 [get_ports clk_a]
create_clock -name clk_b -period 20.000 [get_ports clk_b]

还有一种情况:虚拟时钟。它不绑定到任何物理端口,只用于IO约束的参考。比如外部器件的工作时钟:

create_clock -name virt_clk -period 8.000

注意这里没有get_ports。虚拟时钟在set_input_delay和set_output_delay里经常用到。

我曾经踩过的坑:虚拟时钟的名字不要和物理时钟重名,否则Vivado会报错。另外,虚拟时钟的周期必须和外部器件的实际时钟一致,否则IO时序分析全是错的。

3.3 set_input_delay:告诉工具数据什么时候来

set_input_delay定义的是:相对于时钟沿,外部数据到达FPGA引脚的时间。这个时间包括PCB走线延迟、外部器件输出延迟等。

3.3.1 基本语法

set_input_delay -clock <时钟名> <延迟值(ns)> [get_ports <端口列表>]

举个例子,外部器件在时钟上升沿后2ns输出数据,PCB走线延迟0.5ns:

set_input_delay -clock clk -max 2.500 [get_ports data_in]
set_input_delay -clock clk -min 1.000 [get_ports data_in]

这里-max和-min分别对应最大和最小延迟。Vivado用-max检查建立时间,用-min检查保持时间。

3.3.2 上升沿与下降沿采样

如果外部器件在时钟下降沿输出数据,需要加-clock_fall:

set_input_delay -clock clk -clock_fall -max 2.500 [get_ports data_in]

还有一种情况:数据在时钟上升沿和下降沿都变化,也就是DDR模式。这时要分别定义:

set_input_delay -clock clk -max 1.500 [get_ports ddr_data]
set_input_delay -clock clk -clock_fall -max 1.500 [get_ports ddr_data]
set_input_delay -clock clk -min 0.500 [get_ports ddr_data]
set_input_delay -clock clk -clock_fall -min 0.500 [get_ports ddr_data]

关键点:set_input_delay的值是相对于时钟沿的。正值表示数据在时钟沿之后到达,负值表示在时钟沿之前到达。大多数情况下是正值。

3.4 set_output_delay:告诉工具数据什么时候出去

set_output_delay定义的是:相对于时钟沿,数据从FPGA引脚到外部器件的时间。它和set_input_delay是对称的。

3.4.1 基本语法

set_output_delay -clock <时钟名> <延迟值(ns)> [get_ports <端口列表>]

举个例子,外部器件需要数据在时钟沿前1ns建立,PCB走线延迟0.5ns:

set_output_delay -clock clk -max 1.500 [get_ports data_out]
set_output_delay -clock clk -min 0.500 [get_ports data_out]

这里-max对应外部器件的建立时间要求,-min对应保持时间要求。

3.4.2 输出延迟的参考时钟

输出延迟的参考时钟可以是FPGA内部的时钟,也可以是虚拟时钟。如果外部器件使用独立的时钟源,用虚拟时钟更准确:

create_clock -name ext_clk -period 10.000
set_output_delay -clock ext_clk -max 2.000 [get_ports data_out]

我的经验:写IO约束时,最好先画出时序图。把时钟沿、数据变化沿、建立时间窗口、保持时间窗口都标清楚,再写约束就不容易出错。我见过太多人凭感觉写IO约束,结果时序分析一塌糊涂。

3.5 .xdc文件编写规范

.xdc文件是Vivado的时序约束文件,说白了就是一个文本文件,里面按行写约束命令。但怎么写才规范、可维护?这里有几个要点。

3.5.1 文件结构

我建议按以下顺序组织.xdc文件:

顺序 内容 说明
1 时钟约束 所有create_clock、create_generated_clock
2 输入延迟约束 所有set_input_delay
3 输出延迟约束 所有set_output_delay
4 时序例外 set_false_path、set_multicycle_path等
5 其他约束 管脚约束、电压约束等

3.5.2 注释与分组

用注释把不同模块的约束分开,方便后期维护:

# ============================================
# Clock Constraints
# ============================================
create_clock -name sys_clk -period 10.000 [get_ports clk]

# ============================================
# Input Delay Constraints - ADC Interface
# ============================================
set_input_delay -clock sys_clk -max 2.500 [get_ports adc_data*]
set_input_delay -clock sys_clk -min 1.000 [get_ports adc_data*]

# ============================================
# Output Delay Constraints - DAC Interface
# ============================================
set_output_delay -clock sys_clk -max 1.500 [get_ports dac_data*]
set_output_delay -clock sys_clk -min 0.500 [get_ports dac_data*]

3.5.3 使用通配符

一组信号可以用通配符*批量约束,省时省力:

set_input_delay -clock sys_clk -max 2.000 [get_ports {data_bus[*] addr_bus[*]}]

注意:通配符不要写得太宽泛,比如get_ports *会把所有端口都约束上,容易误伤。我一般按模块分组,每组用明确的通配范围。

3.5.4 避免重复约束

同一个端口被多次约束,Vivado会取最后一个。但这样容易出问题,因为你可能忘了前面已经约束过。我的做法是:每个端口只在一个地方约束,用注释标明。

3.6 实战:一个完整的.xdc示例

下面是一个完整的.xdc文件,包含时钟、输入延迟、输出延迟:

# ============================================
# Clock Constraints
# ============================================
create_clock -name sys_clk -period 10.000 [get_ports clk_p]
create_clock -name ddr_clk -period 5.000 [get_ports ddr_clk_p]

# Virtual clock for external device
create_clock -name ext_clk -period 20.000

# ============================================
# Input Delay - Ethernet PHY
# ============================================
# PHY outputs data at 2.0ns max, 0.8ns min after TX_CLK
set_input_delay -clock sys_clk -max 2.000 [get_ports eth_rx_data*]
set_input_delay -clock sys_clk -min 0.800 [get_ports eth_rx_data*]
set_input_delay -clock sys_clk -max 2.000 [get_ports eth_rx_dv]
set_input_delay -clock sys_clk -min 0.800 [get_ports eth_rx_dv]

# ============================================
# Output Delay - DAC
# ============================================
# DAC requires data setup 1.5ns before clock, hold 0.5ns after clock
set_output_delay -clock sys_clk -max 1.500 [get_ports dac_data*]
set_output_delay -clock sys_clk -min 0.500 [get_ports dac_data*]
set_output_delay -clock sys_clk -max 1.500 [get_ports dac_wr]
set_output_delay -clock sys_clk -min 0.500 [get_ports dac_wr]

# ============================================
# Output Delay - UART (using virtual clock)
# ============================================
set_output_delay -clock ext_clk -max 2.000 [get_ports uart_tx]
set_output_delay -clock ext_clk -min 0.500 [get_ports uart_tx]

总结:时序约束不是写一次就完事的。综合后、实现后都要检查时序报告,看有没有违例。如果发现违例,先检查约束是否正确,再考虑修改RTL。我见过太多人一上来就改代码,结果发现是约束写错了。