☰
Vivado增量实现实战:FPGA布局布线复用、时序检查与避坑
2026/10/7 1:29:39 网站建设 项目流程

做 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 在新版本里对综合阶段也做了增量相关的能力,逻辑改动不大时综合本身也能加速。但那是另一条链路,和实现阶段的增量参考是两回事,不要指望设了实现参考就能把综合时间也省下来。综合该怎么跑还是怎么跑。

最后分享一个判断要不要用增量的简单方法:先问自己"这次改动会不会改变设计里大量单元的物理位置"。如果答案是会,就别用增量;如果答案是"只有一小块地方动了",那就放心用,收益通常很实在。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询