4. 固件包格式设计:Bin 文件、Hex 文件、自定义打包格式(含头部校验)
说到固件升级,第一个要面对的问题就是:你的固件到底长什么样?
说白了,就是怎么把编译出来的二进制数据,打包成一个能让 Bootloader 认得出、解得了、验得过的格式。我见过不少新手,上来就写个 Bootloader,结果发现上位机发下来的文件格式五花八门,解析逻辑写得比升级逻辑还复杂。嗯,这一章咱们就把这事彻底理清楚。
4.1 三种主流固件格式对比
先看一张总览表,心里有个底:
| 格式 | 本质 | 是否含地址信息 | 是否含校验 | 适用场景 |
|---|---|---|---|---|
| Bin | 纯二进制数据流 | 否 | 否 | MCU 内部 Flash 直接烧录 |
| Hex (Intel HEX) | ASCII 文本 + 地址 + 校验 | 是 | 每行校验和 | 烧录器、调试器、分块烧录 |
| 自定义打包格式 | 头部 + 数据 + 尾部校验 | 可选 | CRC32/SHA256 | OTA 升级、安全升级 |
4.2 Bin 文件:最纯粹的固件
Bin 文件就是编译器输出的原始二进制数据。没有头部,没有地址,没有校验。你给它一个起始地址,它就从那里开始写。
核心特点:
- 体积最小,没有冗余信息
- 必须知道烧录起始地址
- 没有完整性校验,传输错误无法发现
我在项目中遇到过一个问题:用 Bin 文件做 OTA,结果网络传输丢了一个字节,Bootloader 直接写进去,设备变砖。从那以后,我只要用 Bin 文件,一定会在传输层加 CRC 校验。
Bin 文件的生成很简单,以 GCC 为例:
# 从 ELF 生成 Bin 文件
arm-none-eabi-objcopy -O binary firmware.elf firmware.bin
# 查看 Bin 文件大小
ls -l firmware.bin
你想想看,Bin 文件就像一张白纸,上面只有数据。Bootloader 拿到它,必须知道往哪里写。所以通常我们会把起始地址硬编码在 Bootloader 里,或者通过其他方式传递。
4.3 Hex 文件:自带地址和校验
Intel HEX 格式是我个人比较喜欢的中间格式。它把二进制数据转成了 ASCII 文本,每行都包含地址、数据类型和校验和。
先看一个典型的 Hex 行:
:10000000 00 112233445566778899AABBCCDDEEFF F0
拆开来看:
| 字段 | 长度 | 说明 |
|---|---|---|
: |
1 字节 | 起始标记 |
10 |
1 字节 | 本行数据长度(16 字节) |
0000 |
2 字节 | 起始地址偏移 |
00 |
1 字节 | 记录类型(00=数据,01=结束) |
1122...FF |
N 字节 | 实际数据 |
F0 |
1 字节 | 校验和 |
Hex 格式有几种记录类型,常用的就两个:
- 00:数据记录,包含实际固件内容
- 01:文件结束记录,表示 Hex 文件到此为止
小技巧:Hex 文件的校验和算法很简单:0x100 - (所有字节之和 & 0xFF)。我写 Bootloader 解析 Hex 时,会先校验每行的 checksum,再提取数据。这样能提前发现传输错误。
Hex 文件的解析代码,核心逻辑大概这样:
// 解析一行 Hex 记录
int parse_hex_line(const char *line, uint32_t *addr, uint8_t *data, int *len) {
if (line[0] != ':') return -1;
int byte_count = hex_to_byte(&line[1]); // 数据长度
uint16_t offset = hex_to_word(&line[3]); // 地址偏移
int rec_type = hex_to_byte(&line[7]); // 记录类型
// 校验和检查
uint8_t checksum = 0;
for (int i = 1; i < 9 + byte_count * 2; i += 2) {
checksum += hex_to_byte(&line[i]);
}
if (checksum != 0) return -2; // 校验失败
if (rec_type == 0x00) {
*addr = offset;
*len = byte_count;
for (int i = 0; i < byte_count; i++) {
data[i] = hex_to_byte(&line[9 + i * 2]);
}
return 0; // 数据记录
} else if (rec_type == 0x01) {
return 1; // 结束记录
}
return -3;
}
Hex 格式的好处是:每个烧录器都能解析。但缺点也很明显——体积比 Bin 大 2-3 倍,而且解析起来比 Bin 慢。对于 OTA 升级来说,这可不是什么好事。
4.4 自定义打包格式:为 OTA 而生
做 OTA 升级,我个人强烈建议用自定义打包格式。为什么?因为你可以把版本号、硬件兼容性、校验码、签名等信息全部塞进头部,Bootloader 拿到包的第一时间就能做决策。
这是我常用的打包格式:
+------------------+------------------+------------------+------------------+
| Magic(4B) | Version(4B) | HW_ID(4B) | PayloadLen(4B) |
+------------------+------------------+------------------+------------------+
| HeaderCRC(4B) | Reserved(8B) | |
+------------------+------------------+ |
| Payload (固件数据) |
| ... |
+------------------------------------------------------------------------+
| PayloadCRC(4B) |
+------------------------------------------------------------------------+
各字段说明:
| 字段 | 大小 | 说明 |
|---|---|---|
| Magic | 4 字节 | 魔数,用于快速识别固件包(如 0xDEADBEEF) |
| Version | 4 字节 | 固件版本号,主版本+次版本+修订号 |
| HW_ID | 4 字节 | 硬件兼容性标识,防止刷错设备 |
| PayloadLen | 4 字节 | 固件数据长度 |
| HeaderCRC | 4 字节 | 头部校验(CRC32),保护头部完整性 |
| Reserved | 8 字节 | 保留字段,用于扩展(如签名、加密标志) |
| Payload | N 字节 | 实际的固件二进制数据 |
| PayloadCRC | 4 字节 | 固件数据校验(CRC32),保护数据完整性 |
注意:HeaderCRC 只校验头部字段(Magic 到 Reserved),不包含 Payload。这样 Bootloader 可以先校验头部,确认版本和硬件兼容性,再决定要不要继续下载 Payload。我曾经见过一个设计,头部和数据混在一起校验,结果版本号错了还得等整个包下载完才发现,白白浪费带宽。
打包代码示例:
typedef struct {
uint32_t magic;
uint32_t version;
uint32_t hw_id;
uint32_t payload_len;
uint32_t header_crc;
uint8_t reserved[8];
} __attribute__((packed)) firmware_header_t;
// 打包固件
int pack_firmware(const uint8_t *payload, uint32_t payload_len,
uint32_t version, uint32_t hw_id,
uint8_t *output, uint32_t *output_len) {
firmware_header_t header;
header.magic = 0xDEADBEEF;
header.version = version;
header.hw_id = hw_id;
header.payload_len = payload_len;
header.header_crc = 0; // 先置零
// 计算头部 CRC
header.header_crc = crc32((uint8_t *)&header, sizeof(header));
// 写入头部
memcpy(output, &header, sizeof(header));
uint32_t offset = sizeof(header);
// 写入 Payload
memcpy(output + offset, payload, payload_len);
offset += payload_len;
// 写入 Payload CRC
uint32_t payload_crc = crc32(payload, payload_len);
memcpy(output + offset, &payload_crc, sizeof(payload_crc));
*output_len = offset + sizeof(payload_crc);
return 0;
}
4.5 知识体系:固件包格式设计全景
下面这张图,把三种格式的关系和选择逻辑串起来了:
4.6 避坑指南
做固件包格式设计,有几个坑我踩过,分享给你:
我曾经犯过的错:
- 头部结构体对齐问题:不同编译器对结构体的对齐方式不同。我曾在 GCC 和 IAR 之间切换,结果头部解析全乱套。解决方案:强制使用
__attribute__((packed)),或者手动序列化每个字段。 - 大小端问题:ARM Cortex-M 默认小端,但有些 MCU 是大端。如果上位机是 x86(小端),MCU 也是小端,那没问题。但万一混用,记得统一字节序。
- Magic 选得太随意:有人用 0x00000000 或 0xFFFFFFFF 做魔数,结果 Flash 擦除后的状态正好匹配,导致误判。我习惯用 0xDEADBEEF 或 0xFEEDFACE 这种不太可能撞车的值。
4.7 小结
三种格式各有千秋。Bin 文件简单直接,适合本地烧录;Hex 文件自带地址和校验,适合调试器;自定义打包格式灵活可控,是 OTA 升级的首选。
我个人建议:量产用 Bin,调试用 Hex,OTA 用自定义格式。别想着一种格式打天下,那只会让你的 Bootloader 变得臃肿不堪。