news 2026/10/19 16:46:14

嵌入式固件BIN与HEX互转合并:从格式解析到工程化脚本实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
嵌入式固件BIN与HEX互转合并:从格式解析到工程化脚本实践

简介:这是一款面向嵌入式开发者的BIN与HEX文件互转合并工具,基于对话框程序实现,适合需要处理单片机固件格式转换的工程师使用。工具支持BIN转HEX、HEX转BIN以及HEX文件合并三类操作,并针对实际使用中的常见问题持续修复:转HEX时A~F字符统一为大写以避免Keil使用出错,解决了bin文件超过64k数据错乱问题,还更新了C8051使用的Keil编译hex文件转换到bin的错误。资源包共22个文件,包含5个h头文件、4个cpp源文件、2个exe可执行程序,以及rc、dsp、dsw等工程配置文件和ReadMe说明文档,压缩包约654KB,可直接运行或参考源码二次开发。目前已有3664人学习下载,适合需要快速完成固件格式转换、排查转换异常或借鉴MFC对话框实现思路的开发者参考使用。

1. 别再把 BIN 和 HEX 当黑匣子:一次搞懂互转与合并的底层逻辑

手里拿到一块板子,烧录器只认 HEX,供应商给的却是 BIN;或者做 OTA 升级,Bootloader 和 App 各自编译出两个 BIN,中间还空着一段地址,得先合并再校验。这类场景在嵌入式开发里几乎每周都会撞上一次。很多人第一反应是找个小工具点两下,但工具一旦报错,就彻底抓瞎——因为根本不知道 BIN 和 HEX 到底差在哪。其实 BIN 就是纯二进制镜像,从起始地址开始一个字节挨一个字节排下去,没有任何地址信息;HEX 是 Intel HEX 格式的文本文件,每一行都带地址、长度和校验,能描述不连续的内存布局。互转的本质就是「补地址」和「剥地址」,合并的本质就是「按地址排序后拼接」。搞懂这两件事,你就不需要再依赖某个特定软件,用脚本就能把活干完,还能顺手把校验和、填充字节、地址空洞这些细节全控住。这篇笔记就按这个思路,把 BIN 与 HEX 的互转、合并、校验、排错一次讲透,适合正在做固件打包、量产烧录、OTA 差分升级的嵌入式工程师。

2. BIN 与 HEX 互转:从格式差异到可复现脚本

2.1 两种格式到底差在哪,为什么不能直接改后缀

BIN 文件是内存的「裸照」。假设你的 MCU 从 0x08000000 开始放代码,那 BIN 的第一个字节就对应 0x08000000,第二个字节对应 0x08000001,依次类推。它不记录任何地址,所以烧录时必须由烧录器或下载算法指定一个起始地址,否则写进去就是错的。HEX 文件则是一行一行文本,每行以冒号开头,后面跟数据长度、地址、记录类型、数据和校验和。常见的记录类型有 00(数据)、01(文件结束)、04(扩展线性地址)、02(扩展段地址)。正因为有 04 记录,HEX 才能表达超过 64KB 的地址空间,也才能描述中间有空洞的固件布局。

很多人图省事,直接把 .bin 改成 .hex 丢给烧录器,结果烧录器报「格式错误」或者烧进去跑飞。原因就是 BIN 里没有冒号、没有校验、没有地址记录,烧录器按 HEX 解析时第一行就失败了。反过来,把 .hex 改成 .bin 也不行,因为 HEX 是文本,里面全是 ASCII 字符,直接当二进制烧进去,等于把一堆「0x3A、0x31、0x30」写进了 Flash,完全不是你要的机器码。

所以互转不是改后缀,而是真正做格式解析和重建。常见做法是用 Python 写一个小脚本,读 HEX 时解析每一行的地址和数据,按地址顺序拼成一个字节数组,再整体写出 BIN;读 BIN 时则按指定基地址生成数据记录,每 16 或 32 字节一行,加上校验和,最后补上结束记录。

2.2 用 Python 实现 HEX 转 BIN:解析、排序、补空洞

下面这个脚本是我平时用得最多的版本,支持 04 扩展线性地址,能处理不连续段,并且会把空洞填成 0xFF。你可以直接抄过去改基地址和填充值。

import sys def hex_to_bin(hex_path, bin_path, fill=0xFF): """ 解析 Intel HEX 文件,输出纯二进制 BIN。 fill: 地址空洞的填充字节,Flash 常用 0xFF。 """ segments = {} # 记录每个地址对应的字节 upper = 0 # 当前扩展线性地址高 16 位 with open(hex_path, 'r', encoding='ascii') as f: for line in f: line = line.strip() if not line or not line.startswith(':'): continue raw = bytes.fromhex(line[1:]) length = raw[0] addr = (raw[1] << 8) | raw[2] rtype = raw[3] data = raw[4:4+length] checksum = raw[4+length] # 校验和验证:所有字节求和后低 8 位应为 0 if (sum(raw) & 0xFF) != 0: raise ValueError(f"校验和错误: {line}") if rtype == 0x00: # 数据记录 base = upper + addr for i, b in enumerate(data): segments[base + i] = b elif rtype == 0x01: # 文件结束 break elif rtype == 0x04: # 扩展线性地址 upper = (data[0] << 8 | data[1]) << 16 elif rtype == 0x02: # 扩展段地址(少见,兼容处理) upper = (data[0] << 8 | data[1]) << 4 else: continue if not segments: raise ValueError("HEX 文件中没有数据记录") min_addr = min(segments.keys()) max_addr = max(segments.keys()) # 按最小地址到最大地址生成连续数组,空洞填 fill buf = bytearray([fill]) * (max_addr - min_addr + 1) for a, b in segments.items(): buf[a - min_addr] = b with open(bin_path, 'wb') as f: f.write(buf) print(f"起始地址: 0x{min_addr:08X}, 结束地址: 0x{max_addr:08X}, 大小: {len(buf)} 字节") if __name__ == '__main__': hex_to_bin(sys.argv[1], sys.argv[2])

逻辑上分四步:先逐行解析 HEX,把每条数据记录按当前扩展地址展开成「地址→字节」的字典;然后校验和检查,任何一行校验失败直接抛异常,避免把坏数据写进 BIN;接着找出最小和最大地址,生成一个连续缓冲区,空洞位置填 0xFF;最后整体写出。参数 fill 可以根据你的 Flash 擦除状态调整,有些外部 Flash 擦除后是 0x00,那就改成 0x00。注意这个脚本会把所有不连续段之间的空洞都填满,所以输出 BIN 的起始地址就是最小地址,烧录时基地址要设成这个值,否则整体偏移。

2.3 用 Python 实现 BIN 转 HEX:按地址生成记录行

反过来,BIN 转 HEX 需要你指定一个基地址,因为 BIN 本身不带地址。下面脚本每行输出 16 字节,自动处理 04 扩展线性地址,并计算校验和。

import sys def bin_to_hex(bin_path, hex_path, base_addr=0x08000000, line_len=16): """ 将 BIN 文件转换为 Intel HEX。 base_addr: BIN 第一个字节对应的地址。 line_len: 每行数据字节数,常用 16 或 32。 """ with open(bin_path, 'rb') as f: data = f.read() lines = [] upper = None for offset in range(0, len(data), line_len): chunk = data[offset:offset+line_len] addr = base_addr + offset # 如果高 16 位发生变化,插入 04 记录 hi = (addr >> 16) & 0xFFFF if hi != upper: upper = hi rec = [0x02, 0x00, 0x00, 0x04, (hi >> 8) & 0xFF, hi & 0xFF] checksum = (-sum(rec)) & 0xFF lines.append(':' + ''.join(f'{b:02X}' for b in rec) + f'{checksum:02X}') low = addr & 0xFFFF rec = [len(chunk), (low >> 8) & 0xFF, low & 0xFF, 0x00] + list(chunk) checksum = (-sum(rec)) & 0xFF lines.append(':' + ''.join(f'{b:02X}' for b in rec) + f'{checksum:02X}') lines.append(':00000001FF') # 文件结束记录 with open(hex_path, 'w', encoding='ascii') as f: f.write('\n'.join(lines) + '\n') print(f"生成 {len(lines)} 行, 基地址 0x{base_addr:08X}, 大小 {len(data)} 字节") if __name__ == '__main__': bin_to_hex(sys.argv[1], sys.argv[2])

关键点有三个:一是每行长度 line_len 可以调,16 字节兼容性最好,32 字节文件更小;二是 04 记录只在跨 64KB 边界时插入,不能每行都插,否则文件臃肿;三是校验和算法是「所有字节累加后取低 8 位,再用 0x100 减去该值」,代码里用负数取与实现。基地址 base_addr 必须和你的链接脚本一致,STM32 通常是 0x08000000,有些 NXP 芯片是 0x00000000,搞错了烧进去必跑飞。

2.4 合并多个 BIN:按地址排序与重叠检测

合并两个 BIN 比互转更容易翻车,因为地址重叠是静默发生的。比如 Bootloader 占 0x08000000 到 0x08007FFF,App 从 0x08008000 开始,合并时直接拼接没问题;但如果 App 编译时链接地址设成了 0x08007000,就会和 Bootloader 尾部重叠,拼接后前面的数据被覆盖,烧进去直接死机。所以合并脚本必须做重叠检测。

def merge_bins(segments, out_path, fill=0xFF): """ segments: [(base_addr, bytes), ...] 按地址排序后合并,检测重叠。 """ segments = sorted(segments, key=lambda x: x[0]) for i in range(1, len(segments)): prev_base, prev_data = segments[i-1] curr_base, _ = segments[i] if prev_base + len(prev_data) > curr_base: raise ValueError( f"地址重叠: 段{i-1}结束于 0x{prev_base+len(prev_data):08X}, " f"段{i}起始于 0x{curr_base:08X}" ) min_addr = segments[0][0] max_addr = max(base + len(data) for base, data in segments) buf = bytearray([fill]) * (max_addr - min_addr) for base, data in segments: buf[base - min_addr:base - min_addr + len(data)] = data with open(out_path, 'wb') as f: f.write(buf) print(f"合并完成: 0x{min_addr:08X} ~ 0x{max_addr:08X}, 共 {len(buf)} 字节")

调用时传入 [(0x08000000, boot_data), (0x08008000, app_data)] 即可。重叠检测放在排序之后,只要前一段的结束地址大于后一段的起始地址就报错,绝不静默覆盖。填充字节同样用 fill 控制。这个函数也可以扩展成支持 HEX 输入,先转成 BIN 再合并。

3. 合并与校验的工程化:地址对齐、填充值与校验和

3.1 地址对齐为什么会影响烧录成功率

很多烧录器对写入地址有对齐要求,比如必须 4 字节对齐,或者必须按扇区对齐。如果你合并后的 BIN 起始地址是 0x08000001,烧录器可能直接拒绝,或者写入后读出来错位。常见做法是保证每个段的起始地址和长度都按 4 字节或 256 字节对齐。链接脚本里通常已经做了对齐,但手动合并时容易忽略。我一般会在合并脚本里加一个对齐检查,发现不对齐就打印警告,让开发者决定是填充还是改链接脚本。

def check_alignment(segments, align=4): for base, data in segments: if base % align != 0: print(f"警告: 段起始地址 0x{base:08X} 未按 {align} 字节对齐") if len(data) % align != 0: print(f"警告: 段长度 {len(data)} 未按 {align} 字节对齐")

对齐本身不改变数据内容,但会影响烧录器内部按字写入的逻辑。有些 Flash 编程算法一次写 4 字节,地址不对齐时它会先读出来再合并,效率低而且容易出错。所以能对齐就对齐,这是血泪经验。

3.2 填充值选 0x00 还是 0xFF,取决于存储介质

填充值选错是另一个隐蔽的坑。NOR Flash 和 MCU 内部 Flash 擦除后通常是 0xFF,所以空洞填 0xFF 最安全,烧录时如果跳过空洞也不会出问题。但 EEPROM 或某些外部 SPI Flash 擦除后是 0x00,这时候填 0xFF 反而会在空洞区域写入非擦除值,导致后续擦除不干净。判断方法很简单:看芯片手册的「Erased State」参数。如果不确定,就填 0xFF,因为绝大多数嵌入式 Flash 都是这个值。另外,如果合并后的 BIN 要参与 CRC 校验,填充值会直接影响 CRC 结果,所以 Bootloader 和 App 的校验算法必须约定好填充值,否则一个填 0xFF 一个填 0x00,校验永远对不上。

3.3 合并后如何验证:用校验和与反读比对

合并完不要直接烧,先做两步验证。第一步,用脚本计算整个 BIN 的 CRC32 或 SHA256,和链接阶段生成的校验值比对。第二步,如果条件允许,把合并后的 BIN 通过烧录器读回来,再算一次校验,确认写入和读出一致。下面是一个简单的 CRC32 计算片段。

import zlib def verify_bin(bin_path, expect_crc=None): with open(bin_path, 'rb') as f: data = f.read() crc = zlib.crc32(data) & 0xFFFFFFFF print(f"大小: {len(data)} 字节, CRC32: 0x{crc:08X}") if expect_crc is not None and crc != expect_crc: raise ValueError(f"CRC 不匹配: 期望 0x{expect_crc:08X}, 实际 0x{crc:08X}") return crc

如果 Bootloader 里用的是累加和而不是 CRC32,那就换成 sum(data) & 0xFFFFFFFF。关键是合并前后用同一种算法,并且把填充值纳入计算范围。我见过有人合并后 CRC 对不上,查了半天发现是合并脚本把空洞填了 0xFF,而 Bootloader 计算时跳过了空洞,两边算法不一致。这种问题只能靠约定和文档解决,没有捷径。

4. 避坑与排查:BIN/HEX 互转合并中的五个典型翻车现场

4.1 现象:HEX 转 BIN 后烧录跑飞,读回来发现开头少了一段

原因:HEX 文件里第一条数据记录的地址不是最小地址,或者存在 04 记录之前就有数据。有些编译器生成的 HEX 会先输出一段配置字,地址在 0x00000000,而你的代码在 0x08000000,转换脚本如果按文件顺序拼接而不是按地址排序,就会把配置字放到 BIN 开头,导致整体偏移。解决:转换脚本必须按地址排序后再生成 BIN,不能依赖文件行顺序。上面 2.2 的脚本用字典存储地址,天然按地址排序,不会出这个问题。

4.2 现象:BIN 转 HEX 后烧录器提示「地址超出范围」

原因:基地址设错了,或者 BIN 文件本身包含了超出芯片 Flash 容量的数据。比如芯片只有 128KB Flash,基地址 0x08000000,BIN 却有 200KB,转换出的 HEX 地址到了 0x08032000,烧录器直接拒绝。解决:转换前先确认 BIN 大小和芯片容量,基地址加 BIN 长度不能超过 Flash 结束地址。另外检查链接脚本里的 ROM 长度设置,有时候是链接脚本写大了,实际代码没那么多,但 BIN 被填充到了最大长度。

4.3 现象:合并两个 BIN 后,App 无法跳转,Bootloader 也挂了

原因:地址重叠,App 的起始地址落在了 Bootloader 的地址范围内,合并时 App 覆盖了 Bootloader 的尾部数据。这种问题最隐蔽,因为合并脚本不报错,烧录也不报错,但运行就死。解决:合并前强制做重叠检测,如 2.4 脚本所示。另外,Bootloader 和 App 的链接脚本要严格划分地址区间,中间最好留一段空白作为隔离,避免边界模糊。

4.4 现象:HEX 文件校验和错误,但用文本编辑器看不出问题

原因:HEX 文件在传输或保存过程中被修改了换行符,或者某一行被截断。Windows 下换行是 \r\n,Linux 下是 \n,有些解析脚本按 \n 分割后,行尾残留 \r,导致 bytes.fromhex 失败。解决:读取时用 strip() 去掉首尾空白,并且逐行校验和。如果校验和失败,打印行号和内容,定位到具体哪一行坏了。不要跳过校验和检查,否则坏数据会静默写进 BIN。

4.5 现象:合并后的 BIN 大小比预期大很多,里面全是填充值

原因:两个段的地址间隔很大,比如 Bootloader 在 0x08000000,App 在 0x08020000,中间空了 128KB,合并脚本把空洞全部填满,BIN 就变成了 256KB。这本身不是错误,但烧录时间变长,而且如果烧录器不支持跳过空白,会浪费大量时间。解决:如果烧录器支持「仅写入非 0xFF 区域」,可以保留空洞;如果不支持,就接受这个大小。另一种做法是分别烧录 Bootloader 和 App,不合并,但 OTA 场景通常需要合并成一个文件。权衡之后,我一般会保留空洞,因为 0xFF 填充不会影响功能,只是文件大一点。

5. 进阶技巧:用脚本一键完成互转、合并与校验

5.1 把互转、合并、校验串成一条命令

实际项目里,我通常把上面几个函数封装成一个命令行工具,支持三种模式:hex2bin、bin2hex、merge。这样在 Makefile 里加一行就能自动完成固件打包。

import argparse def main(): parser = argparse.ArgumentParser() sub = parser.add_subparsers(dest='cmd') p1 = sub.add_parser('hex2bin') p1.add_argument('input'); p1.add_argument('output') p2 = sub.add_parser('bin2hex') p2.add_argument('input'); p2.add_argument('output') p2.add_argument('--base', type=lambda x: int(x, 0), default=0x08000000) p3 = sub.add_parser('merge') p3.add_argument('output') p3.add_argument('segments', nargs='+', help='格式: 地址:文件路径') args = parser.parse_args() if args.cmd == 'hex2bin': hex_to_bin(args.input, args.output) elif args.cmd == 'bin2hex': bin_to_hex(args.input, args.output, args.base) elif args.cmd == 'merge': segs = [] for s in args.segments: addr_str, path = s.split(':', 1) with open(path, 'rb') as f: segs.append((int(addr_str, 0), f.read())) merge_bins(segs, args.output) else: parser.print_help() if __name__ == '__main__': main()

用法示例:python fwtool.py merge out.bin 0x08000000:boot.bin 0x08008000:app.bin。这样在 CI 里可以自动检查重叠、自动填充、自动算 CRC。参数 --base 支持 0x 前缀,方便直接写十六进制地址。

5.2 验证合并结果是否可直接烧录

合并完成后,我习惯用hexdump -C out.bin | head看一眼开头和结尾,确认 Bootloader 的向量表在 0x08000000 处,App 的向量表在 0x08008000 处。如果向量表不对,说明链接地址或合并地址错了。另外,用arm-none-eabi-objdump反汇编一下合并后的 BIN,确认入口点正确。这些步骤看起来繁琐,但比烧进去跑飞再回来查要快得多。

检查项命令/方法预期结果
起始地址hexdump -C out.bin | head -1前 4 字节是栈顶地址,通常是 0x2000xxxx
App 向量表hexdump -C out.bin -s 0x8000 -n 16前 4 字节是 App 栈顶地址
CRC 校验python -c "import zlib;print(hex(zlib.crc32(open('out.bin','rb').read())))"与 Bootloader 约定值一致
文件大小ls -l out.bin不超过 Flash 容量

5.3 一个容易忽略的细节:HEX 文件的行结束符

最后说一个我踩过的坑。有些老式烧录器只认 CRLF 换行的 HEX 文件,而 Python 默认写出来是 LF。虽然大多数现代工具都能兼容,但遇到不兼容的情况,烧录器会报「文件格式错误」。解决办法是在写 HEX 时指定 newline='\r\n',或者写完后用 unix2dos 转一下。从那以后我每次生成 HEX 都强制走一遍格式检查,确认换行符和校验和都没问题再交给产线。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 16:46:08

六大AI科研神器实操详解:从文献检索到论文写作全流程覆盖

1. 为什么2026届科研人必须重新审视AI工具这两年AI工具迭代速度快得让人有点跟不上。我见过不少同学还在用最原始的方式查文献、整理数据&#xff0c;一篇综述光下载PDF、手动归类就花掉两三天。而另一批人已经用AI把文献筛选、数据清洗、图表绘制、论文润色全部串成一条流水线…

作者头像 李华
网站建设 2026/10/19 16:41:31

反向传播中的dX:从公式推导到LLM训练实战

训练做到一半&#xff0c;loss 曲线突然往上蹿&#xff0c;接着直接变 NaN。查了一整天&#xff0c;最后用梯度检查把问题定位在一个自定义算子的反向传播上——不是梯度爆炸&#xff0c;不是学习率太大&#xff0c;而是 (dX G W^T) 这个公式里的矩阵转置方向抄反了。在 LLM 训…

作者头像 李华
网站建设 2026/10/19 16:40:53

YOLOv8自助售货机商品检测:从数据集到部署全流程实战

1. 自助售货机商品检测&#xff1a;毕设到底在做什么自助售货机商品检测这个课题&#xff0c;说白了一件事&#xff1a;让摄像头代替人的眼睛&#xff0c;识别出货架上的商品是什么、是否齐全、还有没有货。听起来不复杂&#xff0c;放在毕业设计里却是个特别完整的项目——有数…

作者头像 李华
网站建设 2026/10/19 16:39:25

养虾指南:低成本部署AI自动化服务的完整方案

先说结论&#xff1a;圈子里常说的“养虾”&#xff0c;指的不是水产养殖&#xff0c;而是“低成本部署一个长期运行的AI自动化服务”。这活儿最早是从自建聊天机器人、定时任务助手那批人嘴里传出来的&#xff0c;后来范围越扩越大&#xff0c;自动日报、订阅源聚合、定时爬虫…

作者头像 李华
网站建设 2026/10/19 16:38:24

工控上位机多协议采集框架设计:从通道到协议的分层实践

1. 工控现场的协议之乱&#xff1a;为什么我决定先搭一套采集框架再写功能1.1 一个现场的真实场景做上位机开发的朋友应该都有过这种经历&#xff1a;项目还没正式启动&#xff0c;需求文档先给你甩过来一页密密麻麻的设备清单。我碰到过最离谱的一个模拟项目&#xff0c;现场设…

作者头像 李华
网站建设 2026/10/19 16:38:24

Matlab NSGA-II多目标优化求解器:支持约束、开箱即用

简介&#xff1a;本资源是一套完整的NSGA-II多目标优化算法Matlab实现代码包&#xff0c;面向高校学生、科研人员及工程优化从业者&#xff0c;用于学习和求解典型多目标优化问题&#xff08;如参数权衡、Pareto前沿探索等&#xff09;。压缩包共10个文件&#xff0c;含8个核心…

作者头像 李华