ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Vivado设计锁定与增量编译实战:告别三小时全量编译

Vivado设计锁定与增量编译实战:告别三小时全量编译 你有没有遇到过这种情况下班前改了一行参数觉得是个微调结果Vivado全量编译跑了三个半小时第二天早上过来看不仅没跑完时序还崩了。我在一个UltraScale的基带工程里反复踩过这个坑后来靠设计锁定和增量编译把单次迭代时间缩了四成以上。这篇文章就把这两块内容拆开讲清楚附带一套可以直接改名的Tcl脚本工程给同样被长编译周期折磨的人一个参考。我默认你已经会用Vivado建工程、跑综合布线但对设计锁定和增量编译只有概念没有实操。我们不讲学院派理论只聊哪些手段真实有效、哪些是摆设、怎么搭配使用才不会让工具越锁越乱。1. 一个下班前的“小改动”全量重跑三小时——问题的根源1.1 一次典型的小改动耗时曲线先说个具体场景。我有一次改了某个滤波器模块的系数位宽从16bit改成20bit顺手在顶层例化处加了一个延时寄存器。这个改动放在代码层面确实不大但Vivado的编译路径不这么想——综合阶段重新map整个网表布局布线阶段把几十万寄存器、DSP、BRAM全部重新考虑一遍逻辑优化和物理优化都要重来。最终耗时综合43分钟布局31分钟布线52分钟物理优化加了20分钟加上各种报告生成整个流水线三个多小时就过去了。这种痛在中小工程里不明显一旦工程规模到了百万门级、器件利用率超过60%全量编译的耗时就不是线性的而是指数级恶化。关键是你只是改了5%的逻辑剩下95%的逻辑位置并没有变化。Vivado非要全部重跑本质上是在浪费算力。1.2 布局漂移才是压死时序的最后一根稻草如果只是时间浪费还能忍真正让人血压飙升的是时序不确定性。同一个RTL跑第一版全量编译WNS是0.05ns通过你改了一行代码再跑WNS变成-0.12ns失败。但你知道吗失败路径往往不在你改动的那部分逻辑上而在几百毫米外一块完全没有动过的模块里。原因就是布局漂移。综合器发现顶层层次里有一个很小的改动可能把某个FF重新定级然后布局器为了满足新的逻辑连接把相邻大片区域重新排布。一个单元的移动引发链式反应连锁导致原本收敛的关键路径走线变长。这种事多来几次你连“改哪里会出事”的规律都总结不出来。这个时候你就明白两件事第一把稳定的逻辑物理位置固定下来第二让工具尽量基于上一次的合法结果做局部调整而不是每次推倒重来。前者是设计锁定后者是增量编译。2. 设计锁定哪几层需要锁分别怎么锁2.1 综合级锁定DONT_TOUCH 和 KEEP_HIERARCHY设计锁定不是只有一个开关而是从综合到布局布线分了好几层。综合层最常用的就是DONT_TOUCH和KEEP_HIERARCHY。先看DONT_TOUCH的含义告诉综合器和实现器这个cell不允许被删掉、吸收、复制、合并、重定时。例如set_property DONT_TOUCH true [get_cells u_dsp_fir0]这个属性的特点是贯穿全程。综合阶段它保护网表结构实现阶段它也保护原语不被合并或重优化。注意它有综合级和实现级两套控制如果只在综合前声明实现时可能被忽略。稳妥做法是在XDC里同时声明或者用下面的形式set_property DONT_TOUCH {true} [get_cells u_dsp_fir0]KEEP_HIERARCHY则是保护层次不被展平。正常情况下Vivado综合会做大面积的层次展平优化把跨模块的信号合并、把中间寄存器吸收掉。这么做时序是友好了但也意味着你在实现阶段几乎无法“指认”某个模块的边界。一旦你在代码里设置了KEEP_HIERARCHY true综合器会保留这个子模块的层次边界网表里能看到完整的模块实体。set_property KEEP_HIERARCHY true [get_cells u_rx_chain]不过KEEP_HIERARCHY不是没有代价它会阻碍跨层优化。比如两个子模块各自例化了一个DSP48本来综合器可以把它们合并成带共享逻辑的高效结构但层次锁住以后就只能各用各的。所以我的习惯是只给需要独立交付或独立时序约束的模块加不要全工程打满。2.2 模块级OOC把别人交付的代码变成黑盒子说完属性再说工程层面的锁定方式OOCOut-of-Context综合。它和前面的属性锁定不是一个维度OOC是让某个模块单独在脱离顶层的情况下完成综合生成独立的.dcp网表文件然后在顶层综合时把它当黑盒子调用。OOC最大的好处有三个第一模块独立综合不依赖顶层约束IP集成商交付时不需要暴露RTL源码第二模块层次绝对稳定因为顶层综合时根本看不到模块内部逻辑第三缩短编译时间因为模块只综合一次后续顶层综合消耗更小。在Vivado里设置OOC有两种路径。如果某个模块已经被识别为IP或者有独立输出文件可以在Flow Navigator里的IP Sources右键它的.xci文件选择Set OOC Mode工具会自动为它生成独立的综合job。如果是自己写的RTL模块需要用脚本方式synth_design -top u_dsp_fir -part xcvu9p-flga2104-2L-i -mode out_of_context注意OOC模块必须有独立约束文件且约束里不能包含顶层物理管脚约束否则会报错。同时你要确保顶层的dont_touch不会和OOC的边界产生冲突。2.3 实现级锁定Pblock区域约束与管脚锁定综合级锁定能保住逻辑结构但管不住物理位置。真正让模块待在“原地”的是Pblock区域约束。Pblock说白了就是给某个模块画一个矩形区域布局器只能在这个区域内摆放它的逻辑单元。create_pblock pblock_rx_chain add_cells_to_pblock pblock_rx_chain [get_cells u_rx_chain] resize_pblock pblock_rx_chain -add {SLICE_X10Y10:SLICE_X30Y40}注意Pblock不是越大越好也不是越小越好。我一般在估算模块占用的SLICE、DSP、BRAM之后给出比估计值富裕15%-20%的面积。太小了布局器塞不下直接报placement error太大了等于没锁布局器有太多自由度依然会产生漂移。更多时候Pblock只锁区域不锁具体坐标。这样工具在小范围内还有优化空间。如果你想把布局结果彻底固定就得配合下面要说的增量编译让工具直接参考上一轮布局的坐标。管脚锁定属于最基础的物理锁定平时大家改XDC里的PACKAGE_PIN和IOSTANDARD已经很熟了这里不再展开。我想强调一点管脚锁定不仅是约束顶层端口在哪里它还间接决定了顶层互联逻辑的物理分布。所以IO约束尽量在项目早期一次性定死后期不要频繁改否则增量编译的收益会大打折扣。3. 增量编译把上次的布局布线结果变成“参考文献”3.1 增量编译的底层逻辑参考DCP不是简单的复制粘贴增量编译在Vivado里分两个阶段增量综合和增量实现。增量综合是通过合成时的参考DCP尽量复用上一轮已经综合好的子模块避免相同的RTL被再次综合。增量实现则是通过上一轮布线完成的DCP指导place_design和route_design优先保持原先的单元位置和走线。很多新手以为是“只编译改动部分”其实不是。在实现阶段增量编译仍然会跑完整的place_design——它把参考DCP中那些未改动单元的合法位置读出来先固定住再对改动引起的额外逻辑做填充。真正全程只改局部的是增量综合的模块级复用但效果也依赖模块边界匹配。换个生活化的比方全量布局像把所有家具全部搬出去重新摆放增量布局像你已经知道沙发、电视、冰箱的位置没变只把新买的书柜塞进空出来的墙角。书柜如果太大塞不下那就得挪旁边的东西了——对应到工具里就是某些cell找不到合法位置退回到全量重排。3.2 GUI配置步骤与Tcl等价命令在GUI里设置增量编译是在Settings的三个地方综合部分打开Settings - Synthesis - Incremental Synthesis勾选Use Incremental Synthesis然后在Reference Checkpoint里指定上一轮综合生成的.dcp文件。这步的作用是让综合器对比当前RTL和参考网表找出没变的模块直接沿用它们的综合结果。实现部分打开Settings - Implementation - Incremental Implementation勾选Use Incremental Implementation指定上一轮布线完成后的.dcp文件。这个文件就是完整的实现结果里面包含了布局坐标和布线信息。如果你习惯用Tcl脚本走批处理等价命令是这样# 基线全量综合 synth_design -top top -part xcvu9p-flga2104-2L-i write_checkpoint ./checkpoints/synth/baseline_synth.dcp # 增量综合 read_checkpoint -incremental ./checkpoints/synth/baseline_synth.dcp synth_design -top top -part xcvu9p-flga2104-2L-i -incremental ./checkpoints/synth/baseline_synth.dcp write_checkpoint ./checkpoints/synth/incr_synth.dcp # 增量实现 read_checkpoint -incremental ./checkpoints/impl/baseline_route.dcp place_design phys_opt_design route_design write_checkpoint ./checkpoints/impl/incr_route.dcp要特别强调的是实现阶段的参考DCP一定是上一轮write_checkpoint保存下来的网表DCP而不是综合DCP。如果你把综合DCP给到实现阶段用place会不知道原布局在哪里增量失去意义。3.3 哪些改动会破坏增量效果增量编译不是万能灵药。工具会把当前设计与参考DCP做diff如果差异太大它会决定不再参考退化成全量编译。常见的破坏性改动包括顶层端口变化、模块例化结构变化、大面积逻辑重写、宏定义参数大规模调整、器件型号变化。这里有个实际经验如果一次改动牵涉了某个被Pblock锁定的模块内部逻辑且面积变化超过阈值工具会放弃参考该区域的布局。但其它未改动模块仍然能沿用参考所以整体时间还是比全量省。真正全量退化的情况我遇到最多的是顶层端口数量和名字变化因为这会牵连顶层网表的每一个跨模块连接。4. 锁定与增量的组合效果以一个UltraScale工程为例4.1 工程基线数据与编译时间对比我以手头一个中等规模的通信基带工程为参考器件是UltraScale VU9P逻辑利用率大概63%DSP用了420多个BRAM利用率41%代码里有一个PCIE硬核、一个DDR4控制器、三个基带算法模块。整体不大不小但足够暴露出全量编译的痛苦。我把这个工程跑了两轮第一轮从干净目录全量综合实现第二轮只修改一个滤波器模块的位宽开启增量编译。耗时对比如下阶段全量编译增量编译节省比例综合42 min23 min45%布局31 min18 min42%物理优化20 min13 min35%布线49 min31 min37%总时间142 min85 min40%注意增量实现阶段依然会跑phys_opt和route只是它们被参考布局约束住搜索空间小了所以单阶段时间也有下降。对于那种几小时起步的大型工程节省更明显。4.2 时序与资源的一致性校验增量编译除了省时间还有一项隐性收益时序更稳定。因为大部分关键路径根本没挪窝上一轮满足的时序约束大概率继续满足。我这个工程的实测数据是基线WNS为-0.02ns经过局部微调后勉强收敛增量版本WNS为-0.05ns略有下降但没有出现上一个版本通过、下一个版本崩掉的离谱反转。不过“基本稳定”不等于“完全一致”。我对比了两版实现报告发现资源使用上DSP数量完全一致BRAM一致FF差了不到0.3%LUT差异在0.5%以内。关键路径里有2条不在改动范围内的路径因为布线微调发生了PPM级别的时延变化但不影响整体收敛。所以使用增量编译后回归验证不能省。功能仿真必须跑时序报告必须重新看硬件测试也要覆盖改动涉及的链路。增量只是提升了迭代效率不是免死金牌。4.3 增量模式下观察到的意外行为调试过程中有个现象值得记录增量布局会对参考DCP里的非法状态特别敏感。有一轮我用了错误的参考DCP——当时误把已经改过引脚约束的旧实现DCP当成了参考导致place阶段出现大量违规的PPLOC冲突工具虽然自动退回全量但WNS反而比上一版差了很多。另外phys_opt_design在增量模式下会倾向于保持参考网表的寄存器位置所以它会把优化重心放在新插入的寄存器上。这导致某些模块的时序收益不如全量物理优化那么明显。如果你发现改动模块的时序怎么优化都差一点可以考虑把该模块的区域约束稍微放大给phys_opt更多活动空间。5. 增量失效识别与常见坑位清单5.1 增量失效的五种日志特征增量编译有没有真正生效不能只看“勾选了增量模式”要看日志。我总结了几条足够识别的日志特征综合阶段如果日志里出现Incremental synthesis was performed之类的确认信息说明综合复用了参考网表如果出现Full synthesis is required说明增量模式被强制关闭。布局阶段观察日志中是否有Placed X cells using reference placement这个数字越大说明保留的原布局越多。布线阶段出现Routing from reference checkpoint相关描述说明布线沿用了参考路径。如果出现Reference checkpoint is not compatible或Mismatch detected说明参考DCP与当前设计不匹配工具已经决定退化为全量。如果日志里有大量unplaced错误但最后又pass可能是Pblock锁定的单元溢出到区域外工具自动扩展了区域。5.2 需要重点提防的实践坑位抛开工具自身的bug增量编译失败大概率是人为问题。下面这些坑我基本都踩过参考DCP路径写错或文件被移动。工程目录迁移后绝对路径失效工具找不到参考文件会在日志里发warning然后静默进入全量编译。人如果不看日志根本不知道增量已经失效。综合阶段和实现阶段的Vivado版本不一致。团队里有人用了2024.1有人用了2023.1两个版本的DCP格式和属性解析会有差异增量参考会报incompatible。DONT_TOUCH被过度使用。如果一整片逻辑的每个单元都加了DONT_TOUCH布局器的活动空间被严重压缩增量模式下很容易出现拥塞反而让时序恶化。OOC模块改了端口名但顶层调用处没同步干净。这会导致综合器认为OOC模块接口变化无法复用原来生成的DCP于是重新综合OOC模块。Pblock给得太小。锁定的模块在更新代码后逻辑变多了原来的区域塞不下布局器要么报错要么强行扩展导致其它模块被挤出原有位置增量参考大面积失效。只在综合阶段设置了增量实现阶段忘了指定参考DCP。结果就是综合快了一点布局布线还是全量重跑总时间没降多少。5.3 团队协作中的基线管理建议如果你是单人开发增量编译的基线管理比较简单每次收敛后把DCP存档下次改动在这个存档上增量。但团队协作就麻烦很多因为两个人同时改RTL谁也不知道自己基于的是哪个基线。我的建议是建立一个固定命名规则的DCP仓库。比如checkpoints/impl/route_YYYYMMDD_HHMM.dcp并在每次实现通过后自动复制到latest_baseline.dcp。跑增量之前先确认当前的latest_baseline.dcp是你期望的版本最好把对应的git commit号写进文件名例如route_20241211_1520_abc1234.dcp。这样即使DCP文件没有纳入版本管理也能回溯到具体代码版本。另外CI流程里跑增量编译要特别注意CI机器如果是干净的workspace需要先去构件缓存里拉取参考DCP。否则参考路径不存在CI会自动变成全量编译时间长了大家会误以为增量编译没有用。我自己见过团队因为这个原因把增量模式关掉的情况实际是参考DCP没从缓存里恢复。6. 附工程一套可以直接改名字用的脚本框架6.1 工程目录规划下面给你一个最小可用的工程框架。我在实际项目里就是按这个结构调整的复制过去把模块名和器件型号改掉就能跑。fpga_prj/ ├── rtl/ │ ├── top.sv │ ├── u_dsp_fir.sv │ ├── u_rx_chain.sv │ └── u_axi_pcie.sv ├── xdc/ │ ├── top.xdc │ └── ooc/ │ └── u_dsp_fir_ooc.xdc ├── scripts/ │ ├── baseline_synth.tcl │ ├── baseline_impl.tcl │ ├── incr_synth.tcl │ └── incr_impl.tcl ├── checkpoints/ │ ├── synth/ │ │ └── latest_synth.dcp │ └── impl/ │ └── latest_route.dcp └── reports/ └── (报告输出目录)6.2 基线综合与实现脚本先跑基线。基线就是全量编译为后续增量提供参考DCP。# scripts/baseline_synth.tcl set part xcvu9p-flga2104-2L-i set top top read_verilog ../rtl/top.sv read_verilog ../rtl/u_dsp_fir.sv read_verilog ../rtl/u_rx_chain.sv read_verilog ../rtl/u_axi_pcie.sv read_xdc ../xdc/top.xdc synth_design -top $top -part $part write_checkpoint ../checkpoints/synth/latest_synth.dcp report_utilization -file ../reports/synth_util.rpt# scripts/baseline_impl.tcl open_checkpoint ../checkpoints/synth/latest_synth.dcp place_design phys_opt_design route_design write_checkpoint ../checkpoints/impl/latest_route.dcp report_timing_summary -file ../reports/impl_timing.rpt report_utilization -file ../reports/impl_util.rpt report_route_status -file ../reports/impl_route.rpt6.3 增量综合与增量实现脚本第二次及以后的迭代用增量脚本。关键是正确指定参考DCP。# scripts/incr_synth.tcl set part xcvu9p-flga2104-2L-i set top top read_verilog ../rtl/top.sv read_verilog ../rtl/u_dsp_fir.sv read_verilog ../rtl/u_rx_chain.sv read_verilog ../rtl/u_axi_pcie.sv read_xdc ../xdc/top.xdc synth_design -top $top -part $part \ -incremental ../checkpoints/synth/latest_synth.dcp write_checkpoint ../checkpoints/synth/incr_synth.dcp# scripts/incr_impl.tcl set part xcvu9p-flga2104-2L-i open_checkpoint ../checkpoints/synth/incr_synth.dcp read_checkpoint -incremental ../checkpoints/impl/latest_route.dcp place_design phys_opt_design route_design write_checkpoint ../checkpoints/impl/incr_route.dcp report_timing_summary -file ../reports/impl_timing_incr.rpt report_utilization -file ../reports/impl_util_incr.rpt report_route_status -file ../reports/impl_route_incr.rpt跑完确认无误再把incr_synth.dcp复制覆盖成latest_synth.dcp、incr_route.dcp复制覆盖成latest_route.dcp。这样下一次迭代的基线就是本轮结果。6.4 使用流程说明整个流程的操作口径第一次跑基线以后改代码后只跑增量。还有一个细节增量脚本里read_checkpoint -incremental必须放在open_checkpoint之后、place_design之前这是很多人的常见顺序错误位置不对会直接跳过增量参考。如果你用的Vivado版本比较老综合阶段的增量参数可能不是-incremental而是需要在GUI里指定-incremental_mode或者通过set_property设置。2019.2之后的版本基本都支持命令行-incremental方式2023.1之后语法更加统一。建议先查一下你所在版本对应的synth_design参数说明再跑脚本。我个人的习惯是每天的迭代都用增量每周五跑一次全量编译作为周基线用全量结果和增量结果对比一次确保增量和全量的漂移在可控范围内。这样既享受了增量编译的效率又不会让布局布线长期偏航。工程框架里我只给了综合和实现脚本没有给launch_runs之类的GUI工程文件。实际开发里如果你偏好纯脚本管理直接用vivado -mode batch -source scripts/incr_synth.tcl跑就行。如果你需要看波形、跑仿真、做功耗分析可以在这个流程基础上继续叠Readback和Xsdb脚本不影响增量逻辑。到这里设计锁定和增量编译的整套打法就算讲完了。最后再分享一个个人经验不要把增量编译当成“免跑全量”的懒惰借口。每过一段时间还是应该主动跑一次全量编译来确认布局漂移没有积累到不可控的状态。我一般是两周一次全量日常全部走增量。这套节奏让我在一个多人协作的通信板卡项目里保持了还算稳定的交付速度工程也从一开始的三小时迭代慢慢稳定到了一小时以内。
返回列表