3、刷写文件格式:S19文件格式解析、HEX文件格式解析、Bin文件格式解析

做嵌入式固件开发这么多年,我接触过不下十几种刷写文件格式。但真正在工业界广泛使用的,说白了就三种:S19、HEX和Bin。这三种格式各有各的脾气,选错了可是要出大问题的。

我记得刚入行那会儿,有一次把HEX文件当Bin文件直接烧录,结果芯片死活跑不起来。查了半天才发现,原来是文件格式搞混了。嗯,今天我就把这三种格式的底裤给你扒干净。

3.1 S19文件格式解析

S19格式,也叫Motorola S-record,是老牌半导体厂商摩托罗拉搞出来的。我最早接触它是在飞思卡尔(现NXP)的MCU上,后来发现很多车规级芯片都在用。

文件结构

S19文件是纯文本格式,每行都以大写字母'S'开头。我习惯把它拆成五个部分来看:

S00F000047656E6572696320535265636F7264F4
S1130000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0
S1130010FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0
S5030003F9
S9030000FC

每行记录的结构是这样的:

字段 长度 说明
类型 2字节 S0~S9,表示记录类型
长度 2字节 从地址开始到校验和之前的字节数
地址 2/3/4字节 数据存放的起始地址
数据 N字节 实际的固件数据
校验和 1字节 取反加一校验

记录类型详解

我个人最常用的就这几种:

  • S0:文件头记录,包含文件名或注释信息。我一般用它来标记固件版本号。
  • S1:16位地址的数据记录,适合小容量芯片(64KB以内)。
  • S2:24位地址的数据记录,覆盖16MB空间,我用的最多。
  • S3:32位地址的数据记录,适合大容量或地址空间很大的芯片。
  • S5:数据记录计数,告诉你前面有多少条S1/S2/S3记录。
  • S9/S8/S7:结束记录,分别对应16/24/32位地址的终止符。
我的小技巧:解析S19文件时,我习惯先扫一遍S5记录,确认总记录数。如果实际解析的数量对不上,说明文件可能被截断了。这招帮我抓过好几次传输错误。

校验和计算

校验和算法其实很简单:从长度字段开始,到数据字段结束,所有字节求和后取反,再取低8位。我写过一个快速验证函数:

uint8_t calc_s19_checksum(const uint8_t *data, uint8_t len)
{
    uint8_t sum = 0;
    for (uint8_t i = 0; i < len; i++) {
        sum += data[i];
    }
    return (~sum) & 0xFF;
}

3.2 HEX文件格式解析

Intel HEX格式,简称HEX文件,是英特尔搞出来的标准。说实话,这是我在项目中最常见的格式,几乎所有的MCU开发工具都支持它。

文件结构

HEX文件也是文本格式,每行以冒号':'开头。看个例子:

:10000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0
:10001000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0
:0400000500000000F7
:00000001FF

每行记录的结构:

字段 长度 说明
起始标记 1字节 固定为':'
数据长度 1字节 本行数据的字节数
地址 2字节 16位起始地址
记录类型 1字节 00~05
数据 N字节 实际固件数据
校验和 1字节 取反加一校验

记录类型详解

这里有六种记录类型,我挑重点说:

  • 00 (数据记录):最常见的类型,存放固件数据。每行最多255字节,但工具通常限制在16或32字节。
  • 01 (文件结束记录):文件末尾的标志,数据长度为0。
  • 02 (扩展段地址):用于8086模式的段地址扩展,现在很少用了。
  • 03 (起始段地址):指定代码的起始执行地址,也是古董级功能。
  • 04 (扩展线性地址):这个很重要!32位地址的高16位。我经常用它来访问大容量Flash的高地址区域。
  • 05 (起始线性地址):指定32位模式的起始执行地址,ARM芯片常用。
曾经踩过的坑:我曾经遇到一个HEX文件,里面混用了04记录和02记录。我的解析器只处理了04记录,结果地址计算全错了。后来我强制要求所有供应商统一使用扩展线性地址模式,再也没出过问题。

地址计算方式

HEX文件的地址计算有点绕,我画个图帮你理解:

// 扩展线性地址模式(推荐)
实际地址 = (扩展地址 << 16) | 记录中的16位地址

// 扩展段地址模式(不推荐)
实际地址 = (段地址 << 4) + 记录中的16位地址

我个人强烈建议只用扩展线性地址模式。段地址模式有地址重叠的问题,容易把人搞晕。

3.3 Bin文件格式解析

Bin文件,说白了就是固件的原始二进制数据。没有格式,没有校验,没有地址信息,就是纯粹的机器码。简单粗暴,但也最容易出问题。

文件特点

  • 无头无尾:没有文件头,没有结束标记,就是纯数据。
  • 无地址信息:文件本身不包含地址,烧录时必须指定起始地址。
  • 无校验:没有内建的校验机制,传输错误只能靠外部手段检测。
  • 体积最小:相比S19和HEX,Bin文件没有额外的格式开销,体积最小。

使用场景

我一般在以下场景用Bin文件:

  • OTA升级:传输效率优先,Bin文件体积小,传输快。
  • 量产烧录:产线工具通常直接烧录Bin文件,速度快。
  • 加密固件:加密后的固件本身就是二进制数据,用Bin格式最合适。
重要提醒:Bin文件没有地址信息,所以你必须知道固件应该烧录到哪个地址。我见过有人把Bin文件烧到错误地址,结果芯片变砖。建议在文件名或配套的配置文件中注明起始地址。

3.4 三种格式对比

我整理了一张对比表,方便你快速选择:

特性 S19 HEX Bin
文件格式 文本 文本 二进制
地址信息
校验机制
文件体积 较大 中等 最小
可读性 中等 较好
适用场景 车规、工业 通用MCU OTA、量产

3.5 格式转换实战

在实际项目中,我经常需要在三种格式之间转换。这里分享几个常用的方法:

使用SRecord工具

SRecord是开源工具,功能强大。我常用的命令:

# S19转HEX
srec_cat firmware.s19 -o firmware.hex -intel

# HEX转Bin
srec_cat firmware.hex -intel -o firmware.bin -binary

# Bin转S19(需要指定起始地址)
srec_cat firmware.bin -binary -offset 0x08000000 -o firmware.s19 -motorola

自己写转换函数

有时候工具不顺手,我就自己写个简单的转换函数。比如从HEX提取Bin数据:

int hex_to_bin(const char *hex_file, const char *bin_file)
{
    FILE *fh = fopen(hex_file, "r");
    FILE *fb = fopen(bin_file, "wb");
    char line[256];
    
    while (fgets(line, sizeof(line), fh)) {
        if (line[0] != ':') continue;
        
        int len = hex_to_byte(&line[1]);
        int type = hex_to_byte(&line[3]);
        
        if (type == 0x00) {  // 数据记录
            for (int i = 0; i < len; i++) {
                uint8_t data = hex_to_byte(&line[9 + i*2]);
                fwrite(&data, 1, 1, fb);
            }
        }
    }
    
    fclose(fh);
    fclose(fb);
    return 0;
}
避坑指南:我曾经在转换HEX到Bin时,忘了处理扩展线性地址记录(类型04)。结果地址大于64KB的数据全写到了错误位置。记住,转换时一定要维护好当前的高16位地址。

好了,三种刷写文件格式就讲到这里。你想想看,理解了它们的底层结构,以后遇到刷写问题就能快速定位了。下一章我们聊聊密钥管理方案,那可是安全刷写的核心。