ARTICLE DETAIL

资讯详情

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

STM32F103 AB双分区OTA升级教程:从Bootloader到回滚机制实战

STM32F103 AB双分区OTA升级教程:从Bootloader到回滚机制实战 产品已经量产交付了现场反馈回来一个逻辑bug你带着J-Link出差到客户那里开箱、接线、烧录、合盖、走人。这种场景做过一次就够够的了。如果你正被STM32F103的升级问题折腾或者想给产品加一个可靠的固件升级能力那这篇AB双分区OTA复现教程应该对你有用。AB分区方案的核心思路很简单把Flash划分成两个应用区一个跑当前固件一个放新固件Bootloader负责在启动时决定跑哪个区。的好处是升级哪怕中途断电、固件写坏旧固件还完好地躺在另一个区永远不会变砖。这篇文章我会从分区设计、Bootloader工作流程、App端固件接收、状态机与回滚机制到实测验证全部走一遍基于STM32F103标准外设库代码风格偏裸机适合已经在用103做产品、想加深固件升级能力的朋友。全文不是理论分析是我在实际项目中踩过坑、量过时序之后沉淀下来的做法照着做大概率能一次跑通。1. 为什么STM32F103需要AB双分区方案——先说清这笔账1.1 单分区OTA的问题在哪里很多入门教程教的是单分区OTABootloader从串口或者网络收到固件直接擦除App区写入新固件然后跳转运行。这个方案有一个致命问题——升级过程是“先擦后写”Flash被擦掉的那一刻起设备里就不存在可运行的程序了。如果写入过程中断电、串口干扰导致数据错误、或者固件本身有问题导致跑不起来结果只有一个设备变砖还得开壳用调试器救。说得直白一点单分区OTA没有任何“后悔药”它把所有的信任都押在“这次升级必须成功”上。而现实是固件传输、Flash写入、断电、电磁干扰任何一个环节出错都可能导致升级失败。对于消费类小批量产品可能还能接受但对工业设备、医疗设备、远程部署的节点来说变砖就意味着售后成本和信任损失。1.2 AB双分区的核心逻辑永远留一个能跑的AB双分区方案的思想其实很简单就是“留后路”。Flash里放两个完整的App区App A和App B两者容量一样都能独立运行。Bootloader启动时根据参数区里的标志位决定从哪个区启动。升级的时候如果当前跑的是App A新固件就写入App B写完后置标志位重启时Bootloader把控制权交给App B。关键在于升级过程中旧固件所在的App A全程没有被碰过。新固件即使写入失败、校验不过、运行崩溃Bootloader检测到了就可以自动切回App A。这就是“永不变砖”的底气来源。这个思路在嵌入式领域并不新鲜Android系统的A/B无缝升级、ESP32的OTA机制本质上都是同一种策略。ESP32能轻松做AB升级是因为它有专门的OTA Data分区和IDF框架帮你处理了大部分逻辑。但STM32F103不行它没有硬件级的双Bank支持没有VTOR寄存器做向量表重定位所有AB分区逻辑都得自己在裸机上用代码搭出来。这也是为什么网上聊ESP32 OTA的文章一大堆STM32F103 AB OTA的完整教程却很少。1.3 AB分区方案和单分区方案的对比下面这张表是我在实际项目中总结的关键差异做方案选型时可以直接参考对比项单分区OTAAB双分区OTAFlash占用小只需一个App区大需要两个完整App区升级失败风险可能变砖几乎不会变砖断电保护无自动回滚到旧固件固件审核机制无写完就跑有校验通过才切换实现复杂度低中等偏高适用场景开发板、玩具量产产品、远程升级如果你的Flash只剩下很小的余量、产品本身允许拆壳重刷单分区方案够用。但只要产品具备OTA的诉求我个人的建议都是直接上AB双分区这点Flash成本换来的可靠性完全值得。2. 硬件平台与分区规划动手之前先把Flash账算明白2.1 芯片选型与Flash容量STM32F103家族有很多型号Flash容量各不相同我自己常用的有C8T664KB、CBT6128KB、ZET6512KB。做AB OTA首先要确认型号变了Flash够不够用。Bootloader区建议至少16KB到24KB原因后面会说。两个App区如果每个只给32KB那么一个工程编译出来超过32KB就直接放不下。现在稍微加点功能模块比如RTOS、Modbus协议栈、LCD驱动固件上40KB很常见。所以如果你只是C8T6这种64KB Flash的芯片做AB分区会非常捉襟见肘。我建议做AB OTA起步用128KB Flash的CBT6这样分区可以做得比较宽裕。如果产品必须用64KB Flash芯片又想要AB升级只能压缩功能把固件控制在24KB以内这样非常痛苦。下面我以128KB Flash的STM32F103CBT6为例给出一个经过实际验证的分区规划。2.2 128KB Flash分区规划表STM32F103的Flash按页划分每页1KB小容量和中容量型号都是1KB/页注意大容量型号是2KB/页别搞混了。整个Flash分区如下分区起始地址大小说明Bootloader区0x0800000024KB启动引导、升级逻辑、回滚判断参数区0x080060004KB存储激活区标志、升级状态、CRC记录App A区0x0800700048KB应用固件AApp B区0x0801300048KB应用固件B保留0x0801F0004KB留作扩展整个128KB Flash用掉了120KB剩下的4KB做保留。Bootloader占24KB很多人觉得浪费但你要知道Bootloader里面除了启动引导还要放串口驱动、Flash驱动、CRC32校验模块以及未来可能加的固件接收协议。我当时第一次做的时候把Bootloader规划成8KB结果编译到一半就放不下了最后不得不重新调分区。2.3 三个关键设计决策和理由为什么Bootloader不能太小一方面Bootloader里跑的是整个设备“最后一道防线”升级协议栈、CRC校验、Flash操作这些代码加起来体积不小另一方面一旦Bootloader自身写坏了整块芯片就彻底变砖连救的机会都没有所以Bootloader区留足余量是值得的。为什么App A和App B必须一样大这是AB方案的基本要求。Bootloader不关心当前跑的是哪个区它只需要按固定地址启动即可。如果A区写满了48KBB区却只有32KB分区逻辑就全乱了。而且固件在A区能跑意味着它的最大体积不超过48KB放到B区也一定能放得下。为什么参数区单独占4KB还要放在最后参数区存放的是Bootloader和App都要访问的全局信息比如“当前激活区是A还是B”“升级状态是什么”“固件头信息存哪”。如果参数区放在App区中间App区一旦被擦写参数区就跟着没了这绝对不行。放最后一页还有一个原因修改参数时需要先擦除整页Flash再写入新值放在独立的页里擦写不会影响其他任何代码。2.4 STM32F103没有VTOR怎么办这里先埋一个伏笔。做App跳转很多新手上来就搜“STM32向量表重映射”一搜发现F103的Cortex-M3内核没有像M4那样提供VTOR寄存器或者说不支持在运行时随意改Vector Table Offset Register就会卡住。F103的解决思路是把向量表从Flash复制到SRAM开头再通过SYSCFG的MEM_MODE寄存器把地址0x00000000映射到SRAM。这段代码我会在第4章App端启动流程里完整给出来你现在只需要知道有这个坑就行。3. Bootloader完整工作流程从标志位判断到Load App3.1 Bootloader到底要干哪些活很多第一次做Bootloader的人把它想象得很复杂其实归纳起来就四件事初始化最小系统时钟、串口、Flash接口读取参数区判断当前应该从哪个App区启动校验目标区的固件完整性栈顶地址、复位向量、CRC32跳转到App执行如果校验不过自动切换启动另一个区简单说Bootloader本身不是一个完整的上位机程序它的核心职责是“开关”——决定这一秒把CPU交给哪份固件。3.2 参数区的数据结构设计参数区是整个AB方案的信息中枢我用一个结构体来定义存储布局typedef struct { uint32_t magic; // 参数区有效标志固定为0xA5A5A5A5 uint32_t active_slot; // 当前激活区0App A1App B uint32_t update_flag; // 升级状态0空闲1升级待确认 uint32_t try_count; // 新固件启动尝试次数 uint32_t reserved[4]; // 保留字段 } ota_param_t;这个结构体定义好之后Bootloader和App两边必须保持一致不然会读到错位数据。magic字段的作用是标记参数区是否写过有效数据。第一次烧录后参数区是0xFFBootloader读magic发现不对就按默认值初始化——默认从App A启动。3.3 固件头Bootloader凭什么判断App是完整的App区的起始地址不能直接就是向量表因为Bootloader需要知道这份固件的版本号、长度、CRC这些信息必须存在某个固定位置。但是硬件要求Flash起始位置必须是栈顶地址不能把自定义结构体放在最开头。我的做法是偏移0x0000到0x03FF放向量表偏移0x0400处放固件头固件代码从0x0800开始实际使用时几乎不可能有人把中断向量表用到超过1KB所以这个方法很稳。typedef struct { uint32_t magic; // 固定0x4F544142即OTAB uint32_t version; // 固件版本号比如0x0100 1.0 uint32_t length; // 固件代码长度不含向量表和固件头 uint32_t crc32; // 固件代码的CRC32校验值 } app_header_t;Bootloader校验时先检查magic然后从App区起始地址0x400处读出app_header_t根据length和crc32对整个代码区做一次CRC计算比对。如果对得上说明固件完整对不上说明写到一半断电了或者数据坏了直接切另一个区。3.4 跳转前的安全检查为什么不能直接跳很多第一次写的朋友最大的疑问是“我不就是定义一个函数指针指向App的Reset_Handler然后调用吗”理论上是可以但直接跳会踩两个坑。第一个坑是外设中断状态。Bootloader如果用过串口接收串口中断可能处于挂起状态。直接跳转到App后App没初始化串口但是中断挂起标志还在一开全局中断就进HardFault或者串口乱跑。所以跳转前必须把NVIC里所有中断清除关闭所有外设中断。第二个坑是栈顶地址合法性。如果App A区根本没烧录Flash里全是0xFF那栈顶地址就是0xFFFFFFFF这在ARM里是个无效的RAM地址一跳就HardFault。跳转前必须检查栈顶地址是否落在0x20000000开头的SRAM范围内。我用的跳转函数如下typedef void (*pFunction)(void); void bootloader_jump_to_app(uint32_t app_addr) { uint32_t app_stack *(volatile uint32_t *)app_addr; uint32_t app_pc *(volatile uint32_t *)(app_addr 4); pFunction app_entry; // 检查栈顶地址是否落在SRAM范围 0x20000000 - 0x20010000 if ((app_stack 0xFFF00000) ! 0x20000000) { // 栈顶地址不合法说明App区没有有效固件 return; } // 检查复位向量是否指向Flash区域 if ((app_pc 0xFFF00000) ! 0x08000000) { return; } // 关闭全局中断并清空NVIC的所有挂起中断 __disable_irq(); for (uint32_t i 0; i 8; i) { NVIC-ICER[i] 0xFFFFFFFF; NVIC-ICPR[i] 0xFFFFFFFF; } // 设置主栈指针并跳转 __set_MSP(app_stack); app_entry (pFunction)(app_pc); app_entry(); }跳转之前最好通过串口打印一行日志记录你准备从哪个区启动、版本号是多少。这个日志在调试阶段几乎是无价之宝——你能一眼看出Bootloader是不是做出了正确的分区选择。3.5 Bootloader完整的启动决策流程整个Bootloader的主循环决策逻辑如下读取参数区magic无效就初始化为默认值激活区设为App A。根据active_slot算出要启动的App区基地址。检查该区固件头magic、CRC和长度如果全部通过进入步骤5。校验失败切换active_slot到另一个区重新执行步骤2。如果两个区都不合法说明整机固件已毁停在Bootloader等待串口修复。跳转前把try_count加1写入参数区然后执行跳转函数。这个流程看起来简单但它保证了最重要的一件事Bootloader本身永远不碰App区里的数据它只看不写。真正改变App区内容的只有App里的升级逻辑。4. App端固件接收与写入把升级包安全搬进另一个区4.1 传输方式选型串口Y-Modem还是自定义协议既然标题是“从零复现”传输方式我建议先用串口。串口实现简单、调试方便、不依赖外部网络环境先把OTA整个链路跑通后续换成WIFI、以太网、LoRa都只是替换“数据来源”这一层。传输协议方面很多人上来就打算用Y-Modem因为超级终端和SecureCRT自带这个协议不用写上位机。Y-Modem确实成熟但它在STM32上有个麻烦Y-Modem是流式传输没有重传确认机制X-Modem有但效率低串口一丢数据就得整个文件重传。而且Y-Modem帧格式复杂裸机解析代码量不小。我的建议是自定义一个简单分包协议只要覆盖以下基本需求分帧、序号、长度、CRC校验、应答重传。控制起来反而更灵活上位机用Python写一个不到200行的小工具就行。协议帧格式设计如下Byte偏移字段说明0帧头固定0xA51帧类型0x01固件开始0x02固件数据0x03固件结束2-3帧序号从0递增用于丢包检测4-5数据长度最大1024字节6..6N-1数据固件内容最后4字节CRC32对整个帧做CRC32上位机发完每一帧后等待MCU返回一个ACK字节超时500ms未收到就重发当前帧。这样逐帧握手虽然有一点点效率损失但可靠性大幅提升。4.2 App端接收固件的状态转换App收到“固件开始”帧后需要做三件事解析固件头检查magic、版本号是否合法。确定目标写入区。当前跑在App A就写到App B当前跑在App B就写到App A目标区一定是“非激活区”。把目标区整块擦除。注意是整块擦不是边收边擦。一直有人问能不能边收边擦我的回答是不要。虽然STM32支持按页擦除边收边擦理论上可行但如果你擦除的速度赶不上串口接收速度串口溢出就是一瞬间的事。一次性把所有页擦完之后再写入逻辑简单得多也更容易排查问题。擦除完成后上位机开始持续发送“固件数据”帧App每收一帧就校验CRC通过则写入Flash然后回ACK。整个写入过程Flash擦除后的写入地址是连续的从目标区起始地址0x400处开始写固件体。4.3 内部Flash擦写的三个关键细节STM32内部Flash操作有几个必须遵守的规则违反了就是HardFault。第一擦除最小单位是页。F103中容量型号一页是1KB擦除前必须调用FLASH_Unlock()解锁结束后调用FLASH_Lock()加锁。第二写入必须以半字16位为单位。你可以连续写多个半字但每次编程至少16位不能逐字节写。代码里面我用的是FLASH_ProgramHalfWord()。第三Flash擦写期间CPU会暂停执行Flash里的代码。虽然STM32的Flash在读取状态下支持流水线执行但擦写过程中从同一块Flash取指令会卡顿。所以擦写函数、协议解析函数最好都放在RAM里执行或者关闭中断防止在擦写过程中被中断打断。我的经验是擦除1KB页的时间大约在20-40ms这期间如果串口中断一来数据只能靠硬件缓冲扛扛不住就丢。所以代码里我在擦除操作前关闭串口中断擦完再打开配合上位机的超时重传机制来保证不丢包。核心写入逻辑void ota_write_firmware(uint32_t dest_addr, uint8_t *data, uint32_t len) { uint32_t i; uint16_t *half_word_data (uint16_t *)data; FLASH_Unlock(); // 写入时按16位半字对齐 for (i 0; i len / 2; i) { FLASH_ProgramHalfWord(dest_addr i * 2, half_word_data[i]); } FLASH_Lock(); }注意这里len必须保证是偶数如果固件长度出现奇数要在固件填充阶段就补一个0xFF字节。4.4 升级包写完后烧进去的固件怎么“激活”所有数据写完还不能直接重启。App要对整个目标区做一次完整CRC校验确认写入Flash的数据和上位机发送的固件完全一致。全片校验一次需要读完整块48KB区域大概几十毫秒值得花。校验通过后App修改参数区active_slot改为目标区的编号update_flag置1表示有固件待确认try_count置0准备给Bootloader使用然后软件复位让Bootloader重新走启动流程。到这里App侧的职责就结束了。4.5 如果用了FreeRTOS需要注意什么如果你是在FreeRTOS环境下做这件事有两点要特别提醒。第一串口接收建议用DMA加空闲中断把固件数据直接搬到内存缓冲区不要在中断回调里做Flash操作。第二固件写入过程建议放在一个独立的高优先级任务里并且用taskENTER_CRITICAL()包住关键的Flash擦写段防止其他任务在这期间抢CPU导致串口超时。升级流程是设备的“关键时刻”该霸道的时候就霸道。5. 状态机、错误恢复与A/B回滚——依靠标志位而不是运气5.1 不要把“回滚”想得太复杂它只是一套标志位逻辑AB分区最容易让人困惑的是“回滚到底是什么时候发生的”。很多人以为回滚是Bootloader做了某种神奇的操作把旧固件“换”回去。其实不是。回滚的逻辑设计得很朴素Bootloader每次启动只做两件判断第一当前激活区是哪个第二这个区里的固件能不能通过校验、能不能正常启动。既然升级时旧固件从来没被碰过那只要把激活区标志切回旧区重启之后Bootloader自然就跳到旧固件里了。整个回滚过程不涉及任何数据搬移。5.2 升级状态机的完整定义我把升级过程划分为五个状态每个状态对应参数区里的一组标志位组合状态含义激活区update_flagtry_countST_IDLE空闲无升级任务A或B00ST_UPDATING正在接收新固件A或B00ST_VERIFY新固件校验通过等待重启新目标区10ST_TRYING新固件启动中尚未确认新目标区11~3ST_CONFIRMED新固件运行正常已确认新目标区00实际运行的路径是这样App A正常工作 → 收到升级指令 → 把固件写到App B → 校验通过 → 修改参数区把激活区切到B、update_flag置1 → 重启 → Bootloader发现update_flag1从B启动同时把try_count加1 → 如果App B运行正常延时几秒后App主动把update_flag清零try_count清零升级完成 → 如果App B起不来Bootloader再次启动时发现try_count已经等于或大于3直接把激活区切回App A。这里最核心的设计就是try_count。它的意思是“我不确定新固件能不能稳定运行所以先给它三次机会。三次都没跑起来我就认为它是坏的回滚。”5.3 为什么必须加“启动后健康确认”这一步如果只做Bootloader的静态校验看起来CRC对了、能跳转就算成功了但这个方案有一个盲区固件可能能启动但启动后在5秒内跑飞了。假设App B有严重的逻辑bug启动后初始化外设就卡死这时候看门狗可能还没喂设备直接挂在那。如果没有“健康确认”机制Bootloader每次重启都会回到App B因为它静态校验通过了。设备永远被困在新固件里用户完全没办法。所以我要求App在启动后主动“汇报健康”。具体做法是App在完成基本外设初始化、确认系统能正常运行后延时几秒然后调用ota_confirm_success()把参数区里的update_flag清零。关键在于延时几秒而不是一启动就确认。如果App启动后10秒内崩溃这10秒内它不会执行确认函数Bootloader在下一次复位时通过try_count判断“这个固件没确认过”继续执行重试或回滚逻辑。这个机制是不是很像Android里的“启动一次成功才算完成Ota”确实是这样AB分区本来就是这个思路的嵌入式实现。5.4 断电场景推演断电到底会发生什么下面是我列出的几种最常见的异常场景和最终行为做AB方案时要对照检查自己的代码是否覆盖了这些情况场景一固件写到一半断电。App B只写了一半参数区里的activation还是App A。重新上电Bootloader发现激活区是A直接启动A完全不受影响。场景二固件写完了参数区裁剪但还没有重启时断电。参数区可能还是旧的或者写了一半。没关系Bootloader读参数区时检查magicmagic不合法就按默认处理。最坏情况是回到App A不会启动半残的App B。场景三新固件启动后还没确认断电重启。参数区里update_flag1try_count1。Bootloader发现App B还没被确认继续启动App Btry_count变成2。如果一直是这种“启动即断电”的状态第三次之后Bootloader就会回滚到App A。这表示App B在这种环境下根本跑不起来回滚是合理结果。场景四新固件确认成功后断电。update_flag0激活区明确指向App BBootloader正常启动App B。升级正式生效。把这个表对着自己的代码过一遍如果以上四种场景都能正确处理AB方案的可靠性就立住了。5.5 参数区写入次数问题STM32内部Flash的擦写寿命通常在1万次左右。如果你每次App启动并确认成功都写参数区那么设备每启动一次就消耗一次擦写寿命。如果设备一天启动20次一年就是7300次不到两年参数区所在的页就报废了。这显然是问题。我的处理方式是只有状态发生迁移时才写参数区。确认成功一次但是往后每次启动发现update_flag0且激活区没变就不要再写Flash了。写参数区的操作代码里要加一个前提判断“如果值没变化直接返回不做擦写”。这算是一个容易被忽略但很重要的细节。6. 实测验证方法与调试经验怎么样才算真正复现成功6.1 实操前的准备清单如果你用的是现成的STM32F103CBT6最小系统板建议准备以下东西ST-Link V2调试器烧录Bootloader和AppUSB转TTL串口模块接PA9/PA10波特率115200一个按键复位开关Python3环境跑上位机发送脚本所有的代码工程我建议分成两个独立工程管理Bootloader一个、App一个不要在同一个工程里用宏控制编译后期维护起来太乱。6.2 第一步测试Bootloader能拉起App A先把Bootloader烧到0x08000000再把App A固件烧到0x08007000。上电后观察串口打印BOOT: param magic invalid, init to default BOOT: active slot: APP_A at 0x08007000 BOOT: app header magic OK, version1.0.0, crc0x12AB34CD BOOT: jumping to APP_A... APP_A: startup OK, now confirm...如果看到这个日志说明Bootloader已经能正确识别App A并跳转执行。如果你这边卡在HARDFAULT或者没输出先检查App工程里有没有做向量表重映射——这一步是F103 OTA最常翻车的地方。6.3 向量表重映射的正确写法App工程的启动代码里在main函数最开始执行向量表重映射#define APP_FLASH_BASE 0x08007000 #define SRAM_BASE 0x20000000 #define VECTOR_SIZE 128 void app_vector_relocate(void) { uint32_t i; uint32_t *vector_table_src (uint32_t *)APP_FLASH_BASE; uint32_t *vector_table_dst (uint32_t *)SRAM_BASE; // 把向量表从Flash拷贝到SRAM起始位置 for (i 0; i VECTOR_SIZE; i) { vector_table_dst[i] vector_table_src[i]; } // 打开SYSCFG时钟把内存映射重定向到SRAM RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); SYSCFG_MemoryRemapConfig(SYSCFG_MemoryRemap_SRAM); // 重设栈顶指针 __set_MSP(vector_table_src[0]); }这段代码的关键在于把向量表完整复制到SRAM开头的0x20000000处然后通过SYSCFG把地址0x00000000映射到SRAM。这样所有中断向量都能从SRAM里取到即使App跑在0x08007000中断也不会乱掉。6.4 第二步测试完整升级链路给App A烧个版本1.0的固件然后用Python上位机把编译好的App B固件版本2.0通过串口发送给App A。App A接收完自动重启观察串口日志APP_A: received update packet, targetAPP_B APP_A: fw verify OK, switch active slot to APP_B, reboot... BOOT: active slot: APP_B at 0x08013000 BOOT: app header magic OK, version2.0.0, crc0x98BADCFE BOOT: jumping to APP_B... APP_B: startup OK, now confirm...到这里整条AB链路已经通了。6.5 第三步测试断电回滚验证最后也是最激动人心的一步验证断电回滚。做法很简单把固件传输速度调慢或者直接在上位机发送过程中按复位键、断开电源模拟异常中断。然后再上电看是不是自动回到旧固件。我测试时的做法是发送App B固件时故意把串口线拔掉模拟传输中断。重新上电后Bootloader检测到App B区CRC校验失败自动切回App A串口输出BOOT: app B crc check FAILED, fallback to APP_A BOOT: active slot: APP_A at 0x08007000 BOOT: jumping to APP_A...看到这个日志你心里的石头才算彻底落地。6.6 常见问题排查表下面是几个实际项目中高频出现的坑整理成表方便排查现象可能原因排查方向跳转后HardFault向量表没重映射确认App工程是否有向量表复制逻辑跳转后栈指针跑飞栈顶地址校验不严检查MSP是否在SRAM范围内中断不响应跳转前忘记清NVIC跳转前ICER和ICPR全部清零Flash写不进忘记解锁或越界确认目标地址在可用Flash范围内CRC校验对不上上位机CRC算法和MCU不一致用同一个测试向量比对两边CRC值升级完一直重启try_count逻辑写错确认App确认函数执行的时机是否正确6.7 进一步扩展的思考串口走通之后如果你后续还希望把OTA升级能力和已有的Modbus RTU设备、或者某个物联网协议对接思路是一样的把现在的“串口接收固件”替换成任何数据通道协议加在应用层即可。比如对接一个Modbus从站只需要把升级指令定义为一个特殊功能码把固件分包塞进寄存器里读出来流程完全复用。AB分区的价值就在这里它和传输通道是解耦的。我个人的体会是AB OTA最怕的不是实现而是“想当然”。很多教程会告诉你“Bootloader校验一下然后跳转”但真正到了量产现场你会发现每一个环节——参数区写入、健康确认、断电恢复——都值得反复推敲。第一次在设备上看到“升级中断电后上电自动回滚到旧固件”那种踏实感是真的会让人上瘾的。希望这篇教程能帮你少走一些弯路把F103的AB OTA一次做对。
返回列表