简介:这是一款面向嵌入式开发者的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,客户验收文档却要 HEX;Bootloader 升级包按扇区偏移拼好了 BIN,但仿真器加载又只吃 HEX;更麻烦的是多片 Flash 分区固件要合成一个整包,地址空洞还得用 0xFF 补齐。BIN 文件和 HEX 文件可以互转合并,说的就是把这几种格式之间的转换、拼接、地址对齐一次做干净,而不是靠某个 IDE 的导出按钮碰运气。它解决的核心问题是:让同一份固件在不同工具链、不同烧录器、不同验收流程之间无损流转。适合谁?写 MCU 固件、做产线工装、维护 OTA 升级包的工程师,尤其是手里同时握着 BIN 和 HEX 两套文件、却说不清它们地址边界的人。
2. 先把 BIN 和 HEX 的地址语义掰开:为什么不能直接拼
2.1 BIN 是裸数据,HEX 是带地址的记录
BIN 文件本质上就是一段连续的二进制字节流,它自己不知道这段数据应该烧到 Flash 的哪个地址。你给它 64KB 数据,它就从第一个字节排到最后一个字节,中间没有地址信息,也没有校验。烧录器拿到 BIN 必须额外告诉它起始地址,否则只能默认从 0x08000000 这类约定地址开始写。
HEX 文件(常见的是 Intel HEX 格式)则完全不同,它是文本格式,每一行是一条记录,记录里包含数据长度、目标地址、记录类型、数据和校验和。也就是说 HEX 自带地址信息,还能表达不连续的内存区域。这就是为什么多分区固件用 HEX 表达更自然,而 BIN 更适合直接喂给烧录器。
理解这个差异之后,互转的逻辑就清楚了:BIN 转 HEX 必须补一个起始地址;HEX 转 BIN 必须处理地址空洞,否则空洞会被压缩掉,导致偏移错乱。
2.2 合并的本质是地址空间上的区间运算
很多人以为合并就是把两个文件的内容首尾相接,这是最常见的误解。真正的合并是在地址空间上做区间运算:每个输入文件对应一个或多个地址区间,合并时要按地址排序、检测重叠、填充空洞。如果两个区间重叠,你必须决定是报错、覆盖还是保留其中一个,这个策略直接决定合并结果对不对。
举个典型场景:Bootloader 占 0x08000000 到 0x08007FFF,Application 占 0x08008000 到 0x0803FFFF。这两个区间不重叠,合并后应该是一个从 0x08000000 开始、总长 0x40000 的连续镜像。但如果你拿到的 Application BIN 是从 0x08008000 开始导出的裸数据,直接和 Bootloader BIN 拼接,得到的文件起始地址就错了,烧进去必然跑飞。
2.3 工具选型:objcopy、srec_cat 还是自己写脚本
常见做法有三条路。第一条是 GNU objcopy,它能把 ELF 转成 BIN 或 HEX,也能指定 --change-addresses 做地址偏移,但它不擅长多文件合并和空洞填充。第二条是 srec_cat,这是专门处理 HEX、SREC、BIN 的工具,支持多输入、地址偏移、填充、裁剪,功能最全,缺点是参数多、文档偏老。第三条是自己写 Python 脚本,用 intelhex 库读写 HEX,用 bytes 处理 BIN,灵活度最高,适合把转换逻辑固化进 CI。
我一般会这样选:单文件格式转换用 objcopy,多文件合并加填充用 srec_cat,需要集成到构建流水线、做自动化校验的,用 Python 脚本。下面分别给出可复现的命令和代码。
3. 用 objcopy 和 srec_cat 跑通单文件互转
3.1 BIN 转 HEX:必须显式指定架构和起始地址
objcopy 从 BIN 转 HEX 时,因为 BIN 没有地址信息,必须用 -I binary 告诉它输入是裸二进制,再用 --change-addresses 指定加载地址。命令如下:
# 把 app.bin 转成 app.hex,加载地址设为 0x08008000 arm-none-eabi-objcopy \ -I binary \ -O ihex \ --change-addresses 0x08008000 \ app.bin app.hex这里 -I binary 是输入格式,-O ihex 是输出 Intel HEX,--change-addresses 把默认的 0 地址整体平移到 0x08008000。注意这个参数是加法偏移,不是设置绝对地址,如果输入 BIN 内部已经隐含了某个基址,你要先算清楚差值。转换完成后用 objdump 或 hexdump 检查第一行记录的地址字段,确认是 08008000 而不是 00000000。
3.2 HEX 转 BIN:用 srec_cat 处理地址空洞
HEX 转 BIN 的坑在于地址不连续。objcopy 遇到空洞会直接压缩,导致 BIN 里数据错位。srec_cat 可以用 -fill 参数把空洞填成指定值,保证输出 BIN 的偏移正确:
# 把 app.hex 转成 app.bin,空洞填 0xFF,输出范围限定在 0x08008000 到 0x0803FFFF srec_cat app.hex -intel \ -fill 0xFF 0x08008000 0x0803FFFF \ -o app.bin -binary-fill 0xFF 后面跟的是填充范围和填充值,这一步很关键。如果不加 -fill,srec_cat 默认只输出有数据的区间,空洞会被跳过,BIN 长度就和预期不符。输出范围建议显式写出,避免 HEX 里混入配置区或选项字节导致 BIN 被撑大。
3.3 合并两个 HEX:按地址排序并检测重叠
合并 Bootloader 和 Application 两个 HEX,srec_cat 可以直接接受多个输入:
# 合并 boot.hex 和 app.hex,重叠区域直接报错 srec_cat boot.hex -intel app.hex -intel \ -o merged.hex -intelsrec_cat 默认行为是后输入的记录覆盖先输入的,如果两个文件地址重叠,它不会主动报错,而是静默覆盖。这在调试阶段很危险。稳妥做法是先用 -crop 或 -exclude 把各自范围裁开,确认无重叠后再合并。也可以加 -contradict 参数让它在检测到冲突时直接失败,把问题暴露在构建阶段而不是烧录阶段。
4. 用 Python 脚本把互转合并做成可复现流程
4.1 安装 intelhex 并读取 HEX 的地址区间
命令行工具适合手动操作,但要集成到 CI 或做批量处理,Python 更可控。先装库:
pip install intelhex读取一个 HEX 文件并打印它的地址区间:
from intelhex import IntelHex def load_hex(path): ih = IntelHex() ih.loadhex(path) # 获取所有有数据的地址段 segments = ih.segments() for start, end in segments: print(f"segment: 0x{start:08X} - 0x{end-1:08X}, size={end-start}") return ih ih = load_hex("app.hex")segments() 返回的是左闭右开区间列表,每个元素是 (start, end)。这一步能帮你快速判断 HEX 里到底有几段数据、有没有意外的地址跳变。如果 segments 数量超过预期,说明链接脚本或导出参数有问题,先别急着转 BIN。
4.2 HEX 转 BIN 时按地址范围裁剪并填充
下面这个函数把 HEX 转成指定地址范围的 BIN,空洞填 0xFF:
from intelhex import IntelHex def hex_to_bin(hex_path, bin_path, start_addr, end_addr, fill=0xFF): ih = IntelHex() ih.loadhex(hex_path) # 只保留目标范围内的数据 ih = ih[ start_addr : end_addr ] # 把空洞填成 fill 值 for addr in range(start_addr, end_addr): if addr not in ih: ih[addr] = fill ih.tobinfile(bin_path) print(f"written {end_addr - start_addr} bytes to {bin_path}") hex_to_bin("app.hex", "app.bin", 0x08008000, 0x08040000)ih[start:end] 做的是切片,超出范围的数据会被丢弃。后面的循环逐字节检查并填充,虽然效率不高,但对几十 KB 到几 MB 的固件完全够用。如果固件很大,可以改用 ih.padding 或先构造一个全 0xFF 的 IntelHex 再合并,速度会快很多。
4.3 合并多个 BIN 并生成带地址的 HEX
合并多个 BIN 时,每个 BIN 都要指定自己的加载地址,然后统一写成一个 HEX:
from intelhex import IntelHex def merge_bins_to_hex(output_hex, parts): """ parts: list of (bin_path, load_addr) """ merged = IntelHex() for bin_path, load_addr in parts: with open(bin_path, "rb") as f: data = f.read() for offset, byte in enumerate(data): addr = load_addr + offset if addr in merged and merged[addr] != byte: raise ValueError(f"conflict at 0x{addr:08X}") merged[addr] = byte merged.write_hex_file(output_hex) print(f"merged {len(parts)} parts into {output_hex}") merge_bins_to_hex("merged.hex", [ ("boot.bin", 0x08000000), ("app.bin", 0x08008000), ])这段代码在写入前做了冲突检测,如果同一地址出现不同字节就抛异常。这是合并流程里最值得保留的一道防线,比事后用工具比对可靠得多。参数 parts 是一个列表,每项包含 BIN 路径和加载地址,顺序不重要,因为最终按地址写入。
5. 避坑与排查:合并后烧录跑飞的五个常见原因
5.1 现象:烧录成功但程序不运行,读回 Flash 发现数据错位
原因通常是 HEX 转 BIN 时没有填充空洞,导致 BIN 长度比预期短,烧录器按连续地址写入后,后半段数据整体前移。解决方法是转换时显式指定输出范围并填充 0xFF,转换后用二进制比对工具确认 BIN 长度等于 end_addr - start_addr。
5.2 现象:合并后的 HEX 在仿真器里加载报地址冲突
原因是两个输入文件地址区间重叠,srec_cat 默认静默覆盖,没有报错。解决方法是合并前先用 segments() 或 srec_cat 的 -crop 把各自范围裁开,合并时加 -contradict 让冲突直接失败。
5.3 现象:BIN 转 HEX 后烧录地址不对,程序从 0x00000000 开始跑
原因是 objcopy 的 --change-addresses 是加法偏移,如果 BIN 本身是从 0 地址导出的,偏移量就是目标基址;但如果 BIN 是从某个中间地址导出的,偏移量要减去原基址。解决方法是转换前确认 BIN 的导出基址,转换后检查 HEX 第一行记录的地址字段。
5.4 现象:合并后的固件体积异常大,超出 Flash 容量
原因是 HEX 里混入了配置区、选项字节或调试段,这些区域的地址可能远离主程序区,导致合并后地址跨度被撑大。解决方法是在转换和合并时用 -crop 或切片限定地址范围,只保留实际要烧录的区间。
5.5 现象:产线烧录工装只认 BIN,但客户要求交付 HEX,两边校验和不一致
原因是 BIN 和 HEX 的校验方式不同,BIN 通常按字节累加或 CRC,HEX 自带每行校验和。解决方法是交付前用同一份源数据分别生成 BIN 和 HEX,再用脚本读回两者内容做逐字节比对,确认数据一致后再分别计算各自的校验值。
6. 把互转合并固化进构建脚本:一个可复用的校验习惯
真正让这套流程可靠的,不是某一条命令,而是把转换、合并、校验串成一条可重复执行的流水线。我一般会在构建目录下放一个 makefile 或 shell 脚本,每次出包自动做三件事:生成 BIN、生成 HEX、读回比对。
下面是一个最小化的校验脚本,用 Python 读回 BIN 和 HEX,逐字节比对:
from intelhex import IntelHex def verify_bin_hex(bin_path, hex_path, base_addr): with open(bin_path, "rb") as f: bin_data = f.read() ih = IntelHex() ih.loadhex(hex_path) for offset, byte in enumerate(bin_data): addr = base_addr + offset if ih[addr] != byte: raise ValueError( f"mismatch at 0x{addr:08X}: bin=0x{byte:02X} hex=0x{ih[addr]:02X}" ) print(f"verified {len(bin_data)} bytes OK") verify_bin_hex("app.bin", "app.hex", 0x08008000)这个脚本的价值在于:它不关心你是用 objcopy 还是 srec_cat 生成的,只认最终产物。只要比对通过,就说明两种格式承载的数据完全一致,交付给产线和客户都不会出岔子。参数 base_addr 必须和 BIN 导出时的基址一致,否则比对会从第一个字节就失败。
还有一个习惯值得坚持:每次合并多个分区后,把合并结果反解成各分区的 BIN,再和原始分区 BIN 逐一比对。这一步能抓出地址偏移算错、填充值写错、重叠覆盖这类问题。我吃过一次亏,Bootloader 和 Application 合并时地址偏移少加了 0x8000,烧进去后 Bootloader 跳转直接跳到 Application 中间,现象是程序随机跑飞,查了两天才定位到合并脚本。从那以后,合并后反解比对就成了固定动作。
如果要把这套流程交给团队,建议把转换参数、地址范围、填充值写进一个配置文件,脚本读配置执行,而不是把地址硬编码在命令里。这样换一颗芯片、改一次分区,只需要改配置,不用翻脚本。希望帮到你。
本文还有配套的精品资源,点击获取