ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

STM32G4双Bank Flash实现安全OTA升级与启动原理详解

STM32G4双Bank Flash实现安全OTA升级与启动原理详解 做嵌入式固件开发的朋友几乎迟早会撞上“现场升级”这道坎。以前我在一些不带双Bank的MCU上做OTA最提心吊胆的就是升级过程中断电或者新固件校验通过但跳过去就死机最后要么返厂要么用烧录器救砖。后来换到STM32G4上做电机控制项目发现G4自带的双Bank Flash是一把非常好用的钥匙——只要把启动机制捋清楚就能实现真正意义上的安全升级甚至在电机转着的时候把固件换掉。这篇东西没有高深的理论全是基于实际工程经验的总结适合正在用G4做产品、想上OTA或者想搞明白双Bank启动原理的朋友。标题里的“双Bank启动与升级”说白了就是两个问题一是G4这颗芯片怎么在硬件层面支持从两套独立固件中选择一套来启动二是怎么把这种支持变成可靠的产品升级方案。这两个问题不解决写出来的升级代码就是空中楼阁。咱们一个个说。1. 先搞清楚双Bank到底是什么1.1 Flash布局与RWW机制STM32G4系列有不少型号的Flash容量做到256KB甚至512KB但“大容量”不是G4的全部卖点真正有意思的是它内部的Flash可以配置成两个独立的Bank。以512KB的型号为例当配置为双Bank模式后Flash被切成两块完全独立的区域Bank1起始地址 0x08000000容量256KBBank2起始地址 0x08040000容量256KB这里的关键词是“完全独立”。独立到什么程度硬件的Flash控制器允许CPU从Bank1取指令执行的同时对Bank2做擦除和编程操作反过来也一样。这个能力在ST的文档里叫RWWRead While Write翻译过来就是“边读边写”。咱们对比一下以前的老办法就好理解了。早年用F1或者F4做在线升级经常要做一次“搬移”先把Flash写入程序拷贝到RAM里再从RAM执行写Flash操作因为CPU一旦去写Flash同一时刻就不能从Flash取指令了一取就死。这个过程非常别扭而且一旦搬移代码里有Bug整个升级就完蛋。而G4的双Bank模式把这个操作变成了硬件层面的并行能力不需要搬移代码直接在Bank1里跑着正常业务另一只手去擦写Bank2。实际上RWW的意义不仅在于升级。你可以把日志、参数表放在Bank2程序在Bank1运行需要更新参数时直接改Bank2的内容不影响主程序的实时性。这在电机控制、电源管理等对实时性敏感的场景里非常实用。1.2 为什么双Bank对升级来说这么关键很多人觉得升级不就是“把新固件写到Flash里然后重启”吗如果产品是自己开发板随便造都行。但量产设备升级最怕三个问题第一升级中断。不管是WiFi断连、用户拔电还是通信干扰升级过程一旦中断Flash里就剩下一个写了一半的僵尸固件机器变砖。双Bank模式下新固件写入的是当前没有运行的那个Bank老固件所在Bank一直保留着所以任何中断都不影响设备继续运行。第二升级期间系统停机。很多设备是不能停的比如工业现场的电机控制器你让它停下来升级整个产线就停了。G4双Bank支持“后台升级”新固件下载和写入期间当前固件照常跑只有最后切换的那一刻才需要重启几十毫秒搞定。第三固件质量风险。就算升级包下载完整新固件也可能有隐藏Bug跑起来就崩。双Bank模式下切到新版本之后如果发现问题可以随时切回旧版本相当于给固件上了一道保险。这三个需求组合起来其实就是产品级OTA的完整画像。搞明白了双Bank的初衷后面所有操作都有了解释。2. 启动流程与选项字节双Bank的“方向盘”2.1 复位后CPU从哪里开始执行要理解双Bank启动得先理解STM32G4复位后的启动流程。G4跟其他STM32一样上电复位后CPU从某个地址取第一条指令这个地址就是启动地址。启动地址由BOOT引脚和选项字节共同决定。在G4上启动源主要有这么几个主Flash、系统Flash内置Bootloader、SRAM。我们一般用主Flash也就是0x08000000。但这里有个容易忽略的细节选项字节里有nBOOT0、nBOOT1、nBOOT_SEL这些位它们决定了到底是“引脚说了算”还是“选项字节说了算”。比如nBOOT_SEL1时Boot0引脚被忽略直接按nBOOT0/nBOOT1的值来选择启动源。这么做的好处是量产时板子上的Boot引脚可以随便接不需要人工干预。如果只看到这一层你会觉得G4的启动流程跟普通的STM32差不多。但双Bank模式出来之后事情多了一个维度从主Flash启动时具体从主Flash的哪个地址启动答案是看选项字节里的BOOT_SW位。2.2 选项字节里的BOOT_SW硬件启动方向开关在双Bank模式下选项字节DBANK1如果启动源是主Flash那么复位后BOOT_SW0CPU从Bank1的起始地址0x08000000取向量表BOOT_SW1CPU从Bank2的起始地址0x08040000取向量表这个机制是硬件层面自动完成的不需要Bootloader跳转也不需要软件干预。CPU复位后读选项字节然后直接定位到对应Bank。有人会问这不就相当于一个“硬件跳线”吗确实是。而且它的好处是这个跳线开关在Flash里程序自己就能改。这意味着一个正在Bank1上运行的App完全可以通过修改这个选项字节让系统下一次复位后自动从Bank2启动新固件就这么“激活”了。这里要特别提醒一个容易混的点BOOT_SW只是决定“从哪个Bank启动”跟“从哪个启动源启动”是两码事。哪怕BOOT_SW1指向Bank2如果BOOT引脚配置成从系统Flash启动那CPU还是去跑系统自带的Bootloader不会理睬Bank2。所以调试的时候要同时确认两个维度。2.3 每个Bank里的App必须自己管好中断向量表双Bank硬件启动机制看着很美好但有一个软件层面的配套工作躲不掉中断向量表。ARM Cortex-M4处理器复位后先读地址0x00000000处的栈顶指针再读地址0x00000004处的复位中断函数地址。当从Bank2启动时硬件会把0x08040000映射为“取向量表的位置”但运行起来之后中断向量表默认还是跟在0x00000000或者映射地址后面。如果你的App在Bank2而中断向量表没有切过去那任何一个中断触发CPU都会去Bank1的开头找中断服务函数——大概率是找错地方直接HardFault。解决办法是每个App在启动早期把向量表偏移寄存器SCB-VTOR设置成自己所在的Bank基地址。比如Bank2的程序要执行SCB-VTOR 0x08040000;对应地编译时Bank2的App要把链接起始地址设为0x08040000向量表就会生成在这个地址。这两个设置必须匹配否则必然出问题。3. 双Bank升级的三种典型方案3.1 方案ABootloader 启动标志位最稳妥的“万金油”第一个方案也是我在量产项目中用得最多的方案。架构很简单芯片里放一小段Bootloader占用Flash最前面的十几个KBBank1放App ABank2放App B在Flash末尾或者备份寄存器里存一个“当前启动哪个Bank”的标志位Bootloader的工作逻辑如下上电后先检查标志位如果标志位指向Bank2就校验一下Bank2的固件完整性通过则跳转不通过则回退到Bank1。如果标志位指向Bank1就直接跳Bank1。跳转操作本身就是前面的JumpToApp函数不复杂。有人觉得多一个Bootloader很麻烦但它的好处非常大Bootloader本身不参与业务不擦除自己也不写自己它只负责引导和兜底。即使两个App都出问题Bootloader还在还能进入升级模式设备永远不会变砖。这个方案下App之间的切换逻辑全由软件标志控制不依赖BOOT_SW。好处是灵活可以在标志区写“新固件已就绪”、“新固件运行正常”、“回滚次数”等各种状态。缺点是每一次启动都要经过Bootloader启动时间多几毫秒但通常无所谓。3.2 方案B硬件双Bank切换改一个选项字节就换系统第二个方案就是最好地利用了G4硬件特性的做法没有独立Bootloader或者说Bootloader被简化到了极致。架构是Bank1和Bank2各放一个完整的App每个App自己具备接收固件、写Flash、修改选项字节的能力。系统运行时要么在Bank1要么在Bank2升级流程就是当前在Bank1运行收到并校验好新固件后把它写入Bank2然后修改选项字节BOOT_SW1执行系统复位硬件自动从Bank2启动。这个方案看起来比方案A“高级”但有个前提条件你能保证Bank2里随时有一份可用固件。如果是第一次量产Bank2是空的BOOT_SW又是0没问题。一旦开发过程中调试过BOOT_SW不小心把启动指向了空Bank那就得靠烧录器救了。不过方案B在量产维护里有个很爽的场景新固件在Bank2上跑崩了想回退只需要在Bank2的App里写一个指令把BOOT_SW改回0再复位就回到Bank1。如果连Bank2的App都跑不起来那就需要外部机制帮忙了比如预留一个IO口检测让Boot引脚或者系统Flash的引导逻辑介入。所以这个方案虽然简洁但工程韧性不如方案A。3.3 方案C函数指针直接跳转不经过复位的“热切换”第三种方案比较特殊它不修改任何选项字节也不复位而是当前正在运行的App把另一个Bank的App“拉起来”。具体做法是确认另一个Bank的栈顶指针和复位函数地址有效然后关闭外设、关闭中断、设置VTOR、设置MSP最后用函数指针跳转过去。这个办法在一些需要“无缝切换”的场景里有意义比如不想让外设经历复位导致的重新初始化。但我要泼一盆冷水这种跳转对代码的编写要求极高。你得确保跳转前后所有外设状态、中断优先级、时钟配置都被处理干净否则新App初始化时很可能会踩到老App留下的坑。而且因为没有经过复位下次真正断电重启时系统还是会按BOOT_SW或者Bootloader标志走新固件并没有被“铭刻”为默认启动项。所以方案C更适合做临时切换、A/B测试不适合作为正式升级手段。4. 完整实操从编译配置到升级链路落地4.1 编译两个App链接脚本是关键双Bank升级的第一步是让Bank1和Bank2的App能分别编译到对应地址。以STM32CubeIDE为例你需要维护两个链接脚本或者用宏来控制。Bank1的App链接配置Flash起始地址0x08000000Flash长度0x40000也就是256KBRAM保持不变Bank2的App链接配置Flash起始地址0x08040000Flash长度0x40000RAM保持不变这套配置在项目里通常通过两组启动文件和链接脚本实现或者用编译宏在同一个工程里切换。具体到代码里每个App开头要设置向量表偏移#define APP_START_ADDR 0x08040000 void SystemInit(void) { // ...原有时钟初始化... SCB-VTOR APP_START_ADDR; }注意SystemInit的位置很重要最好在进入main之前就把VTOR设置好。如果你的代码是在SystemInit之后、main函数里才设置其实也来得及但中间如果发生中断风险就大一点。我习惯把VTOR设置放在SystemInit的最前面。4.2 Bootloader的跳转代码与校验逻辑如果走方案ABootloader的核心逻辑就两部分校验和跳转。校验简单做法是遍历整个App区域计算CRC32跟预先存在App头部或者标志区的CRC值比对。更简单的方式是只校验App前几个关键词是否有效比如栈顶地址是否在RAM范围、复位函数地址是否在Flash范围。这两个检查通过跳转基本就能成功。跳转代码我贴一段typedef void (*AppEntry)(void); void boot_jump_to_app(uint32_t app_addr) { uint32_t app_stack *(volatile uint32_t *)app_addr; uint32_t app_reset *(volatile uint32_t *)(app_addr 4); // 简单合法性检查 if ((app_stack 0x2FFE0000) ! 0x20000000) return; // 栈顶不在RAM范围内拒绝跳转 if ((app_reset 0x08000000) ! 0x08000000) return; // 复位函数不在Flash范围内拒绝跳转 __disable_irq(); // 按需关闭已打开的外设时钟和中断具体看工程 SCB-VTOR app_addr; __set_MSP(app_stack); AppEntry jump (AppEntry)app_reset; jump(); }这里有个细节很值得注意关闭全局中断之前最好把SysTick、DMA、串口中断等全部Deinit一下。因为有些外设的ISR里会访问当前Bank的Flash跳转之后中断向量表换了但外设寄存器还保留着原来的状态一旦中断触发可能跑到错误的处理函数里去。4.3 APP端升级流程写入Bank2的正确姿势当方案A或方案B的App收到完整的升级包后就需要把新固件写入另一个Bank。这个过程的代码我一开始是照搬ST官方例子后来踩了坑才明白G4的Flash写入有几个硬性要求。G4的Flash编程最小单位是双字64位也就是一次要写8个字节。所以你要把数据整理成uint64_t格式或者两个uint32_t拼起来再写HAL_FLASH_Unlock(); FLASH_EraseInitTypeDef erase_cfg {0}; uint32_t page_error 0; erase_cfg.TypeErase FLASH_TYPEERASE_PAGES; erase_cfg.Banks FLASH_BANK_2; erase_cfg.Page 0; erase_cfg.NbPages 128; // 假设Bank2共128页每页2KB HAL_FLASHEx_Erase(erase_cfg, page_error); uint64_t data; for (uint32_t i 0; i image_len; i 8) { data *(uint64_t *)(src_addr i); if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, dst_addr i, data) ! HAL_OK) { // 处理写入失败 break; } } HAL_FLASH_Lock();这里有几件事要特别留意第一擦除时指定的Banks参数非常重要。如果当前程序在Bank1擦除Bank2是安全的但如果你不小心把Banks传成了FLASH_BANK_ALL或者当前程序和数据都在Bank2上那擦除操作会把自己正在执行的代码干掉结果就是当场死机。第二编程过程中不允许打断。这里说的“打断”包括中断服务程序里访问正在编程的Flash区域。如果有一个中断函数恰好放在Bank2而你在写Bank2中断触发后CPU取指令直接卡死。所以最省心的做法是在脏活累活期间把中断优先级降下来或者先把代码和数据都放在Bank1。第三写入之前最好确认目标地址没有被读保护或者PCROP专有代码保护覆盖。调试阶段我遇到过一个问题用ST-Link烧录时勾选了对Bank2的读保护结果App里擦除Bank2总是报错卡了很久才发现是保护位搞鬼。4.4 最后一步激活与切换新固件写完并校验通过后就要把它“激活”了。不同方案激活方式不一样。方案A下只需要修改标志位// 假设标志区在备份寄存器或用户Flash指定位置 flag_set(FLAG_BOOT_BANK, 2); NVIC_SystemReset();复位后Bootloader看到标志是2就会跳转到Bank2。方案B下需要修改选项字节。G4修改选项字节的流程专门有个接口HAL_FLASH_Unlock(); HAL_FLASH_OB_Unlock(); FLASH_OBProgramInitTypeDef ob {0}; HAL_FLASHEx_OBGetConfig(ob); ob.OptionType OPTIONBYTE_OPTION; ob.BOOT_SW OB_BOOT_SW_BANK2; // 或1具体以库定义为准 HAL_FLASHEx_OBProgram(ob); HAL_FLASH_OB_Lock(); HAL_FLASH_Lock();这里有个细节修改选项字节后部分选项字节会立即生效但BOOT_SW这类启动相关的位最好还是跟一个复位配合。实际操作中我就是在改完后直接NVIC_SystemReset()。如果不复位BOOT_SW的改动可能要等下一次上电才生效容易造成困惑。5. 常见问题排查与避坑实录5.1 跳转过去就死机十有八九是向量表或栈顶问题在Bootloader跳转到App的过程中如果App没反应或者一进就HardFault我排查的顺序是固定的第一先看App首地址的4个字节是不是有效的栈顶地址。很多情况下App编译出来首地址不是0x2000xxxx而是别的乱七八糟的值。这种问题要么是链接脚本里Flash起始地址配错要么是烧录时没有把App烧到对应地址。第二看复位函数地址是不是在App的Flash范围内。有时App的复位函数确实不在首地址4的位置比如调试模式下用了其他入口这种情况跳转过去肯定跑飞。第三确认SCB-VTOR有没有设置成功。我遇到过一种情况某个App在SystemInit里写了VTOR但后面某个外设初始化库又把VTOR覆盖回0了导致中断一触发就挂。后来我在所有外设初始化完成后再设置一次VTOR。5.2 Flash写入一直失败检查地址对齐和保护位G4要求8字节对齐如果你的源地址、目标地址没有8字节对齐HAL_FLASH_Program直接返回错误。这个好排查关键是很多人容易忽略数据长度不是8的倍数时最后几个字节要单独处理不能死等。另一个经常出问题的是Flash写保护。ST-Link用久了可能会在调试配置里无意间设置了读保护级别或者写了PCROP。遇到Flash擦写失败第一步就是读一下选项字节看看保护位是不是被改了。如果是解除保护后再测。5.3 升级后新固件跑不起来怎么回滚这就是双Bank比单Bank强的地方。如果你走的是方案ABootloader里的回滚逻辑应该这样设计App启动后主动向标志区写一个“运行正常”的标记比如在main函数跑完外设初始化后由App置位。下次Bootloader启动时如果发现上次运行的那个Bank没有置位“运行正常”就自动切到另一个Bank。方案B里回滚就要依赖App自己了。如果新固件跑不起来它里面的代码根本没机会执行那就没办法自己改BOOT_SW。所以方案B一般会在Boot引脚上做文章或者保留一个外部IO触发强制走系统Flash再由系统Flash里的固件去改选项字节。这也是为什么我一直强调方案A更稳。5.4 双Bank升级与FOC电机控制的实战体会最后聊点实际的。G4这颗芯片在电机控制领域非常常见加上CORDIC数学加速器跑FOC很舒服。但电机控制的现场条件往往很苛刻不能停机不能抖手头没有烧录器只能靠通信口升级。我给客户做过一个方案主程序跑FOCPWM中断频率是20kHz主循环负责通信和远程命令处理。升级时固件包通过CAN总线分包下发主循环里收包、解析、写入Bank2同时PWM中断照常运行电机一点不受影响。这个阶段其实没有任何风险因为Bank1完全没有被动过。等固件包全部收完CRC校验通过我才在某个控制空闲窗口里做两件事一是把新固件的入口地址记到备份寄存器二是执行系统复位。复位后Bootloader根据标志跳转到Bank2的新固件。如果新固件初始化失败我这里还有一个看门狗兜底新固件启动后必须在500ms内喂狗否则看门狗复位Bootloader再次判断“上次固件没正常运行”自动回退到Bank1。这套方案实测下来升级过程对系统的冲击只有最后复位那一刻的几十毫秒其余时间设备完全正常。客户对这个体验非常满意。如果你打算在自己项目里引入双Bank升级我的建议是不要一上来就追求最精简的“无Bootloader方案”先用方案A把整条链路跑通理解每个环节的坑再根据产品形态决定要不要极端优化。毕竟升级功能是最后一道安全网它自己绝不能变成风险点。
返回列表