第三章 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安全的第一条铁律。
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 升级流程中的分区切换策略
分区定好了,升级时怎么切换?常见的有两种策略:
策略一:复制式升级
- 新固件下载到App2
- 校验App2的完整性(CRC/SHA)
- 擦除App1,将App2内容复制到App1
- 更新参数区版本号
- 重启,Bootloader从App1启动
优点:逻辑简单,App1始终是完整可用的。
缺点:复制过程耗时,且需要App1和App2大小完全一致。
策略二:交换式升级(指针切换)
- 新固件下载到App2
- 校验App2的完整性
- 在参数区记录“下次启动从App2启动”
- 重启,Bootloader读取参数,跳转到App2
- 下次升级时,新固件下载到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升级的底层基础就稳了。