第三讲:固件映像格式——你的升级包长什么样?
好,咱们进入第三讲。前面聊了协议框架和通信流程,今天来聊聊升级包里到底装了什么——也就是固件映像格式。
说白了,固件映像就是你要烧进芯片的那堆二进制数据。但直接扔一堆二进制过去?不行。你得告诉接收端:这是什么?从哪开始?对不对?有没有损坏?所以,我们需要一个结构化的格式。
固件映像的结构:头、身、尾
一个标准的固件映像,我习惯把它分成三部分:
- 头部(Header):描述信息,相当于身份证
- 数据区(Data):真正的固件代码和数据
- 尾部(Trailer):收尾信息,比如校验值、填充字节
你想想看,没有头部,Bootloader拿到数据都不知道该往哪写。没有尾部,它也不知道数据传完了没有。这三者缺一不可。
核心原则:头部要足够简单,但信息要足够完整。我见过有人把头部搞成几百字节,里面塞了一堆调试信息——嗯,这在量产时就是浪费Flash。
头部信息:魔数、版本号、长度、校验和
头部里放什么?我总结四个核心字段:
| 字段 | 大小(常见) | 说明 |
|---|---|---|
| 魔数(Magic Number) | 4字节 | 用于识别这是不是合法的固件映像 |
| 版本号 | 4字节 | 主版本+次版本,或者干脆用时间戳 |
| 长度 | 4字节 | 数据区的总字节数 |
| 校验和 | 2~4字节 | 对整个映像(或头部+数据)的校验 |
魔数这东西,说白了就是一个固定值。比如我习惯用 0xDEADBEEF 或者 0xA5A5A5A5。Bootloader收到数据后先读前4字节,如果对不上,直接扔掉——省得后面解析一堆垃圾数据。
版本号怎么设计?我在项目中遇到过两种做法:
- 用
uint32_t拆成主版本(高16位)和次版本(低16位) - 直接用编译时间戳(比如
0x20250314)
我个人更推荐时间戳,因为版本号容易忘改,时间戳是自动生成的,不会重复。
长度字段要注意:它只算数据区,不包括头部和尾部。这样Bootloader拿到长度后,就知道要接收多少字节才算完。
校验和,嗯,这里要重点说。我曾经踩过一个坑:只校验了数据区,没校验头部。结果有一次头部被篡改了,魔数还是对的,但版本号被改成了旧版本,导致升级回退。后来我改成对整个映像(头部+数据)做CRC32校验,再也没出过问题。
避坑指南:我曾经在项目里只用简单的累加和做校验,结果发现两个不同映像的累加和居然一样。从那以后,我坚持用CRC32或者SHA256。别省这点计算量,安全第一。
常见映像格式:HEX、SREC、BIN
实际工作中,你会遇到三种主流格式。我按使用频率排个序:
1. BIN(二进制格式)
最纯粹的格式。没有头部,没有地址信息,就是纯二进制数据。Bootloader直接把它烧到指定地址就行。
- 优点:体积最小,解析最快
- 缺点:没有地址信息,必须事先知道烧录地址
- 适用场景:量产烧录、OTA升级(配合头部信息)
我个人在OTA升级中几乎只用BIN格式。因为Bootloader的头部已经包含了地址和长度信息,不需要再额外解析地址了。
2. HEX(Intel HEX格式)
文本格式,每行以冒号开头,包含地址、数据类型、数据、校验和。比如:
:100000000C944A000C9451000C9458000C945F0071
:100010000C9466000C946D000C9474000C947B00A5
:100020000C9482000C9489000C9490000C949700D9
:00000001FF
每行解释:
:起始标记10本行数据长度(16字节)0000起始地址00数据类型(00=数据,01=结束)- 后面是16字节数据
- 最后两位是校验和
优点:带地址信息,可以烧录到任意位置。支持分段(比如代码在0x8000,数据在0x9000)。
缺点:体积大(文本格式比二进制大2~3倍),解析复杂。
我记得第一次用HEX格式做升级时,解析代码写了整整两天。后来发现一个坑:有些编译器生成的HEX文件地址不连续,中间有空洞。Bootloader如果按顺序烧录,会把空洞也写进去——嗯,那Flash就废了。
小技巧:如果你必须用HEX做升级,建议先把它转成BIN再传输。PC端做转换,嵌入式端只解析BIN,省心很多。
3. SREC(Motorola S-record格式)
和HEX类似,也是文本格式。但它的地址范围更大(支持32位地址),而且每行以 S 开头。
S00A00006C65642E7372654338
S11300000C944A000C9451000C9458000C945F00B4
S11300100C9466000C946D000C9474000C947B0020
S5030003F9
S9030000FC
SREC的地址可以到4字节(32位),而HEX最大只支持16位地址(扩展线性地址记录可以到32位,但比较麻烦)。所以如果你的芯片Flash很大(比如几MB),SREC会更方便。
不过说实话,我在实际项目中很少直接用SREC做升级。它和HEX一样,解析起来比较费劲。除非是跟某些老旧的烧录器配合,否则我建议还是用BIN。
三种格式对比
| 特性 | BIN | HEX | SREC |
|---|---|---|---|
| 格式 | 二进制 | 文本(ASCII) | 文本(ASCII) |
| 体积 | 最小 | 较大(约2~3倍) | 较大(约2~3倍) |
| 地址信息 | 无 | 有(16位+扩展) | 有(32位) |
| 解析复杂度 | 极低 | 中等 | 中等 |
| 适用场景 | OTA升级、量产 | 调试、烧录器 | 大地址空间、烧录器 |
我的建议
如果你在做Bootloader升级协议,我强烈推荐:
- 传输用BIN:配合自定义头部,简单高效
- 调试用HEX:方便看地址,也方便分段烧录
- SREC:除非你的芯片地址空间超过16位,否则没必要
嗯,这一讲就到这里。下一讲我们聊聊升级流程中的状态机设计——那才是真正考验功底的地方。