news 2026/9/9 9:57:26

STM32F103 A/B分区OTA升级完整方案与Bootloader实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103 A/B分区OTA升级完整方案与Bootloader实现

在做嵌入式产品的时候,固件升级一直是绕不开的坎。尤其是用 STM32F103 这种老而弥坚的芯片,很多人一开始图省事,直接在应用里做IAP,结果设备出货后碰到升级掉电、固件校验失败、刷完变砖,售后想死的心都有。我这次把 STM32F103 上实现 A/B 分区 OTA 的完整过程从零复现了一遍,把 Bootloader、App 双区切换、固件校验、传输通道、回滚机制全部打通。

这篇教程不是抄手册,是把我实际踩过的坑和验证过的方案整理出来,适合已经会用 STM32F103 标准库开发、但想把 OTA 做得更稳的工程师,也适合刚接触 Bootloader 想系统搞清楚的入门者。文中会给出分区地址表、CRC32 实现、跳转代码、Flash 写入代码、以及一套可以直接照抄的调试顺序。

1. 为什么要给STM32F103上A/B分区OTA

1.1 单区升级方案的问题

早年做 IAP 最常见的做法是 Bootloader + 一个用户 App 区,升级时先把新固件下载到预留的临时缓冲区或者直接用通信方式边收边擦写 App 区。听起来简单,但这里有三个致命麻烦。

第一,掉电风险。升级中途一旦断电,App 区可能处于擦了一半或者写了一半的状态。下次上电 Bootloader 发现 App 校验不通过,设备就成了砖。第二,临时缓冲区的容量问题。很多 F103 型号 Flash 本来就不大,你要留一个跟 App 等大的下载区,Flash 占用直接翻倍。如果不留缓冲区而是直接擦写 App 区,那升级过程中系统完全不可用,而且依然有掉电风险。第三,没有回滚手段。就算你升级成功,发现新固件有 bug,设备已经跑起来了,只能再烧录一次旧固件,无法自动恢复。

我做过的几个项目里,就遇到过现场升级掉电导致设备需要返厂的情况。从那之后,凡是要求可靠性高的设备,我都倾向用 A/B 双分区方案。A/B 方案的核心思路就是:把固件分成两份,一份是当前正在运行的,一份是专门用来接收新固件的备份区。平时系统跑 A 区,升级时把新固件写入 B 区,校验通过后再切换启动入口。

1.2 A/B双分区的优势与代价

A/B 分区本质上是一种“双保险”设计。想象一下你住在一栋有两个门的房子里,装修一个门的时候你还可以从另一个门进出,就算装修失败也只是把坏门锁上,生活不受影响。设备升级也是一样:当前系统在 A 区跑着,新固件在 B 区下载和校验,整个过程 A 区一直可用,不会出现设备在升级过程中变成板砖的情况。

用表格列出关键区别更直观:

方案掉电安全升级期间可用性回滚能力Flash占用
传统单区IAP低,擦写中断即砖低,升级时覆盖运行区较少
独立下载区+IAP中,掉电可重新下载中,系统停更有限较多
A/B双分区高,任意时刻均有可用固件高,切换原子完成强,可自动回滚两倍App空间

当然代价也很直接,Flash 占用翻倍、逻辑复杂度上升。对于 F103 这类芯片来说,A/B 是否能落地,第一步就要算清楚 Flash 容量够不够。

1.3 先算一笔资源账:F103能不能玩A/B

我个人建议,固件体积在 64KB 以内时,选 256KB 以上的 F103(比如 F103VCT6)做 A/B 分区比较从容;固件如果到 100KB 以上,直接上 512KB 的型号,比如 STM32F103ZET6 或者 RET6,这两个型号都是 512KB Flash、64KB RAM,做 A/B 非常合适。

我在本教程中使用的是一个比较宽松的 512KB 分区方案:Bootloader 占 32KB、独立标志区占 4KB、A 区和 B 区各 192KB、最后留 92KB 给参数和日志存储。算下来刚好填满 512KB,具体分区表下一节给出。

这里有个经验:Bootloader 不要只给 16KB。很多初期 Bootloader 只有跳转逻辑,16KB 够了,但后续如果要加加密验签、固件加密、多协议支持,就会发现 16KB 非常局促。前期多留一点,后面不用返工。

2. 整体架构与存储布局

2.1 Flash分区表设计

STM32F103 大容量型号的 Flash 是 1KB 一页,擦除必须按页操作,所以分区地址尽量按 1KB 对齐,而且每个分区起始位置最好放在擦除边界上。我第一次设计分区时没注意,把 App 的起始地址设成了一个非页对齐的偏地址,结果 FLASH_ErasePage 总是把 Bootloader 尾巴擦掉,设备直接变砖,这个问题排查了很久。

推荐的分区表如下:

分区起始地址大小用途
Bootloader0x0800000032KB启动跳转、升级调度、校验与回滚
系统标志区0x080080004KB活动分区、升级状态、CRC、版本信息
App A0x08009000192KB活动分区A,固件主副本
App B0x08039000192KB备份分区B,新固件下载目标
参数存储区0x0806900092KB运行参数、日志、掉电保护数据

分区之间不要贴太紧。我的 Bootloader 实际编译出来可能只有 15KB,但为了以后扩展还是留足 32KB。A 区和 B 区各 192KB,对于大多数 F103 应用绰绰有余。参数区 92KB 可以存放设备配置、校准数据、日志循环缓冲,甚至还能放一帧 8KB 左右的掉电保护文件。

2.2 标志区数据结构设计

A/B 切换需要一套可靠的“元数据”来记录当前状态。这些数据不能放在 App 里,因为 App 升级时自己可能被覆盖,也不能放在 Bootloader 里,因为 Bootloader 是只读的,所以必须单独划分一个标志区。

我定义的结构体如下:

#define SYS_FLAG_MAGIC 0xA5A55A5A #define UPDATE_STATUS_IDLE 0x00000000 #define UPDATE_STATUS_READY 0x55555555 // 固件已就绪,等待切换 #define UPDATE_STATUS_COMMIT 0xAAAAAAAA // 已提交,不需要回滚 typedef struct { uint32_t magic; // 固定魔数,校验数据有效 uint32_t active_slot; // 0表示App A,1表示App B uint32_t update_flag; // 1表示有待切换的新固件 uint32_t update_status; // 提交状态 uint32_t boot_count; // 新固件启动次数计数 uint32_t image_size; // 固件字节数 uint32_t image_crc; // 固件CRC32 uint8_t version[16]; // 固件版本号字符串 uint32_t reserved[8]; // 预留 } sys_flag_t;

这个结构体共 52 字节左右,写入 Flash 时建议直接占用一页。F103 按页擦除,所以整个标志区 4KB 里,第一页放结构体,剩余空间可以预留做冗余备份。升级写标志时,可以交错写两份结构体,防止更新过程中掉电导致结构体半写坏。

这里的 update_status 字段特别关键。它的作用是把“升级流程”串起来:固件下载完,先在标志区写 update_flag=1,等 Bootloader 切换完分区、App 启动自检通过后,App 再写 update_status=COMMIT,表示这个版本已经稳定运行,不需要回滚。如果 App 启动后迟迟不提交,Bootloader 就会在下次启动时按回滚策略处理。

2.3 编译环境与链接脚本准备

F103 开发我用的还是经典组合:Keil MDK + 标准外设库 V3.5,这套组合虽然老,但资料多、稳定,做 Bootloader 这种底层工作很合适。当然你用 HAL 库也没问题,原理完全一致,只是底层接口不同。

A/B 双分区要求 App 工程编译时就能确定自己要烧到哪个地址。A 区 App 的烧录地址是 0x08009000,B 区 App 的烧录地址是 0x08039000。我通常的做法是建立两个 App 工程,共用代码,只在链接脚本和宏定义上区分。Keil 里对应的是分散加载文件.sct,A 区版本如下:

LR_IROM1 0x08009000 0x00030000 { ER_IROM1 0x08009000 0x00030000 { *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x0000C000 { .ANY (+RW +ZI) } }

B 区版本唯一区别就是把0x08009000换成0x08039000。注意这里0x00030000是 192KB 的区域长度,不要写错。编译完成后,烧录时分区烧:Bootloader 烧到 0x08000000,A 版 App 烧到 0x08009000,B 版 App 烧到 0x08039000。

链接脚本改完之后,还要同步修改系统初始化文件里的向量偏移。在system_stm32f10x.c中有一个VECT_TAB_OFFSET宏,A 区 App 设为0x00009000,B 区 App 设为0x00039000。前期最容易踩的坑是只改了链接脚本,忘了改向量偏移,结果程序一跑中断全乱飞,表面现象就是定时器不工作、串口收不到数据。

3. Bootloader端实现

3.1 Bootloader主流程与状态机

Bootloader 是整个 A/B OTA 的大脑,它的核心任务不是“下载固件”,而是“决定启动谁、以及在需要的时候切换和回滚”。我的 Bootloader 主流程按以下顺序执行:

  1. 初始化时钟、串口调试、看门狗。
  2. 读取标志区的 sys_flag_t 结构体,检查 magic 是否合法。
  3. 如果 update_flag=1,进入升级切换流程。
  4. 校验备份分区的固件 CRC 和大小。
  5. 校验通过后,切换 active_slot,清零 boot_count,把 update_flag 保留,update_status 置为 READY。
  6. 校验失败则清掉 update_flag,保持旧分区不变。
  7. 根据 active_slot 计算当前 App 起始地址,跳转。

如果用图来画就是:上电 -> 读标志 -> 判断是否需要切换 -> 校验 -> 切区 -> 跳转。这里最关键的是第 4 步的校验,不能省。有些人图快,收到升级命令就直接切 active_slot,结果发现固件下载一半根本没写完,设备直接起不来。我见过最惨的一次就是同事把这个步骤简化了,几百台设备全部需要返厂。

看门狗在 Bootloader 里也要注意。由于升级过程可能耗时较长,尤其是串口下载 192KB 固件可能要几分钟,看门狗超时值要设计得足够长,或者在校验过程中周期喂狗。建议在 Bootloader 中加一套简单的日志输出,比如串口打印[BL] check crc: ok[BL] jump to A,这样现场出问题能快速定位。

3.2 固件校验:CRC32查表实现

固件校验我用了 CRC32,先在生产电脑上计算好固件的 CRC32 和大小,通过网络或串口把它写入标志区,然后 Bootloader 在启动时对备份分区逐字节重新计算 CRC32,两个值一致才算通过。CRC32 的查表实现非常成熟,在 F103 上跑 192KB 数据大约只需要几毫秒到几十毫秒,完全可接受。

查表法实现如下:

static uint32_t crc32_table[256]; void crc32_init_table(void) { for (uint32_t i = 0; i < 256; i++) { uint32_t c = i; for (int k = 0; k < 8; k++) { if (c & 1) { c = 0xEDB88320UL ^ (c >> 1); } else { c >>= 1; } } crc32_table[i] = c; } } uint32_t crc32_calc(uint8_t *buf, uint32_t len) { uint32_t crc = 0xFFFFFFFFUL; for (uint32_t i = 0; i < len; i++) { crc = crc32_table[(crc ^ buf[i]) & 0xFF] ^ (crc >> 8); } return crc ^ 0xFFFFFFFFUL; }

Bootloader 里不能直接对整个备份分区用这个函数一次性计算,因为内存不够。我采用分段读取的方式:准备一个 1024 字节的缓冲区,一页一页地读 Flash 并喂给 CRC32 状态,最后得到整个固件的 CRC。注意,CRC32 是流式的,分段计算和一次性计算的最终结果是一样的,前提是每一段的顺序和长度一致。

校验时还有一个容易被忽视的问题:固件大小不对齐。如果固件实际字节数是 123456,而你写入 Flash 时按 4 字节对齐补了 123456 的整倍数,那 校验时要按真实大小计算,不能把 padding 字节算进去。我在第一次调试时就是没处理对齐补位,CRC 怎么都对不上,后来加了一个image_size字段严格控制计算长度才解决。

3.3 跳转函数与中断向量表处理

跳转是整个 Bootloader 最薄也最危险的环节。跳之前要确认目标地址确实是一个合法的 App 镜像,最简单的方法是检查该地址处的栈顶值是否落在 SRAM 区间内。如果栈顶值都不合法,说明 App 区根本没烧录或者已经损坏,这时候强行跳转只会 HardFault。

我写的跳转函数:

typedef void (*pFunction)(void); void jump_to_app(uint32_t app_addr) { uint32_t msp_value = *(volatile uint32_t *)app_addr; uint32_t reset_handler = *(volatile uint32_t *)(app_addr + 4); if ((msp_value & 0xFFF00000) != 0x20000000) { // 栈顶指针不在RAM区间,说明App镜像不合法 return; } __disable_irq(); SysTick->CTRL = 0; SysTick->LOAD = 0; SysTick->VAL = 0; SCB->VTOR = app_addr; __set_MSP(msp_value); pFunction jump = (pFunction)reset_handler; jump(); }

这段代码有几个关键点。第一,关闭中断后再跳转,防止跳转过程中被未处理的中断打断。第二,清零 SysTick,因为 SysTick 可能在 Bootloader 里被打过,不清理会影响 App 的时钟基准。第三,设置 SCB->VTOR 到目标分区基地址,F103 的 Cortex-M3 内核支持这个寄存器,但很多资料里没提清楚,导致很多人跳转后中断不响应。

跳转前还应该把 Bootloader 已经打开的外设时钟关掉,尤其是串口、DMA、定时器。虽然 App 启动时会重新初始化,但如果 Bootloader 里开了外设中断,跳到 App 的瞬间外设还在产生中断,而 App 的中断向量表还没完全准备好,可能一开机就进 HardFault。我通常在跳转函数前面统一调用RCC_DeInit()GPIO_DeInit()把所有外设复位。

3.4 启动计数与自动回滚

A/B 方案最有价值的能力就是自动回滚。新固件切换过去后,如果系统起不来,Bootloader 要能自动切回另一个分区,不能让设备变成砖。我用 boot_count 实现这个机制。

具体逻辑是:Bootloader 每次切换到新分区时把 boot_count 清零并写入标志区,然后跳转到新 App。App 启动后,如果一切正常,应尽快完成自检并调用“提交”流程,提交后把 update_status 置为 COMMIT。如果 App 崩溃或无法完成自检,boot_count 不会更新。

那么什么时候触发回滚?我设计的策略是:每次 Bootloader 上电读到 active_slot 指向的分区没有被提交,就把 boot_count 加 1。如果 boot_count 超过阈值,比如 3 次,就认为当前 App 有问题,Bootloader 自动切换 active_slot 到另一个分区,并清除升级标志。这样即使 App 启动即崩溃,下次重启 Bootloader 也会主动切回旧版本。

这个机制也不是万能的,它依赖设备能反复复位。如果 App 挂起但系统没有复位,boot_count 就不会增加,所以最好配合一个硬件看门狗,App 不能正常喂狗就复位,形成一套完整的“启动-自检-喂狗-提交”链路。

4. App端实现

4.1 App的Flash基地址与分散加载文件

App 端要解决的第一件事是让自己能在指定分区地址运行。除了前面说的分散加载文件,还要特别注意中断向量表的偏移设置。如果你用的是标准库,system_stm32f10x.c里会有:

#define VECT_TAB_OFFSET 0x00009000

在 SystemInit 函数最后会判断是否启用向量表偏移。如果你在 main 函数里手动设置也可以:

SCB->VTOR = APP_A_BASE_ADDR;

这个设置要在系统时钟初始化之后、任何外设中断使能之前完成。否则中断向量表还指向 0x08000000 的 Bootloader 区,一旦某个外设产生中断,CPU 会取到 Bootloader 的中断向量,行为不可控。

还有一个容易踩的坑:局部变量数组过大。F103 的 SRAM 只有 64KB 甚至更小,如果你在接收固件时定义一个 48KB 的数组来缓冲,直接就把栈撑爆了。正确的做法是准备 1KB~4KB 的小缓冲区,一边收一边写 Flash,而不是把整个固件都缓存在内存里再写。以 2KB 缓冲区为例,写一页 Flash 需要 2 次 FLASH_ProgramWord,速度完全可以接受。

4.2 固件接收与Flash写入

App 收到升级命令后,要把新固件写入另一个分区。比如当前 active_slot=0 对应 A 区,那新固件就要写到 B 区。写入过程中有一个关键原则:擦写动作不能阻塞系统太久。如果你的设备还有实时性要求,建议用 DMA + 双缓冲接收,主循环里只在缓冲区满时才执行 Flash 写入。

写入 Flash 的底层代码:

void flash_write_page(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; uint32_t word; FLASH_Unlock(); FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); FLASH_ErasePage(addr); for (i = 0; i < len; i += 4) { memcpy(&word, buf + i, 4); FLASH_ProgramWord(addr + i, word); } FLASH_Lock(); }

写入时要考虑跨页问题。因为 F103 的页是 1KB,如果你接收的缓冲区是 2KB,那么一次写操作实际上跨了两页,需要分别擦除两页再写入,否则第二次写会失败。更稳妥的做法是按页处理:每接收满 1KB,就擦除目标页,然后写入这一页数据。第一次实现的版本里我图省事,直接按 4KB 缓冲区处理,结果跨页没擦对,写入老失败,一度怀疑是 Flash 寿命问题。

固件接收过程中的校验也不能只放在最后。我建议在接收过程中边收边计算 CRC32,写完后把计算结果和上位机传来的 CRC 值比对,不一致就重新下载。毕竟 Flash 擦写次数有限,如果每次都是写完才发现数据不对,对 Flash 寿命也是一种消耗。

4.3 升级触发、版本管理与提交

升级触发方式很多:串口命令行、按键组合、远程网络命令、甚至云端平台指令。触发条件一定要加版本判断,不能收到升级指令就下载。我习惯在升级包里同时带上版本号,App 端收到升级指令后先比对版本号,只有目标版本高于当前版本才继续。

完整升级流程如下:

  1. 收到升级指令,包含目标版本、固件长度、CRC32。
  2. 校验版本号是否高于当前版本。
  3. 擦写备份分区,接收固件数据,边收边写边算 CRC。
  4. 固件全部写入后,校验 CRC,把 image_size、image_crc、版本号写入标志区。
  5. 设置 update_flag=1,软复位。
  6. Bootloader 上电校验并切换分区,跳转到新 App。
  7. 新 App 启动,执行硬件自检,成功则提交,失败则依赖回滚机制。

第 5 步的软复位用NVIC_SystemReset()就行。这里注意,软复位前要把所有挂起的中断清掉,防止复位过程被残留中断干扰。实际项目中我还遇到过软复位后 Bootloader 串口初始化失败的问题,后来排查发现是复位前 DMA 没关干净,导致复位后 DMA 仍在访问总线,干扰外设初始化。所以建议在复位前统一调用RCC_DeInit()把所有时钟复位。

4.4 健康检查与回滚上报

App 端的健康检查决定了回滚机制是否有效。检查项可以根据产品特性设计,但至少包括:关键外设初始化是否成功、主循环是否正常运行、能否正常上报状态。如果设备有通信功能,还可以在启动后尝试上报一次版本号,服务器确认新版本在线,App 再执行提交。

提交操作其实就是写标志区:把 update_status 从 READY 改成 COMMIT,同时清掉 update_flag。这个动作要尽量放在启动流程的后期,也就是所有关键初始化都完成、系统确认稳定运行之后。如果初始化过程中失败,就不要提交,让 Bootloader 在下一次复位时自动回滚。

举个例子,我有一个项目里电机驱动板的 App 启动后需要自检编码器是否正常,如果自检失败就进入错误状态并且不复位也不提交。后来又加了一路看门狗,自检失败就喂不了狗,MCU 自动复位,Bootloader 计数器加了两次后自动回滚。这样即使现场没人干预,设备也能自己恢复到旧版本,售后压力小了很多。

5. 传输通道实现与扩展

5.1 串口+Ymodem本地升级

在调试阶段和生产阶段,我强烈建议先用串口+Ymodem 方式验证 A/B 分区逻辑,不要直接上网络。因为串口最简单、最容易排查问题,把 A/B 切换的每一环都调通之后,再换网络通道就只需要替换“接收固件”这一层,其余逻辑完全不用动。

Ymodem 协议本身有包序号、CRC 校验、结束帧等机制,适合传输 192KB 的固件。上位机用 SecureCRT、Xshell 或者专门的 Ymodem 工具就行。在 F103 端,Ymodem 接收的关键是处理好 128 字节和 1024 字节两种包长,以及每个包的应答时序。有一些移植好的 Ymodem 库可以直接嵌入,但建议至少读一遍源码,理解包格式,后面出了问题才能快速定位。

串口波特率建议在调试阶段用 115200,稳定后再考虑 460800。更高的波特率对时钟精度和线材要求都更高,F103 的 UART 在 72MHz 主频下跑 460800 没问题,但 USB 转串口芯片如果质量一般就容易丢字节。我实际测试过 115200 下传 192KB 固件大约需要 20 秒,460800 下大约 5 秒,对于 A/B 方案来说,20 秒完全可接受,毕竟升级时设备还在正常运行。

5.2 升级服务器方案与网络下载

到了产品化阶段,OTA 一般走网络通道。F103 本身没有以太网,但可以通过 ESP8266、ESP32 或者有线以太网模块扩展。最简单的方式是 F103 通过 UART 接 ESP8266,ESP8266 做透传,F103 发送 HTTP GET 请求下载固件。我记得有篇文章提到过,ESP32 做 OTA 时会实时打印下载进度,这个思路在 F103+ESP8266 组合中也适用,只是把下载的固件写到本地备份分区即可。

服务器端我建议用 nginx 做静态文件托管。设备启动时先请求一个 version.json,内容类似:

{ "version": "1.2.3", "app_file": "app_v1.2.3.bin", "file_size": 150000, "file_crc32": "0xA1B2C3D4" }

设备端解析 version.json,发现版本号高于当前版本,再请求对应的 bin 文件。下载过程中通过 HTTP Content-Length 或者自己管理长度来确认固件完整性。这种结构的灵活之处在于:以后要增加签名校验,只需要在 json 里加一个 sha256 或者设备端增加 RSA/ECDSA 验签逻辑,传输层不用动。

网络传输的固有坑是信号中断和服务器超时。我建议设备端实现断点续传,或者至少做到“下载失败就删掉备份分区,下次重新下载”。由于 A/B 方案本来就是双分区,下载过程中原系统不受影响,所以即使下载了一百次都失败,设备依然在正常运行,这种体验非常加分。

5.3 升级包的生成与发布流程

我专门写了一个小脚本,编译完成后自动生成升级包。流程如下:

  1. 编译得到.bin文件。
  2. 计算文件的字节数和 CRC32。
  3. 生成 version.json。
  4. .binversion.json上传到服务器发布目录。

这个小脚本看起来简单,实际大大降低了发布时的出错率。以前人工计算 CRC 经常算错,导致设备升级时反复校验失败,排查起来很费劲。现在脚本一步到位,发布流程 5 秒完成,还能自动打上版本号。

发布时还有一个细节:旧版本兼容问题。如果设备批量升级是从 v1.0 直接升级到 v1.2,而 v1.0 的固件接收逻辑有 bug,可能会导致 v1.0 无法正确接收 v1.2 的升级包。这种情况只能制定分步升级策略,先升到 v1.1,再升到 v1.2。在 A/B 方案中,旧版本只要还能正常运行,升级通道就还有救,不至于彻底无法升级。

6. 从零复现的调试顺序与常见问题

6.1 按这个顺序做,少走弯路

我建议严格按照下面的顺序来调,每跑通一步再进入下一步,否则问题叠加起来会非常难定位。

第一步,先只写一个最简单的 Bootloader,强制跳转到 A 区固定地址,A 区 App 点个灯。验证跳转机制和链接脚本没问题。第二步,把 App 工程复制一份作为 B 区版本,分别烧录到 A 和 B 两个分区,Bootloader 通过修改 active_slot 来看能不能跳两边的 App。第三步,实现标志区的读写和 CRC32 校验,手动在调试器里修改标志位,验证 Bootloader 的升级切换和回滚逻辑。第四步,接上串口 Ymodem 接收,把新固件写到备份分区,验证整个升级闭环。第五步,再加网络传输、服务器发布、断点续传等扩展功能。

这个顺序的核心思想是:先把最底层、最危险、最难查的问题解决掉,比如跳转和向量表偏移,然后再加入传输层。如果你一上来就写好几百行的网络下载代码,然后直接烧录测试,一旦出问题根本不知道是网络的问题还是 Flash 写入的问题,排错成本会非常高。

6.2 常见问题速查表

实际操作中我从这些坑里一个个踩出来,总结成表格:

现象可能原因解决办法
跳转后程序跑飞/HardFault链接脚本基地址错误或向量偏移未设置检查 App 编译地址和 SCB->VTOR
中断全部不响应向量偏移没设置或跳转后未设置 VTOR在跳转函数中设置 SCB->VTOR
升级后校验一直失败CRC 计算范围包含 padding 字节用 image_size 字段限定计算长度
Flash 写入失败目标地址跨页未擦除按 1KB 页对齐接收写入
软复位后外设不正常DMA/外设未复位关闭跳转前调用 RCC_DeInit
串口接收丢字节波特率过高或接收缓冲区太小降低波特率,增加双缓冲
新固件启动后自动回滚App 没有在预期时间内提交检查 App 端自检和提交逻辑
标志区数据损坏写标志时掉电写两份冗余标志,启动时取合法副本

这个表里每一项都是我实际遇到过的。最坑的还是“跳转后中断不响应”,有时候表现为串口没输出,有时候表现为定时器突然不准,如果你不知道是向量偏移的问题,可能会白白浪费一整天去查外设配置。

6.3 排障三板斧

最后分享我的排障三板斧。第一板斧是串口日志,Bootloader 和 App 各用不同前缀,比如[BL][APP],每个关键步骤都打日志:进入 Bootloader、读取标志、开始校验、校验完成、切换到分区、即将跳转。第二板斧是 LED 状态灯:Bootloader 用红色 LED 表示等待升级,绿色 LED 表示升级成功,慢闪表示回滚。这样现场没有电脑也能用灯色判断状态。第三板斧是调试器读取标志区,直接查看当前 active_slot、update_flag、boot_count 的值,结合串口日志能迅速定位问题出在升级流程的哪一环。

在这套方案调试到稳定之后,我又加了一个小功能:Bootloader 启动时如果检测到参数存储区的魔法数被破坏,会自动使用 Flash 内部默认参数恢复,并记录一条恢复日志。这个功能不是 A/B OTA 的必要部分,但让整个产品在异常掉电场景下更加稳健。我自己的体会是,A/B 方案不是一个“加一段代码”就能完成的点状改动,它需要 Bootloader、App、发布脚本、服务器配合起来,是一个完整的系统设计。每次看到设备升级时毫不慌张地下载、校验、切换,我心里都会觉得当初多花的那几天时间非常值。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 9:56:37

分布式电源接入配电网承载力评估:Matlab复现全流程详解

大概两年前我第一次复现“分布式电源接入配电网承载力评估”方向的论文时&#xff0c;最大的感受不是算法难&#xff0c;而是论文里一句话带过的细节&#xff0c;代码里全是坑。比如“逐步增加分布式电源&#xff08;DG&#xff09;容量”要怎么逐步&#xff1f;步长取多少&…

作者头像 李华
网站建设 2026/9/9 9:54:28

2026年AI办公工具实测推荐:12款效率神器与场景选型指南

2026年再看AI办公工具&#xff0c;最大的变化不是某个模型又聪明了多少&#xff0c;而是工具真正从对话框里走了出来&#xff0c;开始接管文档、会议、表格、演示、视频、轻量编程这些具体的工作环节。我在过去三个月里把市面上叫得上名字的办公AI过了一遍&#xff0c;最后那些…

作者头像 李华
网站建设 2026/9/9 9:54:15

开源终端AI编程助手opencode实战:多模型配置与Skills技能包指南

半个月前&#xff0c;我把主力编码 Agent 从 Claude Code 换成了 opencode。起因是手头一个接手过来的 Go 项目里&#xff0c;遗留代码没有文档、依赖关系一团乱麻&#xff0c;Claude Code 处理起来总要反复切换上下文。后来试用了一圈开源终端 Agent&#xff0c;最终留下了 op…

作者头像 李华
网站建设 2026/9/9 9:51:55

UDP组播实现安防设备自动发现:原理与套接字编程实践

简介&#xff1a;针对海康网络摄像机&#xff08;IPC&#xff09;在局域网内的自动发现需求&#xff0c;这份压缩包提供了基于UDP组播与ONVIF协议实现设备探测的C示例工程。代码覆盖从创建组播套接字、加入239.255.255.250:8899组播组&#xff0c;到发送SOAP搜索请求、解析IPC响…

作者头像 李华
网站建设 2026/9/9 9:51:30

biblatex中文手册:LaTeX参考文献管理的实用指南

简介&#xff1a;biblatex-zh-cn 是 biblatex 宏包官方英文手册的中文译本&#xff0c;面向需要用 LaTeX 编制参考文献的中文用户和进阶 TeX 读者。英文原版手册超过三百页&#xff0c;功能虽强&#xff0c;但检索不便&#xff1b;这套中译版按 Introduction、DatabaseGuide、U…

作者头像 李华
网站建设 2026/9/9 9:50:23

AI生成的登山计划为何危险?大模型幻觉与户外安全深度解析

1. 事件还原&#xff1a;一条“看起来很专业”的AI登山计划 救援队是在第二天清晨接到报警的。三名徒步者被困在沙斯塔山&#xff08;Mount Shasta&#xff09;北坡接近三千九百米的位置&#xff0c;夜里的气温已经降到零下十摄氏度&#xff0c;其中一人出现轻度失温症状。事后…

作者头像 李华