4、固件镜像格式:BIN文件、HEX文件、DFU包格式、自定义镜像头结构

好,咱们今天聊聊固件镜像格式。这玩意儿看着不起眼,但搞错了,轻则升级失败,重则设备变砖。我在蓝牙芯片项目里,至少见过三次因为镜像格式问题导致的线上事故。嗯,咱们一个一个说清楚。

4.1 BIN文件:最纯粹的二进制

BIN文件,说白了就是芯片内存的“照片”。它没有任何额外信息,从起始地址开始,一个字节挨着一个字节,原样烧录到Flash里。

特点:

  • 没有地址信息,烧录时必须指定起始地址
  • 文件大小就是固件实际大小
  • 解析简单,传输效率高

我个人习惯在量产阶段用BIN格式。为什么?因为它干净利落,没有冗余数据。OTA传输时,少传一个字节都是省电。

重要提醒:BIN文件不包含校验信息。如果你只传BIN,接收端必须自己算CRC。我曾经有个项目,就是因为忘了加校验,结果传输过程中一个bit翻转,设备直接死机。

4.2 HEX文件:带地址的“地图”

HEX文件是Intel发明的格式,它把固件数据切分成一条条记录。每条记录都包含:长度、地址、类型、数据、校验和。

你想想看,为什么要有HEX?因为早期烧录器不知道固件该放哪儿。HEX文件自己告诉烧录器:“这段数据放0x08000000,那段放0x08001000”。

HEX记录类型:

类型码 含义 说明
00 数据记录 普通固件数据
01 文件结束 表示HEX文件到此结束
02 扩展段地址 用于16位地址空间扩展
04 扩展线性地址 用于32位地址空间扩展

举个例子,一条典型的HEX记录长这样:

:10000000FF0102030405060708090A0B0C0D0E0F0F0

解析一下:10是数据长度(16字节),0000是地址,00是数据记录类型,后面16字节是数据,F0是校验和。

我的经验:开发阶段用HEX很方便。因为你可以只修改某个地址段的数据,不用重新生成整个固件。但OTA时我建议转成BIN再传,HEX的文本格式会浪费30%左右的带宽。

4.3 DFU包格式:为升级而生

DFU(Device Firmware Upgrade)是Nordic、TI等芯片厂商定义的升级包格式。它不像BIN那么裸,也不像HEX那么啰嗦,而是专门为OTA场景设计的。

典型的DFU包结构:

+------------------+
|   DFU Header     |  ← 固定长度,描述包信息
+------------------+
|   Firmware Data   |  ← 实际固件内容
+------------------+
|   Signature       |  ← 数字签名(可选)
+------------------+
|   CRC32/ SHA256   |  ← 完整性校验
+------------------+

DFU包的好处是:

  • 自带校验,不用额外处理
  • 支持分片传输,适合BLE小包通信
  • 可以包含多个镜像(比如App + Bootloader + SoftDevice)

我记得有一次做Nordic nRF52832的OTA,官方SDK要求DFU包必须包含init packet。这个init packet里记录了固件版本、硬件兼容性、SD版本要求。如果版本不匹配,升级过程直接拒绝。这设计挺聪明的,避免了刷错固件导致变砖。

避坑指南:我曾经遇到一个坑——DFU包的签名验证。如果你开启了安全升级,但私钥丢了,那所有设备都没法再升级了。所以,私钥一定要备份,而且要放在安全的地方。

4.4 自定义镜像头结构

说实话,大部分商业项目最后都会走向自定义镜像头。为什么?因为芯片厂商给的格式不一定满足你的业务需求。

我自己设计的镜像头长这样:

typedef struct {
    uint32_t magic;          // 魔数,用于识别固件合法性
    uint32_t version;        // 固件版本号
    uint32_t size;           // 固件大小
    uint32_t crc32;          // 固件CRC校验
    uint16_t hw_version;     // 硬件版本兼容性
    uint16_t reserved;       // 保留字段
    uint32_t timestamp;      // 编译时间戳
    uint8_t  signature[64];  // ECDSA签名(可选)
} firmware_header_t;

每个字段的作用:

  • magic:我习惯用0xDEADBEEF或0xA5A5A5A5。Bootloader启动时先检查魔数,不对就直接跳过。
  • version:主版本.次版本.修订号,打包成一个32位整数。比如0x01020003表示v1.2.3。
  • size:固件实际大小,用于Bootloader分配内存和校验完整性。
  • crc32:对整个固件数据(不含头)做CRC。我建议用硬件CRC加速,软件算太慢。
  • hw_version:防止把v2板的固件刷到v1板上。我吃过这个亏,那次差点报废一批货。
  • timestamp:Unix时间戳。调试时能快速知道固件是什么时候编译的。

核心建议:自定义镜像头一定要预留扩展字段。我最初只留了4字节,后来想加个安全启动标志,发现没地方了。只能改结构,结果所有Bootloader都得跟着升级。嗯,这教训挺深刻的。

4.5 实际项目中的选择策略

说了这么多,到底该用哪种?我一般这么选:

场景 推荐格式 原因
开发调试 HEX 方便分段烧录,调试器支持好
量产烧录 BIN 速度快,工具链简单
BLE OTA DFU包 自带分片和校验,省心
Wi-Fi OTA 自定义镜像头+BIN 带宽大,可以加更多业务逻辑
多芯片升级 自定义镜像头+多段数据 一个包搞定所有芯片固件

你想想看,如果你的产品只卖几百台,用官方DFU格式完全够用。但如果要卖几十万台,我建议还是花时间设计一套自己的镜像格式。为什么?因为后期要加的功能太多了——远程回滚、AB分区切换、增量升级……官方格式往往跟不上你的需求。

最后一个小技巧:无论用哪种格式,一定要在固件末尾加一个“结束标记”。我习惯用4字节的0xFFFFFFFF。这样Bootloader在扫描Flash时,能明确知道固件在哪里结束,避免读到垃圾数据。

好了,镜像格式这块就聊到这儿。下一节咱们讲讲升级流程设计,那才是真正考验架构能力的地方。