1. 单片机烧录用的 hex 文件到底长什么样:从一行记录看懂 Intel HEX 格式解析
如果你手里有一块 STM32、GD32、N76E003 或者 51 单片机,量产烧录时大概率会拿到一个.hex文件。它和.bin最大的区别是:bin 只有纯数据,烧录器必须提前知道往哪个地址写;而 hex 文件每一行都自带地址信息,烧录器逐行读就能把数据放到正确位置。这也是为什么很多烧录工具默认优先吃 hex。
Intel HEX 是一种纯文本格式,用记事本、Notepad++、VS Code 都能直接打开。你会看到类似这样的内容:
:1000000000800020C1000008C5000008C9000008A0 :100010000000000000000000000000000000000000 :0400000508000000EF :00000001FF每一行都以英文冒号:开头,这叫 RecordMark(记录起始符)。冒号后面是一串十六进制字符,按字节拆开就是一条完整记录。一条记录固定由 5 个部分组成:数据长度、装载地址、记录类型、有效数据、校验和。理解这 5 个字段,你就能独立读懂任意一个 hex 文件,不用再依赖烧录软件的黑盒提示。
这篇文章面向正在做单片机烧录、上位机开发、Bootloader 升级的工程师,也适合刚接触嵌入式、想搞清楚 hex 文件格式解析的初学者。我会逐行拆解字段含义,给出一份可复制的解析对照表,再写一个能直接跑的校验和验证脚本,最后用真实烧录文件带你逐行核对。读完你至少能做到三件事:手动算出一行的校验和、判断某个 hex 文件是否被改坏、用脚本批量验证整个文件。
先记住一个核心结论:Intel HEX 的校验和规则是「所有字节累加后取低 8 位,结果必须为 0」。这一条贯穿全文,后面所有排错都围绕它展开。
2. 逐字段拆解 hex 记录:数据长度、地址、记录类型与校验和计算逻辑
拿第一行:1000000000800020C1000008C5000008C9000008A0来拆。去掉冒号后,每两个十六进制字符是一个字节:
| 字段位置 | 字节内容 | 含义 | 说明 |
|---|---|---|---|
| 第 1 字节 | 10 | 数据长度 | 0x10 = 16,表示后面有 16 字节有效数据 |
| 第 2-3 字节 | 0000 | 装载地址 | 本行数据写入的偏移地址 0x0000 |
| 第 4 字节 | 00 | 记录类型 | 00 表示数据记录 |
| 第 5-20 字节 | 00800020...0008 | 有效数据 | 共 16 字节,就是要烧进 Flash 的内容 |
| 第 21 字节 | A0 | 校验和 | 前面所有字节累加取低 8 位应为 0 |
记录类型一共 6 种,实际烧录文件里最常见的是 00、01、04 三种:
| 类型值 | 名称 | 作用 |
|---|---|---|
| 00 | 数据记录 | 携带要写入 Flash 的真实数据 |
| 01 | 文件结束 | 标记 hex 文件到此结束,固定为:00000001FF |
| 02 | 扩展段地址 | 用段地址方式扩展寻址,较少见 |
| 03 | 开始段地址 | 指定程序入口,一般 51 单片机用 |
| 04 | 扩展线性地址 | 用线性方式扩展高 16 位地址,STM32 常见 |
| 05 | 开始线性地址 | 指定 32 位程序入口地址 |
校验和的计算逻辑:把「数据长度 + 地址高字节 + 地址低字节 + 记录类型 + 所有数据字节」逐字节相加,取结果的低 8 位,再用0x100减去这个低 8 位,得到校验和。等价说法是:把包括校验和在内的所有字节相加,低 8 位必须等于 0。
以第一行为例,累加过程如下:
10 + 00 + 00 + 00 + 00 + 80 + 00 + 20 + C1 + 00 + 00 + 08 + C5 + 00 + 00 + 08 + C9 + 00 + 00 + 08 = 0x360 0x360 & 0xFF = 0x60 0x100 - 0x60 = 0xA0算出来正好是行尾的A0,说明这一行没被改坏。再看结束行:00000001FF:长度 00、地址 0000、类型 01、无数据,累加00+00+00+01=0x01,0x100-0x01=0xFF,和行尾一致。
扩展线性地址(类型 04)是理解大容量单片机 hex 的关键。比如 STM32F103 的 Flash 从 0x08000000 开始,但单条记录的地址字段只有 16 位,最大 0xFFFF,装不下 0x08000000。于是 hex 文件会先出现一行:020000040800F2,其中数据是0800,表示基地址 = 0x0800 << 16 = 0x08000000。在这行之后、下一个类型 04 出现之前,所有数据记录的实际地址 = 基地址 + 记录地址字段。
举个例子:
:020000040800F2 :1000000000800020C1000008C5000008C9000008A0 :100010000000000000000000000000000000000000第一行设定基地址 0x08000000。第二行地址字段 0x0000,实际写入 0x08000000。第三行地址字段 0x0010,实际写入 0x08000010。这样就能覆盖整个 0x08000000 起始的 Flash 空间。
3. 可复制的 hex 解析对照表与校验和验证脚本
光看理论不够,我准备了一份可以直接用的解析对照表,以及一个 Python 校验脚本。你把它保存成hex_check.py,就能批量验证任意 hex 文件。
先看对照表,建议收藏:
| 行内容示例 | 长度 | 地址 | 类型 | 数据 | 校验和 | 实际写入地址 |
|---|---|---|---|---|---|---|
:1000000000800020...A0 | 10 | 0000 | 00 | 16 字节 | A0 | 基地址+0x0000 |
:100010000000...00 | 10 | 0010 | 00 | 16 字节 | 00 | 基地址+0x0010 |
:020000040800F2 | 02 | 0000 | 04 | 0800 | F2 | 设定基地址 0x08000000 |
:0400000508000000EF | 04 | 0000 | 05 | 08000000 | EF | 入口地址 0x08000000 |
:00000001FF | 00 | 0000 | 01 | 无 | FF | 文件结束 |
下面是校验和验证脚本,逐行读取 hex 文件,检查每行校验和是否为 0,并打印解析结果:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- import sys def parse_line(line): line = line.strip() if not line or not line.startswith(':'): return None raw = line[1:] if len(raw) % 2 != 0: return {'error': '字符数为奇数,格式错误', 'line': line} data = bytes.fromhex(raw) length = data[0] addr = (data[1] << 8) | data[2] rtype = data[3] payload = data[4:4 + length] checksum = data[4 + length] total = sum(data) & 0xFF return { 'line': line, 'length': length, 'addr': addr, 'type': rtype, 'data': payload.hex().upper(), 'checksum': checksum, 'sum_ok': total == 0, } def main(path): base = 0 with open(path, 'r', encoding='utf-8', errors='ignore') as f: for idx, line in enumerate(f, 1): r = parse_line(line) if r is None: continue if 'error' in r: print(f'第{idx}行 错误: {r["error"]}') continue if r['type'] == 0x04: base = int(r['data'], 16) << 16 real_addr = base + r['addr'] if r['type'] == 0x00 else None flag = 'OK' if r['sum_ok'] else '校验失败' print(f'第{idx}行 类型={r["type"]:02X} 长度={r["length"]:02X} ' f'地址={r["addr"]:04X} 实际地址={real_addr} ' f'校验和={r["checksum"]:02X} [{flag}]') if r['type'] == 0x01: print('文件结束') break if __name__ == '__main__': main(sys.argv[1] if len(sys.argv) > 1 else 'firmware.hex')运行方式:
python hex_check.py firmware.hex输出会逐行告诉你类型、地址、实际写入地址和校验结果。如果某行显示「校验失败」,说明文件被改坏或复制时丢了字符。
如果你想把解析结果接到自己的上位机里,用 TaoToken 的模型对话能力快速生成不同语言的解析代码也很省事,比如让它把上面的 Python 逻辑翻译成 C# 或 C++,再对照本文的字段表核对。需要的话可以从 https://taotoken.net/api 接入,配合 https://taotoken.net/api-keys 拿到的 Key 使用。
4. 用真实烧录文件逐行核对:验证请求与成功结果
理论讲完,拿一个真实的 STM32 工程 hex 文件来核对。假设文件开头是:
:020000040800F2 :1000000000800020C1000008C5000008C9000008A0 :100010000000000000000000000000000000000000 :100020000000000000000000000000000000000000 :0400000508000000EF :00000001FF第一步,确认第一行:020000040800F2。长度 02,地址 0000,类型 04,数据 0800,校验和 F2。累加02+00+00+04+08+00=0x0E,0x100-0x0E=0xF2,正确。这行把基地址设为 0x08000000。
第二步,看第二行:1000000000800020...A0。类型 00,地址 0000,实际写入 0x08000000。这 16 字节是中断向量表开头,前 4 字节00 80 00 20小端解读为 0x20008000,正是栈顶地址;接着C1 00 00 08是复位向量 0x080000C1。这说明文件确实是 STM32 的固件。
第三步,看:0400000508000000EF。类型 05,数据 08000000,表示程序入口地址。校验累加04+00+00+05+08+00+00+00+00=0x11,0x100-0x11=0xEF,正确。
第四步,看结束行:00000001FF。类型 01,校验和 FF,正确。到这里整个文件结构就核对完了。
用脚本跑一遍,成功输出类似:
第1行 类型=04 长度=02 地址=0000 实际地址=None 校验和=F2 [OK] 第2行 类型=00 长度=10 地址=0000 实际地址=0x08000000 校验和=A0 [OK] 第3行 类型=00 长度=10 地址=0010 实际地址=0x08000010 校验和=00 [OK] 第4行 类型=00 长度=10 地址=0020 实际地址=0x08000020 校验和=00 [OK] 第5行 类型=05 长度=04 地址=0000 实际地址=None 校验和=EF [OK] 第6行 类型=01 长度=00 地址=0000 实际地址=None 校验和=FF [OK] 文件结束所有行都是 OK,说明文件完整。如果某行校验失败,脚本会明确标出,你就能定位到具体哪一行被改坏。
这里有个实用技巧:很多烧录失败并不是 hex 文件本身的问题,而是烧录器把类型 04 的基地址忽略了,导致数据被写到 0x00000000 而不是 0x08000000。遇到「烧进去不运行」的情况,先确认烧录器是否正确处理了类型 04 记录。
5. 本篇常见错误排查:校验失败、地址错乱与烧录器报错
实际工作中,hex 文件相关的报错集中在几类,我按真实报错信息整理成排查表。
第一类:校验和错误。烧录器提示checksum error at line N或record checksum mismatch。原因通常是文件在传输、复制、Git 合并时被改动,或者用文本编辑器保存时改了换行符。排查方法:用第 3 节的脚本跑一遍,定位到具体行,对比原始文件。注意有些编辑器会自动在文件末尾加空行,空行会被解析器跳过,一般不影响,但如果把某行的字符删了一个,校验必然失败。
第二类:地址错乱。烧录器提示address out of range或数据写到了错误区域。常见原因是类型 04 记录被忽略,或者类型 02 和类型 04 混用。类型 02 是段地址,基地址 = 段值 << 4;类型 04 是线性地址,基地址 = 值 << 16。两者计算方式不同,不能混。STM32 一般只用类型 04。
第三类:文件结束记录缺失。烧录器提示no end of file record或一直等待。Intel HEX 规范要求文件必须以:00000001FF结尾。如果这行丢了,有些烧录器会一直读不到结束标志。检查文件最后一行即可。
第四类:OAuth 或本地代理相关报错。如果你在用某些云端工具或本地服务解析 hex,可能遇到local proxy failed或OAuth token expired。这类报错和 hex 格式无关,是工具链的鉴权问题。检查你的 API Key 是否过期,重新生成即可。用 TaoToken 的话,可以在 https://taotoken.net/api-keys 重新签发 Key,再在工具里更新配置。
第五类:reading choices类报错。这通常出现在用脚本或工具批量处理多个 hex 文件时,工具在读取选项或配置时失败。检查输入路径是否正确、文件是否有读权限、文件编码是否为 UTF-8 或 ASCII。hex 文件本身是纯 ASCII,如果被存成 UTF-8 with BOM,开头会多出EF BB BF,导致第一行解析失败。用 VS Code 另存为「UTF-8 无 BOM」即可。
第六类:烧录后程序不运行。hex 校验全过,但单片机没反应。这时候要检查类型 05 的入口地址是否正确,以及类型 04 的基地址是否和芯片实际 Flash 起始地址一致。比如 STM32F103 是 0x08000000,STM32F407 也是 0x08000000,但有些国产芯片是 0x00000000 映射。确认芯片手册里的 Flash 起始地址。
排查时建议按顺序来:先跑校验脚本确认文件完整,再看类型 04 基地址,再看类型 05 入口,最后看烧录器配置。这样能覆盖 90% 以上的问题。
6. 把 hex 解析接进你的烧录工具链:从手动核对到自动化
手动核对适合排查单个文件,但量产或持续集成时,你需要把 hex 解析自动化。思路很简单:在烧录前加一道校验关卡,用脚本验证 hex 文件完整性,校验失败就直接阻断烧录流程。
一个典型的 CI 流程可以这样设计:
python hex_check.py build/firmware.hex > hex_report.txt if grep -q "校验失败" hex_report.txt; then echo "hex 文件校验失败,终止烧录" exit 1 fi echo "hex 文件校验通过,开始烧录"这样每次构建产物都会先过一遍校验,避免把损坏的文件烧进芯片。对于 Bootloader 升级场景,还可以在解析时提取类型 00 的数据,按实际地址重组出 bin 文件,再通过串口或 CAN 发送给目标板。
如果你想让工具链更智能,比如自动识别不同芯片的基地址、自动生成烧录配置,可以借助大模型来辅助生成解析逻辑。TaoToken 的模型对话接口支持把 hex 解析需求直接转成代码,适合快速验证思路。接入地址是 https://taotoken.net/api,配合 https://taotoken.net/doc 里的文档说明使用。对于长期做嵌入式工具开发的团队,Coding Plan 能覆盖日常的代码生成和调试需求,入口在 https://taotoken.net/coding-plan。
最后留一个实用习惯:每次拿到新的 hex 文件,先看最后一行是不是:00000001FF,再看有没有类型 04 记录,最后跑一遍校验脚本。这三步花不了一分钟,但能帮你避开绝大多数烧录翻车现场。