3. Flash分区规划:分区表设计、Bootloader分区、App分区、备份分区、参数分区
分区规划这事儿,说白了就是给Flash这块地皮画户型图。你想想看,一块Flash少则几兆,多则上百兆,怎么分、分几块、每块多大,直接决定了你的升级策略能不能跑起来。
我刚开始做Bootloader那会儿,觉得分区嘛,随便划拉几块就行。结果有一次产品量产了才发现,参数分区太小,日志一写就溢出,整个系统直接挂掉。嗯,从那以后我再也不敢轻视分区表设计了。
3.1 分区表的核心思想
分区表就是一张地图。它告诉Bootloader:你的代码在哪、备份在哪、参数存在哪。没有这张图,Bootloader就是个瞎子。
我个人习惯把分区表放在Flash的最开头,紧挨着中断向量表。为什么?因为Bootloader启动后第一件事就是读分区表,放近了读取快,也方便后续扩展。
分区表设计三原则:
- 固定基址:分区表起始地址固定,不能随代码变化
- 冗余备份:分区表本身要有备份,防止意外擦写
- 版本兼容:分区表结构要预留扩展字段,方便后续增加分区
来看一个典型的分区表结构:
typedef struct {
uint32_t magic; // 魔数,用于校验分区表有效性
uint32_t version; // 分区表版本号
uint32_t partition_cnt; // 分区数量
uint32_t crc32; // 分区表CRC校验
partition_entry_t entries[PARTITION_MAX_CNT];
} partition_table_t;
typedef struct {
uint32_t id; // 分区ID
uint32_t offset; // 分区起始偏移
uint32_t size; // 分区大小
uint32_t flags; // 属性标志(可读/可写/可执行)
char name[16]; // 分区名称
} partition_entry_t;
这里有个坑:魔数和CRC校验必须做。我曾经遇到过Flash某些位意外翻转,分区表读出来全是0xFF,Bootloader直接跑飞。加了CRC后,至少能检测到错误,然后从备份分区表恢复。
3.2 Bootloader分区
Bootloader分区放什么?当然是Bootloader本身。但这里有个细节:Bootloader分区要足够大,能容纳未来3-5个版本的迭代。
我一般这样规划:
- Bootloader分区大小:至少128KB,推荐256KB
- Bootloader分区起始:0x08000000(以STM32为例)
- Bootloader分区结束:0x0803FFFF(256KB)
我的经验:Bootloader分区里最好再划出一小块(4KB)作为Bootloader自己的配置区。用来存启动次数、升级标志、回滚计数等信息。这样即使App分区全毁了,Bootloader还能根据这些信息决定下一步动作。
3.3 App分区
App分区是主战场。双备份升级策略的核心就是:两个App分区,一个跑,一个备。
我习惯这样命名:
- App-A分区:当前运行的主版本
- App-B分区:升级时写入的备份版本
两个分区的大小必须完全一致。为什么?因为升级时Bootloader要把新固件写入备份分区,如果大小不一致,写入逻辑会变得非常复杂。
| 分区名称 | 起始地址 | 大小 | 作用 |
|---|---|---|---|
| App-A | 0x08040000 | 512KB | 当前运行版本 |
| App-B | 0x080C0000 | 512KB | 备份/升级版本 |
这里有个关键点:App分区起始地址必须对齐到Flash的扇区大小。比如STM32F4的扇区是16KB、64KB、128KB不等,你分区起始地址不对齐,擦写操作就会出问题。我踩过这个坑,当时App-B的起始地址没对齐,结果擦除时把App-A的一部分也给擦了,整个系统直接变砖。
3.4 备份分区
备份分区不是App-B。备份分区是给Bootloader自己用的,用来存一份完整的、可恢复的固件镜像。
你可能会问:App-B不就是备份吗?为什么还要单独的备份分区?
嗯,这里要区分一下:
- App-B:是升级过程中的临时存放区,升级完成后App-B会变成新的App-A
- 备份分区:是出厂时的固件镜像,或者用户手动保存的“稳定版本”,永远不会被升级流程覆盖
我个人习惯把备份分区放在Flash的最后一块。因为最后一块通常比较大(比如1MB),而且不常被擦写,寿命更长。
注意:备份分区的大小要能容纳最大的固件镜像。如果固件最大是600KB,备份分区至少要有640KB(留点余量)。别问我为什么知道,我曾经因为备份分区小了64KB,导致一个紧急升级任务无法回滚,最后只能现场刷机。
3.5 参数分区
参数分区是最容易被忽视的。很多人觉得参数嘛,存到App的Flash里不就行了?但你想过没有:App升级时,整个App分区都会被擦除重写,参数怎么办?
所以参数分区必须独立出来。它存放的是:
- 设备序列号、MAC地址
- 校准参数、用户配置
- 升级状态标志、回滚计数
- 日志缓冲区
参数分区的大小我一般给64KB-128KB。别嫌大,日志缓冲区很吃空间。而且参数分区要支持频繁擦写,所以最好用Flash的末尾扇区(磨损均衡考虑)。
// 参数分区结构示例
typedef struct {
uint32_t magic; // 魔数 0xPARAMAGIC
uint32_t version; // 参数版本
uint8_t mac_addr[6]; // MAC地址
uint32_t upgrade_flag; // 升级标志
uint32_t rollback_count; // 回滚次数
uint8_t reserved[4084]; // 预留,对齐到4KB
uint32_t crc32; // CRC校验
} param_sector_t;
3.6 完整分区布局图
说了这么多,咱们直接画一张图,把整个Flash布局看清楚。
3.7 分区规划避坑指南
最后,我把自己这些年踩过的坑总结一下,你直接拿去用:
我曾经踩过的坑:
- 分区大小没对齐扇区:导致擦除操作越界,把别的分区数据擦没了
- 分区表没做CRC:Flash位翻转后,Bootloader读到错误的分区表,直接跑飞
- 参数分区太小:日志写满后覆盖了关键参数,设备配置丢失
- 备份分区和App-B混淆:升级时把备份分区也覆盖了,导致无法回滚到出厂版本
我的建议:
- 分区表放在Flash开头,并保留一份备份在Flash末尾
- 所有分区起始地址对齐到最小擦除扇区大小
- 参数分区至少64KB,日志缓冲区单独划一块
- 备份分区永远只读,升级流程不能碰它
分区规划做得好,后面升级策略才能跑得稳。你想想看,如果分区表都读不出来,再好的双备份策略也是白搭。所以这一步,值得你多花点心思。