3、Flash分区设计:分区表的概念、Bootloader分区、App分区、参数分区、备份分区的布局策略。

说到Bootloader升级,Flash分区设计是绕不开的核心话题。很多刚入行的朋友觉得分区不就是划几个区域嘛,随便分分就行。嗯,我当年也这么想过,直到有一次产品量产了才发现分区不够用,那叫一个尴尬。

分区设计说白了,就是给你的Flash芯片做「房产规划」。哪里放Bootloader,哪里放App,哪里存参数,哪里留备份,都得提前想清楚。今天我就把这块掰开了讲。

3.1 分区表的概念

分区表,本质上是一张「地址-用途」映射表。它告诉Bootloader:哪个区域是干什么的,从哪里开始,到哪里结束。

我个人习惯把分区表放在Flash的最开头,紧挨着中断向量表。这样Bootloader启动后第一件事就是读分区表,知道自己该干什么。

分区表的核心字段:

  • 分区名称:比如"bootloader"、"app"、"param"
  • 起始地址:该分区在Flash中的偏移量
  • 分区大小:该分区占用的字节数
  • 分区属性:可读、可写、可执行、是否加密等
  • 校验值:分区表自身的CRC,防止被篡改

这里有个坑——分区表本身不能太大。我见过有人把分区表做得比App还复杂,结果Bootloader光解析分区表就花了半秒钟。你想想看,用户按一下开机键,等半秒才亮屏,这体验能好吗?

一个典型的分区表结构大概长这样:

typedef struct {
    uint32_t magic;          // 魔数,用于校验分区表有效性
    uint8_t  version;        // 分区表版本号
    uint8_t  partition_cnt;  // 分区数量
    uint8_t  reserved[2];    // 保留字节
    uint32_t crc32;          // 分区表CRC校验
    
    partition_entry_t entries[PARTITION_MAX_COUNT];
} partition_table_t;

typedef struct {
    char     name[16];       // 分区名称
    uint32_t offset;         // 起始偏移
    uint32_t size;           // 分区大小
    uint32_t flags;          // 属性标志位
} partition_entry_t;

3.2 Bootloader分区

Bootloader分区是系统的「第一口气」。它负责初始化硬件、校验App、决定是否进入升级模式。

这个分区有几个硬性要求:

  • 必须放在Flash起始位置:MCU上电后从0x00000000开始执行,Bootloader必须在这里
  • 大小要保守:我个人建议64KB起步,128KB比较稳妥。别贪多,Bootloader功能单一,写不了多少代码
  • 写保护:量产之后,Bootloader分区最好加上硬件写保护。我曾经遇到过一例OTA升级把Bootloader自己给覆盖了的惨案……嗯,从那以后我再也不敢不加保护了

避坑指南:

我曾经在一个项目里把Bootloader分区设成32KB,结果功能越加越多,最后硬塞进去,每次升级都提心吊胆。后来我学乖了——Bootloader分区至少留50%的余量。因为你永远不知道产品生命周期里会不会加新功能。

3.3 App分区

App分区是用户代码的驻地。这里存放的就是你写的业务逻辑、协议栈、UI等等。

App分区设计时要注意几个点:

  • 起始地址要对齐:很多MCU要求App起始地址按扇区对齐,否则擦写会出问题
  • 大小要灵活:如果产品支持OTA升级,App分区最好留出30%-50%的余量。代码只会越写越多,不会越写越少
  • 中断向量表重映射:App启动时要把中断向量表从Bootloader分区重映射到App分区自己的位置

我见过一个典型的App分区布局:

区域 起始地址 大小 说明
中断向量表 0x08010000 512B App自己的中断入口
代码段 0x08010200 192KB 可执行代码
只读数据 0x08040000 64KB 常量、字符串表
BSS段 0x08050000 32KB 未初始化全局变量

3.4 参数分区

参数分区是用来存「掉电不能丢」的数据的。比如WiFi密码、设备ID、校准参数、用户设置等等。

这里有个设计要点——参数分区不能和代码分区共用同一个扇区。为什么?因为参数经常要擦写,而代码几乎不写。如果混在一起,每次改参数都要擦一大片Flash,既慢又伤寿命。

我的经验:

参数分区最好用「双备份」策略。也就是同一份参数存两份,写的时候先写备份,再写主份。如果中途掉电,下次启动时Bootloader会发现主份校验失败,自动从备份恢复。这个技巧我用了好多年,从来没丢过参数。

参数分区的典型大小是4KB到64KB,具体看你的参数有多少。我个人习惯用4KB的扇区,因为擦写速度快,而且浪费少。

3.5 备份分区

备份分区是OTA升级的「安全网」。它的作用很简单——在升级App之前,先把当前能用的App完整拷贝到备份分区。如果新App启动失败,Bootloader可以从备份分区恢复。

备份分区的布局策略有两种:

  • 全量备份:备份分区大小等于App分区大小。简单粗暴,但Flash占用大
  • 差分备份:只备份App分区的关键部分(比如中断向量表和启动代码)。省空间,但恢复逻辑复杂

我个人更推荐全量备份。虽然多占一倍Flash,但恢复逻辑极其简单——直接把备份分区的内容拷回App分区就行。差分备份省下来的那点空间,往往不够你调试恢复逻辑时掉的头发。

注意:

备份分区本身也需要保护。我见过有人把备份分区和参数分区放在同一个扇区,结果参数擦写时把备份也给擦了。升级到一半掉电,两个分区全坏,设备直接变砖。所以,备份分区一定要独立,别跟任何频繁擦写的分区共用扇区。

3.6 分区布局策略总结

说了这么多,我们来画一张图,把整个Flash分区的布局看清楚:

Flash分区布局示意图 Bootloader分区 (64KB - 128KB) 起始地址: 0x08000000 | 写保护 | 硬件初始化 + 升级逻辑 参数分区 (4KB - 64KB) 双备份策略 | 频繁擦写 | 存储WiFi密码、设备ID、校准参数 App分区 (256KB - 1MB) 中断向量表 + 代码段 + 只读数据 + BSS段 起始地址对齐扇区 | 留30%-50%余量 备份分区 (大小 = App分区) 全量备份 | OTA升级安全网 | 独立扇区 说明:分区顺序从上到下,地址从低到高。Bootloader在最前面,备份分区在最后面。 参数分区放在Bootloader和App之间,方便Bootloader在启动时读取参数。

这张图展示的是一个比较经典的布局。Bootloader在最前面,参数分区紧随其后,然后是App分区,最后是备份分区。为什么参数分区要放在Bootloader后面?因为Bootloader启动时需要读参数(比如判断要不要进升级模式),放近了读取速度快。

当然,具体怎么分还得看你的Flash大小和MCU型号。有些MCU的Flash扇区大小不统一,前几个扇区是16KB,后面是64KB,这时候分区就得跟着扇区边界走。

分区设计黄金法则:

  1. Bootloader分区写保护——这是底线,破了就变砖
  2. App分区留余量——别卡着尺寸设计,未来会后悔
  3. 参数分区双备份——掉电不丢数据,用户不会骂娘
  4. 备份分区独立——别跟参数分区共用扇区,血的教训

好了,Flash分区设计这块就聊到这儿。记住一句话:分区设计是Bootloader升级的地基,地基没打好,上面盖多高的楼都白搭。下次你画分区表的时候,多想想我今天说的这些坑,能省不少事。