二、固件打包基础:文件系统结构、分区表概念、Bootloader与Kernel的分离

好,咱们正式开始聊固件打包。说实话,我刚入行那会儿,觉得固件打包不就是把一堆二进制文件塞到一起吗?后来被现实狠狠教育了一顿——打包不规范,量产两行泪

这一节,我带你捋清楚三个核心概念:文件系统结构分区表、以及Bootloader与Kernel为什么要分开。这些都是量产固件打包的基石,搞不懂它们,后面签名、加密、OTA升级全都站不住脚。

2.1 文件系统结构:固件里的“文件夹哲学”

你想想看,一个嵌入式设备跑起来,它需要哪些东西?

  • 内核镜像(vmlinuz / zImage)
  • 设备树文件(.dtb)
  • 根文件系统(rootfs)
  • 应用层程序、库文件、配置文件
  • 可能还有用户数据分区

这些东西不能乱扔。我见过一个项目,工程师把内核和根文件系统直接拼成一个bin文件,烧进去也能跑。但后来要加OTA功能,发现根本没法单独更新内核——因为没做结构分离。

常见的固件文件系统结构有两种:

类型 特点 适用场景
扁平结构 所有数据连续存放,无目录层级 极小系统(如MCU固件)
分层结构 按功能分区,有明确偏移和大小 Linux/RTOS量产设备

我个人习惯用分层结构。哪怕你的Flash只有4MB,也建议把Bootloader、Kernel、Rootfs分开。为什么?后面讲OTA你就明白了。

核心原则:固件打包不是简单的“cat a.bin b.bin > firmware.bin”。你要考虑的是——这个固件将来怎么被解析、怎么被验证、怎么被部分更新

2.2 分区表概念:Flash的“房产证”

分区表,说白了就是一张地图。它告诉Bootloader和内核:哪块区域归谁管,有多大,用来干什么

我在项目中遇到过最坑的事:某次量产,产线反馈烧录后50%的设备无法启动。查了两天,发现是分区表里Kernel分区的大小写错了——少写了4个扇区。内核刚好被截断,校验通不过。

一个典型的分区表长这样:

// 以MTD分区为例(Linux内核中定义)
static struct mtd_partition my_flash_partitions[] = {
    {
        .name   = "bootloader",
        .offset = 0x00000000,
        .size   = 0x00040000,   // 256KB
    },
    {
        .name   = "kernel",
        .offset = 0x00040000,
        .size   = 0x00300000,   // 3MB
    },
    {
        .name   = "rootfs",
        .offset = 0x00340000,
        .size   = 0x00C00000,   // 12MB
    },
    {
        .name   = "userdata",
        .offset = 0x00F40000,
        .size   = 0x000C0000,   // 剩余空间
    },
};

嗯,这里要注意:分区表本身也要打包进固件。有些方案把分区表放在Bootloader头部,有些放在固定偏移(比如偏移0x1FC00处)。我个人建议把分区表单独作为一个分区,或者放在固件头的描述区域里——这样升级分区表时不用动Bootloader。

小技巧:量产时,分区表最好预留10%~20%的冗余空间。我曾经因为没留冗余,客户要加一个新功能分区,结果不得不重新调整整个Flash布局,所有设备都要返厂重烧——教训深刻。

2.3 Bootloader与Kernel的分离:为什么不能“一锅炖”?

新手常问:Bootloader和Kernel都是二进制,为什么非要分开?合在一起烧进去不行吗?

行,但你会后悔。

我解释一下为什么必须分离:

  1. 职责不同,生命周期不同
    • Bootloader:硬件初始化、加载Kernel、启动校验。它几乎不变。
    • Kernel:驱动、调度、文件系统。它经常变(驱动更新、安全补丁)。
  2. 安全签名策略不同
    • Bootloader通常用固定公钥验签,签名算法简单(如RSA2048)。
    • Kernel可以用更灵活的签名方案,甚至支持多级签名。
  3. 恢复机制需要
    • 如果Kernel刷坏了,Bootloader还能启动进入恢复模式。
    • 如果Bootloader刷坏了……嗯,恭喜你,准备上编程器吧。

警告:千万不要把Bootloader和Kernel放在同一个分区!我见过一个产品,为了省事把两者合并,结果OTA升级Kernel时意外断电,Bootloader也被写坏了。整批设备变砖,售后成本直接爆炸。

那它们之间怎么协作?看这个启动流程:

上电 -> Bootloader启动
         |
         v
   读取分区表,定位Kernel分区
         |
         v
   验证Kernel签名(如果开启安全启动)
         |
         v
   解压Kernel到内存,跳转执行
         |
         v
   Kernel挂载Rootfs,启动init进程

你看,每一步都是解耦的。Bootloader只负责“把Kernel请进来并验明正身”,至于Kernel怎么跑、跑什么应用,它不管。

2.4 实战建议:如何设计你的固件打包流程

说了这么多理论,来点实际的。我总结了一套固件打包的“黄金流程”,你可以直接拿去用:

  • 第一步:定义分区表 —— 写在头文件里,版本化管理。
  • 第二步:分别编译 —— Bootloader、Kernel、Rootfs各自独立构建。
  • 第三步:生成固件头 —— 包含魔数、版本号、分区表、校验和。
  • 第四步:打包 —— 按分区顺序拼接,每个分区前加描述符。
  • 第五步:签名 —— 对整个固件或每个分区分别签名(取决于安全等级)。

我的个人习惯:在固件头里加一个“分区数量”字段。这样即使以后增加分区,旧的Bootloader也能通过数量判断是否支持——优雅降级,而不是直接拒绝启动。

好了,这一节的内容就到这里。下一节我们会深入固件签名与校验,到时候我会手把手带你写一个签名脚本。你先把分区表和分离的概念消化掉,后面才能跟上节奏。