01signal.com

在 Vivado 中用部分重配置做远程更新

这是关于 Xilinx Vivado 部分重配置(Partial Reconfiguration,即动态功能交换 DFX)的一个系列四篇文章中的最后一篇。本文主要面向那些希望把部分重配置用于远程更新(Remote Update)场景的人。写作本文时,假设你已经读过了前面三篇。

引言

前面的文章中先讨论了一般意义上的部分重配置,接着介绍了 Vivado 针对这一目的的常规流程。本文则要为把这项技术用于 FPGA 逻辑的远程更新(Remote Update)打下基础。

这个使用场景的主要问题在于:部分比特流(partial bitstream)必须与若干年前生成的初始比特流(initial bitstream)兼容。因此,在对可重构逻辑(reconfigurable logic)进行实现时,必须能够拿到当初的父实现(Parent Implementation),或者必须重新生成一份结果完全相同的父实现——也就是布局布线(place and route)结果完全一致。

一种办法是保持整个 Vivado 工程原封不动,从而避免重新运行父实现;另一种办法是,即便重新运行,也能得到完全相同的结果。然而,无论依靠这两种方法中的哪一种,都很难保证未来还能持续发布兼容的部分比特流。

这个问题有一个可靠的解决方案;但要理解它,首先需要熟悉 DCP 和 OOC。下面先简要介绍这两个概念。

设计检查点(Design Checkpoint,DCP)

回顾一下,Vivado 是借助若干设计运行(Design run)来完成 FPGA 设计的实现的。这些运行通常名为 synth_1、impl_1,还有一些被归类为 Out-of-Context 模块运行(OOC)的附加运行。

每个这样的运行都会执行一个 Tcl 脚本:该脚本会生成一个临时的“内存工程”(in-memory project),加载设计文件,设置各种属性和特征,并调用 Tcl 函数来完成综合、布局、布线以及生成比特流等任务。

这个内存工程不会在磁盘上创建任何文件,也与 GUI 中可见的 Vivado 工程无关。它是一个内存中的对象,用来通过 Tcl 命令连续执行许多操作。

随着实现的推进,Design CheckPoint(DCP)文件会被写到磁盘上(通过 Tcl 命令 write_checkpoint)。DCP 文件的内容是内存工程的一份快照。换句话说,它是一个数据库,反映了已经加载的设计文件,以及从这些文件加载以来对工程执行的所有操作。

例如,综合运行(通常名为 synth_1)会加载所有 HDL 文件、约束文件和 IP 文件,然后调用名为 synth_design 的 Tcl 命令对 HDL 文件进行综合。结果是这些来源(包括 IP 核)合并成的一个大网表(netlist)。不过要注意,这个网表是作为内存工程的一部分存放在内存中的。因此,综合运行的 Tcl 脚本会调用 write_checkpoint 来生成 DCP 文件,作为综合的产物。synth_1 运行到此结束。

实际上,synth_1 生成的 DCP 被称为网表 DCP(netlist DCP),尽管它通常也包含来自 XDC 文件的约束。

随后进行的实现运行通常名为 impl_1。它会创建一个新的内存工程,并读取这个网表 DCP(连同其它文件),作为后续操作的起点。这个运行会写出多个 DCP 文件,每个文件都是工程在某个处理阶段之后的快照(例如:优化设计、布局、物理优化、布线等)。

包括 synth_1 在内的所有运行,都可以把 DCP 加载到自己的内存工程中。它们通常也是这么做的。

Out-of-Context(OOC)模块运行

在 Vivado 中,IP 核通常通过 GUI 工具进行配置。配置完成后,一个脚本会生成源文件(主要是 HDL 和约束文件),并对它们进行综合。综合的结果是一个网表 DCP,随后它会被加载到主工程的综合运行和实现运行中。这样就不必反复重新生成 IP 核的源文件并重复综合,从而缩短整个工程实现所需的时间。

Vivado 把这种把原始产物(一个 IP 核的配置信息、若干 HDL 文件,或其它任何东西)转换成网表 DCP 的运行,在 Vivado 术语中叫做 Out-of-Context 运行。这个词只在 Vivado 语境中使用,据我所知没有别的用途。说实话,这个名字起得不怎么样。

综合运行和实现运行会加载这个网表 DCP,通常是通过一个名为 read_ip 的 Tcl 命令来完成的;实际上就是加载该 OOC 运行生成的 DCP。

Vivado 也允许在主工程中选择一个 HDL 模块,并让它的综合以 OOC 方式执行(方法是右键点击 Project Manager 源文件树中的该源文件,选择 Set as Out-of-Context for Synthesis…)。

OOC 的缺点在于:当综合器(synthesizer)把整个设计作为一个整体工程来处理时,它可以执行跨越模块边界的某些优化。因此,通过 OOC 进行单独综合,可能会降低性能并浪费资源。

OOC、DCP 与部分重配置

当一个源文件被选为某个可重构模块的顶层时,Vivado 会为该模块及其子模块的综合创建一个 OOC 运行。这个运行会生成一个网表 DCP,供相关的实现使用。父实现和子实现都是如此:可重构模块始终由一份独立的 DCP 表示。

不过,这个网表 DCP 在父实现和子实现中的使用方式不同:分配给父实现的网表 DCP 会同时被 synth_1 和 impl_1 加载,就像普通实现中加载 IP 核那样。部分重配置对这个过程的影响,主要体现在布局规划(floorplanning)所施加的布局约束上。

另一方面,子实现没有专门的综合阶段。它把可重构模块的网表 DCP 与父实现的最终 DCP(即完成布局布线(place and route)之后的 DCP)混合在一起。更准确地说,子实现会取父实现的最终 DCP,移除其中可重构逻辑的部分,然后插入自己的可重构模块网表 DCP。有点像把蔬菜中间挖空、做成酿西葫芦之类的菜那样。

现在该把它拆解成 Tcl 命令来看了。

父实现与子实现的细节

想了解实现的内部工作原理,应该去看那个以工程名命名的 Tcl 文件(后缀为 .tcl)。它位于实现文件所生成的那个目录下。

尤其值得一看的是子实现的实现脚本。相关用户指南 UG909 的第 3 章(“Vivado Software Flow”)展示并解释了这段脚本,这并非巧合——尽管它没有直接这么说。

如上所述,父实现并没有太多特殊之处,除了它依赖一份 DCP 作为可重构模块的网表,并且施加了布局规划约束之外。它或多或少就像一个普通的层次化设计。

但接下来,父实现要写出两个比特流而不是一个,所用 Tcl 命令大致如下:

write_bitstream -force -no_partial_bitfile theproject.bit
write_bitstream -force -cell pr_block_ins pr_block_ins_lpf_partial.bit

然后它还会生成供子实现使用的 DCP:

update_design -cell pr_block_ins -black_box
lock_design -level routing
write_checkpoint -force theproject_postroute_physopt_bb.dcp

别忘了,在运行这一段代码时,当前有一个内存工程;这个工程从加载网表 DCP 开始,经历了布局布线和所有其它优化。上面的三行 Tcl 命令是在写出比特流之后执行的,所以此时内存工程正处于真正最后的阶段。

这正是“挖个洞、塞馅料”的时候:update_design 命令把可重构模块变成黑盒(black box)。换句话说,它内部的所有逻辑都被移走,为其它逻辑腾出空间。

然后,lock_design 命令会把设计的布局布线锁住。之后,工程的快照被写进 theproject_postroute_physopt_bb.dcp。其中 bb 当然表示 Black Box(黑盒)。

子实现脚本中与此相关的部分如下:

create_project -in_memory -part xc7k325tffg900-2
set_property design_mode GateLvl [current_fileset]
add_files -quiet .../impl_1/theproject_postroute_physopt_bb.dcp
add_files -quiet .../two_synth_1/pr_block.dcp
set_property SCOPED_TO_CELLS pr_block_ins [get_files .../two_synth_1/pr_block.dcp]
link_design -top theproject -part xc7k325tffg900-2 -reconfig_partitions pr_block_ins
opt_design
write_checkpoint -force theproject_opt.dcp
[ ... ]

从这一步开始,就继续进行布局布线等工作。

注意,上面的实现脚本只消费两个源文件,而且两个都是 DCP:

在后续实现中,第一个 DCP 已被锁定,这保证了静态逻辑不会发生任何移动。可重构模块的布局布线则照常进行,以网表 DCP 为基础。

顺便说一句,如果可重构模块带有 XCI 格式的 IP 核,那么上面两条 add_files 命令之间,每个 IP 核还会插入一行类似下面的命令:

read_ip -quiet .../theproject.srcs/sources_1/ip/blkmem/blkmem.xci

不过这并非部分重配置所独有——任何实现都是这样处理的。

在写出两个比特流之前,子实现会先检查:它所得到的布线后设计是否与父实现的静态逻辑兼容,尤其是在布局布线方面。

完成这一检查的 Tcl 命令大致如下:

pr_verify -full_check -initial /path/to/impl_1/theproject_postroute_physopt.dcp -additional /path/to/child_1_impl_1/theproject_routed.dcp -file child_1_impl_1_pr_verify.log

注意,这条命令比较的是两个 DCP 文件,与内存工程无关。它用父实现的布线后 DCP 与子实现的最终 DCP 进行比较。比较结果输出到以 *_pr_verify.log 命名的文件中。

这种比较保证了部分比特流的兼容性:也就是说,当 FPGA 中已经加载了父实现的比特流时,可以再加载这个部分比特流。检查会覆盖静态分区的逻辑单元以及布线。

如果不兼容,pr_verify 会返回失败状态。这种情况下,Vivado 脚本中创建比特流的步骤就会被阻止。在编写自定义的实现脚本时,务必不要遗漏这一步。

按理说,这种验证不应该失败;可一旦失败,比特流生成就会报出一大堆错误,例如:“ERROR: [Constraints 18-891] HDPRVerify-08: design check point .../impl_1/theproject_postroute_physopt.dcp places instance ... at site SLICE_X118Y125, yet design check point .../impl_2/theproject_routed.dcp does not. Both check point must have the same static placement result”。

这类错误消息很可能会达到 100 条的上限,然后被静默掉。

远程更新场景的解决方案

还记得前面说过的难点吧:当实现可重构逻辑(也就是运行子实现)时,必须保证最初父实现的结果仍然可用。

最简单的“暴力”方案是复制整个 Vivado 工程目录,以及它可能依赖的所有文件。当需要生成新的部分比特流时,把所有文件恢复出来,把父实现强制标记为最新,然后只为子实现生成比特流。这种方法在技术上说得通,但长期来看很可能会让人头疼。如果你选择这样做,请务必手动用原始的父实现 DCP 文件运行 pr_verify,因为这种方法不会发现父实现中无意发生的改动。

还有两种其它方案,它们的依据是前面解释过的父实现与子实现之间的交互方式——也就是通过两个 DCP 文件。这两种方案的明显优点是:你清楚自己在做什么。

这两种方案背后的原则,都是确保部分比特流依赖于当初随初始比特流一同生成的两个 DCP 文件。我把这两个文件称为黄金 DCP(Golden DCP)。

第一种方案是以非工程流程(non-project flow)的 Tcl 脚本方式来完成部分比特流的实现。本质上,就是运行 Vivado 为子实现生成的脚本,但把它修改为使用黄金 DCP。更准确地说,是修改 link_design 命令和 pr_verify 命令的参数,使它们指向黄金 DCP。

这种方案的主要缺点是:以非工程流程运行 Tcl 脚本,与 Vivado 的 GUI 整合得不太好,因此处理消息、打开实现后设计进行查看等操作会变得困难不少。

黄金 DCP 变通法

理想情况下,应该可以自动修改 Vivado 为子实现运行生成的脚本,让它改用黄金 DCP。可惜的是,似乎没有可靠的办法可以这样做。

不过,Vivado 允许在实现中某些阶段的前后执行用户自定义的 Tcl 脚本。这些脚本是从实现运行的脚本中调用的,所以它们不能用来修改运行脚本本身。

但这就为第二种方案打开了一扇门——一种有点难看的方法:用黄金 DCP 覆盖父实现的 DCP。这样一来,子实现会正常运行,但依赖的是黄金 DCP,而不是父实现碰巧生成的东西。

这种方法的优点是,使用 Vivado 的日常工作习惯不会改变:修改可重构逻辑后,Vivado 会重新运行该可重构模块的 OOC 综合,然后运行子实现来生成部分比特流。由于静态逻辑没有改动,Vivado 也就没有理由去启动与它相关的运行。

实现这种黄金 DCP 复制方法的 Tcl 脚本如下:

if { [catch {
    set parentimpldir "[ file normalize "../impl_1"]"
    set goldendir "[ file normalize "/path/to/golden"]"

    file copy -force "[file normalize "$goldendir/theproject_postroute_physopt_bb.dcp"]" "$parentimpldir/"
    file copy -force "[file normalize "$goldendir/theproject_postroute_physopt.dcp"]" "$parentimpldir/"
} errmsg ] } {
    send_msg_id golden-reconfig-1 error "Failed to copy golden parent reconfiguration file(s): $errmsg"
    return -code error
}

这个脚本假设父实现位于与子实现目录相邻的 impl_1 目录中(通常确实是这样),同时两个黄金 DCP 存放在本脚本第 3 行所定义的目录中。

在 Design Runs 标签页中右键点击子运行(例如 child_0_impl_1),选择 Change Run Settings…,在打开的对话框里,把 Design Initialization(init_design)步骤的 tcl.pre 设置为该脚本。或者,如果脚本保存为 golden_pr.tcl,也可以用 Tcl 命令完成:

add_files -fileset utils_1 -norecurse /path/to/golden_pr.tcl
set_property STEPS.INIT_DESIGN.TCL.PRE [ get_files /path/to/golden_pr.tcl -of [get_fileset utils_1] ] [get_runs child_0_impl_1]

使用这个脚本时,要记住的重要一点是:忽略 Vivado 展示的关于父实现的一切信息,以及它生成的任何相关文件。Vivado 可能会打开它的 Implemented Design GUI 并显示报告,但这些内容很可能与当前任务完全无关。所以,这也可能带来一些混乱。

另一个可能让人不爽的地方是:Vivado 可能会因为工程设置甚至约束文件的改变,时不时重新运行父实现。解决办法是右键点击 Design Runs 标签页中相应的行,选择 Force Up-to-Date。这个菜单项只在运行处于 Out-of-Date 状态(即已经完成,但 Vivado 认为需要刷新)时才会出现。对应的 Tcl 命令例如:

set_property needs_refresh false [get_runs synth_1]

除黄金 DCP 之外,还应该保存哪些文件

显然,黄金 DCP 必须存放在安全的地方,以便将来仍然能够生成兼容的比特流文件。除此之外,把整个 Vivado 工程压缩成 .tar.gz / .zip 文件也是一个好主意,这样可以快速恢复工作。或者——也可以同时——用 File > Project > Archive… 或类似下面的命令生成工程归档:

archive_project /path/to/theproject.xpr.zip -force -include_local_ip_cache -include_config_settings

但要注意:Vivado 工程中,工程文件以及其它文件都可能包含绝对路径,所以把工程放到另一个目录或另一台计算机上,可能无法像预期那样工作。归档文件也有同样的问题。

所使用的 Vivado 版本会写在各种报告文件和 .xpr 工程文件中,不过自己额外记一笔也没有坏处。也许还应该保留那个 Vivado 版本的可执行副本,尽管似乎没有明显理由说明软件升级会带来问题。把一个 Vivado 版本的黄金 DCP 与另一个版本的可重构逻辑网表 DCP 混用,很可能也能正常工作,但这并不是 Vivado 设计上支持的做法。

总而言之,最小文件集合是:

注意,UltraScale FPGA 上使用清除比特流时,它必须与 FPGA 中已有的逻辑对应。这就是为什么必须保存与初始比特流对应的清除比特流。还要注意:如果所用的初始比特流来自某个子实现,那么必须保存该子实现的清除比特流。

至于保存父实现的源文件,这一点很重要,因为它决定了静态逻辑与可重构逻辑之间的连接。例如,如果修改了源文件,给可重构逻辑增加了一个端口,而这个端口又出现在了实例化(instantiation)中,那么分区引脚(partition pin)的集合就会改变。这样,可重构逻辑的网表就会带有一些在原静态设计中不存在的对外引脚。

出现这种不匹配时,子实现会失败,并显示类似下面的错误:“ERROR: [Netlist 29-77] Could not replace (cell 'pr_block_bb', library 'work_pr_block_ins_pr_block_ins_4', file 'NOFILE') with (cell 'pr_block', library 'work', file 'pr_block.edf') because of a port interface mismatch; in strict mode, no extra ports are allowed. 8 ports are missing on the original cell. 5 of the missing ports are: 'thingy[7]' 'thingy[6]' 'thingy[5]' 'thingy[1]' 'thingy[0]'”。

这种情况下真正失败的其实是 link_design 命令(见上文),它负责把静态逻辑和可重构逻辑粘合在一起。

要避免这个问题,简单而明显的做法是:不要改动设计中的静态部分,也不要改动可重构逻辑的端口列表。

不过,修改工程中的静态部分其实是可以的:唯一必须保持不变的,是对可重构模块的实例化。只要实现过程顺利通过,包括最后的验证,就不会有问题。

总结

尽管在远程更新场景中使用部分重配置并没有一个完全顺畅的解决方案,但仍有一些可用的策略可以达到这个目标。

重要的一点是:原则上,只要有两个黄金 DCP,就足以构建并验证一个既有工程的部分比特流。

无论本文介绍的这些策略看上去多么不常规,请记住:pr_verify 所进行的验证,是对部分比特流与已就位的静态逻辑之间兼容性的全面检查。只要用于验证的黄金 DCP 是正确的,并且测试通过,就没有什么好担心的了。当然,子实现还要满足时序约束(timing constraints)——不过任何设计实现都要求如此。

本页由机器从英文翻译而来。如有疑问,请参阅原文
Copyright © 2021-2026. All rights reserved. (dcc38493)