简介:面向单片机与嵌入式开发者的Hex2Bin转换工具,涵盖Hex2Bin、单片机应用开发等场景,包含完整C#项目源码,可在Visual Studio 2022中直接打开、阅读和二次开发。在实际烧录或与外部系统交互时,Hex文件虽含地址信息但通用性不如Bin格式,该工具用于将Hex转换为标准Bin文件,由C#编写,关键逻辑附有详细注释,便于理解转换流程,也可按需扩展校验码、加密等附加功能。在嵌入式开发中,当需要将程序固化到指定存储介质或适配不支持Hex的烧录环境时,这种转换尤为实用。资源共97个文件,压缩包约452KB,主要包含cs源码、sln/csproj工程文件、exe可执行程序、说明文档及配置与依赖项,结构完整,打开即可运行或修改。转换前会自动校验Hex文件有效性,避免数据丢失或损坏;工具轻量简洁,适合集成到现有开发流程中作为辅助脚本使用。目前已有437人学习下载,适合具备一定C#基础和Visual Studio使用经验的开发者在实际项目中灵活运用。
1. Hex 转 Bin 没那么简单:烧录软件要的并不一定是 IDE 给的那份文件
在单片机应用开发里,Hex2Bin 这一步几乎每一个人都会遇到:开发环境编译完默认输出 .hex,产测工具、烧录器配套命令行却只认 .bin。我第一次拿这类工具转固件时,转出来的 Bin 体积离谱——512KB 的程序变成了 2GB,烧进板子毫无反应。排查到最后才明白,HEX 是带地址的文本记录,BIN 是连续地址空间的二进制快照,两者之间隔着地址映射、空洞填充和记录类型解析三层问题。这篇就把这个带 C# 源码的小工具讲透:格式原理、操作步骤、翻车现场和改源码的方向,适合刚接手固件打包的工程师,也适合想给内部工具链加一个转换环节的开发者。
2. 先看懂 HEX 记录格式:地址、类型、校验和与 Bin 的映射关系
2.1 每行记录的六段结构:冒号、长度、地址、类型、数据、校验和
HEX 文件本质是 ASCII 文本,每一行是一条记录,用冒号开头。以一条最简单的记录为例:
:03000000010203F7拆开看是六个部分:
::起始标记,固定是 ASCII 冒号;03:本条记录的数据字节数,这里是 3 个字节;0000:本条数据在地址空间中的低 16 位地址,高字节在前;00:记录类型,0x00 表示这是一条数据记录;010203:真正的数据,三个字节,每个字节两个十六进制字符;F7:校验和。
校验和的计算规则是:把长度、地址、类型、数据所有字节加起来,取结果的低 8 位,用 0x100 减去它,得到校验字节。等价的做法是让所有字节(含校验和)相加后低 8 位等于 0x00。工具里通常这样判断:
static bool ValidateHexLine(string body) { int sum = 0; for (int i = 0; i < body.Length; i += 2) { sum += Convert.ToByte(body.Substring(i, 2), 16); } return (sum & 0xFF) == 0x00; }这里body是不含冒号的整行内容,每次取两个十六进制字符转成一个字节累加。最后只看低 8 位,因为校验和的本质是补码到 0x00。写转换工具时这段代码建议保留,不要图快跳过——后面避坑章节会讲跳过它的后果。
2.2 记录类型 00/01/04/02:地址超过 64KB 后靠谁
HEX 里最常见的是四种记录类型:
- 0x00 数据记录:携带实际固件内容,前面 2.1 拆的就是它;
- 0x01 文件结束记录:标准形态是
:00000001FF,表示文件到此结束; - 0x04 扩展线性地址记录:用两个字节给出后续数据记录地址的高 16 位;
- 0x02 扩展段地址记录:老式段地址模式,物理地址 = 段地址左移 4 位再加低 16 位,现在比较少用。
关键在 0x04。每行记录里的地址字段只有 16 位,一个数据记录最多覆盖 64KB 区间;超过 64KB 的固件必须靠 0x04 记录把高地址抬起来。比如:
:020000040800F2 :03000000010203F7第一行是 0x04 记录,数据是0800,表示接下来的数据记录地址都落在 0x08000000 之上;第二行的低 16 位地址是 0x0000,所以这三字节数据的完整地址是 0x08000000。常见 32 位单片机的 Flash 起始地址在 0x08000000 附近,几乎每个工程文件里都会出现 0x04 记录,解析逻辑漏掉它就是灾难。
0x03 和 0x05 记录用于描述程序的起始地址信息,转换到 Bin 时不产生数据,严格模式里应该识别并跳过,不能当成坏行报错。
2.3 地址空洞:Bin 的每个字节都有落脚点
Bin 文件是地址空间的连续快照,不存在“跳过某段地址”的说法。HEX 里如果一条数据记录落在 0x0000,下一条落在 0x1000,中间 0x0010 到 0x0FFF 这段就要填东西,否则后续数据在 Bin 里的位置就对不上。
填充值通常选 0xFF,因为绝大多数 Flash 的擦除态就是 0xFF。转换逻辑可以这样写:
long start = config.StartAddress ?? records[0].addr; long end = records[records.Count - 1].addr + records[records.Count - 1].data.Length - 1; long totalLen = end - start + 1; byte[] output = new byte[totalLen]; Array.Fill<byte>(output, config.FillByte); foreach (var (addr, data) in records) { Array.Copy(data, 0, output, addr - start, data.Length); }Array.Fill在 .NET Core 2.0 之后可用,旧工程里可以写成for循环。config.FillByte默认 0xFF,可以留成可配置项。这段代码顺便解决了起始地址问题:默认拿第一条数据记录的地址做起点,而不是从 0x00000000 开始铺一整块空间——否则像 0x08000000 起始的固件,前面会多出 128MB 的 0xFF。
2.4 为什么是这个工具:C# 选型与能力边界
这个工具包选 C# 做桌面小工具是合理的:WinForms 原生支持文件选择框、日志框、进度条,写出来双击就能跑;命令行入口也容易加,方便塞进打包脚本;.NET 的File.ReadLines是惰性读取,处理几 MB 的 HEX 不至于把内存吃爆。对做单片机应用开发的人来说,C# 语法门槛不高,拿到源码改起来比 C++ 或 Python 打包工具顺手。
边界要认清:它做的是文件格式转换,不负责烧录协议。真正下芯片还是靠烧录器厂商的软件,工具只负责把 IDE 输出的 HEX 变成烧录软件能直接吃的 Bin。把这一点理解清楚,后续加功能就不会跑偏。
3. 上手跑通两条路径:图形界面、命令行加源码对照
3.1 界面操作五步:选文件、看日志、定基址、转换、验证
工具编译出来是个 WinForms 程序,操作流程常规做法是这五步:
- 点“选择 HEX 文件”,指定输入;
- 日志区会自动显示解析结果,包括扫描到的 0x04 记录数、最小数据记录地址;
- 确认“起始地址”一栏的值,工具默认取最小数据记录地址,不需要手动改;
- 指定输出 Bin 路径,点“转换”;
- 转换结束后看日志区输出的文件长度、记录总数、校验结果。
第二步里的最小数据记录地址很关键。如果日志显示0x08000000,说明固件是常见 32 位 MCU 的镜像,起始地址没问题。如果显示0x00000000,再确认一下 HEX 里是不是真的没有 0x04 记录——有 0x04 却显示 0x00000000,说明解析逻辑没处理扩展地址,第三章会讲这个问题。
第 5 步不要只看“转换成功”四个字。日志里必须有输出长度,拿它和 2.3 的公式估算对比,一致再拿去烧录。
3.2 命令行参数:单文件与批量脚本
侧面命令行入口是批量处理的关键。常用参数表如下:
| 参数 | 说明 | 默认值 |
|---|---|---|
-i | 输入 HEX 文件路径 | 必填 |
-o | 输出 Bin 文件路径 | 与输入同名同目录 |
-a | 输出起始地址,十六进制 | 首条 0x00 记录地址 |
-f | 空洞填充字节 | FF |
-crc | 转换结束后计算整体 CRC32 并输出 | 关闭 |
单文件转换命令:
Hex2Bin.exe -i app.hex -o app.bin -crc -a 0x08000000批量转换在 Windows 下用 PowerShell,在 CI 环境里用 bash 循环都常见:
for f in build/*.hex; do Hex2Bin.exe -i "$f" -o "bin/${f%.hex}.bin" -crc || echo "$f failed" >> convert.log done注意||后面把失败信息追加进日志,这样批量跑完能快速定位是哪几个文件出问题。命令行参数应该优先于界面配置,因为无人值守的 CI 机器上弹出界面窗口会直接卡住任务。日志写入文件、错误时返回非零退出码,这是集成进打包流水线的基本要求。
3.3 核心转换代码拆解:从文本行到 byte 数组
不管是界面还是命令行,核心转换逻辑都是同一段。常见的实现是逐行读取,先处理 0x04 记录更新高位地址,再收集 0x00 记录到列表,最后统一排序、填充、写入:
using var reader = new StreamReader(hexPath); long currentHigh = 0; long minAddr = long.MaxValue; long maxAddr = 0; var records = new List<(long addr, byte[] data)>(); while ((line = await reader.ReadLineAsync()) != null) { line = line.Trim(); if (!line.StartsWith(":")) continue; int len = Convert.ToInt32(line.Substring(1, 2), 16); int type = Convert.ToInt32(line.Substring(7, 2), 16); if (type == 0x04) { currentHigh = Convert.ToInt64(line.Substring(9, 4), 16); continue; } if (type == 0x00) { int addr16 = Convert.ToInt32(line.Substring(3, 4), 16); long fullAddr = (currentHigh << 16) | addr16; byte[] data = new byte[len]; for (int i = 0; i < len; i++) { data[i] = Convert.ToByte(line.Substring(9 + i * 2, 2), 16); } records.Add((fullAddr, data)); minAddr = Math.Min(minAddr, fullAddr); maxAddr = Math.Max(maxAddr, fullAddr + len - 1); } } records.Sort((a, b) => a.addr.CompareTo(b.addr));这段的逻辑顺序是:先更新currentHigh,再解析数据记录,保证每条 0x00 记录拿到完整地址。地址用long而不是int,因为 0x04 左移 16 位后数值会超过 32 位有符号整数上限;用List而不是先扫两遍统计大小,是因为单遍读取更简单,内存开销对固件文件来说完全可以接受。StreamReader.ReadLineAsync逐行处理,避免ReadAllText一次性加载大文件。
3.4 文件输入边界:BOM、换行符与 EOF 之后的空白
实际拿到的 HEX 来源五花八门。有的开发环境会在文件开头加 BOM,StreamReader默认能识别 UTF-8 BOM,但如果遇到带 BOM 的 ANSI 文件,第一行首字符会变成不可见字符导致冒号判断失败;有的工具生成的是 CRLF 换行,有的只有 LF,ReadLine都能处理,但如果你自己用Split('\n')就要小心\r残留。
EOF 之后的处理也值得统一:标准规定读完:00000001FF就结束,但有些文件后面还跟着空行甚至注释。常规做法是遇到 0x01 记录后记录一个eofSeen标记,继续把文件读完,空行忽略,非空且非冒号开头的行在严格模式下报错。这样既不漏数据,也不会被多余内容骗过。
4. 转换翻车的五个现场:现象、原因与根治方案
4.1 开头大片 0xFF:起始地址没落对
现象:转出来的 Bin 文件开头几千字节全是 0xFF,真正的程序数据挤在后面;或者反过来,文件长得离谱,512KB 固件转出 2GB。
原因:起始地址计算错了。如果工具把 0x00000000 当作输出起点,而 HEX 里首条数据记录地址是 0x08000000,前面就会铺满 0xFF;如果任意取了一条中间记录做起点,真实数据前面的部分会被截掉。
解决:默认取首条 0x00 记录的地址做起点,转换前在日志里打印这个值,人和脚本都能核对。也可以在命令行指定-a强制覆盖。从那以后我拿到别人转出的 Bin,第一件事就是看开头的 4 个字节是不是向量表,而不是直接看文件大小。
4.2 数据错位在低 64KB:04 记录被漏掉
现象:程序能烧进去但跑飞,或者函数指针全部错乱,跟踪下来发现数据被写到了 0x0000xxxx 而不是 0x0800xxxx。
原因:解析逻辑只处理了 0x00 和 0x01 两种记录类型,遇到 0x04 直接跳过。currentHigh一直是 0,所有地址都落在了低 64KB。32 位 MCU 的固件几乎必然带 0x04 记录,这个坑踩上就是全线错位。
解决:按 3.3 的代码在读取循环里加上 0x04 处理。还有一个防御性检查:转换完统计maxAddr,如果文件里出现过 0x04 记录但最终地址最大值小于 0x10000,多半是高地址记录被错误丢弃,直接报错而不是输出结果。
4.3 坏行被静默跳过:校验和没做严格检查
现象:日志显示“转换成功”,但输出的 Bin 比预期少了几十个字节,烧录后功能异常。
原因:某些工具为了“宽容”,遇到校验和错误或者长度字段对不上的行,用continue跳过继续处理。问题在于坏行里往往夹着真实数据,静默跳过等于丢数据。
解决:严格模式默认开启,校验失败直接抛异常并给出文件行号:
if (!ValidateHexLine(line.Substring(1))) { throw new FormatException($"Line {lineNo}: checksum error"); }如果确实要调试断行文件,可以提供--no-strict开关,但它只应该用于分析问题,不能进正式转换流程。
4.4 大文件卡死:整文件读入内存
现象:几 MB 的 HEX 打开或者转换时界面卡住,任务管理器里内存占用冲到几百 MB。
原因:实现用了File.ReadAllLines把整个文件读进内存,又在转换开始时new byte[巨大长度],小文件看不出问题,文件一大就露馅。
解决:读取改用StreamReader.ReadLineAsync,数据追加进List,写入用FileStream分批写。界面进度可以每处理 5000 行刷新一次,别每行都刷新 UI,否则界面照样卡。这个改动对源码来说只有十几行,收益却是质变。
4.5 烧录后 CRC 对不上:缺少文件级校验
现象:转换成功,烧录成功,芯片能跑,但产测环节拿整个 Bin 做校验时对不上。
原因:HEX 的校验和只针对每一行记录,Bin 文件本身没有任何完整性摘要。烧录器回读验证的是整个 Flash 内容,两边的比较基准不一样。
解决:工具在转换完成后计算 Bin 的整体 CRC32,输出到日志,产测烧录后回读 Bin 再算一遍,两边比对。这个功能单独拿出来放在第 5 章,因为它同时也是二次开发的第一个落点。
5. 二次开发扩展:CRC32、批量转换与地址偏移语义
5.1 加一个 CRC32 输出:给 Bin 一张身份证
标准 CRC-32 多项式是 0xEDB88320,初始值 0xFFFFFFFF,结果再取反。C# 实现不依赖系统库也能几行写完:
public static uint Crc32(byte[] data) { uint crc = 0xFFFFFFFF; foreach (byte b in data) { crc ^= b; for (int i = 0; i < 8; i++) { crc = (crc >> 1) ^ (uint)((crc & 1) == 1 ? 0xEDB88320 : 0); } } return ~crc; }和现有转换流程接起来,就是在写入 Bin 文件后对output数组调用一次,把结果以0x前缀十六进制打印到日志。要注意的是不同工具对 CRC32 的初始值、结果异或处理可能不一样,和烧录器软件比对时先确认两边算法一致,否则差异不在转换环节而在算法参数上。
5.2 批量转换:错误码、跳过已存在与日志
给命令行加两个实用开关,一个是已经存在的输出文件默认不覆盖,防止脚本重跑时把好文件冲掉;另一个是把转换结果写到独立日志,而不是只打印到控制台:
Hex2Bin.exe -i build/a.hex -o bin/a.bin -crc -skip-existing -log convert.log批量脚本里依赖非零退出码判断失败文件。常规做法是转换成功返回 0,任何异常返回 1,这样for循环里的||才能正确捕获。日志格式建议固定成文件名 结果 输出大小 CRC32,一行一个结果,后面用脚本做汇总也不用再解析自由文本。
5.3 地址偏移的两种语义:线性平移与区间截取
改源码时最容易混淆的是-a参数的语义。它有两种常见解释:
| 语义 | 行为 | 适用场景 |
|---|---|---|
| 线性平移 | 把 0x08000000 映射到 0x00000000,数据顺序不变 | 把带地址的镜像变成裸数据,给只认偏移 0 的烧录工具 |
| 区间截取 | 只导出从该地址开始的一段数据,前面不要 | 从整个 Flash 镜像里抠出 BootLoader 或 App 分区 |
实现上差别很大。线性平移是output[addr - start],截取区间是output[addr - cutStart]并且丢弃cutStart之前的内容。工具里默认做线性平移,需要截取时用-len限制输出长度,超出报错:
Hex2Bin.exe -i full.hex -o boot.bin -a 0x08000000 -len 0x4000这个例子表达的是:从 0x08000000 开始,最多导出 16KB,多出来的数据要么属于别的分区,要么说明起始地址给错了。
5.4 自测最小用例:不带 04 与带 04 的对照
改完源码要先跑最小用例,不要直接拿整个工程验证。构造两个文件:
不带 04 记录: :03000000010203F7 :00000001FF 带 04 记录: :020000040800F2 :03000000010203F7 :00000001FF第一个文件转换出的 Bin 应该是三个字节01 02 03;第二个文件相同,只是数据落在 0x08000000。两个用例跑通,说明基础解析和 0x04 处理都对。再往文件里加一条 0x02 段地址记录,验证工具能正确跳过不报错,覆盖就算完整了。
6. 转换后的三个自检技巧:预期大小、空洞分布与回读对比
拿到转换结果先别急着烧,三个技巧能在 30 秒内拦住八成错误。
第一个是算预期大小。从 HEX 里找所有 0x00 记录,最大地址加本行数据长度减最小地址加一,就是 Bin 应有的长度。工具日志里的输出大小和这个数对不上,要么是空洞填充错了,要么是起始地址有问题。
第二个是看空洞分布。写个小脚本把 0x00 记录之间的间隔打出来:
import sys prev_end = None for line in open(sys.argv[1]): line = line.strip() if not line.startswith(':'): continue n = int(line[1:3], 16) addr = int(line[3:7], 16) typ = int(line[7:9], 16) if typ != 0: continue if prev_end is not None and addr > prev_end: print(f"gap {prev_end:x} -> {addr:x} size {addr - prev_end}") prev_end = addr + n print("end:", hex(prev_end))有 gap 不一定是坏事,关键看 gap 出现在哪里。中断向量区后面跟着的连续代码段出现大空洞,就要回到工程里查链接脚本;只是不同分区之间的正常间隔,填 0xFF 反而符合预期。
第三个是回读对比。烧录器自带的回读功能把 Flash 内容读出来存成 Bin,再用工具的-crc算一遍。能跑、能数地址、能对 CRC,才敢说这份固件是可信的。我第一次用转出来的 Bin 做产测,就是栽在 0x04 记录被漏掉这一条上——从那以后每次转换都强制过一遍这三步,再也不信“转换成功”四个字。希望这个习惯能帮到你少踩一次坑。
本文还有配套的精品资源,点击获取