这两年做设备远程维护,FPGA升级始终是个绕不开的话题。我手上一块板卡用的是 Artix-7 + N25Q128 SPI Flash 组合,后端业务希望设备现场不动,维护人员通过网络把新版本 bitstream 推给设备,设备自己完成 FPGA 镜像切换。听起来不复杂,实际做起来却发现,光是把 AXI Quad SPI IP 和 N25Q128 调通,就够喝一壶的。这篇文章把整个流程、关键决策、踩过的坑一起写出来,给同样做 Xilinx 平台远程升级的朋友一个可以直接参考的路线图。
文章会覆盖五个层面:为什么远程升级离不开 SPI Flash、MultiBoot 双区设计、AXI Quad SPI IP 的配置细节、N25Q128 的驱动实现、以及完整升级动作中的校验与回退。全程按实际项目节奏走,相关代码和参数都是实测下来能用的版本。
1. 整体方案设计:远程升级为什么绕不开 SPI Flash 与双镜像分区
很多人第一次接触远程升级时,第一反应是“能不能像单片机一样,通过以太网把固件写入 Flash”。这个思路本身没错,但 FPGA 的启动架构跟 MCU 不一样,方案设计必须先搞清楚“FPGA 上电后到底从哪里取代码”。
1.1 FPGA 启动架构:SRAM 工艺带来的天然“失忆”问题
FPGA 内部基于 SRAM 存储配置数据,掉电后配置全部丢失,所以每次上电都要从外部非易失存储介质加载 bitstream。常见的加载接口包括 SPI Flash、QSPI Flash、BPI 并行 Flash、SD 卡、JTAG 等,其中 SPI Flash 因为引脚少、容量合适、成本低,成了板级设计的首选。
加载过程是这样的:FPGA 上电后,配置逻辑根据模式引脚 M[2:0] 的拨码状态,决定从哪个接口启动。如果是 SPI 模式,配置逻辑会自动产生 SCK 和 CS,把挂在 SPI 总线上的 Flash 内容读进来,完成初始化。这个过程不需要用户逻辑参与,属于芯片内部硬件行为。
理解了这一点,“远程升级”的本质就清楚了。FPGA 正在运行旧版本逻辑时,用户逻辑(比如 MicroBlaze 软核或状态机)先通过网络或总线拿到新镜像数据,然后把数据写入 SPI Flash 的另一个区域。全部写完并通过校验后,再通过特殊指令触发 FPGA 重新配置,让配置逻辑从新区域加载。所以远程升级不是简单“烧 Flash”,而是“换启动源”。
1.2 MultiBoot 双区架构:Golden 镜像与 Update 镜像怎么分
Xilinx 7 系列 FPGA 原生支持 MultiBoot 功能。芯片内部有一段固化逻辑,能从用户指定的 Flash 地址读取 bitstream 头部,如果发现头部无效、配置 CRC 错误,或者配置看门狗超时,就会自动回退到地址 0 处的备份镜像。这个机制对远程升级来说非常宝贵,等于芯片硬件层面就给了我们“免砖”保险。
因此分区设计上,一定要把 Flash 分成至少两个镜像区:
- Golden 镜像区:放在 Flash 起始地址,比如 0x0000000。这个区域的镜像出厂时烧录,是最后一个保底版本,永远不通过远程升级覆盖。除非专门做“引导区升级”的重维护流程,否则普通升级只读不动。
- Update 镜像区:放在 Flash 高地址,比如 0x1000000。每次远程升级都写入这个区域,全部校验通过后才切换启动指针。
实际操作中还要考虑一个问题:Update 镜像本身如果升级到一半断电,写入一半的数据可能让 MultiBoot 启动失败。但因为启动指针还没有切换,系统仍然从 Golden 启动,这是双区架构最大的价值。后面升级流程设计里,这个理念贯穿始终。
1.3 Flash 分区规划:地址对齐、版本记录、CRC 存储
以 N25Q128 举例,容量 128Mbit,换算下来 16MB。一个 Artix-7 的 bitstream 通常在 2MB 到 7MB 之间,具体取决于逻辑规模和配置模式。我一般分区这样安排:
| 起始地址 | 大小 | 用途 |
|---|---|---|
| 0x0000000 | 1MB | Golden 镜像 |
| 0x100000 | 14MB | Update 镜像区 |
| 0xF00000 | 0.5MB | 升级状态与版本标记区 |
| 0xF80000 | 0.5MB | 保留区(存放设备信息、校准数据等) |
需要注意,Flash 扇区擦除最小单位一般是 4KB,所以分区起始地址必须按 4KB 对齐。Update 镜像的起始地址还要注意 WBSTAR(Warm Boot Start Address Register)的地址构造要求,建议也按 4KB 对齐,省去很多麻烦。
另外,版本记录不要放在镜像头部附近,尽量单独划分扇区。因为镜像头部是 FPGA 配置逻辑直接读取的,任何意外破坏都可能导致启动失败。版本区单独放,可以在用户逻辑启动后再读取,安全性更高。
2. AXI Quad SPI IP 的配置与连线:几个决定成败的细节
IP 配置看起来就是 Vivado 里点几下鼠标,但几个参数选错,后面读写 Flash 会非常被动。这里详细说明每个关键选项的取舍。
2.1 为什么用 AXI Quad SPI IP,而不是自己写 SPI 控制器
可能有人会问:FPGA 内部逻辑灵活,用 GPIO 模拟 SPI 时序不就行了吗?确实可以,而且对速度要求在 1MHz 以下时,GPIO 模拟完全够用。但远程升级往往涉及整片 Flash 擦写,数据量几十 MB,模拟 SPI 的 CPU 占用非常高,还要处理各种边沿时序,可靠性很难保证。
AXI Quad SPI IP 是 Xilinx 官方提供的 SPI 控制器,内部带收发 FIFO、状态寄存器、中断支持,还能直接通过 AXI4-Lite 接口被 MicroBlaze 或 Zynq ARM 核访问。实际测试下来,SPI 时钟跑到 40MHz 到 50MHz 很稳定,读写大块数据比 GPIO 模拟快一个数量级。
更重要的一点,这个 IP 支持 Standard SPI、Dual SPI、Quad SPI 三种模式。N25Q128 本身支持 Quad Output Fast Read,用四根数据线同时读,读回校验速度会快很多。这在量产升级场景中很有意义,能明显缩短设备离线窗口。
2.2 Vivado 里参数怎么选:模式、FIFO、时钟分频
在 Vivado IP Catalog 中搜索 AXI Quad SPI,双击配置。我建议重点关注以下参数:
| 参数名 | 推荐取值 | 说明 |
|---|---|---|
| Mode | Quad SPI | 直接用四线模式,兼容标准模式 |
| FIFO Size | 32 或 64 | 读写大块数据时减少 AXI 交互次数 |
| Transaction Width | 8 bit | 最通用,Flash 指令处理方便 |
| SPI Clock Ratio | 8 或 16 | 根据 AXI 时钟频率换算 |
| Master Mode | 勾选 | IP 作为 SPI 主机 |
| CPOL / CPHA | Mode 0 或 Mode 3 | N25Q128 两种都支持,推荐 Mode 0 |
SPI Clock Ratio 的含义是 AXI 总线时钟与 SCK 的分频比。如果 AXI 时钟是 100MHz,Ratio 填 8,SCK 就是 12.5MHz。这个频率对绝大多数 SPI Flash 都足够,属于“稳定优先”的配置。
补一个容易忽略的点:FIFO 深度太浅时,连续写入大块数据会频繁触发 FIFO Full,影响吞吐。但 FIFO 太深又会占更多 BRAM,所以常规项目用 32 就够,调试阶段可以先 64,后面再优化。
2.3 引脚约束和 PCB 上的高频信号注意点
N25Q128 的引脚里,CS#、SCK、DQ0、DQ1 是标准 SPI 必需引脚,DQ2 对应 WP#(写保护),DQ3 对应 HOLD#(保持)。如果只用标准 SPI 模式,DQ2 和 DQ3 可以不上拉也不连 FPGA。但既然用了 Quad 模式,这两个引脚必须接到 FPGA 的普通 IO 上,因为 Quad 读数据时它们要作为 DQ2/DQ3 数据线参与传输。
PCB 设计时,SCK 走线要短,DQ0-DQ3 尽量等长,避免高速模式下信号偏移。Flash 电源引脚旁边至少放一组 0.1uF 和 10uF 去耦电容,大面积擦写瞬间电流很大,电压跌落会导致编程失败。
还有一个 1.8V/3.3V 的坑。N25Q128 有 1.8V 版本和 3.3V 版本,采购时必须确认后缀。如果 FPGA 对应 bank 电压是 3.3V,却贴了 1.8V 的 Flash,上电后读 ID 通常会有问题,轻则读不出来,重则长期工作不稳定。这类问题现象隐蔽,排查起来非常熬人。
3. 驱动实现与读写 N25Q128 的关键流程
IP 配置完成后,真正跟 Flash 打交道的是驱动代码。这一节把 AXI Quad SPI IP 的寄存器结构、常用 SPI 指令、擦写读流程完整梳理一遍。
3.1 寄存器层级先摸清:AXI Quad SPI 的寄存器视图
AXI Quad SPI IP 通过 AXI4-Lite 接口暴露一组寄存器,常见关键寄存器有:
- SPICR(0x00):控制寄存器,包含 SPI 使能、手动片选、FIFO 复位等位域。
- SPISR(0x04):状态寄存器,包含发送 FIFO 空/满、接收 FIFO 空/满等位域。
- SPIDTR(0x08):数据发送寄存器,向 FIFO 写入要发送的字节。
- SPIDRR(0x10):数据接收寄存器,从 FIFO 读出接收到的字节。
底层一次 SPI 传输的套路是固定的:先查发送 FIFO 是否满,不满就往 SPIDTR 写一个字节;接着查接收 FIFO 是否为空,不为空就从 SPIDRR 读一个字节。注意寄存器是 32 位宽度,但发送和接收时有效数据在低 8 位。
还有一个细节,AXI Quad SPI IP 的 FIFO 在初始化后可能残留旧数据,建议先通过 SPICR 里的 FIFO Reset 位做一次复位,再开始正常收发。否则第一次读数据可能读到上一次会话的残留内容。
3.2 N25Q128 常用指令集和底层收发函数
N25Q128 属于标准 SPI NOR Flash,指令集跟市面上大多数 SPI Flash 高度一致。我整理了一张常用指令表:
| 指令 | 指令码 | 说明 |
|---|---|---|
| WREN | 0x06 | 写使能,任何写操作前必须先发 |
| RDSR1 | 0x05 | 读状态寄存器 1 |
| READ | 0x03 | 普通读,无 Dummy 周期 |
| Fast Read | 0x0B | 高速读,带 8 个 Dummy 周期 |
| Quad Output Fast Read | 0x6B | 四线输出读,带 8 个 Dummy 周期 |
| Page Program | 0x02 | 页编程,最多 256 字节 |
| Sector Erase | 0x20 | 扇区擦除,4KB |
| Block Erase | 0xD8 | 块擦除,64KB |
| Read ID | 0x9F | 读 JEDEC ID |
N25Q128 的 JEDEC ID 是 0x20 0xBB 0x18,0x20 表示制造厂商,0xBB 是设备类型,0x18 表示容量。驱动初始化时先读 ID,可以有效防止因为 Flash 贴错、虚焊、IO 连接错误导致的后续一系列诡异问题。
底层的spi_transfer函数是所有 Flash 操作的基础,核心逻辑如下:
int spi_transfer(uint8_t *txbuf, uint8_t *rxbuf, uint32_t len) { for (uint32_t i = 0; i < len; i++) { // 等待发送 FIFO 非满 while (read32(BASE + SPISR) & TX_FIFO_FULL_MASK); write32(BASE + SPIDTR, txbuf[i]); // 等待接收 FIFO 非空 while (read32(BASE + SPISR) & RX_FIFO_EMPTY_MASK); rxbuf[i] = read32(BASE + SPIDRR); } return 0; }注意,发送和接收是同时发生的,SPI 协议本身就是全双工。即使只需要接收数据,也要先发送对应长度的“空字节”或指令字节来推动时钟。
3.3 擦除、写页、读回校验:完整动作序列
写 Flash 之前必须先擦除,因为 NOR Flash 只能把 1 写成 0,不能把 0 写成 1。扇区擦除指令会把整个 4KB 区域全部变成 0xFF。下面是擦除一个扇区的标准流程:
- 发送 WREN(0x06),使能写操作。
- 发送 Sector Erase(0x20 加 3 字节地址)。
- 轮询状态寄存器 1,等待 WIP(bit0)从 1 变成 0,表示擦除完成。
- 可选:读取该扇区内容,确认全部为 0xFF。
页编程流程也很固定:
- 发送 WREN。
- 发送 Page Program(0x02 加 3 字节地址,再加最多 256 字节数据)。
- 轮询 WIP,等待编程完成。
- 读回这 256 字节,与原始数据比对。
这里有三个实测经验值得写出来。
第一,写使能不是发了就行。写完 WREN 后最好读一次状态寄存器,确认 WEL(bit1)被置 1。如果 Flash 处于写保护状态或指令没被正确接收,WEL 不会置位,后续擦写会静默失败。
第二,页编程地址和数据长度都有限制。单次 Page Program 最多只能写 256 字节,而且一页以内的地址偏移不能跨页。如果要从地址 0x1F00 写 256 字节,前 256 字节没问题,但从 0x2000 开始就属于下一页了,必须拆分两次发送。
第三,擦除和编程后必须轮询 WIP,不能一口气发几十个擦除命令再统一等。每个 Flash 内部操作都需要时间,4KB 擦除大概几十毫秒,64KB 擦除可能要几百毫秒。轮询时要设置超时,避免 Flash 异常时系统陷入死循环。
4. 远程升级的完整动作:从生成镜像文件到安全回退
驱动调通只是第一步。真正做远程升级时,还要把“镜像怎么生成”“启动源怎么切换”“失败怎么回退”这几个环节串起来。
4.1 别拿原始 .bit 直接写 Flash,先转换成 .bin
很多人第一次做升级,直接通过网络把 Vivado 生成的 .bit 文件写到 Flash 里,结果重启后起不来。原因是 .bit 文件本身是给 JTAG 和调试器用的,除了配置数据外还包含设备 ID、时间戳等头部信息,不一定适配 SPI 启动的加载格式。
正确做法是用 Vivado 的 write_cfgmem 命令把 .bit 转换成 .bin 文件,再通过上位机下发到设备。例如:
write_cfgmem -format bin -interface SPIx4 -size 128 \ -loadbit {up 0x01000000 "D:/build/upgrade.bit"} \ -file "D:/build/upgrade.bin" -force这个命令生成的 bin 文件,数据内容在 Flash 中的偏移就是 0x01000000。升级程序收到文件后,直接把数据写到 Flash 的 0x01000000 地址即可。
这里有个细节:interface 填 SPIx4 表示按 Quad SPI 的位序输出,如果你的板子决定只用标准 SPI 启动,interface 要填 SPIx1,否则生成的 bit 顺序可能和实际启动模式不匹配。
4.2 触发重启切换:ICAP、WBSTAR 和 IPROG
镜像写完且校验通过后,下一步是把 FPGA 的启动地址切到 Update 镜像区。常规做法是通过 ICAPE2 原语向芯片内部发送配置指令,设置 WBSTAR 并触发 IPROG。
7 系列 FPGA 里,WBSTAR 用来保存下一次热启动的起始地址。写入这个寄存器后,再发送 IPROG(内部程序控制)指令,FPGA 就会立刻重新加载指定地址的 bitstream。典型 Verilog 代码如下:
ICAPE2 #( .ICAP_WIDTH("WORD"), .SIM_CFG_FILE_NAME("NONE") ) u_icape2 ( .O(icap_o), .I(icap_i), .CSIB(icap_cs), .RDWRB(icap_rw) ); // 写 WBSTAR,地址指向 0x01000000 // 然后发送 IPROG 触发重配置具体字节序列需要根据 UG470 里 WBSTAR 和 IPROG 的指令格式构造。不同 FPGA 型号、不同 ICAP 位宽,序列会有差异。核心思路是:先让 ICAP 进入写状态,再发送同步字,然后写入 WBSTAR 值,最后发送 IPROG 命令。
如果工程里有 MicroBlaze 或 Zynq ARM,也可以通过 PS 侧寄存器触发 PL 重配置,原理一样,只是入口不同。
4.3 回退机制:升级失败不变成砖的底线
MultiBoot 的回退逻辑是这样的:如果 WBSTAR 指向的 Update 镜像加载失败,比如头部同步字不对、IDCODE 不匹配、CRC 错误,配置引擎会等待配置看门狗超时,然后自动回到地址 0 加载 Golden 镜像。
这个机制能兜住大部分升级失败场景,但前提是 Golden 镜像必须完整且可启动。所以我前面反复强调,普通远程升级绝不覆盖 Golden 区。如果哪次升级真的把设备搞挂了,现场至少还能通过重启回到 Golden,重新进入升级流程。
另一个容易被忽略的问题:配置看门狗超时时间。Vivado 里有相关参数可以配置,默认值可能过长或过短。过短会导致 Golden 镜像还没加载完就误判失败;过长则会让损坏镜像的加载等待时间很长,设备离线窗口变大。建议在测试板上实际测量正常镜像从 IPROG 到工作的时间,然后留够余量。
5. 联调实测中的高频坑和排查清单
驱动写完、流程打通,并不代表万事大吉。联调阶段最容易出问题,很多坑是文档里不写、不踩不知道的。
5.1 Flash 型号混淆:N25Q128 和 W25Q128 的细微差异
N25Q128 和 W25Q128 都是 128Mbit SPI NOR Flash,容量一样,指令也高度兼容,但至少有两个地方容易出问题。
一个是状态寄存器的细节。虽然 WIP 和 WEL 的位置一致,但状态寄存器里的某些保护位、功能位的默认值可能不同,如果驱动里做了寄存器比特级操作,必须按数据手册核对。
另一个是读 ID 的返回值。代码里如果用固定 ID 校验,换 Flash 型号后就会失败。我在多块板子上使用过两款 Flash,发现最好的方式是:初始化时把读到的 ID 打印或上报,ID 匹配表做成可配置的,不要写死单一型号。
还有一个电压等级问题。N25Q128 有 1.8V、2.5V、3.3V 多种版本,市面上最常见的是 3.3V 版本。如果板卡 bank 电压是 1.8V,却误用 3.3V Flash,长时间工作会不断积累损坏风险。
5.2 数据错位、丢字节:先调 SPI 时钟再说别的
现象很典型:读 ID 正常,小数据读写正常,大块连续读时中间出现一簇 0xFF 或错位数据。排了一圈发现,最后往往是 SPI 时钟频率跑太高,或者 Quad Read 指令的 Dummy 周期配置不对。
N25Q128 的 Quad Output Fast Read(0x6B)默认需要 8 个 Dummy 周期,而某些 Flash 的 Quad Read(0xEB)需要 4 或 6 个。驱动里如果写死了一种 Dummy 数,换芯片后读数据就会整体错位。
排查方法是降频试。把 SPI Clock Ratio 改成 16 甚至 32,如果现象消失,再逐步提升频率,直到找到稳定边界。这个边界跟 PCB 走线长度、接插件质量、FPGA IO 驱动强度都有关,不要盲目追求最高频率。
5.3 ILA 调试时怎么抓对信号,怎么不被优化掉
用 Vivado 的 ILA 在线调试时,建议抓 AXI4-Lite 总线的 AW、W、AR、R 通道信号,而不是抓 SPI 引脚。SPI 引脚信号频率高且变化快,ILA 深度不够时很难看到完整时序;AXI 总线上一个读写操作就是一次明确的事务,方便定位问题。
比较烦的是 ILA 被综合优化掉。如果信号在综合后被优化了,ILA 是看不到波形的。解决办法是在 RTL 代码里对目标信号加(* mark_debug = "true" *)属性,或者直接在 Block Design 里把 ILA 接入对应接口,让工具保留这些信号。
调试完成后要记得把 ILA 从工程里移除或禁用。ILA 会占用大量 BRAM 和布线资源,带 ILA 的版本跑在电性能差的板子上,可能引入额外时序问题,这是很多“测着好好的,正式版出问题”的根源。
5.4 升级没写完就断电:用版本标记兜底
双区架构只能保证“启动源没切换时安全”,但如果升级程序已经写完 WBSTAR 并触发重启,而 Update 镜像偏偏是坏的,那就只能靠 Golden 回退。回退本身没问题,可设备会一直停留在旧版本,升级流程需要一个机制来识别“当前镜像是否有效”。
实际项目中我们采用了一个版本标记扇区。该扇区固定位置存放结构体,包含:
- magic 值,比如 0xA5A5A5A5,用来判断扇区是否已被初始化。
- 当前启动状态,比如 BootNormal、Upgrading、UpgradeCompleted。
- 版本号、镜像 CRC、写入时间等。
升级流程按状态机执行:
- 进入升级前,读取版本标记。如果发现 Upgrade 状态异常,回退到 BootNormal。
- 远程镜像写入 Update 区之前,把标记写成 Upgrading。
- 镜像写完并读回校验通过后,把标记写成 UpgradeCompleted。
- 设置 WBSTAR 指向 Update 区,触发 IPROG。
- 重启后从 Update 启动,用户逻辑运行后把标记改回 BootNormal。
这个状态机配合 MultiBoot 回退,能保证绝大多数断电、断网、镜像损坏场景下,设备最终还能回到一个可用版本。
6. 一段可以直接复用的 C 风格驱动框架
最后给一段通用的驱动框架,按 AXI4-Lite 地址映射实现,方便读者移植到自己的工程。
6.1 基本宏定义与初始化
#define QSPI_BASE 0x44A00000 #define SPICR_OFFSET 0x00 #define SPISR_OFFSET 0x04 #define SPIDTR_OFFSET 0x08 #define SPIDRR_OFFSET 0x10 #define SPICR_SPE 0x00000200 #define SPICR_FRES 0x00000100 #define SPISR_RX_EMPTY 0x00000040 #define SPISR_TX_FULL 0x00000020 void qspi_init(void) { uint32_t cr; cr = read32(QSPI_BASE + SPICR_OFFSET); cr |= SPICR_FRES; write32(QSPI_BASE + SPICR_OFFSET, cr); cr = read32(QSPI_BASE + SPICR_OFFSET); cr &= ~SPICR_FRES; cr |= SPICR_SPE; write32(QSPI_BASE + SPICR_OFFSET, cr); }6.2 底层传输与状态等待
int qspi_transfer(uint8_t *tx, uint8_t *rx, uint32_t len) { for (uint32_t i = 0; i < len; i++) { while (read32(QSPI_BASE + SPISR_OFFSET) & SPISR_TX_FULL); write32(QSPI_BASE + SPIDTR_OFFSET, tx[i]); while (read32(QSPI_BASE + SPISR_OFFSET) & SPISR_RX_EMPTY); rx[i] = read32(QSPI_BASE + SPIDRR_OFFSET); } return 0; } int flash_wait_busy(uint32_t timeout_ms) { uint32_t start = get_tick_ms(); uint8_t tx[2] = {0x05, 0x00}; uint8_t rx[2] = {0, 0}; while (get_tick_ms() - start < timeout_ms) { qspi_transfer(tx, rx, 2); if (!(rx[1] & 0x01)) return 0; } return -1; }6.3 擦除与页编程
int flash_sector_erase(uint32_t addr) { uint8_t tx[4]; flash_write_enable(); tx[0] = 0x20; tx[1] = (addr >> 16) & 0xFF; tx[2] = (addr >> 8) & 0xFF; tx[3] = addr & 0xFF; qspi_transfer(tx, NULL, 4); return flash_wait_busy(1000); } int flash_page_program(uint32_t addr, uint8_t *data, uint16_t len) { uint16_t remain = len; uint8_t tx[4 + 256]; uint16_t offset = 0; while (remain > 0) { uint16_t chunk = (remain > 256) ? 256 : remain; flash_write_enable(); tx[0] = 0x02; tx[1] = ((addr + offset) >> 16) & 0xFF; tx[2] = ((addr + offset) >> 8) & 0xFF; tx[3] = (addr + offset) & 0xFF; memcpy(&tx[4], data + offset, chunk); qspi_transfer(tx, NULL, 4 + chunk); if (flash_wait_busy(100) != 0) return -1; remain -= chunk; offset += chunk; } return 0; }这里把页编程做了拆页处理,保证跨页数据只出现在单个 Page 内。实际项目中,我还会在每 4KB 扇区写完后读回比对,一旦发现错误立即定位到具体扇区,只需要重写该扇区,不必整片推倒重来。
这套框架在多个基于 Artix-7 的板卡上跑过,配合 25MHz 到 40MHz 的 SPI 时钟,升级 8MB 镜像大概需要两到三分钟,大部分时间花在擦除和编程轮询上。如果对时间有更高要求,可以考虑在擦除和编程阶段使用 IP 的中断机制,避免 CPU 空转等待,但逻辑复杂度也会相应上升。
我个人在实际项目中的体会是:远程升级方案里,成功路径永远不是核心难点,失败路径才是。写驱动前先想清楚断电、断网、镜像损坏、Flash 异常这几类场景下设备怎么兜底,双区架构、版本标记、回退机制都设计好了,再开始写代码。最后再分享一个小技巧,升级流程上线前,拿到测试板上反复做断电实验,不固定断点,随机在擦除、页编程、校验、切换启动源各个阶段断电重启,连续跑几十轮,设备能稳定回到 Golden 镜像,这个方案才敢交付到现场。