4、固件镜像管理:Hex、Bin、ELF 文件格式解析,固件版本管理与校验

量产烧录,说白了就是把固件塞进芯片里。但固件本身长什么样,很多人其实没仔细想过。

我见过不少工程师,拿到一个 .hex 文件就直接开烧,烧完了发现程序跑不起来,折腾半天才发现是文件格式选错了。嗯,这种坑,踩过一次就记住了。

这一章,咱们就把固件镜像的几种常见格式掰开揉碎讲清楚。顺便聊聊版本管理和校验——这两件事在量产中要是没做好,后果可能很严重。

4.1 三种固件格式:Hex、Bin、ELF

先看一张图,理清它们之间的关系。

固件镜像格式关系图 ELF 文件 objcopy objcopy -O binary Hex 文件 Bin 文件 烧录工具 目标芯片 说明 ELF 是调试/编译中间格式 Hex/Bin 是烧录最终格式

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 烧录工具的版本校验

量产烧录时,工具应该自动读取固件中的版本号,并与烧录任务要求的版本号比对。不一致就报警,禁止烧录。

我参与设计的一个量产工具,流程是这样的:

  1. 加载固件文件(Hex/Bin)
  2. 解析固件中的版本结构体
  3. 与烧录任务配置的版本号比对
  4. 匹配则继续,不匹配则报错并记录日志
  5. 烧录完成后,回读版本号并写入数据库

4.6 固件校验:确保烧录正确

烧录完了,怎么知道数据对不对?校验。

常用的校验方式有几种:

校验方式 原理 适用场景
CRC32 对整个固件计算 CRC32 值 量产烧录,速度快,碰撞概率低
MD5/SHA256 哈希校验,安全性高 固件升级,防篡改
累加和 所有字节相加取低 16 位 简单场景,但碰撞概率高
逐字节比对 烧录后回读全部数据比对 小容量芯片,耗时较长

量产推荐方案:CRC32 + 回读校验。

具体做法:

  • 烧录前,计算固件的 CRC32 值,写入固件末尾的保留区域
  • 烧录完成后,回读整个 Flash 区域
  • 重新计算 CRC32,与写入的值比对
  • 一致则通过,不一致则标记为不良品

我曾经遇到过一个问题:某批次芯片烧录后 CRC 校验全部通过,但上电后程序跑飞。查了半天,发现是烧录工具在写入最后几个字节时时序有问题,导致 CRC 值本身写错了。嗯,从那以后我加了一条规则——CRC 值要写两遍,回读比对一致才算通过。

4.7 量产中的固件管理流程

最后,我总结一下量产中固件管理的标准流程:

  1. 编译输出 ELF:开发阶段使用,包含调试信息
  2. 生成 Hex/Bin:用 objcopy 转换,Hex 优先
  3. 嵌入版本信息:确保版本结构体在固定地址
  4. 计算校验和:CRC32 写入固件末尾
  5. 烧录工具加载:解析 Hex 地址,校验版本号
  6. 烧录执行:按地址写入 Flash
  7. 回读校验:回读全部数据,CRC32 比对
  8. 记录日志:每片芯片的版本号、校验结果、烧录时间写入数据库

一个小技巧:在固件中预留一个 4 字节的“烧录计数”区域。每次烧录时,工具自动递增这个值。这样即使版本号相同,也能区分是哪一次烧录的。我在追查量产问题时,这个字段帮了大忙。

固件镜像管理,说白了就是三件事:选对格式、管好版本、做足校验。这三件事做好了,量产烧录的 90% 的坑就避开了。