4、固件镜像管理:Hex、Bin、ELF 文件格式解析,固件版本管理与校验
量产烧录,说白了就是把固件塞进芯片里。但固件本身长什么样,很多人其实没仔细想过。
我见过不少工程师,拿到一个 .hex 文件就直接开烧,烧完了发现程序跑不起来,折腾半天才发现是文件格式选错了。嗯,这种坑,踩过一次就记住了。
这一章,咱们就把固件镜像的几种常见格式掰开揉碎讲清楚。顺便聊聊版本管理和校验——这两件事在量产中要是没做好,后果可能很严重。
4.1 三种固件格式:Hex、Bin、ELF
先看一张图,理清它们之间的关系。
4.2 Bin 文件:最纯粹的二进制镜像
Bin 文件,说白了就是芯片 Flash 里内容的直接拷贝。没有地址信息,没有校验和,就是纯粹的 0 和 1。
Bin 文件特点:
- 无地址信息:烧录时必须由工具指定起始地址
- 无校验:文件本身不包含任何校验机制
- 体积最小:没有额外开销,适合大容量存储
- 烧录最快:工具只需逐字节写入
我在项目中遇到过一个问题:某次量产,烧录工具默认从 0x08000000 开始烧 Bin 文件,但固件实际应该放在 0x08010000。结果 1000 片板子全废了。嗯,从那以后我定了个规矩——Bin 文件必须附带烧录地址说明。
4.3 Hex 文件:带地址的烧录格式
Hex 文件(Intel HEX 格式)是文本文件,每一行都包含地址、数据和校验和。烧录工具可以解析这些信息,自动把数据放到正确的位置。
一个典型的 Hex 行长这样:
:10000000 00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF F0
| | |_______________________________________________| |
| 长度 数据(16字节) 校验和
起始地址(0x0000)
Hex 格式有几种记录类型,我列个表:
| 记录类型 | 含义 | 说明 |
|---|---|---|
| 00 | 数据记录 | 普通数据,包含地址和数据内容 |
| 01 | 文件结束 | 标记 Hex 文件结束,必须存在 |
| 02 | 扩展段地址 | 用于 16 位地址空间的高 4 位 |
| 04 | 扩展线性地址 | 用于 32 位地址空间的高 16 位 |
| 05 | 启动线性地址 | 指定程序入口地址(ARM 常用) |
我的建议:量产烧录优先用 Hex 文件。原因很简单——地址信息内嵌在文件里,烧录工具不会搞错位置。我曾经吃过 Bin 文件的亏,后来量产项目全部改用 Hex,再没出过地址相关的批量事故。
4.4 ELF 文件:调试者的好伙伴
ELF(Executable and Linkable Format)是编译器直接输出的格式。它包含的信息远多于 Hex 和 Bin:
- 代码段、数据段、BSS 段的分段信息
- 符号表(函数名、变量名)
- 调试信息(行号、源码路径)
- 重定位信息
但 ELF 文件不能直接用于烧录。为什么?因为烧录工具需要的是纯粹的二进制数据,而不是带符号表和调试信息的复杂结构。
我个人的习惯是:开发调试用 ELF,量产烧录用 Hex。从 ELF 生成 Hex 的命令很简单:
arm-none-eabi-objcopy -O ihex firmware.elf firmware.hex
arm-none-eabi-objcopy -O binary firmware.elf firmware.bin
4.5 固件版本管理
量产最怕什么?最怕烧错了固件版本。我见过一个案例:某团队把测试版固件当成正式版烧了 5000 片,最后全部召回重烧,损失几十万。
版本管理,我建议从这几个方面入手:
4.5.1 固件版本号嵌入
在编译时把版本号写进固件里。常用的做法是在代码中定义一个固定地址的版本结构体:
typedef struct {
uint32_t magic; // 魔数,用于校验
uint8_t major; // 主版本号
uint8_t minor; // 次版本号
uint16_t patch; // 补丁号
char build_date[16]; // 编译日期
char build_time[16]; // 编译时间
uint32_t checksum; // 固件校验和
} firmware_version_t;
// 放在 .rodata 段,固定地址
const firmware_version_t g_fw_version __attribute__((section(".fw_version"))) = {
.magic = 0x56455253, // "VERS"
.major = 2,
.minor = 1,
.patch = 0,
.build_date = __DATE__,
.build_time = __TIME__,
.checksum = 0, // 烧录后计算填充
};
注意:版本结构体的地址必须固定。如果每次编译地址都变,烧录工具就找不到版本信息。我一般把它放在链接脚本的固定位置,比如 Flash 的末尾 256 字节。
4.5.2 烧录工具的版本校验
量产烧录时,工具应该自动读取固件中的版本号,并与烧录任务要求的版本号比对。不一致就报警,禁止烧录。
我参与设计的一个量产工具,流程是这样的:
- 加载固件文件(Hex/Bin)
- 解析固件中的版本结构体
- 与烧录任务配置的版本号比对
- 匹配则继续,不匹配则报错并记录日志
- 烧录完成后,回读版本号并写入数据库
4.6 固件校验:确保烧录正确
烧录完了,怎么知道数据对不对?校验。
常用的校验方式有几种:
| 校验方式 | 原理 | 适用场景 |
|---|---|---|
| CRC32 | 对整个固件计算 CRC32 值 | 量产烧录,速度快,碰撞概率低 |
| MD5/SHA256 | 哈希校验,安全性高 | 固件升级,防篡改 |
| 累加和 | 所有字节相加取低 16 位 | 简单场景,但碰撞概率高 |
| 逐字节比对 | 烧录后回读全部数据比对 | 小容量芯片,耗时较长 |
量产推荐方案:CRC32 + 回读校验。
具体做法:
- 烧录前,计算固件的 CRC32 值,写入固件末尾的保留区域
- 烧录完成后,回读整个 Flash 区域
- 重新计算 CRC32,与写入的值比对
- 一致则通过,不一致则标记为不良品
我曾经遇到过一个问题:某批次芯片烧录后 CRC 校验全部通过,但上电后程序跑飞。查了半天,发现是烧录工具在写入最后几个字节时时序有问题,导致 CRC 值本身写错了。嗯,从那以后我加了一条规则——CRC 值要写两遍,回读比对一致才算通过。
4.7 量产中的固件管理流程
最后,我总结一下量产中固件管理的标准流程:
- 编译输出 ELF:开发阶段使用,包含调试信息
- 生成 Hex/Bin:用 objcopy 转换,Hex 优先
- 嵌入版本信息:确保版本结构体在固定地址
- 计算校验和:CRC32 写入固件末尾
- 烧录工具加载:解析 Hex 地址,校验版本号
- 烧录执行:按地址写入 Flash
- 回读校验:回读全部数据,CRC32 比对
- 记录日志:每片芯片的版本号、校验结果、烧录时间写入数据库
一个小技巧:在固件中预留一个 4 字节的“烧录计数”区域。每次烧录时,工具自动递增这个值。这样即使版本号相同,也能区分是哪一次烧录的。我在追查量产问题时,这个字段帮了大忙。
固件镜像管理,说白了就是三件事:选对格式、管好版本、做足校验。这三件事做好了,量产烧录的 90% 的坑就避开了。