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时代主推的约束文件格式,语法上跟传统的SDC(Synopsys 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。
- WNS(Worst Negative Slack):最差的建立时间裕量。负数说明存在建立时间违例,-0.5ns意味着某条路径比要求慢了0.5ns。
- TNS(Total Negative Slack):所有违例路径的负裕量总和。TNS越大,说明违例路径越多或者越严重。
- WHS(Worst Hold Slack):最差的保持时间裕量。负数代表保持时间违例。
- THS(Total 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是200MHz,clk_out1是50MHz:
create_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_pcb(PCB走线延迟)+ T_setup_fpga(FPGA的建立时间要求,通常取负值)
- min = T_co_external(最小延迟)+ T_pcb_pcb − T_hold_fpga(FPGA的保持时间要求)
实际工程中,PCB走线延迟可以用一个估算值代替(通常几百皮秒),T_co和T_ho数值在外部芯片手册里都有。假设某个ADC手册给出tco_max=3.5ns、tco_min=1.2ns,PCB走线延迟约0.5ns,FPGA的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、换板子,都可能需要重新审视约束。但掌握了方法之后,它就不再是玄学,而是一套有迹可循的工程流程。希望这篇东西能帮你少走点我当年走过的弯路。