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布局看清楚。

Flash分区布局图 Flash分区布局图(双备份升级策略) Flash 总容量:2MB 分区表(4KB) Bootloader分区(256KB) 包含Bootloader代码 + 4KB配置区 App-A分区(512KB) 当前运行的主版本 App-B分区(512KB) 升级时的临时存放区 / 回滚版本 参数分区(128KB) 备份分区(640KB) 说明:分区大小和地址需根据实际Flash容量调整,建议预留10%-20%的余量

3.7 分区规划避坑指南

最后,我把自己这些年踩过的坑总结一下,你直接拿去用:

我曾经踩过的坑:

  • 分区大小没对齐扇区:导致擦除操作越界,把别的分区数据擦没了
  • 分区表没做CRC:Flash位翻转后,Bootloader读到错误的分区表,直接跑飞
  • 参数分区太小:日志写满后覆盖了关键参数,设备配置丢失
  • 备份分区和App-B混淆:升级时把备份分区也覆盖了,导致无法回滚到出厂版本

我的建议:

  • 分区表放在Flash开头,并保留一份备份在Flash末尾
  • 所有分区起始地址对齐到最小擦除扇区大小
  • 参数分区至少64KB,日志缓冲区单独划一块
  • 备份分区永远只读,升级流程不能碰它

分区规划做得好,后面升级策略才能跑得稳。你想想看,如果分区表都读不出来,再好的双备份策略也是白搭。所以这一步,值得你多花点心思。