第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启动时,会按这个顺序尝试:

  1. 检查Bootloader2是否有效(CRC校验)
  2. 如果有效,跳转到Bootloader2
  3. 如果无效,尝试从备份区恢复Bootloader2
  4. 如果备份也无效,进入恢复模式(等待串口或网络升级)

嗯,这里要注意,恢复模式一定要做得足够健壮。我见过有些设备,恢复模式只支持串口,结果现场工程师没带串口线,那叫一个抓狂。我建议至少支持两种恢复方式:串口和网络(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绰绰有余。

好了,这一章的内容就这些。分区布局看似简单,但设计得好不好,直接关系到产品的可靠性和可维护性。下一章咱们聊聊安全启动的具体实现,那才是真正的硬核内容。