3、分区表结构设计:A分区与B分区的布局、分区标识位、分区状态机
好,咱们接着聊。上一章我们把AB分区的整体思路理清了,这一章得落地了。
分区表结构怎么设计?说白了,就是回答三个问题:
- A区和B区在存储上怎么摆?
- 系统怎么知道当前该启动哪个区?
- 分区在不同阶段处于什么状态?
这三个问题搞定了,你的AB分区方案就有了骨架。我个人习惯,在设计阶段就把这些细节敲死,不然后面改起来很痛苦。
3.1 布局:A区与B区的物理与逻辑划分
先看布局。嵌入式设备里,Flash存储空间是有限的。你得把总容量切成两块,一块给A,一块给B。
我见过不少新手,上来就把A区和B区画得一样大,结果发现B区根本用不上那么大空间,白白浪费了。你想想看,如果A区是主运行区,B区只是用来做升级回滚的临时落脚点,那B区其实可以比A区小一点。
布局上,通常有两种方式:
| 布局方式 | 描述 | 适用场景 |
|---|---|---|
| 固定偏移布局 | A区和B区在Flash中有固定的起始地址和大小。比如A区从0x00000开始,B区从0x80000开始。 | 分区大小固定,升级包大小可控的场景。简单可靠,我最早的项目就用这个。 |
| 动态偏移布局 | 分区起始地址和大小记录在分区表中,可以动态调整。 | 需要灵活调整分区大小的场景,比如OTA升级包越来越大,需要动态扩容。 |
我个人更推荐固定偏移布局。为什么?因为简单。嵌入式系统最怕复杂,越简单越不容易出错。动态偏移虽然灵活,但一旦分区表被写坏,整个系统就废了。
来看一个典型的布局示例:
// Flash 布局示例(假设总容量 2MB)
// 地址范围:0x000000 - 0x1FFFFF
// Bootloader 区域
#define BOOTLOADER_START 0x000000
#define BOOTLOADER_SIZE 0x010000 // 64KB
// A 分区
#define PARTITION_A_START 0x010000
#define PARTITION_A_SIZE 0x0F0000 // 960KB (约60%)
// B 分区
#define PARTITION_B_START 0x100000
#define PARTITION_B_SIZE 0x0A0000 // 640KB (约40%)
// 分区表区域(存放标识位和状态)
#define PARTITION_TABLE_START 0x1A0000
#define PARTITION_TABLE_SIZE 0x010000 // 64KB
嗯,这里要注意:分区表区域一定要单独划出来,别跟A区或B区混在一起。我曾经见过一个项目,把分区标识位放在A区的末尾,结果A区升级失败,标识位也跟着坏了,系统彻底变砖。从那以后,我坚持把分区表放在一个独立的、受保护的区域。
3.2 分区标识位:系统如何知道该启动哪个区?
布局定好了,接下来是关键:系统怎么知道当前该启动A区还是B区?
答案就是——分区标识位。
说白了,就是一个标志变量,存放在非易失性存储里(比如Flash的特定扇区)。系统启动时,Bootloader先读这个标识位,然后决定加载哪个分区。
标识位通常包含以下信息:
- 当前活动分区:0表示A区,1表示B区。
- 待升级分区:升级时,标记哪个分区即将被写入。
- 回滚计数:记录回滚次数,防止无限回滚。
- 校验值:对整个标识位区域做CRC校验,防止数据损坏。
核心原则: 标识位的写入必须是原子操作。如果写入过程中掉电,要么保持旧值,要么进入安全状态。绝对不能出现半写状态。
我常用的标识位结构体长这样:
typedef struct {
uint32_t magic; // 魔数,用于验证标识位有效性
uint8_t active_slot; // 0: A区, 1: B区
uint8_t pending_slot; // 待升级分区
uint8_t rollback_count; // 回滚计数
uint8_t reserved; // 保留
uint32_t crc32; // 校验值
} partition_table_t;
这里有个坑:魔数一定要选好。我习惯用 0xABABABAB,既好记,又不容易跟其他数据冲突。如果读到的魔数不对,说明标识位区域可能被损坏了,这时候Bootloader应该进入恢复模式,而不是盲目启动。
3.3 分区状态机:从升级到回滚的完整生命周期
有了布局和标识位,还得有状态机来管理分区的生命周期。不然,系统怎么知道当前分区是健康的、正在升级的、还是需要回滚的?
我设计的状态机通常包含以下几个状态:
| 状态 | 描述 | 触发条件 |
|---|---|---|
| ACTIVE | 当前分区正常运行,系统从该分区启动。 | 启动成功,系统运行正常。 |
| UPDATE_PENDING | 升级包已下载完成,准备写入待升级分区。 | OTA升级包下载完成,校验通过。 |
| UPDATING | 正在向待升级分区写入数据。 | 开始写入升级包。 |
| UPDATE_DONE | 升级包写入完成,等待重启验证。 | 写入完成,CRC校验通过。 |
| VERIFYING | 重启后,从新分区启动,进行功能验证。 | 系统重启,从新分区启动。 |
| ROLLBACK | 新分区验证失败,回滚到旧分区。 | 验证超时或用户触发回滚。 |
| FAILED | 分区损坏或无法恢复。 | 多次回滚失败或硬件故障。 |
状态流转的逻辑,我画了一张图,你看一眼就明白了:
状态机的核心逻辑其实就一句话:升级后必须验证,验证失败必须回滚。
我在项目中遇到过一个问题:验证阶段怎么定义“成功”?是启动成功就算,还是运行一段时间才算?我后来采用了一个折中方案——延迟确认。系统从新分区启动后,先进入一个“观察期”(比如5分钟),如果期间没有发生严重错误,才把标识位从 VERIFYING 改为 ACTIVE。如果观察期内崩溃了,下次启动时Bootloader发现标识位还是 VERIFYING,就自动触发回滚。
关键设计决策: 回滚操作必须由Bootloader执行,不能依赖应用层。因为应用层可能已经坏了,根本跑不起来。Bootloader是最后的保险。