简介:这是一款面向嵌入式开发者的BIN与HEX文件互转合并工具,基于MFC对话框程序实现,参考了公开的转换代码并加以完善,适合需要处理单片机固件格式转换的工程师使用。资源包共22个文件,包含5个h头文件、4个cpp源文件、2个exe可执行程序,以及rc资源脚本、dsp/dsw工程文件、ico图标等,压缩包约654KB,可直接运行或自行编译修改。该工具支持BIN转HEX、HEX转BIN以及HEX文件合并三项核心功能,并针对实际使用中的问题持续更新:修正了转HEX时A~F字符大小写导致Keil出错的问题,解决了BIN文件超过64K时数据错乱的情况,还修复了C8051所用Keil编译的HEX文件转换到BIN时的错误。目前已有3661人学习下载,适合需要快速完成固件格式转换、合并烧录文件的开发者参考使用。
1. 从一次产线刷写翻车说起:BIN 和 HEX 到底该怎么互转合并
上周帮一个做嵌入式量产的朋友排查问题,他们产线烧录时把两个不同地址段的固件分别导出成 BIN,结果合并后设备直接变砖。拆开看,问题出在 BIN 是纯二进制、没有地址信息,两个文件首尾相接并不等于地址连续;而 HEX 自带地址记录,合并逻辑完全是另一套。这类翻车在固件打包、OTA 差分、Bootloader + App 拼接场景里非常常见。BIN 和 HEX 的互转与合并,本质是「地址信息的有无」和「记录格式的解析」两件事。这份资源就是围绕这个痛点,把 bin/hex 互转、按地址合并、校验对齐这些操作讲清楚,适合做嵌入式固件打包、产线烧录、OTA 升级的从业者,新手能照着命令跑通,熟手能看清地址边界和参数取舍。
2. 先搞懂 BIN 与 HEX 的格式差异:为什么不能直接 cat 拼接
2.1 BIN 是裸数据,HEX 是带地址的文本记录
BIN 文件就是一段连续的二进制字节流,它本身不包含任何地址信息。你拿到一个 32KB 的 BIN,只知道它有 32768 个字节,但不知道这段数据应该烧到 Flash 的哪个位置。地址信息通常由烧录工具的参数、链接脚本或者产线配置来指定。
HEX 文件则完全不同。以最常见的 Intel HEX 格式为例,它是由一行行 ASCII 文本组成的记录,每行以冒号开头,包含数据长度、地址、记录类型、数据和校验和。记录类型决定了这行是数据、扩展地址还是文件结束。正因为每行都带地址,HEX 才能表达「这段数据在 0x08000000,那段数据在 0x08020000」这种非连续布局。
这个差异直接决定了互转和合并的策略:BIN 转 HEX 必须补地址,HEX 转 BIN 必须处理地址空洞,合并两个 BIN 必须确认地址连续性,合并两个 HEX 则要按地址记录做归并。
2.2 常见工具链里的格式约定
不同工具链对 BIN 和 HEX 的默认行为不一样。比如某些 IDE 默认输出 HEX,地址从链接脚本的 ROM 起始地址开始;而有些量产工具只认 BIN,需要你在命令行里显式指定基地址。我一般会先确认三件事:固件的链接起始地址、是否有多个不连续的段、目标烧录工具支持哪种格式。这三件事没确认就动手转,后面大概率要返工。
提示:拿到一个 BIN 却不知道基地址时,不要猜。先看链接脚本或 map 文件里的 ROM 区域定义,实在找不到就问固件提供方要 HEX 版本,从 HEX 里反推地址。
3. 用脚本完成 BIN 与 HEX 互转:从命令行到参数细节
3.1 环境准备与工具选型
常见做法是用 Python 的intelhex库,它同时支持读写 Intel HEX 和 BIN,跨平台,脚本化程度高。安装很简单:
pip install intelhex选它而不是直接用objcopy的原因是:objcopy对地址空洞和段合并的处理不够直观,而intelhex提供了merge、tobinfile、fromfile这些语义清晰的接口,排查问题时能逐段打印地址范围。如果你习惯用objcopy,也可以,但下面这些脚本逻辑能帮你理解底层发生了什么。
3.2 BIN 转 HEX:补上基地址
from intelhex import IntelHex def bin_to_hex(bin_path, hex_path, base_addr): """ bin_path: 输入 BIN 文件路径 hex_path: 输出 HEX 文件路径 base_addr: BIN 数据在 Flash 中的起始地址,如 0x08000000 """ ih = IntelHex() # 从 BIN 文件加载数据,指定起始地址 ih.loadbin(bin_path, offset=base_addr) # 写入 HEX 文件 ih.write_hex_file(hex_path) # 打印地址范围,方便核对 print(f"address range: 0x{ih.minaddr():08X} - 0x{ih.maxaddr():08X}") if __name__ == "__main__": bin_to_hex("app.bin", "app.hex", 0x08000000)逻辑说明:loadbin把 BIN 的字节流按顺序放到从offset开始的地址空间里,write_hex_file再按 Intel HEX 格式逐行输出。参数base_addr必须和链接脚本里的 ROM 起始地址一致,否则烧录后跳转地址全错。运行后打印的地址范围要和你预期的一致,比如 0x08000000 到 0x0800FFFF 表示 64KB 数据。
3.3 HEX 转 BIN:处理地址空洞
from intelhex import IntelHex def hex_to_bin(hex_path, bin_path, start_addr=None, end_addr=None): """ hex_path: 输入 HEX 文件 bin_path: 输出 BIN 文件 start_addr/end_addr: 可选,指定导出范围;不指定则用文件实际范围 """ ih = IntelHex(hex_path) if start_addr is not None and end_addr is not None: # 只导出指定地址段 ih = ih[ start_addr : end_addr ] # 地址空洞默认填 0xFF,符合 Flash 擦除后的状态 ih.tobinfile(bin_path, pad=0xFF) print(f"exported: 0x{ih.minaddr():08X} - 0x{ih.maxaddr():08X}") if __name__ == "__main__": hex_to_bin("full.hex", "full.bin")逻辑说明:HEX 里可能存在不连续的段,直接转 BIN 时中间空洞会被填充。pad=0xFF是常见做法,因为 Flash 擦除后就是 0xFF,填 0x00 反而可能导致校验失败。如果你只想要其中一段,用切片语法指定地址范围,避免把 Bootloader 区域也导出来。
3.4 合并两个 BIN:先确认地址连续性
from intelhex import IntelHex def merge_bins(bin1, base1, bin2, base2, out_hex): """ 把两个 BIN 按各自基地址合并成一个 HEX """ ih = IntelHex() ih.loadbin(bin1, offset=base1) ih.loadbin(bin2, offset=base2) # 检查是否有地址重叠 if ih.minaddr() < base1 or ih.maxaddr() > base2 + 0x100000: print("warning: check address overlap manually") ih.write_hex_file(out_hex) print(f"merged range: 0x{ih.minaddr():08X} - 0x{ih.maxaddr():08X}") if __name__ == "__main__": # 假设 bootloader 在 0x08000000,app 在 0x08020000 merge_bins("boot.bin", 0x08000000, "app.bin", 0x08020000, "merged.hex")逻辑说明:合并的关键是两次loadbin的offset不能重叠。如果 app 的起始地址落在 bootloader 范围内,后加载的数据会覆盖先加载的,而且不会有任何报错。所以我在脚本里加了一个粗略的范围检查,实际使用时建议打印两个 BIN 的地址区间做人工核对。合并结果输出为 HEX 而不是 BIN,是因为 HEX 保留了地址信息,后续烧录工具能正确识别两个段。
4. 合并 HEX 与地址对齐:避开重叠和空洞的坑
4.1 两个 HEX 合并的正确姿势
from intelhex import IntelHex def merge_hex(hex1, hex2, out_hex): ih1 = IntelHex(hex1) ih2 = IntelHex(hex2) # 检查地址重叠 overlap_start = max(ih1.minaddr(), ih2.minaddr()) overlap_end = min(ih1.maxaddr(), ih2.maxaddr()) if overlap_start <= overlap_end: raise ValueError(f"address overlap: 0x{overlap_start:08X} - 0x{overlap_end:08X}") # 合并 ih1.merge(ih2) ih1.write_hex_file(out_hex) print(f"merged: 0x{ih1.minaddr():08X} - 0x{ih1.maxaddr():08X}") if __name__ == "__main__": merge_hex("boot.hex", "app.hex", "merged.hex")逻辑说明:merge方法会把第二个 HEX 的数据并入第一个,遇到同一地址时后者覆盖前者。所以合并前必须做重叠检查,否则你可能在不知情的情况下丢了一段代码。重叠检查的逻辑是取两个文件地址范围的交集,如果交集非空就报错。这个检查在产线脚本里应该作为强制步骤。
4.2 地址对齐与填充策略
有些烧录器要求固件按扇区对齐,比如 2KB 或 4KB。如果 BIN 长度不是扇区大小的整数倍,直接烧录可能失败。常见做法是在合并后做一次对齐填充:
from intelhex import IntelHex def align_hex(hex_path, out_hex, align=0x800): ih = IntelHex(hex_path) max_addr = ih.maxaddr() aligned_end = ((max_addr // align) + 1) * align # 在末尾填充 0xFF 到对齐边界 for addr in range(max_addr + 1, aligned_end): ih[addr] = 0xFF ih.write_hex_file(out_hex) print(f"aligned to 0x{aligned_end:08X}")参数align根据你的 Flash 扇区大小设置,常见是 0x800(2KB)或 0x1000(4KB)。填充值用 0xFF 而不是 0x00,原因和前面一样。这个步骤在 OTA 差分升级时尤其重要,因为差分算法通常按固定块大小比对。
4.3 校验合并结果:别只看文件大小
合并完成后,我一般会做两件事:一是用ih.minaddr()和ih.maxaddr()确认地址范围覆盖了预期区域;二是把合并后的 HEX 转回 BIN,用md5sum和原始 BIN 比对(如果地址连续的话)。如果地址不连续,就逐段导出再比对。这一步能抓出大部分地址错位和覆盖问题。
注意:不要用文件大小判断合并是否成功。HEX 是文本格式,同样数据量下大小和 BIN 差好几倍,而且地址记录本身也占空间。
5. 避坑与排查:那些让你加班到凌晨的细节
5.1 现象:烧录后设备不启动,但校验通过
原因:BIN 转 HEX 时基地址填错,比如把 0x08000000 写成了 0x00000000。烧录工具按 HEX 里的地址写入,但芯片复位后从 0x08000000 取指令,取到的是空区域。
解决:转完后用ih.minaddr()打印起始地址,和链接脚本里的 ROM 起始地址逐位核对。不要凭记忆填。
5.2 现象:合并后 app 区域数据被截断
原因:两个 BIN 地址范围有重叠,后加载的覆盖了先加载的,但脚本没有报错。
解决:在合并脚本里强制做重叠检查,如 4.1 节的代码。如果确实需要覆盖,明确写出覆盖逻辑并记录日志。
5.3 现象:HEX 转 BIN 后文件比预期大很多
原因:HEX 里包含了不连续的段,转 BIN 时中间空洞被填充,导致文件膨胀。
解决:用切片指定导出范围,只导出需要的段。或者先打印 HEX 的所有段范围,确认哪些是有效数据。
5.4 现象:OTA 差分升级失败,提示块校验错误
原因:合并或对齐时填充值不一致,有的地方填 0x00,有的地方填 0xFF,差分算法按块比对时认为数据变了。
解决:统一填充值为 0xFF,并在对齐脚本里显式指定。差分前先用同一套脚本处理基准固件和升级固件。
5.5 现象:产线烧录工具报「地址越界」
原因:合并后的 HEX 地址范围超出了芯片实际 Flash 大小,比如把 0x08100000 之后的数据也写进去了。
解决:合并前确认芯片型号和 Flash 容量,在脚本里加一个最大地址检查,超过就拒绝输出。
6. 进阶:把互转合并做成可复用的产线脚本
前面都是单次操作的脚本,实际产线上更常见的是把整个流程串起来:从构建输出目录取 BIN,按配置合并,对齐,转 HEX,校验,最后输出到烧录目录。我一般会写一个pack.py,用配置文件描述每个段的名称、路径、基地址和对齐要求。
import json from intelhex import IntelHex def pack(config_path, out_hex): with open(config_path) as f: cfg = json.load(f) ih = IntelHex() for seg in cfg["segments"]: ih.loadbin(seg["path"], offset=seg["base"]) print(f"loaded {seg['name']}: 0x{seg['base']:08X}") # 对齐 align = cfg.get("align", 0x800) max_addr = ih.maxaddr() aligned_end = ((max_addr // align) + 1) * align for addr in range(max_addr + 1, aligned_end): ih[addr] = 0xFF ih.write_hex_file(out_hex) print(f"output: {out_hex}, range 0x{ih.minaddr():08X} - 0x{ih.maxaddr():08X}") if __name__ == "__main__": pack("pack_config.json", "production.hex")配置文件示例:
{ "align": 2048, "segments": [ {"name": "boot", "path": "boot.bin", "base": "0x08000000"}, {"name": "app", "path": "app.bin", "base": "0x08020000"}, {"name": "param", "path": "param.bin", "base": "0x0807F800"} ] }这样每次固件更新只需要改配置里的路径,脚本逻辑不用动。验证方法也简单:跑完后用ih.minaddr()和ih.maxaddr()确认范围,再把输出的 HEX 用烧录工具的命令行模式做一次 dry run,看是否有地址越界或重叠告警。
从那以后我每次做固件合并,都强制走一遍「打印地址范围 → 重叠检查 → 对齐填充 → 反向校验」这四步,再急也不跳过。希望帮到你。
本文还有配套的精品资源,点击获取