
做FPGA开发的人几乎都经历过这样一幕功能仿真波形漂亮得很上板之后却出各种灵异现象——偶尔跑飞、偶发数据错误、温度一高就歇菜。大部分人第一反应是查逻辑翻来覆去查不出名堂少部分人开始怀疑时序问题可看着Vivado里满屏的时序报告又不知道从哪一行读起。我写这份《Vivado静态时序分析学习笔记》就是想把静态时序分析这套东西讲成“人话”。它不玄乎本质是一道算术题综合和布局布线完成后工具会把设计里所有寄存器和IO路径拉出来计算每一条路径上的信号传播时间然后和时钟周期做对比看谁快谁慢。所谓时序收敛就是让所有路径上的“信号到达时间”都满足寄存器的“建立/保持时间要求”。如果你是刚装了Vivado还不知道怎么下手的新人或者跑过综合实现、见过红色WNS但完全不知道怎么修的工程师这篇笔记适合你。第1篇先讲清楚基本概念、报告阅读和约束入门后面再逐步深入。1. 静态时序分析在做什么从功能仿真过了但上板异常说起1.1 一个真实的“功能对、时序错”案例前阵子我调一个基于FFT核的信号处理链路Active Simulation跑了几万拍数据输出完全正确信心满满地生成比特流下板。结果用ILA一抓数据发现每隔一段时间就会错一个点而且错的位置不固定。我当时第一反应是FIFO读写逻辑有bug花了整整一下午检查代码逻辑上确实挑不出毛病。后来实在没辙打开实现后的时序报告一看WNS是负的有一条跨了三级寄存器的路径组合逻辑延迟超出了时钟周期的余量。也就是说功能上这个电路“应该”是对的但物理上——信号从A寄存器传到B寄存器时到达得太晚B寄存器根本来不及稳定采样。这种问题在仿真里永远看不见因为仿真模型默认组合逻辑零延迟或者延迟足够小。这是静态时序分析最典型的应用场景用数学手段穷举检查所有路径的延时而不依赖测试激励。它和功能仿真互补功能仿真管“逻辑对不对”STA管“实际芯片上到底能跑多快”。1.2 STA检查的四大路径类型看完上面的例子我们再从更系统的角度看一下STA到底在分析什么。Vivado把设计看成一张路径网络从起点到终点把所有路径分成四类寄存器到寄存器路径从一个触发器FF的时钟端出发经过组合逻辑到下一个触发器的数据端。这是FPGA设计里数量最多、也最常出问题的一类。输入引脚到寄存器路径从芯片外部输入引脚进入经过输入缓冲和组合逻辑到内部寄存器数据端。寄存器到输出引脚路径从内部寄存器时钟端出发经过组合逻辑到输出引脚。输入引脚到输出引脚路径纯组合逻辑直通不经过任何寄存器。七成以上的时序报告问题都集中在第一类路径上也就是寄存器之间的逻辑深度太深。但IO路径的约束如果不写上板同样会出问题这一点后面第4章会细讲。1.3 静态分析为什么“静态”很多人会问为什么不给整个设计加激励做时序仿真不是更直接吗问题在于一个稍有规模的工程状态空间是天文数字靠仿真不可能覆盖所有时钟沿组合。而STA不同它不需要激励直接把每条路径的延迟提取出来按最坏情况做数学计算速度极快覆盖率却是“所有寄存器之间”的完整网络。而且STA计算出的结果是可复现的给定同一个网表和同一套约束报告数值是确定的。这是它作为签核Signoff工具的核心价值。2. 建立时间和保持时间时序收敛必须搞懂的底层模型2.1 一个“拍照”的比喻建立时间和保持时间这两个概念用拍照来类比最容易懂。假设你给朋友拍合影朋友需要在快门落下之前摆好姿势这个“提前摆好姿势的最短时间”就是建立时间Setup Time。快门落下之后朋友也不能马上动得保持一瞬间这个“按下快门后必须保持的最短时间”就是保持时间Hold Time。对应到D触发器上数据信号必须在时钟有效沿到来前的建立时间内稳定也必须在时钟有效沿之后的保持时间内继续稳定。如果不满足寄存器可能采到不确定的状态也就是所谓的亚稳态Metastability数据输出可能振荡、可能延迟一拍稳定后级逻辑全乱。2.2 从“数据到达时间”到“Slack”的计算理解了建立/保持时间的物理含义我们就能推出一条路径是否满足时序要求。Vivado内部计算的核心是两条时间线数据到达时间Data Arrival Time和数据需求时间Data Required Time两者之差就是裕量Slack。我常用的简化公式是这样的数据到达时间 源时钟沿到达时间 Tcko Tlogic Tnet 数据需求时间 捕获时钟沿到达时间 时钟周期 - Tsetup - Uncertainty Slack 数据需求时间 - 数据到达时间其中Tcko是寄存器从时钟来到数据输出的固有延迟Clock-to-OutputTlogic是组合逻辑的传播延迟Tnet是布线延迟Uncertainty是时钟抖动和偏斜折算出来的余量。我举个例子你就明白了。假设一个设计跑100MHz时钟周期是10ns。某个寄存器的Tcko是2ns中间组合逻辑3ns布线延迟2ns目的寄存器的Tsetup是0.5nsUncertainty设为0.1ns。那么数据到达时间 2 3 2 7ns 数据需求时间 10 - 0.5 - 0.1 9.4ns Slack 9.4 - 7 2.4nsSlack为正这条路径稳稳收敛。如果组合逻辑做到6ns到达时间变成26210ns需求时间还是9.4nsSlack就是-0.6ns这条路径就违例了报告里会显眼地标红。提示上面只是最简化的模型。真实工程里还有Clock Skew对建立时间和保持时间的相反影响有OCV片上工艺偏差导致的延迟缩放有跨die路径的额外延迟等。但第1篇只要把这个基础模型吃透后续这些只是在这个模型上做修正。2.3 时钟偏斜、抖动和Uncertainty时序计算里的“保险系数”很多朋友第一次接触Uncertainty时很困惑为什么我约束了时钟周期工具还要在我算好的余量里再扣掉一段这就要说到时钟信号本身的“不完美”。时钟偏斜Clock Skew是指同一个时钟沿到达不同寄存器的时刻有差异。比如BUFG网络很长或者某个寄存器处于偏远的SLR时钟到达它会晚一点。对建立时间来说偏斜会让捕获沿晚到反而有正收益但如果是时钟到达源寄存器更晚则会对建立时间不利。对保持时间来说捕获沿越晚到越危险危害更大。时钟抖动Clock Jitter则是时钟自身频率或相位的随机波动常见于PLL、MMCM输出时钟也可能来自板级电源噪声。抖动是随机的没法靠调整逻辑消除只能把它作为设计余量预留出来。Vivado会在约束文件或时序报告中给出一个Uncertainty值它是时钟抖动、偏斜余量和工具自身的悲观性估计的综合体现。我见过一些新手为了把WNS修正直接把uncertainty设成0这是典型的掩耳盗铃——报告倒是绿了板子一跑照样出错。Uncertainty不是可以随意砍掉的“优化空间”它是对这个时钟域真实物理质量的评估。默认值通常已经经过工具验证真要改必须有充分的硬件测量依据。3. Vivado时序报告正确打开方式综合后和实现后怎么看、看哪里3.1 先分清“综合后”与“实现后”两套报告Vivado里有两处能打开时序报告综合后的Open Synthesized Design和实现后的Open Implemented Design。很多新手混淆这两者把综合后的WNS当成最终结果我踩过这个坑在这里重点讲清楚。综合后阶段工具只有逻辑网表和单元库的延迟模型没有实际的布局布线信息所以布线延迟是用估算模型算出来的。这个阶段的时序报告约等于“天气预报”趋势有参考价值但不保证精确。如果综合后WNS只有0.05ns到实现阶段十有八九会变负因为布局布线会插入真实的连线延迟。实现后阶段所有逻辑单元已经落在Slice上、所有布线已经分配好工具可以提取真实的RC延迟来算时序。这才是真正意义上的签核报告。凡是跟你聊“时序有没有收敛”一律以实现后的报告为准。我个人的操作习惯是综合完成后先快速看一眼WNS如果负得特别离谱比如-5ns以上先回代码修逻辑如果只是小幅为负或者刚刚够正直接做实现然后看实现后的具体路径再决定怎么优化。3.2 Report Timing Summary关键字段逐行拆解打开实现后的设计运行Report Timing Summary你会看到一张汇总表。我把几个最关键的行挑出来说字段全称含义判断标准WNSWorst Negative Slack所有路径中最差的建立时间裕量必须≥0越小越危险TNSTotal Negative Slack所有违例路径的负裕量之和越接近0越好反映违例“总量”WHSWorst Hold Slack最差的保持时间裕量必须≥0实际设计中出现负的很少THSTotal Hold Slack所有保持违例路径的负裕量之和越接近0越好TPWSTotal Pulse Width Slack时钟脉冲宽度违例情况必须≥0和时钟质量强相关下面还有一张大表按路径分组列出各种违例路径的具体数值。每一行你会看到Slack、Levels、Routes、High Fanout这些列。Levels代表这条路径上经过的LUT级数Routes代表经过多少个布线节点High Fanout指的是这条路径驱动了多少个负载——这几列的数值直接决定了修复手段的方向Levels太高就要砍逻辑深度High Fanout太大就得做寄存器复制或者加BUFG。3.3 用report_timing追一条关键路径的完整链路汇总报告只能告诉你“哪里有问题”想搞清楚“为什么有问题”必须深入到单条路径。我常用的命令是report_timing_summary -setup -hold -path_type summary report_timing -from [get_cells inst_a/reg_q] -to [get_cells inst_b/reg_d] -setup -max_paths 5 -path_type full第一条命令生成整体概览第二条命令精确追踪某两个寄存器之间的详细路径。注意这些命令要在实现后的工程里运行Open Implemented Design否则提取不到真实布线延迟。打开详细报告后重点看这几段Data Path起点到终点的总延迟里面分Logic DelayLUT等逻辑单元延迟和Net Delay布线延迟。如果Logic Delay占大头说明组合逻辑太深如果Net Delay占大头说明布线跨了很远的距离可能和布局有关。Clock Path报告会告诉你源寄存器和目的寄存器的时钟到达时间差也就是实际Skew。Destination Clock Delay捕获沿相对于源时钟沿的实际延迟。我一般会先看Destination Clock Delay和源寄存器的Clock Delay差值如果异常偏大优先检查时钟约束如果时钟关系正常再看Data Path里到底卡在哪个LUT或哪个走线段。注意报告里经常会看到“Slack (VIOLATED)”和“Slack (MET)”两种状态不要只看最后的绿色红色要养成看具体数值的习惯。有时候WNS是正的但某条路径裕量只有0.02ns这在过温、电压波动时很容易实际失败同样需要关注。4. 约束是时序分析的地基时钟约束与I/O约束实操写法没有约束的时序分析是失去意义的。我刚学Vivado时建了个工程直接跑实现看到满屏红色违例吓得不行后来才发现是因为没写任何XDC约束工具根本不知道我的时钟周期是多少只能按最保守的默认假设来做。从那以后我每次建工程第一件事就是打开XDC文件把主时钟约束写好。4.1 主时钟约束create_clock的正确姿势主时钟通常来自芯片外部比如板级晶振、ADC采样时钟、以太网PHY提供的时钟。约束主时钟的标准写法是create_clock -period 10.000 -name sys_clk [get_ports clk]这里-period的单位是纳秒10ns对应100MHz。-name是你给这条时钟起的逻辑名字方便后续引用。get_ports clk指的是物理端口名注意一定用get_ports而不是get_pins因为主时钟必须定义在端口上。很多人会遇到“为什么clk没有引脚可选”的问题多半是因为管脚名字写错了或者端口没有在工程里映射到板级管脚。先去XDC里确认set_property PACKAGE_PIN的管脚约束写没写再回到create_clock检查名字是否和端口名严格一致Vivado的引脚名是区分大小写的。如果是差分时钟比如从板子的_P和_N差分对进来不要只约束其中一个要两条线都约束create_clock -period 10.000 -name sys_clk [get_ports {clk_p}]然后配合set_property对_N引脚设置差分约束这一部分我在后续笔记里会专门展开。第1篇先把单端时钟写好。还要注意不要在一个物理端口上重复创建多条主时钟也不要在经过BUFG之后再create_clock那样等于把同一个时钟拆成多个独立的时钟域报告中会出现大量莫名其妙的跨时钟域路径。4.2 生成时钟约束create_generated_clock的应用场景主时钟约束好了工具会自动追踪它经过MMCM/PLL等时钟资源后产生的输出时钟这一环基本不需要手动干预。但有一种情况必须手动约束用寄存器做分频。比如你用触发器把100MHz主时钟分频成50MHz生成一个名叫clk_div2的信号。这个时钟是逻辑产生的工具不会自动推断它的周期和相位你必须显式告诉它create_generated_clock -name clk_div2 -source [get_pins u_div/clk] -divide_by 2 [get_pins u_div/clk_out]这里-source指的是触发这个分频的主时钟来源-divide_by指定分频倍数。很多新手漏了这一句导致div时钟驱动的路径全部默认落在未知时钟域里报告一片红。我建议能用MMCM/PLL的地方尽量用专用时钟资源不要用寄存器分频。因为专用时钟资源的抖动和偏斜控制是经过工艺验证的寄存器分频除了时序不好控还容易产生毛刺。4.3 input/output delay外部芯片的时序怎么算热词榜里很多人搜“vivado如何设置管脚input/output delay”说明这是大家普遍卡住的地方。确实时钟约束好理解但IO约束需要你了解外部芯片的时序参数。set_input_delay描述的是外部器件输出的数据相对于某个参考时钟到达FPGA引脚时已经经过了多少延迟。举个例子有个ADC芯片输出数据时钟是25MHz周期40ns芯片手册给出Tco数据输出相对采样时钟沿的延迟最大12ns板级走线约1ns那么输入延迟就是13nsset_input_delay -clock clk_adc -max 13.000 [get_ports adc_data[*]] set_input_delay -clock clk_adc -min 5.000 [get_ports adc_data[*]]-max值用于检查setup-min值用于检查hold。min怎么来看ADC手册里Tco的最小值比如2ns加上板级走线1ns就是3ns一般会留一点余量取5ns。这一对值必须同时写只写max不写minhold检查就是不完整的。输出延迟与之对称描述的是FPGA输出数据到下游器件时下游器件对它的建立/保持时间要求set_output_delay -clock clk_dac -max 10.000 [get_ports dac_data[*]] set_output_delay -clock clk_dac -min 2.000 [get_ports dac_data[*]]这里我做减法说明一下如果下游DAC要求数据在时钟沿前至少8ns稳定Tsetup板级走线1ns再留1ns余量那么output delay的max可以取10ns如果DAC要求时钟沿后至少1ns保持走线1nsmin取2ns。计算思路就是“外部器件的需求”换算成FPGA引脚上的时间要求。IO时序是“上板后偶发错误”的重灾区。很多设计内部时序全部收得很漂亮但数据在跨芯片边界时没有满足建立/保持时间结果数据采样不稳定表现就是偶发错误、时好时坏。5. 典型时序违例的定位链路从WNS为负到修出正数5.1 先判“约束问题”还是“代码问题”时序违规的原因大致可以分两类约束写错了或者代码本身逻辑太深。修复手段完全不同所以第一步不是急着改代码而是先做一个判断。我的排查链路一般是这样的看关键路径的起点和终点分别在哪个时钟域。如果两端时钟不是同一个检查是否忘了添加跨时钟域约束。看Report Clock Interaction检查时钟间是否意外出现“被约束成同步关系但实际是异步”的路径。看关键路径上的Logic Delay和Net Delay分布判断卡在逻辑还是卡在布线。如果一切路径关系都正常再回到RTL找那条路径上的组合逻辑链。排查完约束再看代码顺序不能反。我见过有人花了一整晚把一个原本没问题的模块加了一堆流水线结果过了两天发现只是XDC里漏了一条set_clock_groups——白忙一场。5.2 setup违例组合逻辑过长与高扇出的排查与修复setup违例的本质是数据到达得太慢。它对应三种最常见的物理原因组合逻辑层级太深。一条路径上如果串联了五六级LUT每一级的Cell Delay加上两级LUT之间的Net Delay10ns的周期很容易被吃掉。这时候修复手段是插入流水线寄存器把大组合逻辑切成两拍甚至三拍。代价是数据延迟几个周期如果协议允许这是最直接的办法。高扇出导致布线拥挤。信号驱动上百个寄存器时工具会增加大量缓冲器Net Delay飙升。处理方式有两种一是RTL里手动复制寄存器把一个大扇出分成几组二是给寄存器的FDCE添加MAX_FANOUT属性让工具在综合阶段自动复制。我实际用下来手动复制更可控尤其对关键的复位信号和使能信号。布局跨远了。如果关键路径的起点和终点隔了大半个芯片Net Delay会非常难看。这种情况先试Vivado的实现策略改成Performance_ExtraTimingOpt让布局器优先考虑时序再不行就考虑把相关联的逻辑用Pblock约束到同一区域。如果是IO路径上的setup违例还要回头检查set_input_delay/-max是不是偏大时钟周期是否被低估。不要把IO时序问题硬塞到内部逻辑去修那是修错方向。5.3 hold违例出现概率小但出现就麻烦FPGA器件内部寄存器的hold违例比较少见因为现代工具的布局布线算法会在实现阶段自动插入延迟缓冲器来修复hold。如果实现后的报告里Hold Slack为负首先要确认是不是IO路径上的hold问题。IO路径上的hold违例通常来自set_input_delay的-min值设置得太小或为负值导致工具认为数据“太早有效”hold窗口被压缩工具又没有余量去调整。处理方式是重新核对外部芯片手册的Tco最小值、板级走线延迟把-min设置到一个合理偏保守的值。时钟偏斜严重时也可能制造局部hold问题目标寄存器捕获沿到达过晚导致源数据在保持窗口内变化。这种情况除了检查时钟树结构还可以尝试给该路径设置set_multicycle_path或者优化placement。提示网上有不少人直接用set_false_path去屏蔽hold违例路径我非常不建议这么干。set_false_path的含义是“这条路径完全不用分析”固然能消除report里的红色但它同时也覆盖了所有时序检查如果路径上真有功能数据上板迟早会出问题。只有对真正不参与功能的测试逻辑、异步复位信号释放等场景才适合用它。跨时钟域CDC路径也常被误当做违例来修。假如两个异步时钟域之间的数据是通过两级同步器或者异步FIFO处理的这些路径双方不存在固定相位关系时序分析本来就不该把它们计算成同步路径。正确做法是用set_clock_groups声明set_clock_groups -asynchronous -group clk_a -group clk_b把异步时钟域分组后工具就不会在这两组时钟之间做时序分析。如果你用ILA调试模块观察信号ILA本身的采样时钟也是独立时钟域建议同样加进异步分组免得它干扰主设计时序。5.4 不小心踩过的“修时序”标记set_multicycle_path有朋友问过组合逻辑很深但数据其实不是每个时钟周期都在更新能不能通过set_multicycle_path放宽当然可以但前提是协议允许。假设你的数据每两个周期更新一次那么可以这样约束set_multicycle_path 2 -setup -from [get_cells inst_a/reg_q] -to [get_cells inst_b/reg_d]注意设了setup为2个周期后如果保持约束不跟着加hold检查落在setup同沿结果往往反而更差。一般还要配套set_multicycle_path 1 -hold -from [get_cells inst_a/reg_q] -to [get_cells inst_b/reg_d]这里1代表相对默认hold沿提前一个周期。这条配套命令务必记住我见过很多人在用multicycle时只写setup不写hold结果越修越差。6. 这份学习笔记的踩坑记录和后续路线6.1 我踩过的三个坑回头看我学静态时序分析的过程有几个坑特别典型写出来给同样在路上的朋友避一避。第一个坑没写约束就去看报告。刚开始我用一个简单的计数器工程辛辛苦苦跑完实现看到WNS-3.5ns直接慌了以为代码写错了。后来才知道没有时钟约束时工具用的是极保守的默认时钟模型几乎所有路径都会违例。正确顺序永远是先写约束再跑实现最后分析报告。第二个坑把综合后的WNS当最终结论。有一回综合后WNS是0.08ns我以为稳了直接生成比特流结果上板跑高速模式各种错。后来重新看了实现报告WNS变成-0.6ns。从此我给自己定了条规矩综合后看到WNS小于0.1ns先别急着往下走去优化代码或约束不然实现后大概率翻车。第三个坑误把异步FIFO的跨时钟域路径当违例修。当时一个多时钟域工程里有大量CLB路径显示红色我一口气加了好几个流水线结果功能全乱。后来才意识到那些路径本来就是两个异步时钟域之间的数据交换应该用异步FIFO或者set_clock_groups声明而不是试图去压缩它们的延迟。了解电路结构中哪些是真实同步路径、哪些是异步路径比闷头修数据重要得多。顺便说一句和调试相关的经验用ILA抓信号的时候ILA的采样时钟要和被观察数据的时钟是同源的否则抓到的数据天然就带有跨时钟域的不确定性。并且ILA采样频率不是越高越好频率越高需要更深的采样深度来覆盖同样的时间窗口而ILA本身也有时序开销采样时钟跑得太快可能导致ILA核自己在实现阶段就吃不少资源。调试之前先想清楚采样频率和深度能省不少事。6.2 接下来往哪走这篇笔记只覆盖了静态时序分析最核心的框架。下一篇我打算把多时钟域和set_clock_groups的完整用法、以及MMCM/PLL生成时钟的约束与报告重点讲透再往后可以写SDC文件管理和工程级约束管理的实战包括如何用目标约束文件、如何在工程演进过程中保持时序稳定。想深入研究的朋友建议直接啃Xilinx的官方文档UG903Vivado中使用约束、UG949Vivado方法学。我自己的经验是文档里关于SUA和时序引擎的细节配合一两个真实工程反复看比刷多少教程都管用。最后分享一个我的小习惯每次跑完实现先把Report Timing Summary截图存下来标注一下当天的时钟频率、实现策略、WNS数值这样改完代码再跑一次对比两份报告就能快速知道改动的影响是正面的还是负面的。时序分析是门手艺活手熟之后你会发现它远比想象中有迹可循。