做FPGA开发的应该都听过一句话:"功能对了,时序不对,等于白做。"
刚入行的时候我也吃过这个亏。代码写完了,仿真跑通了,综合一跑,满屏的 Timing Violation,红色警告刺眼得很。当时一头雾水,明明逻辑没问题,怎么就"时序不满足"了?
后来才明白,FPGA 不是单片机,它是硬件。硬件是有延迟的,信号从 A 点跑到 B 点需要时间,这个时间如果超过了时钟周期,数据就跑不赢时钟,芯片就出错了。这就是时序约束要解决的问题。
一、时序约束到底在约束什么?
简单说,时序约束就是告诉 FPGA 工具:你的设计要在什么频率下工作,信号之间有什么时间关系。
FPGA 综合和布局布线的时候,工具是"盲"的。你不告诉它目标频率,它可能给你优化出一个 50MHz 的设计,但你的系统需要 100MHz,最后上板一跑,数据采出来全是错的。
时序约束核心关注几个概念:
Setup Time(建立时间): 数据必须在时钟沿到来之前稳定下来,提前多久稳定,就是建立时间。如果建立时间不满足,说明组合逻辑太长,信号跑得慢,需要优化逻辑或者降频。
Hold Time(保持时间): 数据在时钟沿到来之后还要保持一段时间不变。如果保持时间不满足,说明信号跑得太快了,前一个时钟沿的数据冲到了下一个周期,这个一般靠工具插缓冲解决。
**Clock Period(时钟周期):**这是最常用的约束。100MHz 的时钟,周期就是 10ns,工具会确保所有寄存器之间的数据路径延迟小于 10ns。
二、约束文件怎么写?
不同厂商的格式不一样,Xilinx 用 XDC 文件,Intel(Altera)用 SDC 文件,但语法大同小异,都遵循 SDC(Synopsys Design Constraints)标准。
Xilinx XDC 示例
定义时钟:100MHz,周期10ns
create_clock -period 10.000 -name sys_clk [get_ports clk_in]
定义输入时钟的抖动
set_clock_uncertainty -setup 0.2 [get_clocks sys_clk]
set_clock_uncertainty -hold 0.1 [get_clocks sys_clk]
输入延迟约束(假设上游器件的 clock-to-output 是 2ns)
set_input_delay -clock sys_clk -max 2.0 [get_ports data_in*]
set_input_delay -clock sys_clk -min 0.5 [get_ports data_in*]
输出延迟约束(假设下游器件需要 3ns 建立时间)
set_output_delay -clock sys_clk -max 3.0 [get_ports data_out*]
set_output_delay -clock sys_clk -min 0.5 [get_ports data_out*]
多周期路径(某些路径可以放宽到2个时钟周期)
set_multicycle_path -setup 2 -from [get_cells slow_logic_reg*] -to [get_cells capture_reg*]
伪路径(不需要同步的路径,比如复位信号)
set_false_path -from [get_ports rst_n]
Intel SDC 示例
create_clock -name sys_clk -period 10.000 [get_ports {clk_in}]
set_input_delay -clock sys_clk -max 2.0 [get_ports {data_in[*]}]
set_output_delay -clock sys_clk -max 3.0 [get_ports {data_out[*]}]
set_false_path -from [get_ports {rst_n}]
几个常用约束说明
create_clock: 最基本的约束,定义时钟频率和对应的引脚。如果你的设计有多个时钟,每个都要定义。
set_input_delay / set_output_delay: 当 FPGA 和外部芯片(比如 ADC、DAC、DDR)通信时,需要告诉工具外部器件的时序特性,这样工具才能正确评估接口是否满足时序。
set_multicycle_path: 有些逻辑比较复杂,一个时钟周期跑不完,但你确认它不需要每个时钟都出结果。比如一个复杂的除法运算,每两个时钟才需要一次有效输出,就可以设成 2 个周期的多周期路径,工具就不会死磕一个周期内跑完了。
**set_false_path:**明确告诉工具这条路不用管。最典型的就是异步复位信号、跨时钟域的信号(前提是你已经做了正确的同步处理)。用这个要谨慎,别把该约束的路径给误杀了。
三、写约束的常见坑
只约束了时钟,没约束 IO。很多人写完 create_clock 就觉得完事了,但输入输出延迟不写的话,工具对接口时序是放任的,上板之后和外部芯片对接经常出问题。
约束和目标频率不匹配。设计跑 200MHz,约束写的是 100MHz,工具轻轻松松过时序,但上板就挂。反过来,约束写得太苛刻,工具拼命优化,资源利用率暴涨,最后可能也跑不起来。
跨时钟域没处理。两个不同频率的时钟域之间传数据,如果没有做同步器(比如两级寄存器打拍),时序约束写得再漂亮也没用。这是新手最容易踩的坑之一。
约束写了但没检查。写完约束文件要看看报告,工具会告诉你哪些路径是 critical path,哪些约束被忽略了。别一看"Timing Met"就开心,有时候是因为约束没生效,工具根本没去检查。
四、实际开发中的一些经验
在由你创科技做项目的这几年,我们经手的 FPGA 项目从几十兆的简单控制到几百兆的高速信号处理都有。慢慢总结下来,时序约束这事儿有几个心得:
先跑起来,再跑得快。 刚开始不要一上来就压高频,先把基本约束写好,确保功能正确、时序收敛,再逐步往上提频率。每次调完看报告,看关键路径在哪里,有针对性地优化。
流水线是好东西。 组合逻辑太深是时序违例的头号杀手。加一级寄存器,把长逻辑切断,虽然多了一个时钟周期的延迟,但频率能上去一大截。很多时候就是差这一级寄存器的事。
约束文件当代码管。 我们团队的做法是约束文件和 HDL 代码一起进版本管理,每次修改写清楚原因。项目交接的时候,新同事一看约束历史就知道当初为什么这么写,少走很多弯路。
IP 核的约束别乱改。 用厂商 IP(比如 DDR、PCIe、以太网)的时候,模板里自带的约束是经过验证的,除非你明确知道自己在做什么,否则别随意动。我们以前有同事觉得某个约束"太松了"手动收紧,结果跑了一晚上时序都没过。
五、总结
时序约束的本质就一句话:告诉 FPGA 工具你的设计要求,让它帮你检查设计能不能在目标频率下正常工作。
不要等上板出问题了才回头补约束,约束应该在设计初期就写好,跑完综合布局就看报告,有问题早改。养成这个习惯,FPGA 开发的效率会高很多。
如果你在做 FPGA 相关的项目,或者遇到时序收敛的问题,欢迎来找我们聊聊。由你创科技一直专注 FPGA 开发,高速接口、信号处理、协议解析都做过不少,有实际问题的话一起交流,少走弯路。