
做 FPGA 的朋友大概都碰过这种场面工程综合一次四十分钟实现一次六个小时好不容易跑完发现某个状态机的复位写错了改一行 RTL然后眼睁睁看着布局布线从头再来一遍。Vivado 的增量实现就是冲着这类场景来的——它把上一次成功实现的布局布线结果当作参考只对真正改动过的部分重新布局、重新布线没动过的部分直接复用物理信息一次原本要跑大半天的实现能压到半小时以内。我第一次认真把增量实现用在项目上是在一个 250T 级别的器件上做视频处理链路。那时候团队里有个不成文的规矩谁要是改了一行 RTL就得负责盯着屏幕等到后半夜。后来我把参考 checkpoint 的机制摸清楚改完一行代码到出比特流中间只需要等一次综合加一次压缩过的实现整个过程不超过五十分钟。这篇文章就把我踩过的路、用过的命令、以及几个必须提前避开的坑完整讲一遍。不管你用的是 2019.1 之后哪个版本这里面的思路和操作基本都是通用的。1. 增量实现省下来的到底是哪段时间很多人第一次听说增量实现会下意识以为它是只综合改动的模块其实不是。综合阶段的增量是另一套东西增量实现的战场完全在布局布线这一段。搞清楚这一点后面所有操作逻辑就都顺了。1.1 一次全量实现的时间被谁吃掉了一个中大型 FPGA 工程跑一次全量实现时间大致可以拆成几块。opt_design相对便宜通常几分钟place_design是大头之一占用率高的设计可能一两个小时phys_opt_design看策略激进一点的四十分钟到一个半小时route_design往往是最贵的一环布线资源紧张的大设计跑三四个小时是常事。再加上write_bitstream整个链路一晚上很正常。关键在于当你只改动了几百个 LUT 的时候前面这几个阶段的绝大部分计算其实是在重复劳动。未改动的模块它的单元位置、引脚分配、走线通道上一次已经算得明明白白而且已经通过了时序签核。全量重跑等于把这些结论全部丢掉重新推导一遍结果大概率还是一模一样的。增量实现做的事情就是把上一版的布局布线结果读进来建立一张哪些单元和网络是新的、哪些是旧的的对照表旧的直接沿用新的在剩余的可行空间里放进去。所以它省下来的本质上是重复推导未改动部分的那部分时间。1.2 复用的对象是物理信息不是逻辑这一点必须说清楚增量实现复用是 layout 和 routing也就是物理层面的位置和走线而不是逻辑网表。综合还是照常跑网表还是全新的只有在实现阶段工具才会拿新的网表去比对旧的物理数据库。这也意味着一个直接后果如果综合阶段把某个模块的层次结构、单元命名或者信号名改得面目全非工具就认得没那么准了复用率会掉下来。所以我在改 RTL 的时候有个习惯——尽量做局部修改不要顺手重命名信号不要大规模调整模块层次。名字保持稳定复用率通常能高出十几个百分点这不是玄学是工具做名字匹配的必然结果。另一个后果是增量实现并不保证功能等价。它保的是物理布局逻辑该是什么还是什么。如果新逻辑和旧布局在物理上冲突了工具会尽力调整调整不动就会报拥塞或者时序违例。所以增量实现从来不是免检通道跑完之后的时序报告和 DRC 该看还得看。1.3 别把增量实现和 ECO 混为一谈很多人会问那我为什么不直接做 ECO。这两件事的适用面差别很大。ECO 流程是在已经成型的网表上做手工或半自动的局部改动改几个 LUT、调一下某个寄存器的位置、甚至手工连一根线它不重新跑完整的布局布线速度极快但它对改动规模和改动性质有很严格的限制一旦涉及大范围逻辑替换手工 ECO 基本做不下去。增量实现是自动的、面向中小规模 RTL 改动的流程改动可以涉及成百上千个单元工具自己决定怎么安置。我的经验是改几个信号做时序抢救用 ECO改一段状态机、加一个通道、调一下位宽用增量实现。两者的边界大致就在改动是否还能手工描述这条线上。提示增量实现和 ECO 都属于局部改动工具。如果你的改动涉及时钟结构、复位拓扑或者跨时钟域架构就别指望它们了老老实实跑全量。2. 打开增量实现的几个入口和它们各自的适用面Vivado 里触发增量实现不止一种方式GUI 和 Tcl 都能做但底层属性不一样适用场景也不一样。选错了入口你会发现工具压根没走增量。2.1 GUI 的 Set as Reference 是怎么起作用的图形界面的操作路径是这样的打开工程的 Design Runs 窗口在 Window 菜单里能找到或者从 Flow Navigator 底部的设计运行区右键也能进来找到上一次已经跑完的 impl run右键选择 Set as Reference。这时候那个 run 前面会多一个小图标表示它现在是参考。接着再右键同一个 run直接 Launch或者新建一个 impl run 再 Launch。工具会自动把参考 run 产出的 checkpoint 拿来做增量实现你不需要再去配置什么参数。这个操作本质上就是往 impl run 上写了一个属性指向同工程里的另一个 run。所以它有个天然的限制参考必须是同一个工程里的 run而且这个 run 的产物目录不能被删掉、不能被 reset。很多人第一次用增量失败就是因为顺手把旧的 impl run reset 了参考没了工具悄悄退化成全量跑你还以为增量没效果。2.2 REFERENCE_RUN 和 INCREMENTAL_CHECKPOINT 的区别Tcl 方式有两个属性可以设置它们解决的是不同的问题我把差异整理成一张表属性取值参考来源范围典型场景REFERENCE_RUNrun 名称如impl_1仅限当前工程内的 run日常迭代工程结构不变INCREMENTAL_CHECKPOINTcheckpoint 文件路径任意位置的.dcp跨工程复用、换机器、从历史归档恢复REFERENCE_RUN走的是工程内部的对象引用工程一旦移动位置、run 被 reset引用就断了。INCREMENTAL_CHECKPOINT走的是文件路径只要你把*_routed.dcp归档好换一台机器拷过去也能用这也是我在团队里推荐的统一做法——所有参考 checkpoint 都单独归档不依赖工程目录结构。# 用同工程内的 run 作为参考 set_property REFERENCE_RUN impl_1 [get_runs impl_2] # 用外部归档的 checkpoint 作为参考推荐在团队协作中使用 set_property INCREMENTAL_CHECKPOINT \ {D:/archive/video_pipe/v1.3/top_routed.dcp} [get_runs impl_2] # 回过头确认属性写进去了 puts REFERENCE_RUN [get_property REFERENCE_RUN [get_runs impl_2]] puts INCREMENTAL_CHECKPOINT [get_property INCREMENTAL_CHECKPOINT [get_runs impl_2]]需要注意这两个属性不会同时生效。如果你两个都设了工具按它自己的优先级取一个具体行为在不同版本里略有差别所以我一般只设其中一个避免出现我以为用的是 A实际用的是 B这种糊涂账。2.3 参考 checkpoint 必须满足的状态参考文件不是随便一个 dcp 都能用。它必须是已经完成布线的结果也就是 run 目录下的*_routed.dcp。如果你拿一个只做了布局的*_placed.dcp进去工具要么直接忽略增量、要么给一条告警然后退回全流程。另外还有几个硬性条件缺一个都会静默失效器件型号、封装、速度等级必须完全一致part 字符串有一个字符不同都不认顶层模块名一致顶层被改名基本等于换了设计关键约束文件尤其是时钟定义、管脚约束保持兼容时钟名字改了参考价值会大打折扣工程使用的工具版本不能跨得太远跨大版本时工具会给出兼容性提示。我吃过一次亏为了赶进度把一个 2020.2 的 checkpoint 拿到 2022.2 的工程里当参考工具没有报错也确实跑了增量但最终时序比全量差了一大截重新全量跑一遍反而更干净。跨版本复用的收益是不确定的别把它当成保底方案。3. 从日志和报告里判断这次增量到底值不值增量实现跑完之后很多人只关心快没快其实更该关心的是复用质量怎么样和时序有没有被拖累。这两件事都能从日志和报告里读出来。3.1 布线阶段里的复用统计在实现日志里布线阶段附近会出现关于复用情况的统计信息通常会告诉你这次有多少网络是沿用了参考设计里的走线。不同版本的措辞和消息编号不太一样但核心数字是稳定的就是复用比例。一般长这样绝大部分网络标识为相同或未改动只有一小部分是新增的。我读这行信息的习惯是把它当成一个健康度指标。复用比例在九成以上说明改动确实很小增量的收益会很显著掉到七八成说明改动面已经不小了这时要格外关注时序如果只有五六成甚至更低那这次增量基本没占到什么便宜还不如直接全量跑省下来的时间不值得承担 QoR 下降的风险。还有一个细节值得注意新增网络的数量和新增单元的数量不一定成正比。有时候你只加了几百个 LUT但因为这些 LUT 散落在设计的各个角落被影响的网络会成倍增加。这种情况复用率会突然掉下来属于典型的改动看着小、影响面很大。3.2 用 report_incremental_reuse 量化复用情况日志里的数字比较粗想要精确的复用情况用专门的报告命令。打开实现后的设计然后执行# 打开完成实现的 run open_run impl_2 # 输出复用情况报告 report_incremental_reuse -file ./impl_2_reuse.rpt这份报告会按类别列出复用和新增的对象数量常见的有单元、网络、IO 这几类还会给出各自的比例。它的价值在于让你能把感觉变成数字这次改动到底动到了多少东西一目了然。我一般会把这个报告和上一次的报告放在一起对比。如果某次改动的复用率突然明显下降说明我改的地方触及了比较底层的结构这时候我会把改动范围再收敛一下或者直接放弃增量、走全量流程。把这份报告纳入常规流程能省掉很多为什么这次慢了的困惑。3.3 判断增量是否健康的三个经验标准除了复用率我还会看三件事都满足才认为这次增量是健康的第一看时序。把增量的时序报告和参考版本的报告做对比重点看 WNS 和 TNS 有没有明显恶化。恶化了就说明复用旧布局把新逻辑挤到了不利的位置这种情况下要么调整改动、要么重新全量跑。第二看拥塞。日志里如果有关于布线拥塞的告警说明新逻辑被塞进了已经满负荷的区域。拥塞一旦出现后面的时序会连锁反应。第三看 DRC 和比特流生成是否顺利。增量实现之后如果 DRC 冒出新问题或者写比特流时出现严重的时序告警那这次增量的产物就不能直接交付。注意增量实现产出的比特流和全量实现产出的比特流在功能上应该等价但物理实现不同时序余量也不同。上线前一定要重新做一次完整的时序签核别拿旧版本的时序报告糊弄自己。4. 让增量实现稳定复现的工程习惯增量实现最容易出问题的地方不是命令写错而是工程管理混乱。参考文件找不到、约束悄悄变了、器件配置不一致这些都会让它静默失效。下面这套习惯是从几个项目里沉淀下来的照着做基本不会翻车。4.1 checkpoint 的归档和命名我要求团队里所有要作为参考的 checkpoint都从 run 目录里拷出来单独存放并且用带版本和日期的名字例如top_routed_v1_3_20240612.dcp。理由很直接run 目录是易失的一次 reset_run 就能清空而工程目录本身也可能被挪动或者重建。归档的时候还有两个配套动作。一个是把同一次实现对应的约束文件一起存下来方便日后核对另一个是记录当时的工具版本和器件型号写在同目录的一个小文本里。这些信息在排查为什么这次复用率这么低的时候特别有用因为大多数复用异常最后都归结到约束变了或者器件配置不一样这两件事上。4.2 增量前后必须对齐的几项设置有些设置看起来和布局布线无关实际上会直接影响复用的有效性。我把最容易忽略的几项列出来综合策略换了综合策略网表结构、单元命名可能全变复用率会断崖式下跌顶层端口和管脚约束端口增减会改变 IO 布局IO 布局一变内部布局跟着动时钟约束的名字和周期时钟名改了工具认不出对应关系复用效果打折器件温度等级和速度等级这个不一致直接导致参考不可用。改这些东西的时候我的原则是只改一样跑一次增量看复用率变化。如果一次改好几样出了问题根本不知道是哪一项引起的。4.3 一套可以直接抄的 Tcl 流程下面这段脚本是我现在项目里实际在用的简化版从综合到出比特流走一遍参考 checkpoint 从归档目录取。# ---- 1. 重新综合RTL 已改动---- reset_run synth_1 launch_runs synth_1 -jobs 8 wait_on_run synth_1 # ---- 2. 指定增量参考 ---- set_property INCREMENTAL_CHECKPOINT \ {D:/archive/video_pipe/v1_3/top_routed_v1_3_20240612.dcp} \ [get_runs impl_2] # ---- 3. 跑实现并出比特流 ---- launch_runs impl_2 -to_step write_bitstream -jobs 8 wait_on_run impl_2 # ---- 4. 检查 run 状态确认没有 ERROR ---- if {[get_property PROGRESS [get_runs impl_2]] ! 100%} { error impl_2 did not finish: [get_property STATUS [get_runs impl_2]] } # ---- 5. 输出复用报告和时序摘要 ---- open_run impl_2 report_incremental_reuse -file ./reports/impl_2_reuse.rpt report_timing_summary -file ./reports/impl_2_timing.rpt -delay_type min_max这段脚本里有一处容易踩坑launch_runs后面一定要跟wait_on_run否则后面的open_run会在实现还没结束时就执行报出一堆莫名其妙的错误。另外就是要显式检查 run 的 PROGRESS 属性不要只看屏幕上有没有报红字有些错误在批量模式下不会弹到前台。5. 几个必须提前避开的坑增量实现用得越久越会发现真正麻烦的不是流程而是那些看起来没问题、结果很糟糕的场景。下面这四个是我自己踩过或者帮同事排查过的写出来供参考。5.1 插入 ILA 之后复用率虚高但时序崩了用 ILA 调时序是最常见的操作也是最容易和增量实现打架的场景。ILA 的调试 hub 需要占据一定的布局资源而且它和采样时钟之间的关系比较特殊位置往往会被工具往中间区域塞。当你用旧版本的布局作为参考时这个新加的 hub 只能挤进剩余空间很容易落到一个对时钟树不友好的位置。我遇到过一次加了两个 ILA复用率显示还有八成多看起来挺健康结果时序报告里建立时间违例集中在 ILA 采样路径上。后来把 ILA 去掉复用率反而降了一点但时序全部干净了。结论是插 ILA 的迭代优先考虑全量实现或者至少不要复用插 ILA 之前那一版的布局。ILA 属于看着加得少、实际影响大的典型。顺便说一句调试用的 ILA 尽量不要长期留在正式版本里。调试完就摘掉既省资源也省时间。5.2 改了时钟结构却继续复用旧布局时钟是布局的骨架。BUFG、MMCM 这些资源的位置直接决定了整个设计里大量寄存器的布局倾向。如果你把一个时钟资源换了位置或者新增了一条时钟分支旧布局里所有和这条时钟相关的寄存器位置都会变得不合适。这种改动之后如果还去复用旧布局典型症状是 hold 违例大量出现而且是那种看着莫名其妙的路径。原因是原有走线被强制保留新的时钟延迟和旧的数据路径对不上。我在一次时钟树调整后就遇到过这种情况最后只能放弃增量全量重跑。判断标准很简单只要改动涉及时钟资源、时钟域划分、复位结构就不要用增量实现。这三样东西属于架构层架构层变了就该重新来。5.3 跨版本复用参考 checkpoint 的兼容性前面提过一次这里再展开说。工具的不同大版本之间布局算法的内部数据结构和优化目标都可能变化。旧版本生成的 checkpoint 在新版本里能不能读能读出来复用效果如何都是不确定的。我自己的做法是跨版本升级工程之后第一件事就是丢掉旧的参考 checkpoint重新跑一次全量实现用这一次的结果作为后续增量的基线。多花一晚上的时间换来的是一整条迭代链路都建立在干净的基础上。反过来如果硬要跨版本复用就得接受复用率不确定、时序可能变差这两个风险而且要额外做一轮时序对比。5.4 布线拥塞导致的复用到死角这个坑比较隐蔽。设计的布线资源利用率本来就比较高的时候增量实现会把新逻辑硬塞进旧布局没占用的那些零散空间里。这些空间往往是原本布局刻意绕开的边角区域走线要绕远路拥塞也高。症状是实现能跑完复用率也不错但布线阶段出现拥塞告警时序余量比全量差一截甚至个别网络布线失败。这种情况下工具通常会给一条关于拥塞或者布线未完成的提示千万别忽略。我的应对方式是在决定用增量之前先看一眼当前设计的布线资源占用率。如果已经超过七八成增量带来的收益往往小于它带来的 QoR 损失直接全量跑更划算。提示增量实现不是更快就等于更好。它是一笔交易——用一部分 QoR 换时间。改动越小、设计越宽松这笔交易越划算反之就要谨慎。6. 几个我长期在用的配套小技巧用久了之后会形成一些没有写在文档里、但确实省事的小习惯这里挑几个分享。第一个习惯是永远保留一个 baseline run。我会把每次做全量实现的结果单独留一个 run 不删作为这一版设计的基准。后面所有增量迭代都拿它当参考。好处是复用关系清晰不会出现参考的参考这种套娃情况。一旦发现某次增量结果不好随时可以退回 baseline 重来心态上也会放松很多。第二个习惯是用两个 impl run 做对照。改一处逻辑我有时会同时起一个增量 run 和一个全量 run跑完之后对比时序和资源。虽然多花了一点时间但能让我知道这次增量的代价到底有多大。跑几次之后对什么样的改动用增量是安全的就有直觉了。第三个习惯是把 checkpoint 和复用报告绑定归档。每次增量跑完把report_incremental_reuse的输出和对应的 checkpoint 放在同一个目录下。日后回头查上次那个版本为什么时序好翻出报告一看就清楚了。第四个习惯是关于改动的粒度。我尽量把 RTL 改动拆成小批次提交一批改完跑一次增量而不是攒一大堆改动一次跑完。小批次的复用率高、问题定位容易攒大改动最后往往只能全量跑。还有一个细节值得提一句Vivado 在新版本里对综合阶段也做了增量相关的能力逻辑改动不大时综合本身也能加速。但那是另一条链路和实现阶段的增量参考是两回事不要指望设了实现参考就能把综合时间也省下来。综合该怎么跑还是怎么跑。最后分享一个判断要不要用增量的简单方法先问自己这次改动会不会改变设计里大量单元的物理位置。如果答案是会就别用增量如果答案是只有一小块地方动了那就放心用收益通常很实在。