2. 升级架构设计:Bootloader与App分区设计,Flash布局规划,双备份区(A/B分区)策略

好,咱们进入第二章。这一章讲的是升级架构的骨架——Flash怎么划分、Bootloader和App怎么共存、双备份区到底怎么玩。说白了,就是决定你的固件升级系统长什么样。

我见过不少团队,一上来就写升级代码,结果Flash布局没想清楚,后面改得痛不欲生。嗯,咱们先把地基打牢。

2.1 Flash布局规划:先画好地皮

MCU的Flash就像一块地皮。你要在上面盖房子(Bootloader)、住人(App)、还要留个临时仓库(升级缓存区)。地皮就这么大,怎么分?

我个人习惯,拿到一个新芯片,第一件事就是看它的Flash总大小和扇区(Sector)大小。不同MCU的扇区大小不一样——有的4KB,有的16KB,有的128KB。这个细节很关键,因为擦除操作是按扇区来的。

核心原则: 所有分区的起始地址必须对齐到扇区边界。否则擦除时会擦到别人的地盘。

举个例子,一个典型的512KB Flash布局:

分区 起始地址 大小 说明
Bootloader 0x08000000 64KB 启动、校验、升级入口
App A(主区) 0x08010000 192KB 当前运行的应用固件
App B(备份区) 0x08040000 192KB 新固件下载到这里
参数区 0x08070000 64KB 升级标志、版本号、配置

你看,每个分区都对齐到了扇区边界。Bootloader放在最前面,因为MCU上电默认从0x08000000开始执行。参数区我习惯放在最后,这样擦写App时不会影响到它。

小技巧: 参数区里至少存三个东西——升级标志(是否需要升级)、目标版本号、当前版本号。我曾经遇到过升级到一半断电,重启后不知道要接着升还是回滚,就是因为没存标志位。

2.2 Bootloader与App分区设计:谁管谁?

Bootloader和App的关系,有点像机场的塔台和飞机。塔台(Bootloader)负责指挥,飞机(App)负责执行任务。飞机起飞后,塔台就不管了,除非飞机要降落(升级)。

Bootloader的核心职责就三件事:

  1. 校验App的完整性——启动前检查CRC或签名,确保App没坏。
  2. 决定启动哪个分区——根据升级标志,选A区还是B区。
  3. 执行升级流程——接收新固件,写入备份区,切换标志。

App这边,需要做两件事:

  • 正常运行时,接收升级包并写入备份区。
  • 写完后,设置升级标志,然后软复位。

这里有个容易踩的坑——中断向量表重映射。App的起始地址不是0x08000000,所以它的中断向量表要偏移。以STM32为例,需要在App的启动代码里设置:

// App 起始地址为 0x08010000
#define APP_BASE 0x08010000

// 在 main 函数最前面设置向量表偏移
SCB->VTOR = APP_BASE;

如果不做这一步,中断来了会跑到Bootloader的向量表里去,系统直接崩溃。我记得有个项目,同事调试了三天,最后发现是忘了设VTOR。

2.3 双备份区(A/B分区)策略:升级失败的保险

双备份区,也叫A/B分区。说白了就是准备两份App空间,一份在跑,一份等着被覆盖。

为什么需要这个?你想想看,如果只有一个App区,升级写到一半断电了,或者写进去的固件是坏的,设备就变砖了。双备份区就是为了解决这个问题。

工作流程是这样的:

  1. 设备当前在A区运行。
  2. 收到新固件,写入B区。
  3. 校验B区的完整性(CRC/SHA256)。
  4. 校验通过,设置标志位,下次启动从B区启动。
  5. 如果校验失败,继续从A区启动,升级失败但设备不坏。
注意: 双备份区不是万能的。如果Bootloader本身坏了,或者参数区被擦坏了,设备照样变砖。所以Bootloader区域我一般会做写保护,防止意外擦除。

下面这张图展示了A/B分区的切换逻辑:

A/B 分区升级切换流程 上电 / 复位 Bootloader 启动 校验 A 区 校验 B 区 从 A 区启动 从 B 区启动 运行 App(正常模式) 判断依据:升级标志位 0xAA → 从B区启动 其他值 → 从A区启动

2.4 升级标志与状态机

升级标志放在参数区,我一般用一个结构体来管理:

typedef struct {
    uint32_t magic;          // 魔数,用于校验参数区是否有效
    uint32_t boot_flag;      // 0xAA: 从B区启动, 0x55: 从A区启动
    uint32_t update_status;  // 0: 空闲, 1: 下载中, 2: 校验中, 3: 完成
    uint32_t version_a;      // A区版本号
    uint32_t version_b;      // B区版本号
    uint32_t crc32;          // 整个结构体的CRC校验
} UpgradeParam_t;

升级状态机其实很简单,就四个状态:

  • 空闲——正常跑App,啥也不干。
  • 下载中——App在接收升级包,写入备份区。
  • 校验中——下载完成后,Bootloader或App对备份区做完整性校验。
  • 完成——校验通过,设置标志,准备切换。
避坑指南: 我曾经在下载中状态断电,重启后参数区的update_status还是"下载中",导致Bootloader以为升级没完成,反复尝试恢复。后来我加了一个超时机制——如果"下载中"状态持续超过30秒,就认为升级失败,回滚到原分区。

2.5 实际项目中的取舍

双备份区不是所有场景都适用。我列个对比表,帮你判断:

场景 推荐方案 原因
Flash > 512KB,产品价值高 A/B分区 安全第一,不怕变砖
Flash < 256KB,成本敏感 单分区 + 外部备份 Flash不够分,用外部Flash或EEPROM存备份
OTA升级频繁 A/B分区 升级失败可回滚,用户体验好
出厂后几乎不升级 单分区 没必要浪费Flash空间

嗯,总结一下。Flash布局是升级架构的根基,Bootloader是守门员,A/B分区是安全网。这三样东西想清楚了,后面的升级代码写起来就顺了。

我个人建议,不管项目大小,至少留一个升级标志位和版本号字段。哪怕现在用不上,以后要加OTA功能时,你会发现当初这个决定有多明智。