简介面向单片机与嵌入式开发者的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.Fillbyte(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 来源五花八门。有的开发环境会在文件开头加 BOMStreamReader默认能识别 UTF-8 BOM但如果遇到带 BOM 的 ANSI 文件第一行首字符会变成不可见字符导致冒号判断失败有的工具生成的是 CRLF 换行有的只有 LFReadLine都能处理但如果你自己用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 数据错位在低 64KB04 记录被漏掉现象程序能烧进去但跑飞或者函数指针全部错乱跟踪下来发现数据被写到了 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(fgap {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 记录被漏掉这一条上——从那以后每次转换都强制过一遍这三步再也不信“转换成功”四个字。希望这个习惯能帮到你少踩一次坑。本文还有配套的精品资源点击获取