2、固件镜像基础:ELF、HEX、BIN文件格式解析,固件版本号与校验和
做嵌入式开发这么多年,我见过不少新手在固件升级上栽跟头。说白了,很多人连自己烧进去的是什么文件都没搞清楚。今天咱们就聊聊固件镜像的几种常见格式,以及版本号和校验和这些看似简单、实则坑不少的东西。
2.1 三种固件格式:ELF、HEX、BIN
你打开编译输出目录,通常会看到三个文件:.elf、.hex、.bin。它们到底有什么区别?我简单总结一下:
| 格式 | 包含信息 | 是否可执行 | 典型用途 |
|---|---|---|---|
| ELF | 代码、数据、调试信息、符号表 | 是(需加载器) | 调试、仿真 |
| HEX | 代码、数据、地址信息、校验 | 是(烧录器解析) | 烧录、量产 |
| BIN | 纯二进制数据 | 是(需指定地址) | OTA升级、存储 |
2.2 ELF 文件:调试的命根子
ELF(Executable and Linkable Format)是编译器的最终输出。它不光有机器码,还带着符号表、调试信息。我个人习惯在开发阶段一直保留 ELF 文件,因为一旦程序跑飞了,你得靠它反查源码位置。
举个例子,你用 readelf 命令看看:
$ readelf -h firmware.elf
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Entry point address: 0x8000000
Start of section headers: 1200 (bytes into file)
看到那个 Entry point address 了吗?这就是程序启动后第一条指令的地址。我在项目中遇到过有人把 ELF 直接当 BIN 用,结果烧进去根本跑不起来——因为 ELF 文件头占了前面几百个字节,CPU 直接执行了文件头,不崩才怪。
2.3 HEX 文件:带地址的二进制
HEX 文件是 Intel 定义的一种格式,每行都包含地址、数据和校验。它的好处是:烧录工具能自动把数据放到正确的位置。
看一个典型的 HEX 行:
:10000000FF0040E1080080E5080090E50800A0E5B7
我来拆解一下:
:起始标记10本行数据长度(16字节)0000起始地址00记录类型(00=数据,01=结束)FF0040E1...16字节数据B7校验和
嗯,这里要注意:HEX 文件每行末尾的校验和是累加校验。如果烧录时提示校验失败,多半是文件传输过程中出了问题。我曾经因为串口线接触不良,烧了三次才成功,后来学乖了,每次烧录前先算一遍校验。
objcopy 可以把 ELF 转成 HEX:arm-none-eabi-objcopy -O ihex firmware.elf firmware.hex
2.4 BIN 文件:最纯粹的固件
BIN 文件就是赤裸裸的二进制数据,没有地址信息,没有校验,什么都没有。你想想看,OTA 升级时为什么都用 BIN?因为它体积最小,传输最快。
但 BIN 有个致命问题:你不知道它该烧到哪个地址。所以用 BIN 文件时,必须配合一个明确的烧录地址。比如 STM32 的 Flash 起始地址是 0x08000000,你烧录时得告诉工具:从这儿开始放。
$ arm-none-eabi-objcopy -O binary firmware.elf firmware.bin
$ ls -lh firmware.bin
-rw-r--r-- 1 user user 128K Mar 15 10:30 firmware.bin
128KB,比 ELF 小多了吧?这就是为什么 OTA 升级首选 BIN。
2.5 固件版本号:别让用户猜
版本号这东西,看似简单,但我在项目中见过太多混乱的命名了。什么 v1.2.3_final_真的最终版.bin,这种命名方式迟早出问题。
我建议用三段式版本号:
- 主版本号:重大架构变更,不兼容升级
- 次版本号:新增功能,向下兼容
- 修订号:Bug 修复,小改动
比如 2.1.5,表示主版本 2,次版本 1,第 5 次修订。版本号要固化在固件里,通常放在 Flash 的固定位置。我习惯在链接脚本里预留 4 字节给版本号:
// firmware_version.h
#define FIRMWARE_MAJOR 2
#define FIRMWARE_MINOR 1
#define FIRMWARE_PATCH 5
#define FIRMWARE_VERSION ((FIRMWARE_MAJOR << 16) | \
(FIRMWARE_MINOR << 8) | \
FIRMWARE_PATCH)
这样,升级程序读取固件头部的版本号,就能判断是否需要升级。我曾经遇到过用户拿旧版本覆盖新版本的情况,就是因为没做版本比较。从那以后,我强制要求:版本号必须递增,且升级前必须校验。
2.6 校验和:固件的最后一道防线
固件在传输过程中可能被篡改,或者存储介质出问题。校验和就是用来检测这些错误的。常用的校验方式有几种:
| 校验方式 | 长度 | 安全性 | 适用场景 |
|---|---|---|---|
| 累加和 | 1字节 | 低 | 简单校验,HEX文件 |
| CRC32 | 4字节 | 中 | OTA升级,文件完整性 |
| MD5/SHA256 | 16/32字节 | 高 | 安全校验,防篡改 |
我个人最常用 CRC32。它计算快,冲突概率低,4 字节的开销也完全可以接受。下面是一个简单的 CRC32 实现:
uint32_t crc32(const uint8_t *data, uint32_t len) {
uint32_t crc = 0xFFFFFFFF;
for (uint32_t i = 0; i < len; i++) {
crc ^= data[i];
for (int j = 0; j < 8; j++) {
if (crc & 1) {
crc = (crc >> 1) ^ 0xEDB88320;
} else {
crc >>= 1;
}
}
}
return ~crc;
}
在生成固件时,我会把 CRC32 值附加到 BIN 文件的末尾。升级程序先校验 CRC,再决定是否写入 Flash。你想想看,如果固件在下载过程中损坏了,没有校验就直接写进去,设备变砖的概率有多大?
2.7 实战建议:固件打包流程
说了这么多,我总结一下实际项目中怎么处理固件镜像:
- 编译:生成 ELF 文件,用于调试
- 转换:用 objcopy 转成 HEX(烧录)和 BIN(OTA)
- 加版本号:在 BIN 文件头部写入版本号
- 算校验:计算整个 BIN 文件的 CRC32,附加到尾部
- 打包:生成最终的升级包(版本号 + 固件数据 + CRC)
嗯,这套流程我用了好几年,基本没出过问题。你刚开始做 OTA 时,建议先拿一块开发板反复测试,确认校验逻辑没问题再上量产。
好了,关于固件镜像的基础知识就聊到这儿。下一章咱们会深入 OTA 升级的协议设计,包括怎么分包、怎么断点续传。到时候再细聊。