一块板子上有两块大 FPGA:一块跑 Linux 的 Zynq UltraScale+ RFSoC,旁边一块 Kintex UltraScale,中间是一条 10 千兆串行链路。过去要改第二块芯片的逻辑,就得接上编程器,因为那块芯片一断电 就什么都不记得。我们把整个更新过程搬到了两块芯片本来就在传数据的那条链路上。动手的工作由 AI 在 Python2Verilog 框架下完成,工程师在每一步给方向。
为什么显而易见的那条路走不了链路
FPGA 的逻辑存在易失性存储里,所以加载有两条路。一条是把固件直接下载进片内的配置存储器,立刻生效, 下次断电就没了。另一条是把固件写进旁边的闪存,芯片每次上电自己去读。调试阶段一直走第一条,靠一条单独的 线从工作站下载。
第一条路搬不上链路。写配置存储器会把整颗芯片重写一遍,承载链路的逻辑也在其中,传到一半链路自己就断了。 先烧闪存再重启,是远程更新的标准做法,我们做的就是这一条。
把烧写通道挂在数据链路上
第二块芯片旁边的闪存有 64 兆字节,接在专用的配置引脚上。这组引脚有个好处:设计跑起来之后,还能通过 片内的一个接口继续访问它们,所以这颗闪存不占用任何普通封装引脚。
帧头带标记的帧分流进烧写通道,其余流量照旧通过。标记后面是一个 SPI 控制器,放在自己的自由运行时钟域里, 链路上的抖动打不断一次正在进行的擦写。批量操作一次往返搬 128 千字节。闪存的命令集留在主机侧,所以 FPGA 上的逻辑不认识任何厂商操作码,同一条通道换一颗闪存照样能用。
真机上三步验收,每步都有数字。走链路读回的器件识别码,与原理图上印的那颗闪存型号完全对应。高地址上的 单扇区试验擦除、写入、回读、再擦除,逐字节精确。然后是整份固件:8.56 兆字节写到零地址,回读用了 165 秒, 没有一个字节不符。
启动测试说不,寄存器还自相矛盾
FPGA 上电去哪里找固件,由三个模式引脚决定,原理图上印的默认值就是从闪存启动。可是按模式引脚重新配置 之后,芯片直接黑掉了。读配置状态寄存器,它说模式引脚是 000,那不是从闪存启动。同一份寄存器还说配置从未 完成,而这个设计明明在运行,还在通过链路应答。
一份自相矛盾的寄存器定不了任何事,所以真正算数的是那次重新配置的行为,而它已经给出了答案。回到原理图, 用几何提取把那一小块抠出来:三个模式引脚全接在一个四位拨码开关上,图纸上印的默认值在装配时没有拨到位。 我们想要的设置和装配现状只差一位,所以只需要拨动一个位置,而且哪个方向算“开”都无所谓。
怎么证明芯片跑的是哪一份
拨完开关,芯片自己完成了配置。不过“配置完成”是很弱的证据:它说不出芯片加载的是闪存里那份,还是之前 从线缆灌进去的残留。设计里有个计数器记录 SPI 接口发过多少字节,读出来是 64,恰好是一次全新加载上电预热 发出的字节数,一个都不多。这个计数器有零点,所以这是一次从零开始的加载。
然后整件事变成远程的。一条带内寄存器写入,回执先离开链路,芯片再重启,这样一次沉默的失败就不会被当成 一次沉默的成功。片内重新配置接口从闪存重新加载设计,几秒后链路重锁,两端都在新固件上应答。
同一套流程,跑在更宽的链路上
随后这套流程又在更宽的通路上验了一次:两条通道捆绑起来,芯片内部的数据通路也加宽到 128 位,让捆绑 出来的带宽真正用得上。同一批 256 千字节回环从每秒 3.0 千兆涨到 5.24 千兆,链路侧的用户带宽上限从 9.2 提到 18.6 千兆,于是瓶颈从线上挪进了芯片里。在捆绑通路上整套更新重跑一遍:9.54 兆字节,146 个扇区 擦除、写入并校验,没有一个字节不符,用时 295 秒,随后远程重启,每一项检查都在第一次读取时就应答。烧写和 重启这两件事,现在跑在它们自己烧出来的系统之上。
留下的三条准则
这三条都写回了框架,成了每次构建都会跑的检查。
信行为,不信记录。从一个运行中的设计读出的状态寄存器,同时报告了两件不可能都成立的事。 这样的记录证明不了任何东西。真发一次命令,让硬件自己回答。
回执先走,动作后发。以重启收尾的命令会丢掉自己的回复,于是在主机看来成功和失败长得 一模一样。回执先离开链路,芯片才开始重启。
启动来源的证据要有零点。想知道芯片跑的是哪一份固件,就看一个在全新加载时必然清零的 计数器。配置有没有报告完成,与固件从哪里来无关。
这件事的意义
一次需要人和线缆的更新,给现场每一次修复都设了成本下限,也决定了你敢发什么货。把更新搬到系统本来就有的 链路上,这个下限就没了:主机把固件烧进目标芯片的闪存,发一条命令,目标带着新固件回来,断电重来也一样。 让它成立的,还是同一套做法。每一层都用真实读数验过,相信硬件的实际状态,而不是描述它的那份文档。


