简介:面向嵌入式开发者和电子工程师的HEX转BIN工具,基于Intel HEX与BIN格式原理实现,支持将常见HEX文件快速转换为可直接烧录的二进制文件,同时兼顾S-record等格式转换。资源包含完整C源代码、可执行程序及说明文档,既满足日常开发需求,也适合学习文件格式解析与转换算法。压缩包共30个文件,以C源码(.c/.h)、可执行程序(.exe)和文本说明(.txt/.pod/README)为主,另有工程文件与Makefile,便于二次编译与定制,整体仅353KB,小巧实用。工具支持拖放式操作,转换逻辑清晰,附带的mot2bin辅助程序可处理多种固件格式,对理解HEX地址字段、校验和机制以及BIN紧凑存储方式均有帮助。现已吸引3741人学习下载,是嵌入式与单片机开发者处理固件文件的高价值工具。 聊到嵌入式开发和单片机量产,有一个操作几乎每个人都会撞上:调试器、IDE默认输出的是HEX文件,但产线的烧录工装只认BIN,或者bootloader升级要的是纯BIN包,又或者需要用脚本把固件拼成一个镜像。我最早也被这个转换坑过一次:拿着Keil生成的hex直接扔给烧录器,结果报地址校验失败,后来才发现根本不是烧录器的问题,而是我没搞懂hex和bin到底是什么关系。这篇就把hex转bin这件事从头到尾拆开讲清楚,包括原理、工具、脚本和最容易踩的坑,适合正在跟hex/bin打交道的嵌入式开发、单片机爱好者和产线工艺工程师。
1. 为什么产线和bootloader都点名要BIN而不是HEX
1.1 你IDE里默认编译出来的东西,其实不是"固件本体"
很多人刚接触单片机时会有个误解:Keil或者STM32CubeIDE编译完生成的那个.hex,就是固件。严格说,hex只是固件的一种"带地址信封"表达,真正的机器码数据藏在hex的记录区里,周围全是地址、校验、类型这些附加信息。
IDE默认生成hex,是为了方便调试器烧录。ST-Link、J-Link这类调试器拿到hex后,会自己解析每一行,把数据放到对应的Flash地址上,这条路走得很顺,所以多数人从来没想过要把hex转成bin。
但一旦进入量产阶段,情况就不一样了。脱机编程器、产线工装、bootloader串口升级,很多时候只收"裸数据"BIN文件。它们不关心你这段代码原本应该放在0x08000000还是0x08002000,只负责把文件内容按固定偏移写进Flash。这时候你再丢一个hex过去,要么被拒收,要么解析出错。
1.2 HEX的"地址"是优势,也是转换时要处理的包袱
用快递来类比最直白:HEX文件像是带门牌号的快递面单,BIN文件就是里面的货本身。面单让快递员知道每件货送到哪里,但对仓库装箱来说,你要的是把货按顺序码好,门牌号反而成了多余信息。
HEX携带的地址信息在调试阶段非常有用,比如你可以把一个hex里同时包含App区、配置字区、OTA备份区的数据,烧录器按地址依次摆放。但这也意味着,任何转换工具都必须正确处理这些地址,不能简单地把所有行数据拼在一起。地址字段处理错了,转出来的bin就是废的。
1.3 三种常见场景,逼着你必须转BIN
- 场景A:量产脱机烧录。像PowerWriter这类编程器虽然也能解析hex,但很多产线工艺要求固件以bin形式归档,甚至要求把bootloader和app合并成一个bin文件一次烧完。
- 场景B:OTA升级。bootloader通过网络或串口接收固件包,它按照"文件偏移=Flash偏移"的方式写入,bin这种无头无尾的裸数据最合适,hex反而需要额外解析逻辑。
- 场景C:固件拼接和加密。把多个bin按地址拼接、在固定偏移处插入版本号或CRC校验值,这些操作在bin上做非常容易,在hex上做则极其痛苦。
我先给个对比表,后面所有工具选型都围绕这个差异展开:
| 对比项 | HEX文件 | BIN文件 |
|---|---|---|
| 内容形式 | ASCII文本,逐行带地址和校验 | 纯二进制数据,无任何附加信息 |
| 地址信息 | 每行自带绝对/扩展地址 | 无地址,烧录时另指定基地址 |
| 文件大小 | 通常比bin大约1倍 | 紧凑,等于实际固件区域大小 |
| 适用场景 | IDE调试、仿真器烧录 | 量产烧录、OTA升级、固件拼接 |
| 可读性 | 文本可打开查看 | 需用hexdump/编辑器查看 |
2. HEX文件格式拆解:转换前必须弄懂的字段与记录类型
2.1 单行记录到底在说什么
转换工具的核心工作,就是逐行读取hex,提取数据并拼接到连续缓冲区里。所以看不懂一行hex的组成,写出来的转换脚本十有八九会出错。
Intel HEX的每一行都以冒号开头,后面是十六进制ASCII字符,总体结构是:
: 长度(2位) 地址(4位) 类型(2位) 数据(2N位) 校验(2位)
我拿一个典型的Keil输出记录做例子:
:020000040800F2
拆开来看:
02:本行数据长度是2字节0000:地址偏移是0x000004:记录类型是扩展线性地址0800:数据是0x0800F2:校验和
这一行的意思是:后续所有数据记录的高16位地址是0x0800,也就是实际Flash基地址0x08000000。后面的每一条00类型记录,地址都会自动加上这个基地址。
校验和的计算方式是:把长度、地址、类型、数据所有字节求和,取低字节,再用0x100减去这个值。以这行为例:
0x02 + 0x00 + 0x00 + 0x04 + 0x08 + 0x00 = 0x0E
0x100 - 0x0E = 0xF2
正好等于结尾的F2,说明这行没被破坏。转换工具遇到校验和错误时,不应该静默跳过,而是要报警。
2.2 常见Record Type,以及哪些该忽略
HEX格式里记录类型有好几种,但现代ARM Cortex-M工程几乎只用其中几种:
| 类型值 | 名称 | 含义 | 转换时的处理 |
|---|---|---|---|
| 00 | 数据记录 | 实际固件数据 | 提取并拼接 |
| 01 | 文件结束记录 | 表示HEX结束 | 停止解析 |
| 02 | 扩展段地址 | 用于16位老架构 | 现代很少用,可忽略或按规则处理 |
| 03 | 起始段地址 | 指定入口地址 | 不需要处理 |
| 04 | 扩展线性地址 | 指定高16位基地址 | 必须处理,否则大容量芯片地址会错 |
| 05 | 起始线性地址 | 指定入口地址 | 不需要处理 |
最容易翻车的点就是04记录。有些在线转换工具实现得很简陋,从不去更新扩展线性地址,一旦固件里出现两个基地址,比如0x08000000和0x08010000,转出来的bin就只在低地址段有数据,高地址段全部丢失或错位。
2.3 地址不连续,是所有转bin工具面临的核心问题
BIN文件是一个线性数据流,它本身不携带"这段数据该放在哪里"的信息。所以把hex转成bin时,必须回答一个问题:如果hex里存在多个不连续的地址段,BIN应该怎么表达?
业界一般有两种做法。第一种是连续填充,把低地址到高地址之间的空洞用0xFF填满,烧录器按顺序写入,最终Flash内容和hex一致。第二种是只输出首个数据段,其余段丢弃或单独输出,这种适合一个hex里包含了非必须数据的场景。
我在实际项目里见过一个典型错误:某个芯片固件把OTA备份区也编进了hex,转换工具没做过滤,结果bin文件从几百KB膨胀到几MB,产线烧录时间长了十几倍。所以遇到转换出来的bin异常巨大,第一反应应该是打开hex看看有没有混入备用区、config区之类的非固件段。
3. 五种常用转换方案,从IDE内置到命令行一次讲透
3.1 Keil MDK自带fromelf:最适合日常开发
很多用Keil的人不知道,Keil其实自带格式转换能力,不需要额外装工具。在Options for Target -> User页签里,After Build/Rebuild栏加上一行命令,每次编译完自动生成bin:
"C:\Keil_v5\ARM\ARMCC\bin\fromelf.exe" --bin --output="$L@L.bin" "#L"这里有两个细节要注意。第一,不同工具链版本的fromelf路径不一样:ARMCC在ARM\ARMCC\bin下,ARMCLANG在ARM\ARMCLANG\bin下。第二,#L表示当前工程链接后的.axf文件,$L@L.bin表示生成一个与工程同名、同目录的bin文件。不要手写死绝对路径,否则工程换目录就要改。
fromelf本质上读的是.axf文件而不是hex文件,它能把ELF里的加载段(Load Region)导出为裸二进制,这个能力比很多小工具都强。
3.2 IAR与STM32CubeIDE的对应命令
- IAR使用ielftool,命令是:
ielftool --bin ".\Debug\Exe\project.out" ".\project.bin"- STM32CubeIDE基于GCC工具链,使用arm-none-eabi-objcopy:
arm-none-eabi-objcopy -O binary firmware.elf firmware.bin用GCC的objcopy时要注意,它默认导出的bin会包含所有可加载段,如果你的链接脚本里有非Flash段,可能需要用-j参数只选择特定段,否则bin可能包含RAM区域的初始值,文件地址和烧录地址对不上。
3.3 SRecord工具集:处理复杂转换的瑞士军刀
如果只是单个hex转bin,上面三种方式足够。但当你需要合并多个hex、在指定地址填充数据、裁剪地址范围时,SRecord工具集几乎是标配。
# 单个hex转bin srec_cat firmware.hex -Intel -o firmware.bin -Binary # 按照0xFF填充空洞,并对齐到4字节 srec_cat firmware.hex -Intel -fill 0xFF -within firmware.hex -range-padding 4 -o firmware.bin -Binary # 把两个hex按地址合并 srec_cat boot.hex -Intel app.hex -Intel -o merged.hex -Intelsrec_cat的参数虽然多,但只需要记住一个逻辑:输入部分写文件 -格式,处理部分写操作,输出部分写-o 文件 -格式。它最大的优势是不挑地址段,任何不连续地址都能准确表达。
3.4 在线小工具和GUI工具:应急够用,别当主力
网上能搜到很多"hex转bin"的在线网页和Windows小工具,适合偶尔转一次的场景。但我劝大家留意两点:一是很多在线工具对扩展线性地址支持不完整,转出来高地址数据丢失;二是固件本身是有价值的工程资产,随便上传到在线网站有泄露风险。
如果你只是临时看看bin文件内容,可以搜索"bin文件在线查看器"这类页面,或者本地用xxd、hexdump查看,不必依赖在线服务。
3.5 为什么不推荐直接在hex上做文本处理
有人图省事,用文本编辑器把hex文件里的冒号、地址、校验都删掉再另存为bin。这个操作极其危险,因为hex是ASCII编码,直接删除后得到的是文本字符的ASCII码,不是原始机器码,烧录进去必死。转换的本质是"按十六进制解析后再还原为二进制字节",这一步必须由程序完成,不能靠肉眼和文本替换。
4. 一个能用的Python转换脚本:从地址合成到边界填充
4.1 脚本的整体思路
如果你手里有几十个小固件要批量转换,或者需要在转换过程中加自定义逻辑,用Python写一个自己的转换工具最省心。
核心逻辑分四步:
- 逐行读取hex,解析出长度、地址、类型、数据。
- 遇到
04记录时更新一个base变量,保存扩展线性地址。 - 遇到
00记录时,把base + 本行地址作为绝对地址,连同数据一起存入列表。 - 结束后找出数据段的最小起始地址和最大结束地址,创建一个连续缓冲区,空洞填充0xFF,再把数据写入对应偏移。
4.2 完整可运行脚本
import sys def parse_hex_line(line): line = line.strip() if not line.startswith(':'): return None raw = bytes.fromhex(line[1:]) byte_count = raw[0] address = int.from_bytes(raw[1:3], 'big') record_type = raw[3] payload = raw[4:4 + byte_count] return byte_count, address, record_type, payload def hex_to_bin(hex_path, bin_path): base = 0 segments = [] with open(hex_path, 'r') as f: for line in f: parsed = parse_hex_line(line) if parsed is None: continue byte_count, address, record_type, payload = parsed if record_type == 0x04: # 扩展线性地址 base = int.from_bytes(payload, 'big') << 16 elif record_type == 0x00: # 数据记录 segments.append((base + address, payload)) elif record_type == 0x01: # 文件结束 break if not segments: raise RuntimeError('no data record found in hex file') segments.sort(key=lambda x: x[0]) start = segments[0][0] end = max(addr + len(data) for addr, data in segments) # Flash擦除态是0xFF,所以用0xFF填充地址空洞 image = bytearray([0xFF]) * (end - start) for addr, data in segments: offset = addr - start image[offset:offset + len(data)] = data with open(bin_path, 'wb') as f: f.write(image) print(f'start=0x{start:08X}, end=0x{end:08X}, size={len(image)} bytes') print(f'saved to {bin_path}') if __name__ == '__main__': if len(sys.argv) != 3: print('usage: python hex2bin.py input.hex output.bin') sys.exit(1) hex_to_bin(sys.argv[1], sys.argv[2])脚本里最关键的一行是bytearray([0xFF]) * (end - start),它创建了一个全0xFF的缓冲区。这样即使hex地址段之间有空洞,转换后的bin补位逻辑也清晰可控。
4.3 两个必须理解的边界问题
第一个问题:bin的起始地址。这个脚本以hex中第一个数据段的绝对地址为0偏移,也就是说,如果固件本来从0x08000000开始,转换后的bin第0字节就对应0x08000000。这是大多数烧录工具默认的处理方式,但也有人希望bin不管hex内容是什么,都强制从0x08000000开始补齐,这需要在脚本里增加一个forced_base参数,把start强制设为目标地址。
第二个问题:向bin尾部填充0xFF。很多bootloader要求固件长度是4字节或64字节对齐,转换后如果长度不对,需要在文件末尾补0xFF。我遇到过一个案例:OTA升级时bootloader按块读取固件并计算CRC,由于bin末尾不足4字节,尾部多余的CRC计算区域读到的是随机值,导致升级反复失败。后来在脚本里加了对齐逻辑,问题立刻消失。
4.4 用Keil生成的hex实测
我用一个STM32F103工程做了实测,Keil编译后生成hex,同时用上面脚本转换。脚本输出类似:
start=0x08000000, end=0x08002345, size=9029 bytes此时用fromelf生成的bin做对比,两者大小完全一致。再把bin文件丢进J-Flash,烧录地址填0x08000000,烧录后运行正常。这一套验证流程做完,才敢说转换万无一失。
5. 烧录、校验与排查:转换完成后真正决定成败的环节
5.1 烧录BIN时的基地址填写,是最大的隐形坑
HEX文件自带地址,烧录工具不需要你填基地址,解析完直接按地址分布烧写。而BIN文件没有地址,烧录时必须额外指定"文件第一个字节写到Flash哪个位置"。
J-Flash烧录bin文件时,会弹出一个对话框让你填start address。很多人图省事填0x08000000,但如果你的bin是用"最小地址为0偏移"方式转出来的,而hex最小地址是0x08003000,那这个bin烧进去后就整体偏移了0x3000字节,启动后大概率跑飞或者进HardFault。
我的建议是:每次转换完,先记下脚本打印的start地址,烧录时基地址填这个值。批量产线工具一般会按工程配置固定基地址,不会让你每次填,但你需要在首次配置时把地址设置对。
5.2 烧录后校验失败与"文件头hex"类报错
热搜词里经常出现"文件头hex"、unexpected hex digit这类情况。听起来像是转换工具的问题,但我在实际排查中发现,绝大多数是源文件本身坏了,而不是转换逻辑错了。
比如从邮件、网盘下载的hex文件被换行符替换,或者被PDF、聊天工具截断,烧录时解析不到合法的冒号起始记录,报错就是这么来的。处理方式是先用文本编辑器打开hex文件,看第一行和最后一行的格式是否完整,再用hexdump之类的工具对比文件大小,确认没被破坏后再转换。
BIN文件也一样,烧录前建议先算哈希:
sha256sum firmware.binWindows PowerShell下用:
Get-FileHash .\firmware.bin -Algorithm SHA256然后和编译机上的哈希比对。如果两边哈希不一致,说明文件在传输或拷贝过程中损坏,重新传输比反复排查烧录器更有意义。
5.3 PowerWriter这类编程器为什么要"hex转pkg"
有朋友在群里问,PowerWriter烧录器为什么要把hex转成pkg文件,是不是hex没法用?其实不是。量产烧录器出现"hex转pkg"操作,通常是为了生成一个打包了多个段、带校验和配置项的脱机烧录工程,方便产线一次烧录多个分区。
遇到这种需求,不必自己手动转换。正确做法是把hex文件直接导入烧录器软件,让它在内部完成解析和地址映射,最后生成它自己的工程文件。如果你先把hex转成bin再交给烧录器,反而会丢失多段地址信息,得不偿失。
5.4 转换后bin尺寸异常,先查引用段
我在第2章提过,转换工具会按地址连续填充。如果你发现bin文件莫名其妙多了几兆字节,而源码就几KB,那基本可以断定hex里包含了一个高地址段。
常见来源有三种:
- OTA备份区,很多升级方案会把当前固件备份在Flash后半段,编译时也编进了hex。
- 选项字节或用户数据段,有些芯片把配置信息放在单独扇区。
- 芯片厂商提供的出厂固件库,包含多个不连续加载段。
处理办法是先用srec_cat或者脚本打印出所有段的起始地址和长度,看一遍就知道哪些该留、哪些该丢。不要把垃圾段一起烧进APP区,产线烧一次慢一次。
最后分享一个我自己的习惯:如今每个工程里都会提前配好fromelf自动生成bin,再额外维护一个Python脚本做上线前检查。转换完成后第一件事不是急着烧录,而是看一眼起始地址、文件大小和哈希值,三个都对了才敢往板子上写。这套流程帮我挡掉了至少三次产线事故,也希望你能少踩几个我踩过的坑。
本文还有配套的精品资源,点击获取