尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32F103 AB双分区OTA升级实战:Bootloader与回滚机制详解

STM32F103 AB双分区OTA升级实战:Bootloader与回滚机制详解 1. 项目概述为什么我决定给F103做AB OTA做嵌入式这几年OTA升级一直是个绕不开的话题。STM32F103的OTA方案网上一搜一大把但大部分都是“Bootloader APP单分区”的玩法Bootloader里收固件写进Flash校验完直接跳转做完就完事。这种方案看起来简单真正用到产品上就暴露问题了最典型的就是升级到一半断电、固件刷坏了、新版本有bug跑不起来设备直接变砖只能返厂或者拿串口线去救。我这次要复现的就是一个能真正用在产品上的方案STM32F103 AB双分区 OTA远程升级。标题写的是“AB_OTA”核心思路是给固件准备两个独立运行槽位平时在A槽跑新固件下载到B槽校验通过后切换启动槽位新固件万一跑不起来Bootloader能自动回滚到A槽。这套机制在Android和不少车规ECU上已经是非常成熟的做法放到STM32F103上一样适用只是受限于Flash容量和引脚资源要做不少裁剪和取舍。这篇文章适合谁看正在给STM32F103项目加OTA功能、被单分区升级搞怕了的人或者刚接触AB分区概念、想找一个完整可复现教程的嵌入式开发者。内容覆盖分区规划、Bootloader核心逻辑、APP端中断向量偏移、远程下载流程、回滚机制、问题排查照着做就能把一个带AB OTA的工程跑起来。先说清楚一个前提。AB双分区对Flash容量有硬性要求我个人建议起步用STM32F103RCT6256KB FlashC8T6只有64KB Flash做完Bootloader加两个槽位基本就没什么余量了。如果你手上只有C8T6也不是完全没救后面会讲一种“单运行区暂存区”的变通做法但体验和安全性跟真正的双分区还是有差距。1.1 传统OTA方案最大的问题传统的单分区OTA一般把Flash分成Bootloader区和APP区。APP区里跑的固件要升级流程通常是Bootloader通过串口、WiFi或者其他接口接收新固件一边收一边往APP区写写完做一次CRC校验校验通过就跳转执行。问题在于整个升级过程是在“破坏旧固件”的基础上进行的新固件还没完全写进去旧的就已经被擦掉一部分了。这时候只要断电、通信超时、校验失败APP区就是一个半成品状态Bootloader跳过去也跑不起来设备就卡死在启动阶段。有人说单分区也能加“恢复模式”用按键强制进Bootloader重新刷但这对无人值守的远程设备来说没有意义。设备装在几十公里外的配电箱里、农业大棚里、或者用户家里不可能派人跑过去按键刷机。更头疼的是如果升级包本身有问题能正常收下来、校验也通过、Bootloader也跳过去了结果固件跑几秒就崩溃设备又变砖了这种场景单分区是完全没有容错能力的。我最早做OTA的时候也被这个问题坑过。给一批设备远程升级固件包没测透升级完设备集体起不来最后只能一个个拆机接串口线救。从那以后我就明白一件事OTA方案绝对不能只考虑“怎么把新固件写进去”必须把“新固件挂了怎么办”这个兜底问题一并设计进去AB分区就是冲着这个需求来的。1.2 AB双分区方案到底好在哪AB分区的设计思路很直白把APP区域拆成SlotA和SlotB两个独立槽位每个槽位都能独立启动。系统平时在SlotA运行升级的时候把新固件下载到SlotB而不是直接覆盖当前正在运行的程序。下载完成并校验通过后App或者用户主动发起“切换请求”复位进入BootloaderBootloader检查SlotB的固件有效性确认没问题就把启动位置切到SlotB。这里有个很关键的设计点固件下载到SlotB的过程中SlotA一直保持完好即使下载到一半断电下次上电Bootloader检测到SlotB不完整直接忽略它继续从SlotA启动设备完全不受影响。新固件在SlotB跑起来之后也不是立刻就“生效”的而是要在规定时间内向系统上报“确认信号”证明自己真的能正常工作。如果新固件起不来、或者起来了但是没上报确认Bootloader会在尝试次数耗尽后自动切回SlotA实现回滚。一套成熟的AB机制在Bootloader层面做三件事就够了判断哪个槽位有效、管理切换状态、控制尝试次数。业务层面的“下载新固件到后备槽位”反而可以完全放在App里做不需要Bootloader懂网络协议、不用懂文件系统、不用懂差分包这就让Bootloader异常精简越简单的代码越不容易出错这个道理做嵌入式的应该都懂。2. 内存规划F103这种小Flash也能塞下双分区AB分区在PC和手机上非常标准Android系统的A/B分区甚至还包括system、vendor、boot好几个分区。STM32F103的Flash一共才几十到几百KB肯定不能照搬Android那套必须按F103的资源特性重新规划。做之前先把F103的Flash操作特性搞清楚不然后面写驱动的时候会踩一堆坑。2.1 先认清F103的Flash资源STM32F103不同型号的Flash容量差别很大这里涉及两个关键点总容量和页大小。中容量型号比如C8T6、RBT6Flash是64KB到128KB每页1KB大容量型号比如RCT6、VCT6、ZET6Flash是256KB到512KB每页2KB。擦除操作是按页来的调用标准库的FLASH_ErasePage时传的是页起始地址地址必须按页对齐不是按字节对齐这个细节后面写代码的时候非常重要。页大小直接影响整页擦除的时间F103擦除一页大概需要20到40毫秒写一个半字16位大概几十微秒。如果固件有60KB用半字方式写入光写数据就得几十毫秒不止再加上擦除时间整个升级过程会持续几秒甚至更久。规划看门狗超时时间、设计数据传输帧的时候这些时间参数都要算进去。Flash擦写寿命也是要做产品必须考虑的点。F103的Flash标称擦写寿命是1万次对OTA场景来说每次升级会擦写目标槽位的若干个页一天升级一次也能用很多年所以寿命本身不是瓶颈。真正的风险在于频繁擦写同一个标志位页后面讲标志位存储的时候我会专门说这个问题。2.2 我的分区表和固件头部设计我这次选F103RCT6做样板256KB Flash规划如下区域起始地址大小用途Bootloader0x0800000032KB启动管理、切换逻辑、本地串口升级兜底SlotA0x0800800064KB出厂固件槽位A默认运行SlotB0x0801800064KB升级槽位B下载目标Flag区0x080280002KB1页存放启动标志、尝试次数等保留区0x08028800剩余空间留给日志、参数存储或备用这里留了32KB给Bootloader实际代码一般只有8到12KB多留空间纯粹是为了以后扩展方便比如加命令行工具、加串口XModem协议、加更多诊断功能。两个槽位各64KB编译完的App如果超过64KB需要压缩代码体积或者改用更大Flash的型号。CRC32校验和固件头部会占掉每个槽位开头的一部分空间所以实际能用的固件空间是64KB减去头部大小我习惯留足余量固件控制在56KB以内比较稳妥。每个槽位开头放一个结构化的固件头部Bootloader靠它判断这个槽位是否有效typedef struct { uint32_t magic; // 固定魔数 0x53544D41即 STMA uint16_t version; // 固件版本号 uint16_t slot_id; // 目标槽位0表示SlotA1表示SlotB uint32_t app_size; // 固件数据长度不含头部 uint32_t crc32; // 对app_size长度数据的CRC32校验值 uint32_t reserved; // 预留可填0 } fw_header_t;烧录固件的时候把编译出来的bin文件头部加上这24个字节再写入对应槽位。Bootloader启动时先读magic不是魔数直接视为槽位无效再检查app_size是否在合理范围内避免读到垃圾数据后做超长CRC计算最后对整个固件数据区做CRC32和头部存的CRC32比对全通过才认为是有效固件。2.3 标志位存储与防丢策略AB切换的核心状态都放在Flag区包括当前活跃槽位是A还是B、有没有待切换的槽位、当前固件尝试启动了多少次、确认标志是否已经上报。这些标志变量必须掉电不丢失所以只能存Flash。但F103的Flash不能像EEPROM那样按字节改写入前必须先整页擦除哪怕只改一个字节也要把整页擦了重写。最简单可靠的做法是单独划分一个页专门存标志结构体每次修改都执行“擦除整页 - 写入完整结构体”这两个步骤。OTA场景下一次完整升级流程中标志位的写入次数很少整个生命周期也就几十次上百次单独占一页完全不会磨损Flash。再多说一句防丢策略。标志区里我放了双份结构体写入时写两份读取时如果第一份CRC校验不过就读第二份两份都坏了才判定为非法状态并执行默认启动逻辑。实际测试中极少出现两份同时损坏的情况但对无人值守设备来说这种冗余能多一道保险。标志位结构体定义大致如下typedef struct { uint32_t magic; // 0xA5A5A5A5 uint8_t active_slot; // 0: A槽1: B槽 uint8_t pending_slot; // 0xFF表示无待切换槽位 uint8_t boot_attempts; // 当前槽位尝试启动次数 uint8_t confirmed; // 1表示新固件已确认正常 uint32_t crc32; // 对整个结构体的CRC32 } boot_flag_t;这种设计把运行时的状态管理全部收拢到Bootloader内部App不需要关心Flag区怎么组织只要调用统一的接口请求切换和上报确认就行。3. Bootloader实现启动切换和回滚是核心Bootloader是AB OTA里最关键的软件模块代码量不大但逻辑必须非常严谨。写Bootloader的时候心里要有一个优先级不管Flag区里是什么状态最终都要保证设备能启动一个“已知有效”的固件如果两个槽位都不可用至少能进入一个可以本地刷机的维护模式而不是死循环。3.1 上电判断流程Bootloader的main函数思路很清晰大概分四步。第一步做必要的硬件初始化包括时钟、串口、LED、按键检测这个阶段尽量少初始化外设够用就行。第二步读Flag区的boot_flag_t结构体根据magic和CRC判断标志是否有效。第三步执行槽位选择逻辑看当前有没有pending切换请求如果有就校验目标槽位校验通过则切换、校验失败则清除pending继续用原槽位。第四步根据确认状态和尝试次数决定启动哪个槽位如果当前激活槽位连续启动失败超过阈值就切到另一个槽位。槽位选择逻辑用代码表示会更直观int select_boot_slot(void) { boot_flag_t flags; read_boot_flags(flags); // 有pending切换请求 if (flags.pending_slot ! 0xFF) { if (slot_valid(flags.pending_slot) 0) { // 目标槽位校验失败清pending继续用原槽位 flags.pending_slot 0xFF; save_boot_flags(flags); } else { // 校验通过切换当前激活槽位 flags.active_slot flags.pending_slot; flags.pending_slot 0xFF; flags.boot_attempts 0; flags.confirmed 0; save_boot_flags(flags); return flags.active_slot; } } // 当前槽位有效则直接启动 if (slot_valid(flags.active_slot) 0) { return flags.active_slot; } // 当前槽位无效或尝试次数耗尽回滚到另一个槽位 int other 1 - flags.active_slot; if (slot_valid(other) 0) { flags.active_slot other; flags.boot_attempts 0; flags.confirmed 0; save_boot_flags(flags); return other; } // 两个槽位都不可用进入维护模式等待本地刷机 enter_maintenance_mode(); return -1; }这里面尝试次数是回滚的关键。新槽位切换过来后boot_attempts从0开始每次启动Bootloader都会把它加1App正常运行后又会把confirmed置1Bootloader看到confirmed为1就不再递增尝试次数。如果新固件有问题每次上电跑几秒就复位Bootloader每次都会发现confirmed还是0并且boot_attempts已经超过阈值我一般设3次于是判定这个固件“不合格”自动切回旧槽位。3.2 跳转APP的代码细节跳转是Bootloader里最容易被忽视的环节。很多人写跳转就是两行代码把栈指针和PC设一下就行实际跑起来各种诡异问题。跳转前必须处理干净中断和外设状态否则APP初始化的时候很容易出问题。完整的跳转函数如下typedef void (*pFunction)(void); void jump_to_app(uint32_t app_addr) { uint32_t app_sp *(volatile uint32_t *)app_addr; uint32_t app_pc *(volatile uint32_t *)(app_addr 4); // 检查栈指针是否在SRAM范围内防止跳到非法地址 if ((app_sp 0xFFF00000) ! 0x20000000) { return; } // 跳转前关闭全局中断 __disable_irq(); // 反初始化Bootloader用到的外设 USART_DeInit(DEBUG_UART); I2C_DeInit(I2C1); SPI_DeInit(SPI1); TIM_DeInit(TIM2); SysTick-CTRL 0; SysTick-LOAD 0; SysTick-VAL 0; // 设置中断向量表偏移 SCB-VTOR app_addr; pFunction jump (pFunction)app_pc; __set_MSP(app_sp); jump(); }注意几个细节。第一取栈指针和复位向量要在跳转前完成不是跳过去之后再从内存读。第二栈指针的地址要落在SRAM区间里F103的SRAM一般是0x20000000到0x2000FFFF超出了就说明槽位里根本没有合法固件头。第三Bootloader里所有用过的外设都要反初始化尤其是串口不关的话APP初始化串口时会发现寄存器状态不对或者串口中断里突然蹦出一个Bootloader时期残留的数据。第四SCB-VTOR要设置成APP的实际起始地址这样APP里的中断向量才能被正确找到。中断向量表偏移的问题在APP端还有配套设置后面专门讲。跳转方式还有个小选择通过设置MSP直接跳还是通过修改VTOR后软复位启动。在标准AB方案里我推荐“直接跳转”因为软复位会让Bootloader重新跑一遍启动判断虽然也能进APP但如果App确认标志还没写这个复位过程可能多消耗一次尝试次数。直接跳转干净利落省去Bootloader重新初始化的开销。3.3 固件校验和有效性检查槽位有效性检查函数slot_valid是Bootloader里调用最频繁的函数它的实现直接决定系统容错能力。检查流程分三段先读槽位起始的fw_header_t检查magic和app_size合法性再对槽位内app_size长度的数据算一遍CRC32跟头部里的crc32字段比对。只有三个条件全部满足才认为这个槽位可以启动。CRC32算法使用查表法实现F103的主频72MHz跑几十KB数据的CRC计算大概只需要几十毫秒完全可以接受。代码实现如下static const uint32_t crc32_table[256] { /* 标准CRC32查表生成多项式0xEDB88320 */ }; uint32_t crc32_calc(uint32_t crc, const uint8_t *buf, uint32_t len) { crc ~crc; for (uint32_t i 0; i len; i) { crc (crc 8) ^ crc32_table[(crc ^ buf[i]) 0xFF]; } return ~crc; } int slot_valid(uint8_t slot_id) { uint32_t base (slot_id 0) ? SLOT_A_ADDR : SLOT_B_ADDR; fw_header_t hdr; memcpy(hdr, (uint8_t *)base, sizeof(hdr)); if (hdr.magic ! 0x53544D41) return -1; if (hdr.app_size 0 || hdr.app_size (SLOT_SIZE - sizeof(hdr))) return -1; uint32_t calc_crc crc32_calc(0, (uint8_t *)(base sizeof(hdr)), hdr.app_size); return (calc_crc hdr.crc32) ? 0 : -1; }这里要注意F103的Flash在CPU上是可以直接寻址的所以直接用指针读取Flash内容完全没问题不需要专门的读取API。Bootloader里能不用官方库的Flash读函数就不用直接内存映射访问反而更简单高效。Bootloader本身还预留了一个本地刷机维护模式通过按键或者串口命令触发。维护模式里实现一个最简单的YModem接收协议把PC上的bin文件下载到非活跃槽位接收完成后做一次校验校验通过就置pending切换标志并复位。这个功能平时用不太到但真的遇到设备OTA挂了、主动上报心跳消失的时候这个本地通道就是最后的救命稻草不用开壳接JTAG就能恢复。4. APP端改造中断向量偏移和升级触发Bootloader做得再好APP端不配合也跑不起来。APP改造涉及两个核心点一是编译配置要改成“可被Bootloader跳转”的模式也就是中断向量表偏移二是App内部要实现固件下载、写入非活跃槽位、请求切换、上报确认这一整套流程。4.1 Keil链接脚本和VTOR设置正常情况下F103的固件链接地址是0x08000000中断向量表也在0x08000000。现在SlotA从0x08008000开始SlotB从0x08018000开始APP固件的链接地址就必须改到对应槽位的起始地址中断向量表也跟着偏移。我用的Keil MDK5修改方式有两种。一种是在Options for Target - Target页签里把IROM1的Start改成0x08008000Size改成0x10000另一种是直接改sct分散加载文件。第一种最常用。注意RAM地址不需要动F103的SRAM还是从0x20000000开始只是Flash地址变了。链接地址改完还有一个极其关键的配置system_stm32f10x.c文件里的VECT_TAB_OFFSET宏。默认是0x00000000APP运行在SlotA时必须改成0x8000运行在SlotB时改成0x18000。代码简化如下#define VECT_TAB_OFFSET 0x8000 // 根据当前槽位修改系统初始化函数SystemInit中会执行SCB-VTOR FLASH_BASE | VECT_TAB_OFFSET这样中断向量表就被重定位到槽位起始地址。这里有个很坑的点同一个APP工程烧到SlotA和SlotB需要的VECT_TAB_OFFSET不一样如果你用一份代码打算两个槽位都能跑不能硬编码这个偏移量。我推荐的解决办法是APP启动的最早期动态读取当前运行地址用宏定义判断。因为Cortex-M3内核里PC寄存器最开始就在某个地址段通过__get_MSP可以间接拿到启动位置。更直接的是在入口汇编或者SystemInit之前定义extern uint32_t __Vectors; uint32_t running_addr (uint32_t)__Vectors; // 链接时指定的向量表地址如果链接地址就是0x08008000那VTOR直接用链接脚本里的__Vectors地址即可不用关心它在哪个槽位。SystemInit里改成SCB-VTOR (uint32_t)__Vectors;这样不管链接到哪个地址都能自动适配一份代码同时支持SlotA和SlotB省去频繁改宏的麻烦。4.2 下载通道与写入逻辑固件下载通道我做了两条。第一条是串口通道适合开发调试和产线灌装通过一个自定义的简单协议收bin文件每包256字节带帧头、帧序号、CRC16。第二条才是真正意义上的OTA远程通道App里通过ESP8266模块连接HTTP服务器GET请求下载bin文件到内存缓冲区然后逐段写入非活跃槽位。写入非活跃槽位时Flash操作必须遵守F103的硬件规则。写入前先FLASH_Unlock解锁然后按页擦除目标槽位之后用FLASH_ProgramHalfWord按16位写入数据写完上锁。这里最容易犯的错误是有人直接用ProgramWord写32位回报错“Operation error”因为F103的Flash控制器只支持半字编程这是芯片硬件决定的没有参数可以改。写的时候要自己维护写入偏移目标槽位起始地址是固定的固件头部占24字节要预留出来App编译出来的纯bin数据从头部后面开始写写完后在槽位起始处填好fw_header_t结构体再把整份bin的数据区和头部里的CRC32计算出来填上最后才是一个有效的槽位。写入流程的伪代码int download_firmware_to_slot(uint8_t slot, const uint8_t *fw_data, uint32_t fw_len, uint16_t version) { uint32_t slot_base (slot 0) ? SLOT_A_ADDR : SLOT_B_ADDR; uint32_t data_base slot_base sizeof(fw_header_t); uint32_t offset 0; FLASH_Unlock(); // 按页擦除 while (data_base offset slot_base SLOT_SIZE) { FLASH_ErasePage(FLASH_Addr_Page(data_base offset)); offset FLASH_PAGE_SIZE; } // 半字写入固件数据 offset 0; while (offset fw_len) { uint16_t halfword fw_data[offset] | ((uint16_t)fw_data[offset 1] 8); FLASH_ProgramHalfWord(data_base offset, halfword); offset 2; } // 写固件头部 fw_header_t hdr; hdr.magic 0x53544D41; hdr.version version; hdr.slot_id slot; hdr.app_size fw_len; hdr.crc32 crc32_calc(0, fw_data, fw_len); hdr.reserved 0; write_flash_bytes(slot_base, (uint8_t *)hdr, sizeof(hdr)); FLASH_Lock(); return 0; }关于页大小前面说过不同型号不一样这里的FLASH_PAGE_SIZE需要按芯片型号定义RCT6是2KBC8T6是1KB。擦页地址也要按页对齐如果目标槽位起始地址或者固件数据起始地址不满足页对齐条件擦除后写入时会有部分区域被误擦最好的办法是槽位起始地址和头部大小都设计成页对齐的倍数。4.3 激活确认与回滚计数固件下载完成、校验通过后App接下来要调用请求切换接口把“待切换槽位”写入Flag区然后软复位。这段逻辑可以封装成独立模块比如fota.cvoid fota_request_switch(uint8_t target_slot) { boot_flag_t flags; read_boot_flags(flags); flags.pending_slot target_slot; flags.confirmed 0; flags.boot_attempts 0; save_boot_flags(flags); NVIC_SystemReset(); }复位后Bootloader检测到pending_slot是有效的就会把active_slot切到目标槽位启动新固件。这时候新固件处于“试用期”必须主动上报确认。我在App里用一个状态机处理系统初始化完成后启动一个10秒的确认窗口这个窗口内业务模块如果完成了关键初始化比如联网成功、采集模块自检通过、心跳正常上报就调用fota_confirm_current()把confirmed置1。如果确认窗口超时没有确认Bootloader在下一次启动时会累加尝试次数超过3次直接回滚到另一个槽位。这对应到一个工程上的核心认知新固件“能启动”不等于“正常”AB机制的确认环节就是要把这种业务层面的健康状态反馈给Bootloader让系统自己判断是否需要回滚而不是靠人在现场看。5. 实测与排查从板子到手到远程升级跑通方案设计完最终要落到板子上跑。这一节我记录一下完整的实测过程和踩坑经历包含本地串口链路测试、WiFi远程链路测试、故障注入测试以及我整理的一版排查手册。5.1 测试流程实录硬件准备STM32F103RCT6最小系统板一个USB转串口模块一个ESP8266串口WiFi模块ST-Link下载器。软件准备Keil MDK5标准外设库V3.5一个nginx静态文件服务器用来存放bin升级包串口调试助手。第一步先烧BootloaderBootloader编译后通过ST-Link烧写到0x08000000。烧完打开串口按复位键串口打印Bootloader版本和当前状态正常情况下会提示“SlotA invalid”因为SlotA还是空的。第二步通过Bootloader维护模式把v1.0固件下载到SlotA。我在Bootloader里做了个简单命令串口发送“1”进入YModem接收模式然后用SecureCRT的YModem发送APP bin文件。发送完后Bootloader自动校验并启动SlotA串口打印“SlotA valid, jump to app addr 0x08008000”屏幕开始输出App的日志说明基础链路通了。第三步测试本地串口升级链路。修改App代码把版本号从v1.0改成v2.0编译生成新bin通过串口把bin发送给App。App收到后写入SlotB写入完成后控制台打印“download ok, crc match, request switch”。复位后Bootloader打印“pending slot B valid, switch active to B”然后v2.0固件跑起来并在10秒内上报确认串口显示“confirmed ok, boot_attempts reset”。第四步测试远程OTA。把v2.0的bin放到nginx服务器目录下通过串口给App下发一条AT指令让ESP8266连接家里的WiFi然后App发起HTTP GET请求拉取bin下载过程中我直接拔掉路由器电源模拟网络中断。重新上电后设备正常从SlotA的v1.0启动日志显示“download incomplete, SlotB invalid, keep boot from A”。这个测试非常关键验证了AB最核心的优势下载失败不影响当前固件。第五步故障注入测试。故意编译一个v3.0固件在main函数开头加一个无限循环不初始化外设不确认或者说加一个故意触发HardFault的代码。把v3.0OTA上去Bootloader切到SlotB设备跑起来后没确认我连续手动复位3次第4次上电Bootloader检测到boot_attempts超过3直接把active_slot换回SlotA设备恢复正常。这个回滚测试通过AB机制才算真正闭环。5.2 常见问题排查速查表实测中踩了不少坑整理成表格按出现的概率和破坏力排了一下现象可能原因排查方法跳转到APP后系统跑飞或进HardFault中断向量表偏移没设置、跳转前没关中断、外设未反初始化检查SCB-VTOR是否等于槽位起始地址跳转前加__disable_irq跳转前调用外设DeInitAPP能跑但串口打印乱码跳转前Bootloader串口没有DeInit串口配置被污染跳转前调用USART_DeInit并复位对应GPIO烧录时Keil报错“No Algorithm found”烧录地址超出芯片Flash范围或Flash算法配置错误检查IROM1地址范围和ST-Link烧录算法选择确保和型号匹配Flash写入报错或写不进没调用FLASH_Unlock、写32位数据、页地址没对齐确认按半字写确认擦页地址按页对齐OTA下载完成后校验失败下载过程中串口丢包、帧数据没拼接完整、bin文件本身被转换工具改坏用STM32 ST-LINK Utility读出槽位内容拿bin工具对比检查CRC算法一致性新固件起来了但很快回到旧固件Bootloader尝试次数耗尽新固件没有及时上报confirmed检查App确认逻辑是否在预期时间内执行把确认窗口调大再看下载过程中断电恢复后设备频繁重启非活跃槽位留下残包Bootloader每次启动都会校验并尝试清理检查Bootloader逻辑pending不为空时如果校验失败应直接清pending并启动原槽位不要反复尝试5.3 一些只有踩过坑才懂的细节第一OTA升级用的bin文件生成方式。Keil默认编译输出axf需要配置生成bin。我习惯在Keil的User页签After Build/Rebuild里加一行命令fromelf.exe --bin --output.\obj\app.bin .\obj\app.axf生成bin之前记得在Linker页签取消勾选Use Memory Layout from Target Dialog必要时手动调整分散加载文件否则有时候生成的bin大小和链接地址对不上。还有一个容易踩的坑如果代码里用了C库的printf等浮点功能生成的bin可能会包含大量C库初始化代码实际固件大小比map文件里预估的更大编译后一定要看app.bin的字节数超过槽位容量就麻烦了。第二App和Bootloader共用一个串口打印时要注意跳转前的状态清理。我在Bootloader里用USART1打印跳转后App也用USART1如果不做DeInitApp初始化串口时可能会发现USART1的CR1寄存器还开着TX中断导致串口刚初始化就进中断莫名其妙跑飞。这类问题调试起来极其痛苦串口状态又看不到排查了一个下午才发现是Bootloader没关干净。第三CRC算法必须全局统一。Bootloader和App里用的CRC32计算函数必须完全一致包括初值、多项式、输出是否取反。我见过有项目在Bootloader里用标准CRC32在App里用简化版CRC32校验永远对不上排查到最后发现是两个函数算出来的结果不一样。做方案的第一步就是把CRC算法固定下来两边共用一套源码不要各写各的。第四App上传确认的时机要设计好。如果确认窗口太短新固件还没来得及初始化完就被判失败回滚如果确认过于随意比如开机就确认那AB的回滚机制就形同虚设。我的经验是把确认点放在“对外通信成功”这个业务节点上比如设备联网成功后、或者心跳上报成功后确认窗口至少给10秒。这样新固件真正能对外服务了才被认为“合格”。第五开发阶段强烈建议留一个直接从Flash启动的旁路。我平时调试App不想每次都从Bootloader跳转就会在Keil里临时把链接地址改回0x08000000通过ST-Link直接调试。这样App本身的断点、单步都方便不用考虑Bootloader的影响。等App功能稳定了再切回0x08008000做联合测试。这个方法帮我节省了大量联调时间。AB OTA这套机制在F103上跑通之后迁移到其他M3/M4芯片上非常容易核心逻辑不变只需要调整Flash起始地址、页大小、链接脚本这些硬件相关的参数。我现在手上几个新项目不管是用GD32F103还是STM32F407基本都是把这套Bootloader直接搬过去改改地址就能用。如果你正准备给自己的F103项目上OTA我建议不要一开始就同时搞WiFi、加密、差分升级这些花活先把最基础的AB双分区和回滚机制跑通这个骨架立住了后面想加什么功能都从容得多。
返回列表