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 直接执行了文件头,不崩才怪。

注意:ELF 文件不能直接用于 OTA 升级。它包含大量调试信息,体积大,而且烧录器不认。量产时一定要转成 HEX 或 BIN。

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。

核心结论:调试用 ELF,烧录用 HEX,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。你想想看,如果固件在下载过程中损坏了,没有校验就直接写进去,设备变砖的概率有多大?

血的教训:我曾经有一次 OTA 升级,网络波动导致固件中间少了一段数据。因为没有做校验,设备写入了残缺的固件,结果启动后直接死循环。从那以后,我所有 OTA 方案都强制加 CRC32 校验,不通过就不允许升级。

2.7 实战建议:固件打包流程

说了这么多,我总结一下实际项目中怎么处理固件镜像:

  1. 编译:生成 ELF 文件,用于调试
  2. 转换:用 objcopy 转成 HEX(烧录)和 BIN(OTA)
  3. 加版本号:在 BIN 文件头部写入版本号
  4. 算校验:计算整个 BIN 文件的 CRC32,附加到尾部
  5. 打包:生成最终的升级包(版本号 + 固件数据 + CRC)

嗯,这套流程我用了好几年,基本没出过问题。你刚开始做 OTA 时,建议先拿一块开发板反复测试,确认校验逻辑没问题再上量产。

额外提醒:版本号和校验和的位置要固定,最好在项目初期就定义好。中途改位置,会导致旧版本设备无法识别新固件格式,那就尴尬了。

好了,关于固件镜像的基础知识就聊到这儿。下一章咱们会深入 OTA 升级的协议设计,包括怎么分包、怎么断点续传。到时候再细聊。