十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32F103 AB OTA升级实战:Bootloader、Modbus RTU与双分区回滚方案

STM32F103 AB OTA升级实战:Bootloader、Modbus RTU与双分区回滚方案 做嵌入式这一行最怕的就是设备已经部署到客户现场结果发现固件有Bug。带电开壳、飞线接JTAG、蹲在现场反复烧录……先不谈专业技术含量光是客户的夺命连环Call就够你喝一壶。如果你手里有不少基于STM32F103的产品又想把“远程升级”这件事从“看着就头大”真正落地成一套稳妥的方案那这篇AB OTA复现教程应该正好对胃口。文章不聊空话直接给出一套能跑起来的最小系统STM32F103系列芯片、标准库V3.5开发环境、RS232串口作为传输通道、Freemodbus v1.6迁移实现Modbus RTU通信固件层面采用A/B双分区设计配合Bootloader完成升级、校验、跳转和回滚。无论你是刚入手F103的老哥还是已经在用CubeMX开发但被OTA困扰的工程师这套思路都能直接拿过去参考。1. 项目整体设计与分区规划1.1 为什么要做AB OTA现场升级不翻车的底线设计先回答一个很多人纠结过的问题升级方案那么多为什么偏偏选AB分区单分区加备份恢复不行吗Bootloader加网络下载直接覆盖不行吗单分区方案最容易理解Bootloader把新固件直接写进App区写完跳转。问题是如果写入过程中掉电、固件本身有隐藏Bug、或者CRC校验逻辑写得不严谨设备直接变砖只能回厂或者现场拆机刷写。很多老工程师回忆“OTA升级项目半年一大半时间在救砖”就是因为这个。AB分区的核心思路是在Flash里同时保留两个App镜像——分区A和分区B其中一个运行另一个作为升级目标。升级时只动非运行区写完之后切换标志位重启后在Bootloader里验证新分区可用再决定继续切换还是回滚到旧分区。这套方案的工程意义非常直接任何时候Flash里都有一颗“已知能跑”的固件升级失败不至于变砖回滚不需要重新下载数据只需改一个标志位如果客户现场有双版本回退需求比如算法调参想切回旧版AB分区天然支持代价是Flash容量占用翻倍。但现在的F103高容量型号比如RCT6有256KB Flash、ZET6有512KB Flash分两个80~100KB的App区完全够用。如果你用的是F103C8T6这种64KB的小容量芯片AB方案就会比较紧张建议评估一下固件实际大小再动手。1.2 Flash分区与地址计算我这次用STM32F103RCT6做演示256KB Flash从零开始规划分区。分区名起始地址大小说明Bootloader区0x0800000032KB (0x8000)升级引导、串口驱动、Freemodbus协议App A区0x0800800096KB (0x18000)运行分区A固件链接地址App B区0x0802000096KB (0x18000)运行分区B固件链接地址参数/日志区0x0803800032KB (0x8000)升级标志、版本记录、状态保存几个计算过程给大家拆开讲。Bootloader为什么留32KB如果你只是做一个跳转小程序8KB绰绰有余。但我们要在Bootloader里跑Freemodbus、做Flash擦写、做CRC32校验、打印调试日志标准库加printf一链接体积很容易冲到20KB以上。留32KB是给自己留余地。记住Bootloader功能做得越完整App越省心但Bootloader体积越大留给App的Flash就越少这个平衡要看实际项目。App区为什么是96KBRCT6总容量256KB减去Bootloader 32KB和参数区32KB剩余192KBA/B对半分每个区96KB。App编译出来只要不超过96KB就能放。生成Map文件后用文本编辑器打开搜索“Total ROM Size”或者查看*.map文件里各个段的结束地址就能确认固件实际占了多大空间。参数区放最后32KB我实际上只用了最后几页F103大容量芯片每页2KB剩下的大半留作日志或者固件升级缓存。这个区域存放一个结构体用来记录当前活跃分区、待切换分区、固件长度、CRC值、版本号、启动次数等信息。1.3 AB OTA的完整升级状态机分区定好了接下来是最容易写乱的状态机。很多初次接触AB OTA的朋友代码写一半就开始混乱就是因为没有先把状态定义清楚。我这套方案里定义了六个状态状态含义可能来源IDLE空闲无升级任务设备上电初始化DOWNLOADING正在接收固件数据收到开始传输命令VERIFY固件接收完成正在校验CRC32收到结束传输命令PENDING新固件校验通过等待激活Bootloader确认结果ACTIVE新分区已经成为正式运行分区App启动后上报成功ROLLBACK新固件运行失败回滚旧分区启动次数超阈值完整流程是这样设备在A区正常运行上位机通过Modbus RTU发送升级命令App收到后进入DOWNLOADING状态开始往B区非当前运行区写入固件数据。每帧写进去都做一次回读校验或者累加校验全部写完做整体CRC32校验。校验通过后把参数区的状态改成PENDING然后软复位进入Bootloader。Bootloader判断参数区有PENDING状态的待切换固件先再校验一次保险起见防止App端写错位置然后置为ACTIVE并跳转到B区。B区App启动后先上报一条“升级成功”的消息给上位机同时把参数区的启动计数清零。如果App在B区压根没跑起来Bootloader里的看门狗会超时复位启动计数累加超过预设阈值就自动回滚到A区。这段链路听起来长但每一步拆开都很清晰。状态机是整个AB OTA的骨架代码可以之后再写状态转移必须先想明白。2. Bootloader从启动到跳转2.1 Bootloader整体启动流程Bootloader是上电后第一个执行程序也是AB OTA最核心的守门员。它的任务不是“下载固件”那是App和上位机的事而是“决定跳到哪里去”。我把Bootloader的启动流程精简成以下伪代码int main(void) { system_init(); // 时钟、串口、Flash、看门狗 param_load(g_param); // 从参数区读取升级记录 if (g_param.flag PENDING) { // 有等待激活的新固件先验证一次 if (verify_image(g_param.target_addr, g_param.image_len, g_param.image_crc)) { g_param.active g_param.target; g_param.flag ACTIVE; param_save(g_param); } else { // 校验失败放弃切换标记回滚 g_param.flag IDLE; param_save(g_param); } } if (g_param.flag ACTIVE) { // 启动计数器防止新固件起不来 g_param.boot_count; param_save(g_param); if (g_param.boot_count MAX_BOOT_COUNT) { // 新固件连续启动失败回滚 switch_to_rollback(); } } uint32_t app_addr (g_param.active PART_A) ? APP_A_ADDR : APP_B_ADDR; jump_to_app(app_addr); }有几点实践经验要说明。第一看门狗必须尽早打开而且Bootloader里不能喂狗至少在判断回滚逻辑之前不能喂。这样如果App跑不起来复位后Bootloader能再次获得控制权。独立看门狗IWDG超时建议设1秒左右够Bootloader完成判断又不会让系统卡死太久。第二参数区读写要加额定信号。我用的magic number是0xA5A5A5A5每次读参数区先检查magic不是这个值就按出厂默认处理。第一次烧录全新芯片时参数区全是0xFF不加magic判断的话会把一堆垃圾数据当成有效参数。第三Bootloader里也加一个串口命令入口。虽然正常情况下它几十毫秒就跳走了但调试阶段或者参数区被写坏时你能用一个超级终端连接设备输入特定命令强制停留在Bootloader里做恢复。这个“隐藏后门”在开发期价值极高。2.2 跳转函数与中断向量表重映射跳转这段有很多细节处理不好就是HardFault。先贴一个我验证过的跳转函数typedef void (*p_app_func)(void); void jump_to_app(uint32_t app_addr) { uint32_t app_sp *(volatile uint32_t *)app_addr; p_app_func app_reset (p_app_func)(*(volatile uint32_t *)(app_addr 4)); // 检查栈顶指针是否指向RAM区 if ((app_sp 0xFFF00000) ! 0x20000000) { return; } // 关闭全局中断 __disable_irq(); // 清除所有挂起的中断 for (int i 0; i 8; i) { NVIC-ICER[i] 0xFFFFFFFF; NVIC-ICPR[i] 0xFFFFFFFF; } // 设置主栈指针并跳转 __set_MSP(app_sp); // 重映射中断向量表F103的VTOR在0xE000ED08 SCB-VTOR app_addr; app_reset(); }这里有几个关键点检查栈顶地址是因为App的起始4字节存放的是初始栈指针MSP值。如果这个值不在RAM地址范围内F103的RAM从0x20000000开始说明这段Flash里压根没有有效固件跳过去必然死机。跳转前要关闭所有中断并清空NVIC的挂起标志。原因很朴素Bootloader里可能打开了串口中断、定时器中断跳转到App后App的启动代码要重新配置这些外设。如果中断在切换瞬间触发而App的中断服务函数还没准备好程序就会跑飞。我见过不止一个项目死在这个细节上。清挂起标志最好用NVIC-ICER和NVIC-ICPR分别关闭使能并清除挂起。只清挂起不关使能也可能有问题两个都做最稳。SCB-VTOR就是中断向量表偏移寄存器。老版本标准库有NVIC_SetVectorTable()但V3.5里已经不建议用我直接用寄存器操作。App端也需要做同样的处理否则中断向量表还在Flash起始地址一旦App发生任何中断都会跳到Bootloader区域去取向量结果必然是HardFault。2.3 链接脚本与编译配置要点跳转解决完下一个坑就是链接地址。F103不像Cortex-A芯片有MMU能做地址映射代码里的绝对地址在编译时就定死了。所以A区和B区的App必须在不同的链接地址下各编译一次。我是这样做的。工程目录下放两个链接脚本app_a.ld关键片段MEMORY { FLASH (rx) : ORIGIN 0x08008000, LENGTH 96K RAM (xrw) : ORIGIN 0x20000000, LENGTH 48K }app_b.ld关键片段MEMORY { FLASH (rx) : ORIGIN 0x08020000, LENGTH 96K RAM (xrw) : ORIGIN 0x20000000, LENGTH 48K }用Makefile或者批处理脚本调两次编译一次用app_a.ld一次用app_b.ld分别产出app_a.bin和app_b.bin。编译完记得打开Map文件确认一下__Vectors符号的地址确实在对应分区的起始位置。App的启动文件startup_stm32f10x_hd.s里中断向量表默认放在Flash起始地址也就是链接脚本里FLASH的ORIGIN位置所以不需要手动修改启动文件链接器会自动对齐。App端main函数开头做两件事第一把SCB-VTOR设成自己所在分区的起始地址第二把自己所在的分区号写进一个全局变量方便OTA逻辑判断当前运行在哪个区。区分A/B的办法最土也最有效各分区固件在编译时通过宏定义写入不同分区号或者运行时直接从SCB-VTOR读取当前地址来判断。3. 串口链路与Modbus RTU协议实现3.1 传输选型为什么是RS232加Modbus RTUOTA的传输通道可选项很多以太网、4G模块、Wi-Fi模块、LoRa、CAN、串口。为什么这套教程挑RS232加Modbus RTU原因很简单这是工业现场最“不挑环境”的组合。很多电力、工控、仪器仪表类设备本身就带RS232或者RS485接口硬件不用改。Modbus RTU协议栈有现成的开源库标准统一哪怕你换一套上位机软件只要遵循相同的功能码和数据格式协议层完全不用动。更重要的是Bootloader里也要实现同样的通信协议Freemodbus是C语言写的去掉操作系统相关代码后可以跑得非常精简塞进Bootloader毫无压力。如果你的设备是RS485原理完全一样只是把波特率和收发方向控制DE/RE引脚多处理一下。串口初期调试建议先用RS232逻辑简单不需要控制收发切换等协议层跑通再换RS485不迟。3.2 Freemodbus v1.6移植要点Freemodbus库本身不复杂核心文件是mb.c、mbfunc.c、mbcrc.c需要你根据平台填充的是几个移植层文件portserial.c串口底层驱动负责串口初始化、发送一帧数据、接收字节中断回调porttimer.c定时器驱动用于产生Modbus RTU的3.5字符超时中断port.h定义字节类型、临界区关/开中断、字节序等宏我用标准库V3.5串口用USART2波特率115200。注意波特率越高Modbus RTU对定时器的精度要求越高。3.5字符时间间隔的计算公式是3.5乘以字符位数除以波特率。115200波特率、8数据位、1停止位、无校验的情况下1 bit时间 1/115200 8.68us一个字符算11位含起始位和停止位3.5字符时间大约是3.5 * 11 / 115200 334us。定时器建议直接基于SysTick或者TIM2做微秒级计数精度不够会让RTU拆帧错乱。具体的移植方式Freemodbus包里有portserial.c模板。你需要把里面几个函数填上BOOL xMBPortSerialInit(UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, UCHAR ucParity) { // 配置USART2的GPIO、模式、波特率 USART_InitStructure.USART_BaudRate ulBaudRate; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART2, USART_InitStructure); USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); USART_Cmd(USART2, ENABLE); return TRUE; } void vMBPortSerialEnable(BOOL xRxEnable, BOOL xTxEnable) { // 使能/禁止接收、发送中断 if (xRxEnable) { USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); } else { USART_ITConfig(USART2, USART_IT_RXNE, DISABLE); } } BOOL xMBPortSerialPutByte(CHAR ucByte) { while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) RESET); USART_SendData(USART2, (uint8_t)ucByte); return TRUE; } BOOL xMBPortSerialGetByte(CHAR *pucByte) { *pucByte (CHAR)USART_ReceiveData(USART2); return TRUE; }串口中断里收到一个字节后调用pxMBFrameCBByteReceived()发送完成TXE或者TC后调用pxMBFrameCBTransmitted()Freemodbus内部会自己处理帧组装和CRC校验。3.3 自定义OTA功能码与帧格式设计Freemodbus自带的标准功能码03读寄存器、06写单个寄存器、16写多个寄存器等足够做常规数据交互但OTA传输需要的是“大块流水数据”所以我在标准库之外自定义了一组私有功能码。Modbus规范里非官方功能码区域主要集中在0x65~0x72和0x78~0x7F实际工业项目里很多人也用0x50附近做私有扩展只要上位机和下位机约定一致就没有兼容性问题。我定义的OTA私有功能码功能码名称方向含义0x50OTA_START上位机→设备开始升级携带目标分区、固件长度、CRC320x51OTA_DATA上位机→设备传输一帧固件数据携带帧序号和数据体0x52OTA_END上位机→设备结束传输触发整体校验0x53OTA_STATUS设备→上位机查询设备当前升级状态0x50开始升级的报文格式这样定义字节偏移内容长度说明0从机地址1如0x011功能码10x502目标分区10xAA表示A区0xBB表示B区3~6固件长度4大端单位字节7~10CRC324整包固件的CRC32校验值11~18版本号8ASCII字符串之后CRC162Modbus RTU帧校验0x51数据帧报文中固件数据区我每帧固定200字节。为什么是200Modbus RTU最大帧长度是256字节去掉地址1字节、功能码1字节、帧序号2字节、数据长度1字节、CRC校验2字节真正的数据空间上限是247字节。留点余量选200字节一帧总长206字节紧凑又不越界。计算一下总帧数96KB固件除以200字节大约需要491帧。115200波特率下每帧约18ms纯数据时间大约9秒。如果加大前导头和校验全流程15秒以内能完成。如果你用9600波特率总时间会拉到两三分钟也不是不能用但现场操作体验会差很多。3.4 串口传输可靠性帧间隔和超时重传Modbus RTU是靠“静默时间”来分割帧的一帧结束之后如果线路上超过3.5个字符时间没有新数据从机就认为当前帧接收完毕。Freemodbus的porttimer.c就是在做这件事接收中断每收到一个字节就重置定时器定时器超时3.5字符时间就调用vMBPortSerialEnable关闭接收中断然后把完整帧交给上层解析。OTA传输最怕的就是帧间隔太长被误拆帧。因为固件包里可能连续出现多个字节的0x00或者0xFF这些字节本身不会让UART产生额外延迟但如果上位机发送时两次写串口之间间隔超过3.5字符时间115200波特率下约334us下位机就会把一帧拆成两帧OTA数据就乱了。解决办法有两个实践中最稳的是上位机每帧一次性写入完整缓冲区不要一个字节一个字节地写。串口驱动层面或者操作系统调度层面都要保证连续发送。还有一个辅助办法把RTU超时计时器放宽到5字符时间牺牲一点点实时性换取对噪声的容忍很多工业环境里这样改更好用。重传机制放在上位机侧实现。上位机发一帧0x51数据后等待设备回一个应答我用的应答是设备已写入的帧序号超时500ms没有应答就重发当前帧。设备侧要记录“最后成功写入的帧序号”这样重传时直接告诉上位机从哪里继续。4. App端接收固件与Flash操作4.1 Flash擦写策略边收边写还是收完再写在App里接收OTA数据和写到Flash顺序上有个纠结是一边收一边写还是全部收到RAM再写F103的RAM只有48KB装不下96KB固件所以主流做法是边收边写。边收边写又分两种细节策略。第一种收到一整帧200字节后立即擦除对应页并写入。这种策略实时性最高但F103大容量芯片每页是2KB擦除粒度大于写入粒度每次擦页都会连带把新旧数据混在一起所以更合理的做法是边收边“按页缓存”攒够2KB再擦写一整页。我实际用的方案是按页缓存RAM里开一个2KB缓冲区每收到一帧200字节就往缓冲区里塞塞满10帧2KB后一次性擦除目标Flash页把缓冲区写入然后清空继续。如果最后一页不够2KB在OTA_END时把残余数据补齐写入。这样做的好处是大幅减少Flash页擦除次数。96KB固件如果每200字节就擦一次页要擦约491次按页缓存后只需擦48页寿命和耗时都友好很多。4.2 Flash写入代码与掉电保护标准库V3.5的Flash驱动函数还是老一套FLASH_Unlock、FLASH_ErasePage、FLASH_ProgramHalfWord。要点是写入操作必须按半字16位对齐。如果数据长度是奇数最后一字节要单独处理否则会写坏地址。我的写入函数做了地址对齐和半字写入void flash_write_buf(uint32_t addr, uint8_t *buf, uint32_t len) { FLASH_Unlock(); FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); uint16_t *p16 (uint16_t *)buf; uint32_t aligned_len len ~1; // 偶数部分 for (uint32_t i 0; i aligned_len; i 2) { FLASH_ProgramHalfWord(addr i, *p16); } if (len 1) { // 奇数尾部处理 uint16_t last_word buf[len - 1]; FLASH_ProgramHalfWord(addr aligned_len, last_word); } FLASH_Lock(); }注意写Flash期间绝对不能关中断或者喂看门狗把时间拖长。F103在擦写页时是阻塞的一次页擦除典型耗时20~40ms。如果看门狗超时时间设太短比如几十ms烧录过程中会被狗咬复位。所以OTA写入期间要么暂时延长看门狗喂狗间隔要么在每次擦页前先喂一次狗。掉电保护靠的是“状态机双区”写入过程中掉电参数区标记还没改为PENDINGApp侧最多丢一帧数据重新上电后继续传输即可不需要额外做复杂的掉电续传。4.3 固件校验与版本检查固件写完不是结束必须做整体校验。在OTA_END流程里App把所有已经写入的数据读回来算一遍CRC32和OTA_START帧中携带的CRC32比对。CRC32算整个Flash区域时注意不要读参数区只读目标分区范围内的数据。Flash读取速度远快于串口96KB全程读回来算CRC在72MHz主频下几十毫秒就能完成几乎没有时间成本。版本检查要放在校验之前做而且必须做“防呆”处理。我遇到过现场同事拿着旧版本的升级包去升新设备结果固件越升越旧的情况。所以在OTA_START时App会拿新版本号和当前运行固件版本号对比新版号才允许升级相同版本号提示重复旧版本号默认拒绝除非上位机带强制降级标志。这个策略避免了很多低级事故。校验不通过时App要返回明确错误码并且不修改任何分区切换标志。设备继续跑老固件完全不受影响。4.4 双分区切换与回滚机制实现这是整套方案最有含金量的部分。App在接收到完整固件并通过CRC校验后往参数区写入目标分区号和PENDING标志然后软复位。具体参数结构体这样定义typedef struct { uint32_t magic; // 0xA5A5A5A5 uint8_t active_region; // 0xAAA区 0xBBB区 uint8_t pending_flag; // 0x00无 0x01待激活 uint8_t boot_count; // 启动计数 uint8_t reserved; uint32_t image_len; // 新固件长度 uint32_t image_crc; // 新固件CRC32 uint8_t version[8]; // 新固件版本号 } sys_param_t;回滚的判定逻辑是这样的Bootloader每次跳转到ACTIVE状态的分区前把boot_count加1。App正常运行后比如跑了10秒或者收到上位机确认消息把boot_count清零。这样如果新固件一启动就死机看门狗复位后Bootloader再跑发现boot_count已经超过阈值比如3次就自动把active_region改回旧分区回滚完成。这个方案在工程实践里最大的坑是“App清了启动计数但实际上功能有隐藏Bug运行几十分钟才崩溃”。AB回滚只能防启动崩溃防不了运行时逻辑错误。所以更严谨的做法是App运行后上报心跳上位机确认新版本稳定运行一定时间后再发指令固化状态、允许下次覆盖。简单场景下Bootloader的启动计数回滚已经足够用了。5. 实战踩坑与问题排查实录5.1 跳转后HardFault从症状反推原因跳转后最常见的现象就是HardFault。排查的时候按顺序来先确认跳转地址对不对。调试器停在HardFault_Handler里查看PC寄存器和LR寄存器如果地址落在0x08020000附近B区地址但代码里却按A区地址取值那就是链接脚本和实际跳转地址对不上。再确认SCB-VTOR有没有设置成功。在jump_to_app里设置后可以加一行读回检查SCB-VTOR app_addr; uint32_t check SCB-VTOR;如果读回值和期望值不一致大概率是不小心关了总线时钟或者访问了错误地址。最后确认中断向量表是否真的被编译到对应分区起始位置。打开Map文件搜索__Vectors它必须在0x08000000加上分区偏移的位置。如果还在0x08000000说明启动文件或者链接脚本有问题。5.2 串口丢帧帧乱码的排查套路串口层的问题比Flash问题更隐蔽。如果你的OTA总是传输到一半就CRC失败或者Freemodbus频繁超时按这三个方向排查。第一量一下波形。示波器挂到TX/RX引脚上看帧与帧之间的间隔是否稳定。如果间隔抖动明显大概率是上位机发送端有延时。我遇到过一次Windows下用Python的serial.write()每帧分多次拼接前导数据之间间隔拉到好几毫秒下位机直接拆帧。第二检查波特率误差。F103用外部8MHz晶振如果晶振本身精度差或者内部RC没配准115200波特率会累积误差。用示波器量一下实际波形周期如果偏差超过2%就要换晶振或者降低波特率。第三检查Freemodbus定时器配置。porttimer.c里的计时周期要和波特率严格匹配。我踩过最坑的一次是把定时器改成了微秒计数但宏定义用的还是US_TIMER_TICKS_PER_US1结果帧间隔判断整整快了一倍所有合法的RTU帧都被当成噪声丢弃。5.3 Flash写入失败和校验不过的原因分析Flash写入失败除了地址越界、Flash锁没解锁之外最常见的原因是FLASH_ProgramHalfWord的地址没对齐。RAM缓冲区起始地址如果是uint8_t数组取地址时看末位是不是2的倍数。用uint16_t *强转之前先确认原地址对齐否则直接崩溃。校验不过的情况要区分是写入错误还是读取错误。我的排查方法是写入完成后立即把同一个地址的数据读出来和源缓冲区对比打印出错时的Flash地址、期望值和实际值。如果出错地址按规律偏移比如总是差一个偶数偏移量恭喜你写入时地址指针算错了。5.4 再进一步升级包加签验签和防回滚基础AB OTA跑通后如果产品要出批量强烈建议加上固件签名验证。方案不需要太复杂用对称加密或者简单哈希加盐也行但正规产品一般用非对称签名固件编译后用私钥签名Bootloader里内置公钥跳转前验证签名非法固件直接拒绝。加签之后Bootloader的校验流程变成这样先算CRC32确认数据完整再做签名验证确认来源可信两者都通过才允许切换分区。防回滚则在参数区加一个版本号字段只允许版本递增不允许降级。这些措施在汽车电子和电力设备行业已经是硬性要求如果你做的产品以后要走认证提前把验签框架搭好能省很多事。6. 复现这套方案的一点实践经验最后分享几个我做完整套方案后最有体感的细节。第一开发调试阶段Bootloader一定要留串口命令行交互口。我一开始把Bootloader写得“一把梭”上电就跳App结果参数区改错一次整块板子变砖只能拿ST-Link重新烧。后来加了一个调试后门上电后PC发任意字符设备就停留在Bootloader命令行模式可以读Flash、看参数区、擦除固件、写测试数据。这个后门让我后续调试效率提升了不止一倍。第二A/B两套固件的区分标志要在App里做得很显眼。比如开机后串口打印APP-A v1.2.0或者APP-B v1.2.0OLED屏上也显示当前分区。否则测试的时候很容易搞不清楚自己到底在跑哪份固件明明改了B区代码却一直在看A区现象最后排查半天发现方向错了。第三升级过程最好周期性打印进度。哪怕只是串口输出一行[OTA] 45/491 frame received配合上位机日志能够极其高效地定位是上位机发送问题、串口链路问题还是下位机写入问题。一条清晰的数据流水线比任何花哨的调试工具都管用。AB OTA这套东西其实不复杂但细节量非常大。从分区规划、链接脚本、Bootloader跳转、Modbus协议封装到App侧的Flash写入和回滚策略每一环单拎出来都是基础功串起来就是一套能上生产环境的升级体系。照着上面的步骤把最小系统跑通再根据自己的产品形态替换通信链路比如换成4G模块、以太网、CAN剩下的工作就是填协议适配的边边角角了。
返回列表