公司动态

STM32双Bank即时固件更新:零停机IAP升级方案全解析

📅 2026/8/30 23:54:04
STM32双Bank即时固件更新:零停机IAP升级方案全解析
第一次被客户当面问“你们这个设备升级为什么非要断电”的时候我确实有点答不上来。那会儿做的是一个户外采集器用的是单 Bank 的经典 IAP每次升级都要先停掉业务逻辑把新固件拷到 RAM 再慢慢写 Flash整个过程少说几十秒设备完全瘫痪。当场被质疑的那一刻我就知道单 Bank 这套思路撑不了太久。后来换了支持双 Bank Flash 的 STM32把“跑当前程序”和“写新固件”彻底从硬件上分开才真正做到了应用不停机、升级不中断。这篇文章就把我在双存储区 STM32 上做即时固件更新的整套笔记整理出来包括硬件选型、地址布局、Bootloader 状态机、Bank 切换、跳转与向量表重定向、Flash 驱动为什么必须放 RAM 执行以及一堆我实际踩过的坑。写给准备做在线升级或者正在被停机升级折磨的嵌入式朋友。1. 为什么我放弃了停机升级双Bank带来的改变1.1 单Bank IAP最让人头疼的三个问题经典的单 Bank IAP 流程大家应该都很熟Bootloader 先跑起来然后从 UART/CAN/以太网接收完整固件放到 RAM 缓存区或者边收边写 Flash写完后校验最后跳转到 App。听起来简单但实际产品里一旦要求“运行中不能停”这套方案就很别扭。第一是应用暂停问题。单 Bank 结构下Flash 里只有一个 App 在运行你要升级就必须在这个 App 上开一个升级入口收完固件先跳回 Bootloader或者直接在 App 里执行 Flash 擦写。无论哪种业务逻辑都被打断了。如果设备正连着现场总线、正在控制电机、正在采集重要数据这一停可能就是事故。第二是擦写时长不可忽视。Flash 擦除一个扇区不同系列从几十毫秒到几秒不等如果固件有几十上百 KB几个扇区叠起来就是肉眼可见的卡顿。我见过有人在 App 里直接调 HAL_FLASHEx_Erase全局中断也不关结果擦除一半来了个 CAN 中断代码段正好在 Flash 上中断一取指就卡死最后只能看门狗复位。第三是断电风险。单 Bank 升级最怕写到一半断电Bootloader 区没被破坏还好一旦 App 区写了一半而校验区还没更新设备直接变砖只能靠恢复出厂或者外置烧录器救回来。单 Bank 能做备份恢复但要额外开一块存储做双缓存逻辑复杂Flash 占用也大。1.2 双Bank即时更新的核心思路双 Bank 的意思就是 Flash 物理上分成两个独立的存储块比如 2MB Flash 分为 Bank1 和 Bank2各 1MB。每个 Bank 都能独立擦写、独立放一套完整固件。这样升级思路就完全变了当前 App 跑在 Bank1新固件通过 Bootloader 或 App 内的升级模块写到 Bank2写完之后做一次校验然后软复位Bootloader 根据标志位决定跳转到 Bank2。整个擦写期间 Bank1 的应用照常运行用户感觉不到任何中断这就是所谓“即时更新Live Update”。一些新的 STM32 系列比如 STM32H7、STM32G4、STM32F7 的高密度型号还支持 RWWRead-While-Write读时写。也就是 CPU 可以从 Bank2 取指执行同时 Flash 控制器在擦写 Bank1 的数据区。硬件层面就把“跑程序”和“写 Flash”解耦了。即使是同一时刻发生中断中断向量和中断服务函数都落在正在执行的 Bank 里取指不会和 Flash 编程冲突实时性基本不受影响。这里有个很容易误解的点双 Bank 并不等于一定能 RWW。比如部分 STM32F4 的高密度型号也有双 Bank但 CPU 在任意时刻只能访问一个 Bank具体的 RWW 支持要看型号参考手册里的 Flash 章节。所以做方案前第一件事就是查数据手册别想当然。1.3 哪些场景真正需要即时更新不是所有项目都需要双 Bank。我个人的判断标准很简单如果升级时允许设备停机 10 秒以上那单 Bank RAM 缓存 完整校验就够用成本低、代码简单也更容易验证。但如果你的产品属于下面几类双 Bank 就是刚需现场设备升级期间还在和 PLC、上位机保持通信断开就算故障比如产线设备、储能逆变器、充电桩。远程设备无人值守升级断了电就要靠恢复机制比如井场仪表、光伏板下的采集器、LoRa 节点。对中断敏感的设备比如运动控制卡正在走轨迹的时候你让它停 5 秒机械部分可能就撞了。需要频繁灰度更新的设备比如做算法迭代的视觉网关新版本跑起来要观察不行立刻回滚旧版本双 Bank 天然支持双版本切换。我在做远程网关时就吃了亏单 Bank 方案塞到现场升级到一半网络断了固件写坏最后只能派人跑去现场用烧录器恢复。从那以后凡是需要远程升级的产品我都强制上双 Bank。2. 选型前先看清哪些STM32能玩双Bank有没有RWW2.1 型号支持情况与参数先把常用型号整理成一张表方便大家查。注意具体参数要以你自己手上的芯片型号对应参考手册为准尤其是 RWW 支持这一列。系列典型型号Flash 容量Bank 划分RWW 支持备注STM32F4F427/437/429/4392MB2 x 1MB通常不支持或受限双 Bank 主要用于并行编程/EEPROM 模拟做即时更新要看具体器件STM32F7F722/732/746/756/767/769512KB~2MB2 x 256KB~1MB支持通过选项字节可配置双 BankSTM32H7H743/753/750/7A31MB~2MB2 x 512KB~1MB支持双 Bank 固定Flash 扇区大128KB 起STM32G4G431/474/491128KB~512KB可配置双 Bank支持比较新的系列适合电机控制加 OTASTM32L4L4R/L4S2MB2 x 1MB支持低功耗场景表里几处要注意。STM32F4 的双 Bank 主要设计初衷是“同时擦写一个 Bank读另一个 Bank”但很多型号没有做完整的 RWW 支持实际跑起来要详细看手册里 RWW 相关描述。STM32H7 的 Flash 扇区特别大H743 的 2MB 版本每 Bank 1MB每个扇区 128KB也就是说你哪怕改一个字节也要先擦掉 128KB这个对升级策略影响很大。G4 的优势是可以通过选项字节把 256KB 分成两个 128KB 的 Bank小封装产品很实用。选型的时候不要只盯着 Flash 容量还要看 Flash 编程粒度。H7 的写入是 256 位32 字节一次F7 常见是 64 位或者按编程单元G4 是 64 位。这个影响你的固件写入函数怎么写也影响数据缓冲区的对齐。我写过一版从 F7 直接搬到 H7 的升级代码结果踩了 32 字节对齐的坑后面专门有一节讲。2.2 RWW能力就是方案的分水岭如果你查手册看到器件支持 RWW那恭喜可以做到真正的“零停机即时更新”。代码跑在 Bank2Flash 控制器擦写 Bank1两者并行CPU 不会因为 Flash 编程而暂停取指。这样即使你的 Flash 擦写函数写得慢一点应用的实时任务调度也不会被明显拖垮。如果不支持 RWW那“双 Bank”就退化成“双副本”升级前从 Bank2 启动然后把新固件写到 Bank1写的过程中 CPU 还是会被 Flash 编程动作阻塞因为 Flash 控制器不让你在写 Bank1 的同时从 Bank2 取指。这个时候升级仍然可以“跑一段停一段”但做不到完全无感。对很多产品来说能接受毫秒级或微秒级的短暂停顿也够用了。判断方法很简单在参考手册里搜 “Read while Write” 或 “RWW”看 Flash 章节的描述。比如 STM32F746 的 RM0385 里明确写了支持 RWWH743 的 RM0433 也写了。如果手册没提默认不支持别猜。2.3 工程上要提前准备的三个条件想把双 Bank 即时更新跑起来不是写个跳转函数就完了工程上要提前做三件事。第一确定链接脚本里的 Flash 起始地址和长度。Bootloader 通常放在 0x08000000 开始的一段区域App1 放 Bank1 偏移 Bootloader 之后的位置App2 放 Bank2 起始地址。每个 App 的链接脚本要对应改。用 GCC 的话就是修改 LDFLAGS 或者 .ld 文件里的 FLASH 段用 Keil 的话就是在 Target 选项卡里改 IROM1 起始地址和大小。第二预留好 Bootloader 的下载通道。因为双 Bank 方案里Bootloader 负责判断从哪个 Bank 启动、管理升级标志、执行跳转。Bootloader 本身必须稳定不能被 App 覆盖。我习惯把 Bootloader 限制在 64KB 或 128KB 以内剩余的 Bank1 空间全部给 App1。第三准备一个可靠的“当前运行 Bank”标志区。这个标志可以是 Flash 专门的一个扇区也可以是备份寄存器比如 STM32 的 RTC Backup Register。备份寄存器功耗极低断电不丢而且写起来不会有 Flash 擦写阻塞问题只是软件复位时不会清除可以用来记录“下次启动进哪个 Bank”和“上一次启动是否成功”。3. 整体架构与运行流程3.1 地址布局Bootloader、App、升级标志区我以一个典型 STM32H743 双 Bank 方案为例画一个地址布局描述。H743 有 2MB FlashBank1 起始于 0x08000000Bank2 起始于 0x08100000。Bootloader 放在 Bank1 起始位置 0x08000000长度 0x0001000064KB。App1 放在 Bank1 的偏移位置 0x08010000App2 放在 Bank2 起始位置 0x08100000。区域起始地址大小内容Bootloader0x0800000064KB启动判断、升级流程、Flash 驱动App10x08010000Bank1 剩余旧固件 / 新固件App20x08100000Bank2 全部新固件 / 旧固件升级标志区Flash 最后扇区或备份寄存器小boot pending、confirmed、版本号这个布局有个好处Bootloader 固定不变App1 和 App2 都是完整固件二者互不依赖。无论从哪个 Bank 启动Bootloader 都能再次接管下一次升级。如果不想 Bootloader 占 Bank1 空间也可以把 Bootloader 做到 0x08000000 之外比如用系统存储器但那样烧录和调试都不方便不推荐。3.2 核心状态机Bootloader 我简化成五个状态。上电复位读取升级标志区。如果没有 pending 标志说明没有升级任务直接根据当前有效版本跳转到 App1 或 App2。如果有 pending 标志说明上次写入了新固件还没确认Bootloader 先做一次完整校验比如 CRC32 或 SHA256。校验通过则清除 pending设置 confirmed跳转到新 Bank。校验失败则清除 pending回退到旧 Bank 启动。App 里也要配合新固件第一次启动后如果自检通过应当主动清除 confirmed 等待标志或者写一个“启动成功”标记到标志区。这样下一次 Bootloader 才知道新固件是好的不用再次回滚。这个状态机的关键点在于“pending”和“confirmed”双标志。只用单个“固件是否完整”的标志不够因为固件完整不代表能跑通很可能初始化到一半崩了。双标志配合看门狗基本能防住绝大多数变砖场景。3.3 升级流程完整走一遍假设现在设备跑的是 Bank1 里的 App1 版本 V1.2我们要升级到 V1.3。第一步App1 收到上位机发来的升级指令或者云端推下来的固件包App1 进入升级模式停止对外的非关键服务但保留通信链路和看门狗喂狗任务。第二步App1 把固件包逐段写入 Bank2。这一步如果 MCU 支持 RWWBank1 上的 App1 还能继续执行如果不支持App1 也要保证“数据接收正常”优先即使短暂停顿也必须接收完整数据。第三步全部写完后App1 对 Bank2 做 CRC 校验确认固件数据没问题。第四步App1 在标志区写入“pending Bank2V1.3”然后软复位。第五步Bootloader 上电看到 pending校验 Bank2 完整清除 pending设置 confirmed Bank2跳转到 Bank2。第六步App1 的 V1.3 启动自检通过后清除 confirmed等待下一次升级。如果第六步自检失败或者 App 起不来看门狗会复位Bootloader 再次启动发现没有 confirmed判定升级失败回滚到 Bank1 的 V1.2。用户唯一的感觉就是“升级好像失败了但设备还是原来的版本”不会变砖。4. 核心机制与关键代码4.1 Bank切换的正确姿势对 STM32H7 这类固定双 Bank 的芯片Bank 切换其实就是“选择从哪个地址启动 App”。这里有个新手容易踩的坑不要试图用选项字节做启动切换虽然在 F4/F7 上确实有 BOOT_SWAP 或 nDBANK 一类配置但 Bootloader 代码里做软件跳转要简单可靠得多而且不需要改选项字节。软件跳转的思路是在 Bootloader 里读目标 Bank 起始地址的前两个字第一个字是初始 MSP第二个字是复位向量然后设置主堆栈指针把 VTOR 指向目标地址再跳转过去。下面这段是我一直沿用的跳转函数/* jump_to_app.c */ #include stm32h7xx_hal.h typedef void (*pFunction)(void); static void Jump_To_Address(uint32_t address) { uint32_t msp_value; uint32_t reset_value; pFunction jump_func; msp_value *(__IO uint32_t *)address; reset_value *(__IO uint32_t *)(address 4); if ((msp_value 0xFFF00000) 0x20000000) { /* 栈顶指针在 RAM 范围内视为合法 */ SCB-VTOR address; __disable_irq(); __set_MSP(msp_value); jump_func (pFunction)reset_value; jump_func(); } while (1) { /* 跳转失败则死循环由看门狗复位 */ } }注意跳转前要做三件事关闭全局中断、复位 SysTick 和所有外设、重定向向量表。我见过有人只调了 __set_MSP 就跳结果 App 一初始化就进 HardFault因为 SysTick 中断还在用 Bootloader 的上下文。正确做法是先把在用的外设 DeInit把 SysTick 停掉再关中断最后跳转。4.2 跳转与中断向量表重定向向量表重定向是双 Bank 升级最绕的地方之一。ARM Cortex-M 的中断控制器默认从 0x00000000 取向量表但 STM32 通过 VTOR 寄存器可以重定向到任意地址。对于从 Bank2 启动的 App需要把 SCB-VTOR 设置成 0x08100000这样中断才能进 Bank2 的异常处理函数。有些读者会问H7 有零等待状态和指令缓存跳过去之后代码能正常执行吗答案是可以但要记得处理 I/D Cache 和 Flash 延迟。H7 的 Flash 读取走 AXI 总线跳转后如果开了 Cache旧数据可能还在 Cache 里导致代码不一致。我习惯在跳转前做一次 SCB_InvalidateICache 和 SCB_InvalidateDCache确保新代码是重新从目标 Bank 取的。另外如果 App 自己用了 RTOS要确认 RTOS 的启动代码会不会覆盖 VTOR。FreeRTOS 本身不碰 VTOR但有些 BSP 库会在 SystemInit 里重新设置 VTOR。如果你的链接脚本把 VECT_TAB_OFFSET 写死了那从 Bank2 启动时会出问题。解决方法是让 App 在启动早期根据自身地址动态设置 VTOR不要依赖编译器默认的偏移。下面是一个 App 初始化时的向量表重定向示例/* app_main.c */ void SystemInit_Redirect_VectorTable(void) { uint32_t app_base FLASH_BANK2_BASE; /* 0x08100000 */ SCB-VTOR app_base; __DSB(); __ISB(); }4.3 Flash驱动为什么必须放在RAM里跑这一步是 RWW 实现的核心很多人卡在这里。STM32H7 等器件在执行 Flash 写入或擦除时Flash 控制器会占用对应的总线资源CPU 如果从同一个 Bank 取指会和编程操作冲突。虽然支持 RWW 的芯片可以在编程 Bank1 时从 Bank2 取指但你的 Flash 编程代码本身如果放在 Bank1 或 Bank2 的 Flash 上那总有冲突的可能。保险且通用的做法是把 Flash 擦写和编程相关的函数放到 SRAM 里执行。这样无论你正在擦哪个 BankCPU 取指都从 RAM 来完全避开 Bank 冲突。实现方式用 GCC 的话就是给函数加__attribute__((section(.ramfunc)))然后在启动代码里把这部分从 Flash 拷贝到 RAM。Keil 下更简单直接在函数上标记__RAM_FUNC或者放到指定 section 即可。这里要注意如果你的函数有内联调用其他 Flash 里的函数链接器可能把它优化掉或者留下调用跳转最好把 Flash 控制器操作封装成独立的、不调用外部 Flash 函数的函数放在 RAM 区。下面是一个 H7 上从 RAM 执行的扇区擦除示例结构/* flash_program_ram.c */ __attribute__((section(.ramfunc))) static int Flash_Erase_Sector(uint32_t sector_addr) { FLASH_EraseInitTypeDef erase_cfg; uint32_t sector_error 0; uint32_t status; erase_cfg.TypeErase FLASH_TYPEERASE_SECTORS; erase_cfg.Banks FLASH_BANK_1; erase_cfg.Sector Get_Sector_Index(sector_addr); erase_cfg.NbSectors 1; erase_cfg.VoltageRange FLASH_VOLTAGE_RANGE_3; status HAL_FLASHEx_Erase(erase_cfg, sector_error); return status; }实际工程里整个升级相关的 Flash 操作包括解锁、擦除、编程、校验我都建议放到 RAM section。这样既保证不会因为 Flash 编程卡死 CPU将来想切换 Bank 也方便。4.4 最小可运行实现整理一个最小可运行的 Bootloader 流程。Bootloader 启动后从标志区读状态判断要不要升级。如果要升级就从通信接口接收固件按扇区擦写目标 Bank写完做 CRC然后跳转。固件接收我用 UART 中断加状态机数据帧格式很简单帧头命令长度数据CRC320xAA550x01 写入4 字节固件数据块4 字节App 侧先解析固件包头部知道目标 Bank、固件长度、固件 CRC。然后分包发送给 Bootloader 或者 App 自带的升级模块。收到一包就写一包写的地址是目标 Bank 基地址加偏移。因为 H7 写入是 32 字节一次一包数据最好也是 32 字节对齐避免跨地址编程。伪代码/* upgrade_engine.c */ void Upgrade_Process(uint8_t *pkt, uint32_t len) { if (!upgrade_phase_started) { if (Parse_Firmware_Header(pkt, header)) { upgrade_phase_started 1; target_addr Get_Target_Bank_Base(); flash_unlock(); } return; } if (Is_Data_Packet(pkt)) { Flash_Program_Aligned(target_addr data_offset, pkt_data, data_len); } if (Is_End_Packet(pkt)) { crc_ok Verify_Target_Bank(header.crc); if (crc_ok) { Write_Flag(PENDING_NEW_BANK); NVIC_SystemReset(); } } }这只是一个骨架。真正常用工程里还得加上超时重传、帧序号去重、固件加密等功能。但核心机制就是这几步接收、写 Flash、校验、置标志、复位、跳转。5. 实测数据与踩坑实录5.1 实测升级时序我在 STM32H743 上跑过一版 512KB App 的双 Bank 升级擦除 Bank2 全部扇区大概需要几秒到十几秒这个时间受 Flash 电压、主频和是否并发读写影响很大。真正的 RWW 优势体现在“擦 Bank2 时 Bank1 上的业务照常跑”。我在升级期间给一个 PWM 输出做示波器监测可以看到 PWM 波形几乎没有缺口只有极少数个 CPU 停顿点业务完全无感。如果你用的是不支持 RWW 的型号那擦除期间 CPU 还是会被阻塞但通过把擦写函数拆成小扇区、配合任务调度可以在每两个扇区之间让出 CPU把“不可用时间”从几十秒压缩到几十毫秒也算勉强能做到“准即时”。5.2 我踩过的几个典型坑第一个坑是 H7 的 Flash 编程对齐。H743 一次写入要求 32 字节对齐而且命令是按 256 位数据块来的。我第一次直接用HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, ...)处理结果编译过、运行不报错但数据写进去就是不对。查了半天才发现 H7 要用FLASH_TYPEPROGRAM_QUADWORD。这个问题在 F7/G4 上一般不会出现但搬到 H7 必须改。第二个坑是调试器连不上。有次我把 Flash 选项字节里的双 Bank 配置改错了导致芯片上电后完全跑飞SWD 都连不上。这时候别慌用 ST-LINK Utility 或者 CubeProgrammer 的 connect under reset 模式先把芯片复位住再全片擦除恢复。这个流程我写过一个小提醒贴在工位上Bank 配置改动前先备份整个 Flash 和选项字节。第三个坑是升级期间看门狗没喂。如果你把 Flash 擦写放到 RAM 里跑但整个擦写过程是一个长阻塞函数看门狗照样会超时复位。我当时用 IWDG512KB 擦除要十几秒看门狗默认 4 秒就 reset每次升级到一半就重启。后来把擦除循环拆块每擦完一个扇区喂一次狗才解决。第四个坑是跳转前的缓存问题。H7 开了 D-Cache 后跳转前如果没有 Clean/InvalidateApp 初始化时读到的数据可能是旧的表现为随机性故障。凡是跳转必清理 Cache这个不能省。表格整理一下常见问题现象根因解决跳转后 HardFault外设没 DeInit 或 SysTick 在搞鬼跳转前全部外设复位关中断清理 PendSV/SysTick固件写入后校验不一致编程粒度不对H7 用了 DOUBLEWORD改用 QUADWORD并保证 32 字节对齐擦除期间看门狗复位擦除耗时太长没喂狗分块擦除每块之间喂狗从 Bank2 启动后中断不进VTOR 没指向 Bank2App 初始化时动态设置 SCB-VTORSWD 连不上选项字节被改废了connect under reset全片擦除恢复5.3 回滚设计不能让设备变砖双 Bank 最大的卖点就是能安全回滚。但回滚不是简单切换地址要设计好标志语义。我的方案是在标志区维护两个 32 位字upgrade_pending和upgrade_confirmed。当 App1 把新固件写完先写upgrade_pending NEW_BANK然后复位。Bootloader 看到 pending校验新 Bank 有效后把upgrade_confirmed NEW_BANK再清除 pending跳转。新 App 启动后自检通过主动清除upgrade_confirmed或者把它写成VALID NEW_BANK。如果新 App 起不来看门狗复位Bootloader 发现upgrade_confirmed不是 NEW_BANK就知道新固件没有确认于是回滚到旧 Bank。这一套用 Flash 保存标志也可以但要注意 Flash 擦写次数和等待时间更推荐用 RTC Backup Register写一次只要几十纳秒不磨损 Flash。缺点是掉电会丢但我们的场景里掉电丢标志也没关系大不了回旧版本设备不会变砖。6. 我的体会双Bank不是唯一解但它是性价比最高的解做完这个双 Bank 即时更新方案后我对“升级”这件事的理解变了不少。以前总觉得升级就是个下载功能写个串口助手就能搞定现在明白它其实是整个系统的可靠性设计。双 Bank 用硬件冗余换软件自由度让升级从“高风险操作”变成了“可验证、可回滚的常规运维动作”。我也遇到过有人问为什么不用外部 Flash 存固件再搬运如果外部 Flash 是标配那确实可以。但很多产品为省成本没有外部 Flash芯片自带的双 Bank 就是零成本方案。还有人问为什么不用 TFTP、HTTP 这些协议直接写固件其实协议只是传输层的事底层落到 Flash 上还得靠这一套 Bank 切换和校验机制。如果你现在的产品允许停机升级我建议先别急着上双 Bank把单 Bank 的 IAP 打磨好同样能解决大部分问题。如果已经在为“升级停机”头疼那 STM32 的双 Bank 方案值得你花一两个迭代周期认真做一次。做的时候记住一句话Bank 能不能切得过去是能力问题切过去之后能不能回得来才是设计问题。把升级标志、校验、回滚链路都跑通这个系统才真正算可靠。