二、固件打包基础:文件系统结构、分区表概念、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都是二进制,为什么非要分开?合在一起烧进去不行吗?
行,但你会后悔。
我解释一下为什么必须分离:
- 职责不同,生命周期不同
- Bootloader:硬件初始化、加载Kernel、启动校验。它几乎不变。
- Kernel:驱动、调度、文件系统。它经常变(驱动更新、安全补丁)。
- 安全签名策略不同
- Bootloader通常用固定公钥验签,签名算法简单(如RSA2048)。
- Kernel可以用更灵活的签名方案,甚至支持多级签名。
- 恢复机制需要
- 如果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也能通过数量判断是否支持——优雅降级,而不是直接拒绝启动。
好了,这一节的内容就到这里。下一节我们会深入固件签名与校验,到时候我会手把手带你写一个签名脚本。你先把分区表和分离的概念消化掉,后面才能跟上节奏。