
简介这是一款面向嵌入式与单片机开发者的Hex转Bin转换小工具完整包含C#源码、Visual Studio工程文件与可直接运行的程序。该工具以C#写成程序逻辑清晰便于阅读和二次修改。它针对Hex文件带地址信息、Bin文件更通用的特性解决特定烧录场景或系统交互中需要转换格式的问题。工具基于Visual Studio 2022开发源码内注释详尽支持在生成的Bin文件中加入校验码、加密信息等扩展功能保障数据传输与存储安全同时会对输入Hex文件做校验减少数据丢失风险。资源包共97个文件主要有C#源文件、可执行文件、动态库、配置文件和说明文档等压缩后仅452KB轻量且结构清晰。目前已有437人学习下载。这套代码不仅可作为日常转换工具更是一份适合C#初学者进阶、理解文件格式解析与二进制处理的实战范例读者可据此学习Hex记录解析、数据重组、CRC校验等思路并结合自身需求快速改造出定制化的固件工具适用于批量转换与固件定制开发等场景。1. Hex 转 Bin 这件事为什么每个嵌入式工程师桌面上都得放一个Keil 编译完默认吐出来的是 .hex 文件芯片原厂 SDK 例程里躺着的也大多是 .hex可真到量产烧录、OTA 升级包打包、OpenOCD 命令行下载这个环节几乎所有工具链都在问你要 .bin。Hex2Bin 这个转换工具带着完整的 C# 项目源码正好补上这个落差——它把文本格式的 Intel HEX 解析成纯二进制的内存镜像逻辑清晰、可改可扩展花十分钟读一遍源码就能把它塞进你自己的上位机工具链。适合刚接触单片机开发、被 hex 和 bin 格式绕晕的新手也适合手里维护着一堆烧录脚本、想找一个稳定转换内核的老工程师。2. Hex 和 Bin 的底细Intel HEX 记录结构、地址模型与转换三步走2.1 Hex 是带地图的文本Bin 是裸内存镜像Hex 文件每一行都是 ASCII 字符以冒号开头冒号后面跟一串十六进制字符里面包含数据长度、16 位地址、记录类型、数据负载和校验和。同样一份 1KB 的固件代码Hex 文件大约要占 2.2KB 到 3KB 的文本量Bin 文件就是 1024 字节整。这不是说 Hex 浪费而是它把地址、记录类型、校验和这类元数据都写进去了相当于带了一张地图就算某一行在传输中被截断解析程序也能立刻发现。Bin 文件则完全没有地址标签和校验和就是按地址排列的字节流。烧录器把 Bin 放到某个起始地址之后第一个字节就是起点地址的内容后续字节依次排列。所以 Bin 必须知道起点在哪。起点信息在转换过程中来自 Hex 的地址记录转换完就丢了得在工具输出里单独记一笔这也是很多人在烧录环节翻车的根源。2.2 记录类型00、01、04 是命脉02 也别放过Intel HEX 格式里每条记录的类型字段决定了这一行该怎么解释。平时最多见的就三种00 数据记录、01 文件结束记录、04 扩展线性地址记录。ARM 内核的芯片用的基本都是 04它能通过负载里的 16 位值左移 16 位把地址空间扩展到 32 位。你在 STM32 的 Hex 文件里看到:020000040800F2意思是后面的数据记录基址是 0x08000000。记录类型名称负载含义常见场合00数据记录16 位地址 若干字节数据每一行实际固件内容01文件结束无负载标准为:00000001FF文件最后一行02扩展段地址负载 2 字节左移 4 位作为段基址8051、AVR 等老架构03起始段地址指定 CS:IP 入口地址极少见04扩展线性地址负载 2 字节左移 16 位作为基址ARM、STM32、GD3205起始线性地址指定 32 位入口地址Keil 有时输出02 和 04 的差异很容易被忽略但处理错了整个转换结果都是错的。02 的负载左移 4 位04 的负载左移 16 位一个字节的差别地址差出去 4096 倍。写转换器的时候这两种类型都要走同一个基址维护逻辑只是移位量不同。05 记录是给调试器用的转换 Bin 时读到直接跳过就行不影响数据区。2.3 转换的本质寻址、填充、裁剪三件事把 Hex 转成 Bin干的事情就三步。第一步逐行解析维护当前扩展基址算出每条数据记录真正的绝对地址。第二步把数据字节放到以这个绝对地址为索引的缓冲区里。第三步确定输出范围——是把外包络内所有字节全写出去还是只输出实际有数据的区间这一步直接决定 Bin 文件的大小和可用性。真正复杂的其实是第三步的边界问题。Keil 生成的 Hex 经常包含多个不连续的数据段Bootloader 在 0x08000000App 在 0x08008000中间隔着一大段空白。如果按最小地址到最大地址的外包络输出这段空白会用填充字节塞满Bin 文件直接被撑大。裁剪策略、填充值选 0xFF 还是 0x00、按段拆分还是按区间截取这些都是转换器设计时必须回答的问题第 3 章的代码和第 5 章的避坑记录会逐一展开。3. C# 实现 Hex2Bin逐行解析、内存映射与紧凑裁剪3.1 单行解析器校验和验证、地址拆分、记录类型识别转换器的地基是一个能正确处理单行 Hex 记录的解析函数。下面这段 C# 方法把一行 ASCII 文本解析成记录类型、地址和负载字节同时完成 Intel HEX 的校验和验证。// 解析单行 Intel HEX 记录校验通过返回 true private static bool TryParseHexLine(string line, out byte recordType, out int address, out byte[] data) { recordType 0; address 0; data new byte[0]; line line.Trim(); if (line.Length 11 || !line.StartsWith(:)) return false; // 手动做 Hex 到 byte 的转换兼容老 .NET Framework byte[] raw new byte[(line.Length - 1) / 2]; for (int i 0; i raw.Length; i) { raw[i] Convert.ToByte(line.Substring(1 i * 2, 2), 16); } // Intel HEX 校验规则所有字节含校验和累加低 8 位必须为 0 byte sum 0; for (int i 0; i raw.Length; i) sum raw[i]; if (sum ! 0x00) return false; int length raw[0]; address (raw[1] 8) | raw[2]; recordType raw[3]; // 长度字段必须和实际负载匹配 if (length 5 ! raw.Length) return false; data new byte[length]; Array.Copy(raw, 4, data, 0, length); return true; }这里有两个细节值得注意。校验和是累加后低字节为 0不是某些教程里写的 0x100 减去累加和两种写法算出的结果一样但累加为 0的判断更直观出错概率低。地址拆分用的是raw[1] 8 | raw[2]因为 Intel HEX 的 16 位地址是大端排列高位在前。很多新手在这里写成raw[2] 8 | raw[1]转换结果从第 256 字节之后全部错位这种错误非常隐蔽。Convert.ToByte(substring, 16)的写法在 .NET Framework 4.x 和 .NET 6 里都能用比Convert.FromHexString兼容性好得多。后者是 .NET 5 才引入的 APIWinForms 老工程直接编译不过。专门的转换函数对畸形输入要返回 false 而不是抛异常这样外层可以把错误聚合成日志而不是中断整个文件转换。3.2 主体转换流程基址维护、数据铺平、连续段拆分有了单行解析函数接着是转换主流程。这个函数读入所有行维护扩展基址变量把每条数据记录按绝对地址填入字典同时记录最小和最大有效地址最后输出字节数组。private static byte[] BuildBinary(string[] lines) { long extBaseAddr 0; long minAddr long.MaxValue; long maxAddr long.MinValue; var buffer new Dictionarylong, byte(); foreach (string line in lines) { if (!TryParseHexLine(line, out byte type, out int addr, out byte[] data)) continue; if (type 0x04) // 扩展线性地址左移 16 位 { extBaseAddr ((long)data[0] 8 | data[1]) 16; } else if (type 0x02) // 扩展段地址左移 4 位 { extBaseAddr ((long)data[0] 8 | data[1]) 4; } else if (type 0x00) // 数据记录填充缓冲区 { long offset extBaseAddr addr; for (int i 0; i data.Length; i) { buffer[offset i] data[i]; if (offset i minAddr) minAddr offset i; if (offset i maxAddr) maxAddr offset i; } } else if (type 0x01) { break; // 文件结束记录后续内容忽略 } } if (minAddr long.MaxValue) throw new InvalidDataException(Hex 文件中没有数据记录); // 按外包络开辟缓冲区未记录地址用 0xFF 填充 byte[] bin new byte[maxAddr - minAddr 1]; for (int i 0; i bin.Length; i) bin[i] 0xFF; foreach (var kv in buffer) bin[kv.Key - minAddr] kv.Value; return bin; }这个实现把寻址和填充合在了同一个循环里代码量小逻辑直白。extBaseAddr是 long 而不是 int因为 04 记录左移 16 位之后地址可能超过 32 位无符号整数的表达范围虽然单片机场景里基本碰不到但用 long 能让极端情况的地址回绕问题提前暴露。用字典缓存地址和字节是刻意在实现简单和性能可接受之间取的平衡。固件几十 KB 的场景毫秒级完成完全够用。生产级的转换器面对几 MB 的固件我会改成两遍扫描第一遍只解析行但不存数据确定 minAddr 和 maxAddr第二遍直接填充new byte[length]数组内存占用从字典的 O(n) 对象开销降到纯字节数组速度提升一个量级。字典方案在这个工具里是正确的选择因为源码可读性比极限性能重要。3.3 裁剪与写文件外包络不是终点紧凑才是目标外包络的输出结果虽然功能正确但往往带有大量空洞。一个 Bootloader 加 App 的工程Hex 里两个区间分别位于 0x08000000 和 0x08008000外包络 Bin 会把中间 64KB 的空白全部填成 0xFF这对 OTA 传输是灾难。所以工具必须提供裁剪逻辑。// 裁剪头部连续 0xFF int firstValid 0; while (firstValid bin.Length bin[firstValid] 0xFF) firstValid; // 裁剪尾部连续 0xFF int lastValid bin.Length - 1; while (lastValid 0 bin[lastValid] 0xFF) lastValid--; if (firstValid lastValid) { throw new InvalidDataException(裁剪后没有有效数据检查填充值设置); } byte[] compact new byte[lastValid - firstValid 1]; Array.Copy(bin, firstValid, compact, 0, compact.Length);裁剪的边界情况比看起来要苛刻。如果固件全部内容恰好就是 0xFF这样裁完 firstValid 会超过 lastValid直接抛出空数组错误。更隐蔽的问题是填充值不一定是 0xFF——有些 Flash 空白区读出来是 0x00有些编译器会对未初始化区域填 0xA5如果工具硬编码裁剪 0xFF碰到这些场景就会把有效数据裁掉。我一般会把填充值和裁剪阈值都做成参数默认 0xFF但允许命令行指定。裁剪完必须输出一个日志记录原始 minAddr、裁剪后的 firstValid 地址、原始长度和裁剪后长度。这些信息后续做烧录地址配置和 OTA 校验时都要用。很多转换工具把日志省略了结果 Bin 是转换出来了用户拿着去烧录下载地址填 0x08000000 还是 0x08000400 完全靠猜这就是没把地址信息传递到下游的表现。4. 把工具当生产力命令行参数、拖拽界面与烧录器联动4.1 命令行模式批处理脚本里一条命令解放双手这个工具不能只活在图形界面里量产烧录和 CI 构建场景需要的是命令行调用。命令行参数的解析逻辑放在 Main 函数里设计上保持简单直观不做花哨的命令行框架读代码的人一眼就能看懂。static int Main(string[] args) { if (args.Length 2) { Console.WriteLine(用法: Hex2Bin input.hex output.bin [选项]); Console.WriteLine(选项:); Console.WriteLine( --trim 裁剪首尾连续的 0xFF); Console.WriteLine( --fill 0xFF 指定未记录地址的填充字节); Console.WriteLine( --start 0x... 只输出指定起始地址之后的数据); Console.WriteLine( --end 0x... 只输出指定结束地址之前的数据); Console.WriteLine( --verbose 打印每条记录的解析日志); return 1; } string input args[0]; string output args[1]; // ... 参数解析和转换调用 ... Console.WriteLine($转换完成: {minAddr:X8} - {maxAddr:X8}, 输出 {bin.Length} 字节); return 0; }参数设计的思路是从实际烧录场景反推的。--start和--end解决的是只要 App 区、不要 Bootloader 区的需求比如产线给没有 Bootloader 的板子烧程序直接指定--start 0x08000000 --end 0x08008000转换结果就是标准 App 镜像。--trim处理 OTA 场景把尾部空白裁掉省流量。--verbose是排查问题的后门遇到转换结果可疑时打开能看到每一行的地址和长度。命令行模式在批处理里的典型用法是这样的Hex2Bin.exe build/firmware.hex build/firmware.bin --trim if %ERRORLEVEL% NEQ 0 exit /b 1配合 Keil 的 After Build 命令甚至可以实现编译完成自动生成 Bin。在 Options for Target - User 标签页的 After Build/Rebuild 框里填D:\tools\Hex2Bin.exe #L #L.bin --trimKeil 会把当前生成的 Hex 路径传入编译完自动产出紧凑 Bin。这一步接上之后开发流程里就再也不用手动拖文件了。4.2 图形界面拖拽即转换顺带显示关键地址信息命令行适合脚本图形界面适合工程师手动操作。WinForms 版本的界面只放两个核心元素一个拖拽区一个日志框。下面是拖拽接收的核心事件处理逻辑。private void Form1_DragEnter(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(DataFormats.FileDrop)) e.Effect DragDropEffects.Copy; } private void Form1_DragDrop(object sender, DragEventArgs e) { string[] files (string[])e.Data.GetData(DataFormats.FileDrop); if (files.Length 0) return; string hexFile files[0]; string binFile Path.ChangeExtension(hexFile, .bin); try { // 转换并返回地址信息 var result Hex2BinConverter.Convert(hexFile, trim: true, fillValue: 0xFF); File.WriteAllBytes(binFile, result.Data); txtLog.AppendText($OK: {hexFile}\r\n); txtLog.AppendText($地址区间: 0x{result.MinAddr:X8} - 0x{result.MaxAddr:X8}\r\n); txtLog.AppendText($输出大小: {result.Data.Length} 字节 - {binFile}\r\n); } catch (Exception ex) { txtLog.AppendText($失败: {ex.Message}\r\n); } }拖拽界面把命令行模式里不会显示的地址信息直接暴露出来用户能看到数据实际落在哪个区间这在烧录阶段能避免大量低级错误。日志框用 AppendText 而不是赋值保证多次拖拽时日志累积可追溯。界面不做文件过滤器之外的复杂功能转换逻辑全部收敛到 Hex2BinConverter 静态类里UI 层只负责调用和展示。我一般会在界面上加一个复制烧录地址按钮一键把 minAddr 复制到剪贴板配合 STM32CubeProgrammer 的下载地址输入框用。这个小功能看着不起眼产线工程师配置烧录器时非常顺手。界面代码保持在这个粒度新手上手没压力想加功能也有清晰的扩展点。4.3 参数对照表参数默认值场景说明无参数-打印帮助信息--trim不裁剪OTA 打包时减小固件体积--fill 0xNN0xFF指定空白区填充字节匹配目标 Flash 特性--start 0xaddr无只保留指定地址之后的段常用于分离 App--end 0xaddr无只保留指定地址之前的段--verbose关闭打印每行记录解析日志用于排错参数之间存在优先级关系--start和--end同时出现时取交集区间--trim的作用范围是在这个交集之后。实际使用中我自己最常用的组合是--start 0x08000000 --trim前者锁定 App 起始地址后者裁掉尾部空白。命令行工具的返回值设计也很重要0 表示成功、非 0 表示失败这样批处理里才能用%ERRORLEVEL%判断流程是否继续。5. 避坑手册Hex2Bin 转换翻车的五个典型场景5.1 第一行解析失败都是 BOM 惹的祸现象转换器报错 无效记录但用记事本或 UltraEdit 打开 Hex 文件第一行明明是正常的:020000040800F2。反复检查格式都没问题就是在工具里读不进去。原因Hex 文件被某些编辑器保存成了 UTF-8 with BOM 格式文件开头悄悄塞了 EF BB BF 三个字节。StreamReader 默认按无 BOM 读取时第一行字符串最前面会多出一个\uFEFF字符StartsWith(:)直接返回 false解析器判定这一行不合法。解决解析函数里对每行先Trim()再检查首字符或者读取时用new StreamReader(path, Encoding.UTF8, true)让 BOM 被自动剥离。最稳妥的办法是在 TryParseHexLine 开头把行首的\uFEFF显式去掉。代码里加了这一步这辈子都不会再被这个坑绊住。5.2 老架构芯片转换后数据全错位02 记录被无视现象8051 和 AVR 工程的 Hex 文件转出的 Bin 完全不可用烧进去程序不跑反汇编看到数据全部错位。原因这些老架构的 Hex 文件里用的是 02 扩展段地址记录负载值要左移 4 位作为基址。很多转换工具只处理了 04 记录遇到 02 直接跳过后续数据记录的地址全部基于错误的基址计算结果自然全错。ARM 工程里 02 几乎绝迹导致很多开发者根本不知道这个类型的存在。解决转换器必须同时处理 02 和 04而且日志里要明确打印检测到的记录类型和计算出的基址。看到扩展段地址 0x1234左移 4 位 0x12340这样的日志问题当场暴露。我的习惯是解析器遇到 02 时打一条警告提醒核对链接配置ARM 工程里大概率是链接脚本有意为之但值得确认一次。5.3 转换出的 Bin 文件巨大外包络把空洞全填了现象STM32 工程只有 32KB 有效代码转换出来的 Bin 文件却有 1MB 甚至更大打开一看开头和中间全是 0xFF。原因Keil 的链接脚本经常把 Bootloader 区、App 区、配置字区间分布得很开Hex 文件里存在多个不连续的数据段。转换器采用最小地址到最大地址的外包络输出策略时中间所有空洞全用填充字节补齐文件被异常撑大。固件本身是好的但 Bin 作为传输载体严重浪费空间。解决不采用外包络改成连续数据段检测。当两条相邻数据记录的地址跳跃超过阈值比如 64 字节时把当前段截断新段单独输出。或者更简单粗暴一点用--start和--end参数手工指定目标区间把中间空洞直接排除。日志里看到检测到 N 个不连续段的提示就应该去确认每个段的用途。5.4 --trim 裁剪后 OTA 升级校验失败长度信息丢了现象开启裁剪后固件包从 128KB 减到 96KB传输速度明显提升但设备端升级时报校验失败反复对比 CRC 对不上。原因设备端固件校验是按完整 App 区间计算的Bootloader 拿到升级包后按固定长度做校验。裁剪只减小了传输体积却丢掉了原始长度这个关键信息设备端用新旧长度算出来的 CRC 自然不一致。解决裁剪模式必须额外输出一个元数据文件记录原始起始地址、原始长度、裁剪后长度。设备端 Bootloader 先读元数据按原始长度把收到的数据补齐到完整区间再做校验。我一般用 JSON 格式输出字段里同时保存基址和 CRC32这样升级流程从传输裸数据变成传输固件包元数据两端逻辑都清晰了。5.5 烧录后程序不跑Bin 的起始地址根本没传过去现象用 STM32CubeProgrammer 烧录转换出来的 Bin 文件下载提示成功但复位后程序不运行连中断向量表都不对。原因Bin 文件本身不包含地址信息烧录工具默认把地址填成了 0x00000000 或上次残留的值。而 Hex 文件里的原始地址是 0x08000000转换工具虽然知道这个地址却没有把它传给烧录流程最终烧录位置全错。解决转换之后先看日志里的 minAddr烧录工具里把下载地址显式填成这个值。更彻底的做法是在工具界面直接生成一条烧录命令比如 STM32CubeProgrammer CLI 模式或 OpenOCD 的 program 命令都支持指定地址。从那以后我每转换一个 Bin都会强制看一眼输出的地址区间再动手配置烧录器这个习惯帮我挡掉了多次低级返工。6. 进阶CRC32 校验、尾部压缩与烧录器无痛联动转换器能出 Bin 只是及格线让它真正融进固件发布流程才算毕业。第一个进阶动作是给转换结果算 CRC32这一步可以做成转换器的内置选项在写出 Bin 后自动生成校验值。.NET 6 及以上直接用System.IO.Hashing.Crc32老工程切换成查表法实现几十行代码而已。using System.IO.Hashing; byte[] payload File.ReadAllBytes(firmware.bin); var crc new Crc32(); crc.Append(payload); Console.WriteLine($CRC32: {crc.GetCurrentHashAsUInt32():X8});这个 CRC 值写进升级元数据里设备端 Bootloader 校验通过才允许跳转。有了它固件发出去之后如果被传输过程破坏设备端立刻能识别不会等到运行到一半才崩溃。配合第 5.4 节的裁剪逻辑元数据 JSON 里记录 baseAddr、原始长度、裁剪后长度、CRC32 四个字段完整度就很好了。第二个进阶操作是尾部压缩加偏移还原。有些场景裁剪完的 Bin 依然带着不必要的空白可以进一步用 RLE 或者直接压缩库处理。但压缩方案必须和 Bootloader 协商好——设备端解压后要能回到原始内存布局这要求元数据里保留完整的 Flash 区间描述。MD5 也是升级场景里常见的完整性校验选择它的普及度高、工具链兼容性好很多量产工具直接内置 MD5 对比比 CRC32 还要省事。第三个进阶是工具直接输出 OpenOCD 烧录命令让 PC 和开发板之间形成闭环。以下命令把转换后的 Bin 烧进 STM32F1 并在烧录后复位运行实测验证通过。openocd -f interface/stlink-v2.cfg \ -f target/stm32f1x.cfg \ -c program firmware.bin 0x08000000 verify reset exit这里的0x08000000不是随便填的它就是转换日志里报告的数据起始地址。把这一步写进发布脚本固件从 Hex 到烧录完成全程不需要打开任何图形界面。从那以后我每次给客户发固件包都强制走一遍 Hex2Bin 转换、CRC32 自校验、和 Keil 直接导出的 Bin 做大小对比这三步确认无异常才交付产线。这个习惯帮我挡掉了不止一次低级返工希望帮到你。本文还有配套的精品资源点击获取