第4章 Bootloader分区布局:典型分区布局与冗余设计
好,咱们进入正题。这一章聊的是Bootloader分区布局,说白了就是你的固件在Flash里怎么摆。我见过太多项目,前期分区规划随便搞搞,结果后期维护时叫苦连天。你想想看,一个产品卖出去几万台,突然发现分区不够用,那得多尴尬。
4.1 典型分区布局
一个成熟的嵌入式系统,Flash分区通常包含这么几块:Bootloader1、Bootloader2、环境变量区、内核区、根文件系统区。嗯,这里要注意,不同芯片的Flash大小不一样,但布局思路是相通的。
核心原则:分区要留够余量,别卡着边界设计。我习惯给每个分区多留10%-20%的空间,这钱花得值。
4.1.1 Bootloader1 与 Bootloader2
为什么要有两个Bootloader?说白了就是为了安全。Bootloader1是出厂固化的,几乎不改动。Bootloader2才是真正干活的那个,负责加载内核、校验签名、处理升级。
我在项目中遇到过这么个坑:有一次OTA升级写到一半断电了,Bootloader2被写坏了。幸好Bootloader1还在,它能检测到Bootloader2校验失败,自动进入恢复模式。要是只有一个Bootloader,那板子就成砖了。
| 分区 | 大小(典型值) | 作用 |
|---|---|---|
| Bootloader1 | 64KB - 128KB | 出厂固化,负责启动Bootloader2或恢复模式 |
| Bootloader2 | 256KB - 512KB | 主引导程序,支持OTA、签名校验、环境变量管理 |
4.1.2 环境变量区
环境变量区,我习惯叫它"小仓库"。里面存的是启动参数、MAC地址、序列号、升级标志位这些东西。为什么单独划一块?因为Bootloader和内核都要读写它。
我的经验:环境变量区建议用双备份。我曾经遇到过Flash某个块老化,环境变量读出来全是0xFF,结果设备启动后MAC地址全乱了。从那以后,我所有项目都做双备份环境变量。
环境变量区的典型布局:
// 环境变量结构体示例
typedef struct {
uint32_t magic; // 魔数,用于校验有效性
uint32_t crc32; // 整个区域的CRC校验
uint8_t mac_addr[6]; // MAC地址
uint32_t boot_count; // 启动次数
uint8_t upgrade_flag; // 升级标志
uint8_t reserved[128]; // 预留空间
} env_block_t;
// 双备份:主区和备份区
#define ENV_PRIMARY_OFFSET 0x20000 // 主区
#define ENV_BACKUP_OFFSET 0x21000 // 备份区
4.1.3 内核区
内核区存放的是Linux内核镜像或者RTOS的二进制文件。这里有个关键点:内核镜像通常要压缩,Bootloader负责解压并跳转。
我个人习惯把内核区再分成两个子区:A区和B区。为什么?为了实现无缝OTA升级。你想想看,升级时先写到B区,校验通过后再切换启动分区,就算升级失败也能回滚到A区。
警告:内核区的大小一定要算清楚。我见过一个项目,内核从3MB涨到了5MB,结果分区不够用,最后不得不重新布局Flash。提前留够余量,别省那点空间。
4.1.4 根文件系统区
根文件系统区,这是最占空间的地方。根据你的应用场景,可能是squashfs只读文件系统,也可能是ubifs可读写文件系统。
我建议的做法是:根文件系统也做双备份。虽然成本高一点,但想想看,如果升级过程中文件系统损坏,设备还能从备份区启动,这体验就好太多了。
| 文件系统类型 | 特点 | 适用场景 |
|---|---|---|
| squashfs | 只读、高压缩比 | 系统固件、不常修改的部分 |
| ubifs | 可读写、支持磨损均衡 | 需要保存用户数据的场景 |
| jffs2 | 可读写、老牌文件系统 | 兼容性要求高的项目 |
4.2 冗余设计
冗余设计,说白了就是"别把所有鸡蛋放在一个篮子里"。嵌入式设备经常在恶劣环境下工作,Flash坏块、写中断、电源波动,这些都可能让分区数据损坏。
4.2.1 双备份策略
我常用的双备份策略是这样的:
- Bootloader双备份:Bootloader1只负责启动Bootloader2,Bootloader2负责所有业务逻辑。如果Bootloader2损坏,Bootloader1能检测到并进入恢复模式。
- 环境变量双备份:主区和备份区各存一份,启动时比较CRC,取有效的那一份。
- 内核和文件系统双备份:A/B分区轮换,升级时写备用分区,校验通过后切换。
避坑指南:我曾经遇到过双备份都损坏的情况——Flash整块坏掉了。后来我加了一个"出厂恢复区",里面存了一份最原始的固件。虽然占空间,但关键时刻能救命。
4.2.2 校验机制
光有备份还不够,你得能检测出数据是否损坏。我习惯在每个分区头部加一个校验头:
// 分区校验头
typedef struct {
uint32_t magic; // 魔数,比如 0xDEADBEEF
uint32_t version; // 分区版本号
uint32_t size; // 分区有效数据大小
uint32_t crc32; // 数据区CRC32校验
uint8_t reserved[16]; // 预留
} partition_header_t;
// 启动时校验流程
int check_partition(uint32_t offset) {
partition_header_t header;
flash_read(offset, &header, sizeof(header));
// 检查魔数
if (header.magic != 0xDEADBEEF) return -1;
// 检查CRC
uint32_t calc_crc = crc32_calc(offset + sizeof(header), header.size);
if (calc_crc != header.crc32) return -2;
return 0; // 校验通过
}
4.2.3 启动流程中的冗余处理
Bootloader启动时,会按这个顺序尝试:
- 检查Bootloader2是否有效(CRC校验)
- 如果有效,跳转到Bootloader2
- 如果无效,尝试从备份区恢复Bootloader2
- 如果备份也无效,进入恢复模式(等待串口或网络升级)
嗯,这里要注意,恢复模式一定要做得足够健壮。我见过有些设备,恢复模式只支持串口,结果现场工程师没带串口线,那叫一个抓狂。我建议至少支持两种恢复方式:串口和网络(TFTP或HTTP)。
我的习惯:在Bootloader1里放一个最小化的恢复程序,哪怕只有几KB,只要能接收固件并写入Flash就行。这样就算Bootloader2和内核全坏了,设备也能救回来。
4.3 实际项目中的分区布局示例
给你看一个我实际用过的分区布局,Flash大小是16MB:
| 分区 | 起始地址 | 大小 | 说明 |
|---|---|---|---|
| Bootloader1 | 0x000000 | 64KB | 出厂固化,只读 |
| Bootloader2 | 0x010000 | 256KB | 主引导程序,可升级 |
| 环境变量主区 | 0x050000 | 64KB | 环境变量主备份 |
| 环境变量备份区 | 0x060000 | 64KB | 环境变量副备份 |
| 内核A区 | 0x070000 | 2MB | 内核镜像A |
| 内核B区 | 0x270000 | 2MB | 内核镜像B |
| 文件系统A区 | 0x470000 | 4MB | 根文件系统A |
| 文件系统B区 | 0x870000 | 4MB | 根文件系统B |
| 用户数据区 | 0xC70000 | 剩余空间 | 用户配置、日志等 |
这个布局我用了好几年,基本没出过问题。你可能会问,为什么Bootloader1只给64KB?因为它的功能很简单,就是校验Bootloader2然后跳转,64KB绰绰有余。
好了,这一章的内容就这些。分区布局看似简单,但设计得好不好,直接关系到产品的可靠性和可维护性。下一章咱们聊聊安全启动的具体实现,那才是真正的硬核内容。