简介这是一款面向嵌入式开发者的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, fill0xFF): 解析 Intel HEX 文件输出纯二进制 BIN。 fill: 地址空洞的填充字节Flash 常用 0xFF。 segments {} # 记录每个地址对应的字节 upper 0 # 当前扩展线性地址高 16 位 with open(hex_path, r, encodingascii) 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:4length] checksum raw[4length] # 校验和验证所有字节求和后低 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_addr0x08000000, line_len16): 将 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:offsetline_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, encodingascii) 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 到 0x08007FFFApp 从 0x08008000 开始合并时直接拼接没问题但如果 App 编译时链接地址设成了 0x08007000就会和 Bootloader 尾部重叠拼接后前面的数据被覆盖烧进去直接死机。所以合并脚本必须做重叠检测。def merge_bins(segments, out_path, fill0xFF): segments: [(base_addr, bytes), ...] 按地址排序后合并检测重叠。 segments sorted(segments, keylambda 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_baselen(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, align4): 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_crcNone): 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(fCRC 不匹配: 期望 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基地址 0x08000000BIN 却有 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\nLinux 下是 \n有些解析脚本按 \n 分割后行尾残留 \r导致 bytes.fromhex 失败。解决读取时用 strip() 去掉首尾空白并且逐行校验和。如果校验和失败打印行号和内容定位到具体哪一行坏了。不要跳过校验和检查否则坏数据会静默写进 BIN。4.5 现象合并后的 BIN 大小比预期大很多里面全是填充值原因两个段的地址间隔很大比如 Bootloader 在 0x08000000App 在 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(destcmd) 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, typelambda x: int(x, 0), default0x08000000) 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 字节是栈顶地址通常是 0x2000xxxxApp 向量表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 都强制走一遍格式检查确认换行符和校验和都没问题再交给产线。希望帮到你。本文还有配套的精品资源点击获取