第三章 Flash分区设计:Bootloader、App1、App2、参数区的布局策略与大小计算

做OTA升级,第一个绕不开的问题就是:Flash怎么分?

我见过不少团队,代码写好了,功能调通了,结果发现Flash分区没规划好。要么Bootloader放不下,要么升级时没地方存新固件。最后只能推倒重来,板子都得重新画。说实话,这坑我踩过不止一次。

今天咱们就把Flash分区这件事聊透。你想想看,一个无线MCU,Flash通常就那么几百KB到几MB。怎么在这有限的空间里,塞下Bootloader、两个App区、一个参数区,还要保证升级过程安全可靠?

3.1 分区架构:为什么是“三区”而不是“两区”?

最简单的OTA方案,其实只需要两个区:Bootloader + App。升级时,Bootloader把新固件直接覆盖到App区。但这里有个致命问题——升级过程中断电了怎么办?

App区写到一半,数据全乱。重启后Bootloader发现App校验失败,系统就变砖了。

所以,工业级OTA必须用“三区架构”:

  • Bootloader区:负责启动引导和升级流程控制,一般不更新
  • App1区:当前运行的应用固件
  • App2区:下载新固件的暂存区,也叫“备份区”或“升级缓冲区”
  • 参数区:存放升级状态、版本号、配置参数等关键信息

升级流程说白了就是:新固件先下载到App2,校验通过后,Bootloader把App2的内容复制到App1,或者直接交换两个区的启动指针。这样即使升级中途断电,App1还是完好的,系统至少能回滚到旧版本。

核心原则:任何时候,至少保留一个可正常启动的App副本。这是OTA安全的第一条铁律。

典型无线MCU Flash分区布局(三区架构) Flash总容量(如:1MB / 2MB) Bootloader 启动引导 升级控制 (只读/不更新) App1(运行区) 当前运行固件 正常启动入口 (可回滚) App2(备份区) 新固件下载目标 升级暂存 (校验后生效) 参数 状态 版本 配置

3.2 分区大小怎么算?别拍脑袋

分区大小不是随便定的。我见过有人直接把Flash四等分,结果Bootloader空着一大半,App区却不够用。正确的做法是:先测量,再计算,最后留余量。

3.2.1 Bootloader区大小

Bootloader一般包含:

  • 启动向量表和初始化代码(约4-8KB)
  • Flash驱动(擦除/写入/校验,约2-4KB)
  • 通信协议栈(如UART、SPI、I2C,约4-12KB)
  • 固件校验逻辑(CRC/SHA,约2-4KB)
  • 升级状态机(约2-4KB)

经验值:大多数无线MCU的Bootloader在32KB-64KB之间。如果用了加密签名验证,建议留到64KB-96KB。

我的习惯:先把Bootloader功能写出来,编译后看实际大小,然后向上取整到Flash扇区大小的整数倍。比如扇区是4KB,实际Bootloader是37KB,那就取40KB(10个扇区)。

3.2.2 App1和App2区大小

这两个区的大小通常相等。为什么?因为升级时要么复制,要么交换。如果大小不等,逻辑会变得非常复杂。

计算公式很简单:

App区大小 = 实际固件大小 × (1 + 余量系数) + 对齐开销

余量系数建议:

  • 产品初期(功能还在加):50%-100%
  • 产品稳定期(功能冻结):20%-30%
  • 维护期(只修bug):10%-20%

举个例子:你的固件当前是120KB,产品还在迭代,余量按50%算:

App区大小 = 120KB × 1.5 = 180KB
向上对齐到扇区(4KB):184KB(46个扇区)

我曾经犯过的错:在一个项目里,我把App区只留了10%余量。结果客户要求加一个新功能,固件直接超了。最后只能砍功能、优化代码,折腾了两周。从那以后,我至少留30%的余量。

3.2.3 参数区大小

参数区存什么?

  • 升级状态标志(4字节)
  • 当前版本号(8-16字节)
  • 备份版本号(8-16字节)
  • 升级结果记录(16-32字节)
  • 设备配置参数(可变,通常64-256字节)
  • 坏块标记或重映射表(可选)

参数区不需要太大,但要注意:Flash擦写有寿命。如果频繁更新参数,建议用“日志式存储”或“双备份区”,避免频繁擦写同一块区域导致提前报废。

我个人习惯:参数区至少分配2个扇区(比如8KB),一个做主区,一个做备份。写参数时先写备份,校验成功后再更新主区。这样即使写一半断电,参数也不会丢。

3.3 分区对齐:一个容易被忽略的细节

Flash的擦除操作是以“扇区”为单位的。不同MCU的扇区大小不一样:

MCU系列 典型扇区大小 擦除粒度
STM32G0/G4 2KB 页擦除
STM32L4/L5 2KB 页擦除
ESP32 4KB 扇区擦除
nRF52系列 4KB 页擦除
GD32F3/F4 2KB/4KB 页擦除

分区起始地址和大小,必须是扇区大小的整数倍。否则,你擦除一个扇区时,可能会把相邻分区的数据也擦掉。这问题我在调试时遇到过,查了两天才发现是分区没对齐。

对齐检查公式:

分区起始地址 % 扇区大小 == 0
分区大小 % 扇区大小 == 0

两个条件必须同时满足。

3.4 实际案例:1MB Flash分区方案

拿一个常见的1MB Flash无线MCU举例。假设固件当前大小约200KB,产品还在迭代期:

分区 起始地址 大小 扇区数(4KB/扇区) 说明
Bootloader 0x08000000 64KB 16 固定,不更新
参数区 0x08010000 8KB 2 双备份,日志式
App1(运行区) 0x08012000 464KB 116 当前固件
App2(备份区) 0x08080000 464KB 116 升级暂存
保留/扩展 0x080EE000 72KB 18 未来扩展

你看,App区我给了464KB,而固件才200KB。余量超过100%。有人可能觉得浪费,但我的经验是:Flash分区最怕的不是浪费,而是不够用。多出来的空间,以后可以存日志、存配置、甚至做二次OTA缓冲。

3.5 升级流程中的分区切换策略

分区定好了,升级时怎么切换?常见的有两种策略:

策略一:复制式升级

  1. 新固件下载到App2
  2. 校验App2的完整性(CRC/SHA)
  3. 擦除App1,将App2内容复制到App1
  4. 更新参数区版本号
  5. 重启,Bootloader从App1启动

优点:逻辑简单,App1始终是完整可用的。

缺点:复制过程耗时,且需要App1和App2大小完全一致。

策略二:交换式升级(指针切换)

  1. 新固件下载到App2
  2. 校验App2的完整性
  3. 在参数区记录“下次启动从App2启动”
  4. 重启,Bootloader读取参数,跳转到App2
  5. 下次升级时,新固件下载到App1,再切换指针

优点:无需复制,升级速度快,适合大固件。

缺点:Bootloader需要支持动态跳转地址,逻辑稍复杂。

我推荐的做法:如果Flash空间允许,用交换式。如果MCU性能较弱或Flash较小,用复制式。我在一个nRF52840的项目里用的就是交换式,升级时间从原来的8秒降到了2秒,用户体验提升很明显。

3.6 避坑指南:分区设计中的常见问题

  • 中断向量表重映射:App1和App2的起始地址不同,中断向量表必须重定位。大部分MCU支持SCB->VTOR寄存器设置,别忘了在App启动代码里配置。
  • Bootloader升级:Bootloader本身要不要升级?如果要,需要额外留一个“安全Bootloader”区,或者用双Bootloader方案。我个人建议:除非必要,否则不要升级Bootloader,风险太高。
  • 参数区磨损均衡:如果设备频繁OTA(比如测试阶段),参数区可能被反复擦写。用“环形缓冲区”或“日志式存储”来延长寿命。
  • Flash锁定保护:Bootloader区建议设置读/写保护,防止意外擦写。很多MCU的Flash控制器支持按区设置保护。

我曾经踩过的坑:在一个量产项目中,Bootloader和App1的地址没有对齐到扇区边界。结果有一次OTA升级时,擦除App1的第一个扇区,把Bootloader的最后几行代码也擦掉了。设备直接变砖,2000多台设备需要返厂。从那以后,分区对齐成了我代码审查的必查项。

好了,Flash分区设计就聊到这儿。记住三个关键词:三区架构、大小计算、扇区对齐。把这三点吃透了,OTA升级的底层基础就稳了。