ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Vivado时序约束实战:从Constraints Wizard到XDC文件全解析

Vivado时序约束实战:从Constraints Wizard到XDC文件全解析 1. 为什么时序约束总是绕不过去做FPGA开发的人迟早都要面对时序约束这件事。我刚接触Vivado那会儿以为写好Verilog、仿真通过、能跑出波形就算完事了结果第一次把工程跑完Implementation、打开Timing Summary一看满屏红色的violation整个人是懵的。后来才明白仿真通过只是功能上看起来对实际上板子能不能稳定跑、频率能跑到多少、上电之后会不会偶发出错全都压在时序收敛这四个字上。这篇东西主要解决三类人的痛点一是刚装上Vivado、被Constraints Wizard搞得一头雾水的新手二是已经会写一点代码但每次综合实现之后不知道怎么把时序报告看明白的进阶用户三是那种明明约束文件写了结果发现根本没生效、或者DRC一直报错的“倒霉蛋”。我自己在这条路上踩过不少坑比如把约束写在SDC文件里但Vivado根本不认比如忘了加create_generated_clock导致分频时钟没有被正确分析再比如在Vivado里手滑把时序约束加到了综合选项里导致每次run都要重新搞一遍。下面把整个过程用最直白的方式拆开讲。标题里提到的Constraints Wizard和XDC文件就是Vivado里面处理时序约束的两个核心入口。XDC的全称是Xilinx Design Constraints是Xilinx在Vivado时代主推的约束文件格式语法上跟传统的SDCSynopsys Design Constraints非常接近但不是完全一样。Constraints Wizard是Vivado图形界面里的一个向导工具它会根据你工程里实际存在的时钟信号、接口信号自动生成一份基础的XDC省去手写大量基础约束的麻烦。但它生成的东西往往只是“地基”真正的debug和收敛还是得自己上手。说实话时序约束这件事难的不是写语法而是理解它背后的意图。约束不只是告诉工具“这个时钟是多少兆赫兹”而是在告诉工具一个完整的时序模型包括数据从哪来、到哪去、什么时候有效、允许有多少延迟。工具拿到这些信息才知道布局布线该怎么安排才能在满足功能的前提下尽量满足时序。下面我用一个完整的流程来讲清楚这件事。2. Constraints Wizard到底帮你干了什么2.1 Constraints Wizard和XDC文件的关系Vivado的Constraints Wizard通常在Open Synthesized Design或Open Implemented Design之后才能用入口在Layout左下角的Flow Navigator里叫Constraints Wizard。它会扫描当前设计把已例化的时钟信号、输入输出端口列出来然后让你在界面上填写频率、占空比、输入输出延迟等信息最后Generate出一份或几份XDC文件。这里有个特别容易让新手误解的点Wizard生成XDC但Wizard不是XDC本身。你之后在XDC里手写的所有约束跟Wizard生成的约束是两套独立的文本内容Vivado在综合和实现时会把工程里所有的XDC文件按顺序读进来按优先级统一生效。也就是说Wizard只是给你一个起步的模板后期大量精细的约束还是要靠手写补充。拿我的一个实际工程举例一块Artix-7的板子外接了一个100MHz的有源晶振经过PLL分频出50MHz和200MHz两个时钟给内部逻辑。打开Constraints Wizard之后它自动识别出了clk_100m这个主时钟端口以及两个PLL输出时钟。我在Wizard里把100MHz填进去占空比默认50%生成之后打开生成的XDC里面大概是这个样子的create_clock -period 10.000 -name clk_100m -waveform {0.000 5.000} [get_ports clk_100m]这行约束的含义很直白创建一个名为clk_100m的时钟周期10ns波形在0ns到5ns之间是高电平。Vivado会根据这行定义推导出PLL输出时钟的约束但注意PLL生成的时钟通常会在工程里另外出现如果Wizard没有自动生成generate_clock你得自己去加等会儿到第3节我会专门讲。2.2 那些Wizard生成不了、必须自己写的约束Wizard能处理的是基础场景单端时钟、差分时钟它有对应选项选引脚极性、基本的输入输出延迟模板。但实际工程里Wizard生成后往往还需要手动加这些约束生成时钟约束用create_generated_clock显式定义分频、倍频、相位调整后的时钟异步时钟组用set_clock_groups定义哪些时钟之间不需要做时序分析输入输出延迟的精细调整Wizard填入的IO delay只是估算真正对接外部芯片时序比如ADC/DAC、DDR颗粒时必须按手册参数算得非常精确伪路径与多周期路径用set_false_path和set_multicycle_path跳过无关路径或放宽某些路径的时序要求时钟不确定性set_clock_uncertainty用来模拟时钟抖动和偏斜Wizard只会给默认值工程里通常要根据器件手册修改。我在一个跑DDR3的工程里从Wizard生成的约束只有四条基础时钟但最终手写的XDC有接近120行一大半都是上面这几种类型。所以千万不要以为“跑了Wizard就等于约束完成了”那才是万里长征第一步。Wizard的定位是减少查端口名字和基础语法的负担最终的时序收敛必须靠自己对设计的理解去补全约束。3. XDC文件的关键语法写给还看不懂约束的人3.1 主时钟与生成时钟主时钟primary clock指的是进入FPGA的板级时钟通常来自晶振或者外部芯片的时钟输出。它用create_clock约束一般绑在某个输入端口上。生成时钟generated clock则是FPGA内部PLL、MMCM或逻辑分频电路产生的时钟用create_generated_clock约束。为什么生成时钟必须显式约束因为对Vivado来说PLL等硬核的输出时钟虽然能在一定条件下自动传播但当你用逻辑自己分频比如计数器分频时工具并不会自动认识这个新时钟如果不约束它工具会把它当作普通数据信号去分析时序报告会变得完全不可信。这时候要给create_generated_clock指定源时钟和分频关系create_generated_clock -name clk_div2 -source [get_pins {pll_i/clk_out1}] -divide_by 2 [get_pins {divider_reg/Q}]这行表示在divider_reg/Q这个引脚上存在一个名为clk_div2的时钟它由pll_i/clk_out1这个时钟经过2分频得到。把这条约束加到XDC之后Vivado就能正确分析所有由clk_div2驱动的寄存器。有一个高频坑我必须强调create_generated_clock的-source对象在综合后和实现后可能因为网表名字变化而找不到。同一个信号在综合后的名字叫pll_i/clk_out1在实现后可能变成pll_i/clk_out1_BUFG如果你只写了其中一个另一个阶段可能会报ERROR。我的做法是尽量把-source写在一个比较稳定的层次或者在两个阶段分别打开网表去核对名字另外一种做法是直接在XDC里同时写上两条相同约束不同源名Vivado不会因为重复约束而出错它会自动合并处理。3.2 虚拟时钟与输入输出延迟虚拟时钟virtual clock是只存在于约束文件里、不实际连到任何引脚上的时钟主要用来分析外部芯片和FPGA之间的数据时序。举一个最常见的场景FPGA从外部ADC读取数据ADC随路时钟adc_clk输出给FPGA同时输出数据adc_data。那么对FPGA来说adc_clk是输入时钟adc_data的建立时间、保持时间都要以adc_clk为参考来约束而adc_clk相对FPGA内部的系统时钟没有直接相位关系这时候就需要定义虚拟时钟作为参考create_clock -name adc_virtual_clk -period 20.000 [get_ports {adc_clk}] set_input_delay -clock adc_virtual_clk -max 4.000 [get_ports {adc_data}] set_input_delay -clock adc_virtual_clk -min 1.000 [get_ports {adc_data}]这样Vivado会以20ns的周期去检查adc_data的建立与保持。如果是约束输出接口比如FPGA给DAC发数据那么用set_output_delay逻辑也是类似。这里的max和min不是随便填的要对照外部芯片数据手册上的tco、tsu、th参数来做加减法具体公式在第5节里用例子说明。3.3 伪路径与多周期路径set_false_path大概是被滥用最多的约束了因为很多人一看到时序违例就直接上它。但到底是什么路径可以设为false path只有这类情况才合适跨时钟域的同步器两级触发器打拍、复位释放逻辑、测试模式信号、以及一些根本不关心时序的控制信号。set_false_path -from [get_clocks clk_a] -to [get_clocks clk_b]假如你在两个异步时钟域之间传输数据中间做了两级同步器那么跨时钟域的路径就可以设为false path因为它的时序由同步器保证不靠Vivado布置出来的路径长度来保证。但如果两个时钟域之间需要真正的数据通信比如用异步FIFO传递数据那set_false_path只能加在FIFO的读写指针同步路径上数据总线本身不能随便设。很多新手把两个时钟域之间所有路径全设成false path结果板子上跑起来数据偶尔出错查了半天找不到原因最后发现就是约束设置把本来需要时序保证的路径给放过了。这个坑我踩过后来学乖了不到万不得已绝不整片跨时钟域地设false path。set_multicycle_path则是有意识放宽某些路径的检查。比如数据在某个寄存器里每三个周期才更新一次工具默认会按单周期路径分析这时候就需要告诉工具“别那么严格”set_multicycle_path -setup 3 -from [get_pins {src_reg/C}] -to [get_pins {dst_reg/D}] set_multicycle_path -hold 2 -from [get_pins {src_reg/C}] -to [get_pins {dst_reg/D}]注意setup和hold这对数字的组合关系setup设为3时hold必须设置为setup的值减1这是工具约定俗成的规则不然会算出错误的保持时间约束。新手经常只写setup不写hold结果报告里出现一堆吓人的hold violation其实多半是hold没跟着改。4. 时序报告怎么读别被一堆英文缩写吓住4.1 WNS、TNS、WHS、THS分别代表什么每次跑完Implementation打开Implementation Timing Summary第一个看到的就是四个缩写WNS、TNS、WHS、THS。WNSWorst Negative Slack最差的建立时间裕量。负数说明存在建立时间违例-0.5ns意味着某条路径比要求慢了0.5ns。TNSTotal Negative Slack所有违例路径的负裕量总和。TNS越大说明违例路径越多或者越严重。WHSWorst Hold Slack最差的保持时间裕量。负数代表保持时间违例。THSTotal Hold Slack所有保持时间违例的负裕量总和。正常情况下四个值都应该是正数而且WNS最好留有一定余量业界一般建议至少留0.1ns以上。余量为零并不是不能用但温度、电压一波动可能就挂了。我在写产品级固件时通常要求WNS收敛到0.3ns以上才算稳。4.2 定位一条违例路径的办法看WNS是负的之后别急着改代码。先在Flow Navigator里打开Implementation → Open Implemented Design → Timing Summary然后点击WNS那一栏下面的红色数字Vivado会展开对应的路径列表双击某一条违例路径就能看到详细的Path Analysis界面。Path Analysis界面里面左边是路径上从起点到终点经过的每个节点包括组合逻辑延迟、布线延迟、时钟到达时间右边是时序路径的示意图。我一般按这几个步骤排查先看起点和终点分别是什么时钟域。如果是从clk_a到clk_b的路径优先怀疑跨时钟域问题检查是否漏设异步时钟组。看路径延迟主要花在哪个部分。如果布线延迟占比特别大说明布局布得太远可能是floorplan不合理也可能是某个高扇出信号拖慢了布局。看组合逻辑级数。如果一条路径上有很多个LUT串联那就需要考虑在代码里插入流水线寄存器。看起点终点的时钟是否经过MMCM/PLL。如果时钟偏移太大调整时钟约束或改用全局时钟资源。有一次我遇到一条路径WNS-0.4ns起点在A模块终点在B模块两个模块在代码层级里分得很开布局时被告知不能靠近因为中间隔了一个大RAM。最后解法是在数据通路上加了两级流水寄存器把一个周期的工作量拆成三个周期完成代价是数据延迟两个周期但WNS从-0.4ns变成0.5ns。这种取舍在做FPGA时非常常见。4.3 跨时钟域路径在报告里的表现跨时钟域路径的时序报告有时候看起来特别乱原因是Vivado默认会对所有时钟相关的路径做分析不管它们的时钟是否相关。如果你设了set_clock_groups -asynchronous那么相关路径会被排除到分析范围之外报告里就不会出现一片又红又多的跨时钟域违例。如果你没设那么两个无关时钟之间会计算出大量路径而这些路径的slack值看起来毫无规律有正有负负的原因也不是因为你设计有bug而是两个时钟源之间的相位关系根本没定义好。这种情况我在初学阶段遇到过很多次看到报告一堆红急得不行结果把所有时钟之间的路径设为asynchronous之后整个世界清净了。所以读报告之前先确认时钟约束是不是完整的、异步时钟是不是已经约束好了。没有干净的时钟约束时序报告就是一堆带噪数据你怎么分析都是白费力气。5. 从零开始给一个工程加约束完整实操流程5.1 先搞定时钟再谈其他拿到一块新板子我的习惯是先把所有输入的时钟信号放进XDC再去管IO和延迟。原因很简单如果时钟不对后面所有分析都没有意义。以下是我常用的一个模板结合前面提到的Artix-7工程举例。假设板子上有一个100MHz差分晶振经LVDS进入FPGA内部用MMCM产生三个时钟# 差分主时钟 create_clock -name sys_clk -period 10.000 [get_ports {clk_p}] create_clock -name sys_clk -period 10.000 [get_ports {clk_n}]差分时钟其实有两种做法一种是如上约束正负两个端口另一种是通过IBUFDS后约束缓冲输出信号实际效果等价。我推荐在引脚上约束因为更直观。接下来是MMCM输出时钟Wizard一般会自己生成但如果没生成要手动加。查找MMCM输出引脚名的方法是打开Elaborated Design或Synthesized Design在原理图里选中MMCM看它的clk_out0、clk_out1等输出端口名。假设clk_out0是200MHzclk_out1是50MHzcreate_generated_clock -name clk_200m -source [get_pins {mmcm_i/clk_in1}] -multiply_by 2 [get_pins {mmcm_i/clk_out0}] create_generated_clock -name clk_50m -source [get_pins {mmcm_i/clk_in1}] -divide_by 2 [get_pins {mmcm_i/clk_out1}]如果MMCM配置了BUFG那输出引脚名可能带_BUFG后缀这时要打开Implemented Design的Device视图去确认实际名字或者先跑一次综合后约束等实现后再修改。5.2 输入输出延迟的计算方法输入输出延迟的计算是很多人感觉最难的部分但其实套路固定记住几个公式就能搞定。对于输入数据set_input_delay的max和min分别是这样计算的max T_co_external外部器件的时钟到输出最大延迟 T_pcb_pcbPCB走线延迟 T_setup_fpgaFPGA的建立时间要求通常取负值min T_co_external最小延迟 T_pcb_pcb − T_hold_fpgaFPGA的保持时间要求实际工程中PCB走线延迟可以用一个估算值代替通常几百皮秒T_co和T_ho数值在外部芯片手册里都有。假设某个ADC手册给出tco_max3.5ns、tco_min1.2nsPCB走线延迟约0.5nsFPGA的tsu和th分别按0.2ns和0.1ns计算那么约束就是set_input_delay -clock [get_clocks adc_virtual_clk] -max 3.8 [get_ports {adc_data}] set_input_delay -clock [get_clocks adc_virtual_clk] -min 0.6 [get_ports {adc_data}]对应输出接口set_output_delay的计算逻辑类似但要从FPGA视角出发set_output_delay -clock [get_clocks dac_virtual_clk] -max [expr 5.0 - 0.5] [get_ports {dac_data}] set_output_delay -clock [get_clocks dac_virtual_clk] -min [expr 2.0 0.5] [get_ports {dac_data}]这里的5.0是外部DAC要求的数据建立时间2.0是保持时间0.5是PCB走线延迟。Vivado的XDC里可以直接用expr表达式省去自己心算这个技巧很多教程不会提。5.3 把异步时钟域分开工程里有两个以上彼此无关的时钟时必须在XDC里写清楚哪些时钟域是异步的否则Vivado会默认它们之间需要做时序分析产生大量无意义的违例路径。set_clock_groups -asynchronous -group [get_clocks {clk_200m}] -group [get_clocks {clk_50m}]这里的group参数可以写多个时钟意思是第一个group内的所有时钟与第二个group内的所有时钟之间是异步关系。注意group内部的时钟之间依然保持同步关系并做时序分析。多个异步时钟域还可以用更简洁的写法set_clock_groups -asynchronous -group {clk_200m} -group {clk_50m} -group {uart_clk}每个group之间两两异步。这条约束建议在Wizard生成基础时钟之后就立刻加上别等出现一堆红色违例再加省的自己吓自己。5.4 约束文件在Vivado里的存放位置和编译顺序XDC文件在Vivado里的管理方式跟源码文件类似但又不太一样。源码文件通过add_files加入工程XDC则通常放在Constraints目录下。不过我把XDC放在哪里其实不影响最终行为关键是什么时候生效综合时Vivado会读一次XDC用于指导综合优化实现时Vivado会再读一次XDC用于布局布线。所以每次修改XDC后都要重新跑综合或实现才会生效。如果只改约束不重新综合在Implementation下点Reload Timing Constraints也能让新约束参与实现后的时序分析。多个XDC文件的处理顺序也很重要。Vivado里可以在Settings → General → Constraint Set里指定多个文件的执行顺序后读入的约束优先级更高。如果同一个时钟在两个XDC里被约束成了不同频率后读入的会覆盖先读入的。新手常见的坑是工程里既有自动生成的约束文件又有自己手写的约束文件两个文件都对同一个时钟写了create_clock结果频率互相覆盖时序报告看着没问题实际约束的是错误频率。我的建议是只保留一个主XDC文件把所有约束统一写进去同时把自动生成的约束文件从工程中排除掉避免同类约束冲突。6. 常见报错和避坑经验全是真金白银换来的教训6.1 DRC RTSTAT-2到底在说什么标题里的热搜词有一个“vivado 报错 drc rtstat-2”这里多说两句。RTSTAT-2是Vivado在Implementation阶段检查出来的一个DRC违规常见提示文字大致是[DRC RTSTAT-2] Some registers have no clock or are not being timed: ...意思是检测到一部分寄存器没有时钟连接或者这些寄存器没有参与时序分析。出现这个错误最常见的原因有三个有一块逻辑的时钟信号没有约束Vivado不知道它跑在哪个时钟域里某个寄存器被复位信号异步置位/复位且复位释放与时钟没有同步关系导致工具无法建立完整时序模型代码里有被优化掉的逻辑但寄存器还在。排查时先看DRC消息里提示的具体寄存器实例路径回到代码里定位是哪个模块然后检查它的时钟连接。如果确定是被冗余逻辑导致的可以在相关信号上加(* keep true *)属性保留或者查查是不是复位逻辑写得不规范。不要直接忽略这个DRC因为它反映的是设计结构问题不是约束问题就算你硬让Implementation通过板子上的行为也可能很奇怪。6.2 create_generated_clock报错找不到源引脚这个报错信息一般是ERROR: [Vivado 12-585] create_generated_clock ... cannot find source pin原因很简单XDC里的source引脚名字在综合后的网表里不存在。解决办法是在综合后先打开Synthesized Design用get_pins命令搜索实际存在的引脚名get_pins -hierarchical -filter {NAME ~ *clk_out1*}把搜到的名字填到-source里。另外还有一种情况是名字在综合后正确但实现后因为BUFG插入导致名字变了这时需要打开Implemented Design再搜一次。一个更稳定的做法是把create_generated_clock加在层次化引脚比较浅的地方比如直接约束在MMCM的输出缓冲器后面这样名字变化的概率小很多。实在不行也可以把综合后使用的XDC和实现后使用的XDC分开维护虽然麻烦一点但能确保每个阶段都不报错。6.3 约束写了对但报告没变到底是为什么有时候你会遇到这种情况XDC里明明加了约束重新跑完综合实现报告却跟没加一样。这类问题十有八九是约束没有生效排查步骤是这样的先打开Synthesized Design或Implemented Design在Tcl Console手动执行report_clocks看约束的时钟是否出现在列表里。如果没出现说明约束文件没被读进来或者约束对象名字不对检查约束文件是否被工程正确包含。在Sources窗口的Constraints文件夹下看是否有XDC文件如果有但文件旁边有灰色图标说明被exclude了右键重新enable检查约束文件里的语法错误。Vivado执行XDC时遇到语法错误可能直接跳过后续部分打开Messages窗口看有没有error级别的提示看看是不是冲突覆盖了。用一个get_clocks -filter查询你约束的时钟名如果出现两个同名时钟那可能存在重复约束后读入的覆盖了前一个。这几个步骤能解决大部分“约束没生效”的问题。我个人还有一种习惯是在XDC里加一些简单的注释标记比如### CLOCK ###方便在综合后的报告里快速定位约束来源Vivado的report_timing_summary里也会显示每条约束来自哪个文件哪一行。6.4 大面积路径违例先从代码结构找原因有时不是约束的问题而是代码写得不适合时序收敛。比如一个组合逻辑链上串了30个LUT无论怎么约束都不可能过。遇到大面积违例优先做这几件事把大规模组合逻辑拆分成多级流水线每级之间用寄存器打一拍高扇出的信号比如复位、使能信号扇出超过几百优先走全局时钟或复位网络避免每个寄存器单独驱动检查是否有变量在always块里被综合成了锁存器优先使用DSP48、BRAM等硬核资源实现乘加、存储等运算而不是用LUT硬搭。有一次帮同事排查一个严重违例那哥们写了一个大位宽的乘法器直接用纯逻辑实现了WNS惨到-5ns。后来换成DSP48硬核IP问题直接消失。工具的作用是把你描述的逻辑变成物理实现但物理实现的根本上限是你描述的逻辑效率。代码结构不行约束写得再漂亮也没用。7. 关于综合和实现的一些额外心得大家刚学Vivado时经常会在Flow Navigator里看到Synthesis和Implementation两个步骤偶尔还会看到Run Synthesis、Run Implementation变红报错。其实综合和实现是两件不同的事。综合Synthesis是把RTL代码转成逻辑网表主要做逻辑优化和技术映射实现Implementation则是把综合后的网表布局到FPGA的物理资源上并完成布线。时序约束在这两个阶段都会参与。综合阶段用约束指导逻辑优化比如工具会计算出关键路径然后优化这些路径上的逻辑深度实现阶段用约束指导布局布线比如工具会尝试将关键路径上的单元放得近一点布线时优先保证关键路径满足时序。所以如果你只跑综合、不跑实现性能数字和时序报告都不算数。必须跑完Implementation之后看Timing Summary那个结果才是实际布线后的真实性能。还有一点Vivado的Implementation里有一个叫Post-Route Timing Summary的报告是在布线完成之后重新提取实际延迟算出来的比Post-Synthesis的准确得多也是最接近板级运行的评估值。我见过不少新手只跑Synthesis看到Timing Summary是绿的就觉得万事大吉结果到Implementation阶段发现一堆红。这是正常的因为综合后的延迟估算跟实际布线差异很大尤其是高扇出信号、长走线实际延迟可能翻倍。以后记住一个结论以Implementation后的时序报告为准综合后的报告只做参考。8. 最后分享一个我调试时序问题时的小习惯文章已经写得够长了最后分享一个调节奏的技巧。我调试时序问题时从来不在一个巨大的工程里直接乱试。先把所有违例路径按照“时钟域”分组打印出来。然后针对每一组按“先查时钟约束、再查跨时钟域、再查路径结构、最后查物理布局”的顺序逐层排查。Vivado的report_timing_summary里有个选项可以按时钟域单独出报告report_timing_summary -delay_type max -max_paths 100 -name timing_summary_clk_200m -from [get_clocks clk_200m]这样能把某个时钟域里面的路径单独拉出来看不会被其他时钟域的违例干扰。如果某个时钟域的违例路径特别多优先看是不是采样时钟本身定义出了问题如果某个时钟域的违例就一两条再放大去看路径的具体延迟构成。另外还有一个笨但非常有效的方法加流水线。不是无脑加而是沿着从起点到终点的路径看哪一级组合逻辑延迟最大在那个位置插入寄存器。每次都先把最差的那条路径修好再重新跑Implementation看下一个最差路径。这样迭代三五轮基本上能收敛到目标频率。把最堵的那条路修通其他路自然顺了。时序约束不是一个一劳永逸的事。每次改代码、调IP、换板子都可能需要重新审视约束。但掌握了方法之后它就不再是玄学而是一套有迹可循的工程流程。希望这篇东西能帮你少走点我当年走过的弯路。
返回列表