有段时间我一直在纠结一个问题:产品已经交付到用户手上,固件发现 bug 或者要加新功能,总不能把设备寄回来拆机烧录吧。后来我把思路转到了 OTA 升级上,选型时没有直接上带硬件双 Bank 的高端芯片,而是在手头库存最多的 STM32F103 上折腾了一套 AB 分区方案。整套流程从 Bootloader 编写、App 工程改造、升级包生成到服务器端配置,我完整走了一遍,这里把从零复现的过程和踩过的坑都记录下来。项目标题是“STM32F103_AB_OTA_从零复现教程”,核心就是让一块普通的 F103 通过串口或局域网 HTTP 方式实现带 AB 双分区回滚能力的远程升级。
这套方案适合正在做物联网设备、仪器仪表、小车机器人或者任何基于 STM32F103 但又想拥有可靠升级机制的开发者参考。AB 分区的价值在于:当前运行的固件崩溃了,设备还能自动回到上一个正常版本,不用派人到现场拆机。很多人一听 AB 分区觉得是 Linux/Android 那套复杂玩意儿,但实际上在 F103 这种 Cortex-M3 芯片上,用软件分区模拟出同样的可靠性,并不难。
1. 项目整体设计与 AB 分区思路
1.1 为什么在 F103 上做 AB 分区,它和普通 IAP 有什么区别
先说一个很多人误解的点:STM32F103 内部 Flash 不支持真正的硬件双 Bank,不像 STM32F7、H7 系列有 dual bank 可以一边擦除一边执行。F103 的 AB 分区本质上是靠软件在片内 Flash 里划分两块区域,A 区和 B 区各自存放一份完整的固件。Bootloader 永远只从其中一个分区启动 App,当升级固件写入另一个分区并校验成功后,Bootloader 再切换启动目标。
这个思路和民航客机双引擎有点类似:一台引擎失效,另一台还能把你带回家。普通的单分区 IAP 升级方案最怕什么?升级过程中断电。固件只写了一半,Bootloader 跳转过去直接跑飞,设备变砖。AB 分区则把风险降到一个很低的水位:写入新固件时,当前在跑的旧固件一点不受影响,只有新固件完整写入且校验通过了,才切换启动位置。
在 F103 这种 Flash 只有 512KB 的中端芯片上做 AB 分区,需要接受一个现实:固件可用的空间会减半。但如果你的固件本身在 100KB 以内,这个代价完全值得。我这次复现使用的是 F103ZET6,512KB Flash,规划了两块主分区各 160KB,加上 Bootloader 64KB,剩下留给参数存储区,空间仍然充裕。
1.2 分区规划:地址、大小、用途怎么定
做 AB 分区第一步不是写代码,而是把 Flash 地址规划表画出来。F103ZET6 的片内 Flash 从 0x08000000 开始,总共 512KB。我的规划表如下:
| 区域 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| Bootloader | 0x08000000 | 64KB | 启动引导、升级逻辑、回滚控制 |
| Slot A | 0x08010000 | 160KB | App A 运行副本 |
| Slot B | 0x08038000 | 160KB | App B 升级副本 |
| Metadata | 0x08060000 | 8KB | 分区状态、版本、CRC、启动计数 |
| 参数区 | 0x08062000 | 剩余 | 业务参数、校准数据 |
分区地址的关键是对齐条件。STM32F103 的 Flash 擦除单位是 1KB(页),所以每个分区起始地址必须是 0x400 的整数倍。我在实际规划时把 Bootloader 选在 0x08000000,这是芯片复位后唯一能自动启动的区域,Bootloader 占用 64KB 留出足够余量。Slot A 从 0x08010000 开始,Slot B 从 0x08038000 开始,中间用 0x08010000 到 0x08038000 刚好是 160KB 的整数倍。
Metadata 区域很多人会忽略,但它是 AB 双分区的灵魂。我把版本号、本次启动分区号、固件长度、CRC 值、启动成功标志、启动失败回滚计数都放在这个区域。这个区域独立于两个 App 分区之外,每次启动流程都要读取它来决策:接下来跳 Slot A 还是 Slot B,还是需要回滚。
1.3 技术选型:标准外设库、编译器与传输通道
F103 的开发方式主要有三种:标准外设库、HAL 库、LL 库。这次我选择的是标准外设库 V3.5,理由很简单:Bootloader 只涉及 Flash、USART、GPIO、看门狗这几个外设,标准库代码量最小,编译出来的固件小,Bootloader 分区 64KB 完全够用。App 工程则可以用你自己习惯的 HAL 或者裸机框架。
编译器我用 Keil MDK 5.3x 版本,AC5 编译器。工程的散列文件(sct)需要针对分区做修改,后面我会详细说。如果使用 GCC 工具链,对应修改链接脚本即可,原理完全一致。
传输通道我做了两种:一是 UART 串口(配合 Ymodem 协议),这种方式适合产线和现场维护;二是 HTTP over 局域网(配合 lwIP 协议栈),这种方式适合设备在现场联网后远程推送。两种通道最终都只是把固件数据写到 Slot 分区,上层的 AB 策略和校验逻辑完全复用,所以本篇的 AB 策略部分与具体传输通道是解耦的。
2. Bootloader 核心设计与跳转实现
2.1 Bootloader 的完整工作流程
Bootloader 是整套 AB 方案里最需要抠细节的部分。它的启动流程是:
- 初始化时钟、串口、GPIO、Flash 接口
- 从 Metadata 读取当前分区状态
- 如果上一次启动标记为“成功”,继续当前分区
- 如果上一次启动标记为“未确认”,启动计数加 1,如果计数超过阈值,切换分区
- 校验当前启动分区的固件头(栈顶指针、复位向量、CRC)
- 通过跳转函数进入 App
- 如果引导期间收到升级指令(串口指令或网络触发),进入升级模式
这个流程中我特别强调启动成功确认机制。AB 分区最怕的是“以为升级成功了,结果新固件一跑就死”。所以我的方案是:App 启动后正常运行 10 秒,主动写回一个“启动成功”标志到 Metadata。Bootloader 下次启动看到这个标志,才确认当前分区是好的。如果 App 一启动就跑飞,Bootloader 下次启动看到的还是“未确认”状态,启动计数加 1,连续 3 次失败就自动回滚到另一分区。
2.2 Metadata 信息结构设计
Metadata 放在 0x08060000,独立于两个 App 分区。数据结构我定义如下:
#define META_MAGIC 0xA5A5A5A5 #define META_VERSION 1 typedef struct { uint32_t magic; // 魔数,判断 Metadata 是否有效 uint16_t version; // Metadata 结构版本 uint8_t boot_slot; // 当前启动的分区:0=Slot A,1=Slot B uint8_t boot_pending_flag; // 是否有待确认的启动 uint32_t app_version; // 当前生效固件版本号 uint32_t app_length; // 固件长度 uint32_t app_crc; // 固件 CRC32 校验值 uint8_t boot_count; // 连续启动失败计数 uint16_t rollback_threshold; // 回滚阈值,超过则切换分区 uint8_t reserved[64]; } OTA_Metadata;这个结构体必须保证写入 Flash 时对齐到 4 字节。我在 App 里也定义了一份相同的结构体,便于读取和修改。Metadata 区域我分配了 8KB,实际结构体只有不到 100 字节,剩余空间留给未来扩展,比如记录升级时间、升级次数、版本历史。
写 Metadata 时要注意一个关键点:不能直接更新结构体里的单个字段,因为 Flash 只能按位从 1 写成 0,不能在不擦除的情况下把 0 改回 1。所以我采用的方式是:每次修改 Metadata,直接擦除整个 8KB 区域,再一次性写回。频率不高,只在升级切换和启动确认时触发。
2.3 跳转函数的实现与陷阱
Bootloader 跳转 App 的代码网上有一堆,但很多人直接抄,跳完就跑飞。正确写法是:
typedef void (*AppResetHandler)(void); void JumpToApp(uint32_t app_addr) { uint32_t app_stack_addr; AppResetHandler app_reset; // 检查栈顶地址是否在 RAM 范围内 app_stack_addr = *(volatile uint32_t *)app_addr; if ((app_stack_addr & 0xFFF00000) != 0x20000000) { return; // 栈顶非法,说明这个地址没有有效的固件 } // 检查复位向量是否在 Flash 地址范围内 app_reset = (AppResetHandler)*(volatile uint32_t *)(app_addr + 4); if (((uint32_t)app_reset & 0xFFF00000) != 0x08000000) { return; // 复位向量非法 } // 关闭全局中断,防止跳转过程中断嵌套 __disable_irq(); // 把中断向量表指向 App 分区起始地址 SCB->VTOR = app_addr; // 设置主栈指针后跳转 __set_MSP(app_stack_addr); app_reset(); }跳转前为什么要检查栈顶和复位向量?因为在升级过程中可能发生过断电、数据错误等情况,固件数据本身不完整。如果直接跳到一个全是 0xFF 的地址,栈顶指针是 0xFFFFFFFF,硬件 Fault 会立刻触发,系统卡死。我见过太多人在跳转前不检查,结果升级包没写全,设备变砖,只能拆机用 SWD 恢复。这里加两道检查能拦截掉 99% 的异常跳转。
还有一个容易被忽略的细节:跳转前要关闭所有用到的外设中断和 DMA,把 USART、定时器等外设恢复默认状态。我在做第一版时忽略了这个,Bootloader 里串口接收用的中断没有关掉,跳转后外设中断残留导致 App 启动后立刻进 HardFault。后来我在跳转函数里统一加了外设 DeInit 操作。
2.4 App 工程的链接脚本修改
App 如果要从 Slot A 运行,编译时就必须把代码链接到 0x08010000 的地址,而不是默认的 0x08000000。Keil 下修改方式有两种:
第一种,在 Options for Target 里把 IROM1 起始地址改为 0x08010000,大小改为 0x28000(160KB)。这种方法简单,但生成的 HEX 文件自带地址偏移,烧录时如果没注意会直接烧到错误的位置。
第二种,修改分散加载文件(sct)。我习惯在工程里单独建一个 .sct 文件,内容大致如下:
LR_IROM1 0x08010000 0x00028000 { ER_IROM1 0x08010000 0x00028000 { *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00010000 { .ANY (+RW +ZI) } }同时要把 Startup 汇编文件里的中断向量表复制逻辑拿到 App 里处理。我的做法是在 App 的 SystemInit 之后,手动设置SCB->VTOR = APP_FLASH_BASE,这里的APP_FLASH_BASE是 0x08010000 还是 0x08038000,由 Bootloader 在跳转前写好的全局变量决定。App 编译时不需要指定具体跑到哪个 Slot,Bootloader 跳转前会把实际运行地址通过一个固定 RAM 地址传给 App。
烧录验证时,我习惯把两个 Slot 分别编译成 bin 文件,一个当作基线版本,一个当作测试升级版本。这样可以在本地就把整套流程跑通,不用每次都在服务器上处理。
3. App 端升级服务与下载逻辑
3.1 升级任务的触发方式:主动拉取与被动接收
App 端的升级逻辑我设计了两种触发方式。主动拉取模式下,App 定时向升级服务器发送版本查询请求,服务器返回最新版本号,如果高于本机当前版本,App 就请求升级包的元信息,然后启动下载。被动接收模式下,服务器或调试上位机直接给设备发一条自定义串口指令(或者 UDP 广播指令),设备收到后进入升级流程。
主动拉取适合已经接入网络的设备,被动接收适合产线或者现场调试。两种方式共用同一套升级执行函数:
void OTA_ProcessUpgrade(OTA_UpgradeInfo *info) { // 1. 获取目标分区地址(当前跑 A 就写入 B,反之亦然) // 2. 擦除目标分区的 Flash // 3. 边接收边写入,并累加 CRC // 4. 写完校验完整包 CRC // 5. 更新 Metadata,置位 boot_pending_flag // 6. 系统软复位,进入 Bootloader }这一步有个设计心得:升级包在下载过程中不修改当前运行分区的任何字节,所以即使下载到一半断电,重启后 Bootloader 发现当前分区还是旧的正常固件,直接启动,设备完全不受影响。这个特性极大地提升了我在实际工程中的安全感。
3.2 HTTP 下载固件到 Slot 分区的实现要点
走 HTTP 方式时,我用的协议栈是 lwIP(无操作系统模式)。F103 的 RAM 有 64KB,lwIP 的内存占用稍微控制一下问题不大。HTTP Client 部分我没用第三方的库,手写了一个精简版,因为需求很明确:GET 一个文件,把响应体写入 Slot 分区。
HTTP 下载的关键点是分包写入 Flash。不能等到把整个固件下载完再写,因为 F103 的 RAM 放不下 160KB 的数据。我用的循环是:
- 用 TCP 接收一个块(比如 1KB)
- 把块写入对应的 Slot 地址
- 接收下一个块,地址偏移一个块
- 不断累计 CRC32
这里有一个重要细节:每次写入 Flash 前要先擦除整页(1KB)。如果直接往没有擦除的 Flash 地址写,写入的数据和原来的 0x00 或已有值做 AND 运算,肯定出错。所以我在正式开始下载之前,会先把整个目标 Slot 分区逐页擦除。擦除 160KB = 160 页,在 72MHz 主频下耗时约 1~2 秒,完全可接受。
HTTP 服务器端我用了 nginx 做静态文件服务,把编译生成的 bin 文件放到指定目录,同时准备好一份 JSON 文件记录版本号和 CRC 值。App 先请求 JSON 拿到版本信息,再决定是否拉取 bin 文件,这样可以避免下载一半发现版本不对的尴尬。
如果没有网络环境,串口 Ymodem 通道是很好的保底方案。Ymodem 协议本身就带有 CRC16 校验和分包确认机制,非常成熟。我在 Bootloader 里集成了一小段 Ymodem 接收逻辑,这样即使 App 整个跑飞,只要 Bootloader 还活着,通过串口就能恢复系统,相当于一个软件层面的救生索。
3.3 CRC 校验与固件头格式
固件包我不建议直接传裸 bin,最好在前面加一个自定义固件头。我的固件头格式只有 16 字节:
typedef struct { uint32_t magic; // 0x4F54414D ("OTAM") uint32_t version; // 固件版本号,单调递增 uint32_t length; // 有效固件长度,不含头 uint32_t crc32; // 有效固件的 CRC32 } OTA_FirmwareHeader;Bootloader 或者 App 在写入前先校验固件头:magic 是否正确,length 是否在合理范围(不超过分区大小减 8KB),CRC 是否匹配。然后才开始正式写入。这个小头的好处是开发者可以在服务器端用脚本一键生成,比如 Python 脚本读取 bin 文件、追加头信息、重命名为app_v1.2.3.ota。
CRC32 我这里用的标准 zlib CRC32 实现。动手写的时候要特别注意初始值和最终异或值,网上有些 CRC32 实现是 CRC32/C 变体,算出来的结果和上位机不一致。为了省事,我在上位机端直接引用了 Python 的zlib.crc32(),STM32 端用查表法实现同一个算法,两边校准过一次之后再也没有出现校验不一致的问题。
3.4 升级完成后的复位与启动确认
固件全部写完并且 CRC 校验通过后,App 下一步是更新 Metadata 的 boot_pending_flag,然后调用 NVIC_SystemReset() 软复位。注意要先把升级目标 Slot 信息写到 Metadata,而不是等 Bootloader 现场猜。
// App 升级完成后(简化版) OTA_Metadata meta; ReadMetadata(&meta); if (current_slot == SLOT_A) { meta.boot_slot = SLOT_B; } else { meta.boot_slot = SLOT_A; } meta.boot_pending_flag = 1; meta.app_version = new_version; meta.boot_count = 0; WriteMetadata(&meta); NVIC_SystemReset();重启之后,Bootloader 读取 Metadata,看到 boot_pending_flag 是 1,于是跳转到新 Slot 而不是旧 Slot。App 正常运行后,在 main 函数开头的一段时间后调用:
// 启动确认函数 void OTA_ConfirmBootSuccess(void) { OTA_Metadata meta; ReadMetadata(&meta); if (meta.boot_pending_flag == 1) { meta.boot_pending_flag = 0; WriteMetadata(&meta); } }注意时序:确认启动成功的函数要在 App 关键初始化做完之后再调用,比如外设初始化、通信握手完成。如果 App 启动后连主循环都没进,那它根本没有机会去确认。这就会出现一个问题:新固件明明已经跑到 main 了,但还没来得及确认系统就崩溃,下次重启 Bootloader 还是会回滚。这在业务上不算错误,反而是一种保险机制:只有真正稳定运行的固件才被认定为“成功”。
我在实际项目里把确认函数放在一个定时器回调里,延时 10 秒执行,确认前如果看门狗复位了,说明早期初始化有问题,系统回滚到旧版本,逻辑完全自洽。
4. 上位机与服务器端配套工具
4.1 用 Python 脚本生成 OTA 升级包
整套系统跑起来之后,最影响效率的是每次编译完手工处理固件包。我写了一个 Python 脚本build_ota.py,功能是读取 Keil 生成的 bin 文件,计算 CRC 和版本号,拼上固件头,生成最终的 .ota 文件,同时生成一个ota_info.json供 nginx 目录下的版本检查使用。
import zlib import json import struct import sys def build_ota(bin_file, version, out_file): with open(bin_file, 'rb') as f: data = f.read() crc = zlib.crc32(data) & 0xFFFFFFFF header = struct.pack('<4sIII', b'OTAM', version, len(data), crc) with open(out_file, 'wb') as f: f.write(header) f.write(data) info = { "version": version, "url": out_file, "length": len(data) + 16, "crc32": crc } with open('ota_info.json', 'w') as f: json.dump(info, f, indent=2) print(f"OTA package generated: {out_file} (v{version}, {len(data)+16} bytes)") if __name__ == '__main__': build_ota(sys.argv[1], int(sys.argv[2]), sys.argv[3])脚本必须和工程放一起纳入版本管理。我见过一些团队打包升级文件靠手工点击工具,出一次错就要返工一台设备,代价非常高。自动化这一步看起来不起眼,但对效率提升是巨大的。
4.2 nginx 配置与局域网联调
局域网 OTA 需要一个简单的 HTTP 服务。nginx 的静态服务配置很短:
server { listen 8080; server_name _; root /opt/ota_files; autoindex on; }把app_v1.2.3.ota和ota_info.json都放到/opt/ota_files目录下即可。联调时注意关闭防火墙的 8080 端口限制。我遇到过一个很经典的局域网问题:两台设备之间 ping 能通,TCP 连接能建,但 HTTP 请求发出去之后迟迟等不到响应,最后排查发现是 Windows 防火墙拦了入站连接。这里建议直接把 F103 设备的固定 IP 加到防火墙白名单里。
默认端口我选了 8080 而不是 80,因为 80 端口在不少办公网络里被运营商或路由器拦截。F103 端请求时用完整的 URL,比如http://192.168.1.100:8080/app_v1.2.3.ota。
有人问要不要在 STM32 端实现 HTTPS,我的答案很直接:在 F103 上做 HTTPS 认证和加密开销太大,可靠性反而不容易保证。工业现场局域网内,固件包本身有 CRC 保护,传输通道有 TCP 校验,安全等级足够。如果真的担心传输内容被篡改,可以对固件头增加一个简单的签名机制,但这是后话。
4.3 命令行升级测试小工具
为了验证整套流程,我写了一个命令行小工具,负责通过串口把升级包推给 Bootloader 或 App:
ota_tool send --port COM5 --baud 115200 --file app_v1.2.3.ota ota_tool query --port COM5 --baud 115200工具内部实现了 Ymodem 发送方逻辑。实际使用下来,115200bps 发送 100KB 固件大约需要 10 秒左右,速度不是瓶颈,瓶颈反而是 Flash 的擦写时间。我试过把波特率提高到 460800,时间能缩短到 3~4 秒,但稳定性有所下降,最终选定 230400bps 作为性价比平衡点。
5. 复现过程中踩过的坑与排查技巧
5.1 跳转到 App 后系统跑飞,一直卡在 HardFault
这是做跳转功能时最常遇到的问题。我的排查思路是分三步:先查中断向量表是否设置成功,再查栈顶指针是否有效,最后查 App 的链接地址是否真的和烧录地址一致。
第一步,在跳转函数里临时加打印,输出 SCB->VTOR 的值,看是不是 0x08010000。如果 Bootloader 里没有重定向向量表,App 的中断回调全部指向 0x08000000 的向量表,App 里配置的中断(比如 SysTick、USART)就会指向 Bootloader 的向量定义,行为不可预知。第二步,确认 App 的编译链接地址,在 Keil 的 .map 文件里搜索__initial_sp和Reset_Handler,看看地址是否落在 Slot 分区范围。第三步,栈顶指针是否指向有效的 RAM 地址,这个在跳转函数里已经有检查,不用额外看。
我记得有一次怎么查都查不出问题,后来发现是 Bootloader 编译时优化选项开了 -O3,跳转前对某些寄存器的状态判断被优化掉了。把优化级别降为 -O0 后一切正常。这个案例提醒我:Bootloader 这种和硬件寄存器近距离交互的代码,最好关闭优化或不高于 -O1。
5.2 升级成功后反复回到 Bootloader,App 一直起不来
这种问题往往出在“启动确认”环节。App 跑起来了,但没能在 10 秒窗口内把 boot_pending_flag 清零,Bootloader 下次启动时看到标志仍是 1,认为启动失败,执行回滚。还有一种可能是 Metadata 写入本身失败了:Bootloader 里写的 boot_slot 和 App 实际运行的 Slot 不一致。
解决办法是在 App 最早期(时钟、串口初始化后)就执行一次 OTA_ConfirmBootSuccess(),如果后面主循环跑飞,最多造成一次无谓的回滚,但至少设备不会“永久卡死”。另一种做法是把回滚阈值调大,比如 5 次,防止偶尔一次干扰触发回滚。
5.3 Flash 写入过程中进入中断导致硬件错误
这个坑比较隐蔽。STM32F103 在 Flash 编程/擦除期间,Flash 存储器处于忙状态,此时如果中断服务程序里访问了 Flash(比如从常量区读取字符串、跳转到中断向量表),会让 CPU 进入 Bus Fault。我的升级函数里是边接串口边写 Flash,串口中断每次进来都要从常量区取字符串、查表,结果就是写入过程中随机触发 HardFault。
解决方式有两种:一种是在擦写 Flash 前关全局中断,擦写完成后再打开;另一种是保证中断处理函数全部用 RAM 里的变量。第一种简单粗暴,擦写一页的时间大概 1~2ms,关闭中断对实时性影响很小。我当时选择了在擦写函数内部做临界区保护:
void FLASH_WritePageSafe(uint32_t addr, uint32_t *data, uint32_t len) { __disable_irq(); FLASH_Unlock(); // 逐字写入 for (uint32_t i = 0; i < len; i++) { FLASH_ProgramWord(addr + i * 4, data[i]); } FLASH_Lock(); __enable_irq(); }不过关中断时间不能太长,如果写了 160KB 固件全程关中断,系统看起来就像死机。所以我的策略是:每写完一页(1KB)就打开一次中断,让其他任务喘口气。又安全又响应及时。
5.4 HTTP 下载慢或者卡在建立连接阶段
F103 走 lwIP 无操作系统模式时,处理 HTTP 请求需要注意 TCP 窗口大小和接收超时。lwIP 默认的 TCP_MSS 是 1460 字节,如果 App 在请求头里没有声明关闭 Nagle 算法,小包可能会被合并,导致下载速度很慢。我在 HTTP 请求发出后,主动调用了tcp_nagle_disable()。
另一个容易犯错的是接收缓冲区的大小。F103 的 RAM 只有 64KB,我给的 lwIP mem 和 pbuf 池偏小,结果 HTTP 响应体超过几百 KB 时频繁丢包重传。后来我把内存池调大,腾出约 20KB 给 lwIP,稳定跑完整个下载过程。
如果设备在局域网里 ping 得通但 HTTP 连接不上,重点抓包。先用 Wireshark 抓 TCP 连接的 SYN/SYN-ACK/ACK 是否完整。我遇到过设备端防火墙(如果有)和 Windows 防火墙双重拦截的情况,关了之后立刻就好了。
5.5 快速排障速查表
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 跳转 App 后全速跑飞 | 中断向量表未重定向 / 链接地址错误 | 检查 SCB->VTOR 和 .map 文件 |
| 升级后反复重启进入 Bootloader | 启动确认标志未清除 | 早期调用 OTA_ConfirmBootSuccess() |
| Flash 写入期间 HardFault | 中断里访问 Flash | 擦写页时关中断 |
| HTTP 下载极慢 | TCP 窗口太小 / Nagle 算法 | 关闭 Nagle,调大 lwIP 内存池 |
| 串口 Ymodem 传完校验失败 | 波特率漂移 / 固件头 CRC 与包 CRC 不一致 | 降低波特率,重算 CRC |
| Bootloader 里读到的版本总是旧的 | Metadata 擦写失败或逻辑顺序错误 | 检查擦除流程,写入前必须擦整页 |
6. 进一步扩展思路
AB 分区框架搭好之后,扩展方向很多。比如在 Metadata 里记录升级历史,做一个简单的设备升级全程追踪;再比如把升级包压缩,F103 没有硬件解压单元,软件解压低配方案用 MiniLZO,体积和解压速度都比较适中。还有就是把 AB 策略和 RS485 组网结合起来,做一条总线上多设备同时升级的模式,升级包统一广播,每个设备按自己的 Slot 写入对应地址。
如果后期换用的主控是 STM32F4 或 F7 系列,这套 AB 代码的跳转逻辑和 Metadata 结构基本可以无缝迁移,只需要把 Flash 擦写底层函数和链接地址改掉,上层策略几乎不用动。
最后再分享一个小技巧:调试 AB OTA 时,最好在工程里加一个“强制回滚测试”的隐藏命令,可以在任意时刻把 Metadata 里的 boot_pending_flag 置位再复位。这样在产线验证时,不用真的把一个坏 App 烧进去再试回滚,既能减少出错概率,又能快速验证 Bootloader 的回滚策略是否正确。这套方案我前前后后调了大概两周,遇到的最多的坑都集中在 Flash 操作时序和跳转细节上,把这些细节处理好,整个系统其实非常稳定。