
简介这款BIN与HEX互转合并小工具面向嵌入式开发者、单片机工程师以及需要处理固件格式的硬件爱好者解决日常开发中固件格式转换与多段HEX合并的痛点。压缩包共22个文件以C源码.h/.cpp、可执行程序.exe以及Visual C工程配置文件为主整体大小仅654KB轻量且便于携带。工具基于对话框程序实现参考已有转换思路并做了功能扩展支持BIN转HEX、HEX转BIN以及HEX文件合并同时针对Keil编译环境将输出字符统一转为大写避免使用出错并修复了bin文件超过64k时的数据错乱问题以及C8051芯片使用Keil编译的hex文件转换到bin时的错误。更新维护记录清晰体现作者对常见问题的持续打磨。目前已有3652人学习下载对于需要直接使用工具或参考源码进行二次开发的读者来说是一份实用且完整的参考。1. BIN和HEX的本质差异为什么会有两种固件格式嵌入式开发里BIN和HEX是两种最常用的固件发布格式。我最早接触这俩概念时也犯过迷糊以为只是扩展名不同。实际干过几个项目后发现这俩格式的底层逻辑差别非常大搞不清楚的话轻则生成的固件烧进去没反应重则把Flash地址全写乱、直接变砖。1.1 BIN是“按地址顺序铺开”的原始镜像BIN文件可以说是最朴素的一种固件形态——它就是纯二进制数据从头到尾一个字节接一个字节。你可以把它理解成一张没有坐标的照片数据本身不携带“我该放在哪”的信息。比如一个STM32工程编译出的固件假设链接地址是0x08000000那BIN文件的第一个字节就应该烧到0x08000000第二个字节烧到0x08000001以此类推。烧录器往Flash里写的时候完全靠你手动输入或者从下载算法里获取“起始地址”这个参数。地址填对了程序就能跑地址填错轻则白屏重则跑飞进HardFault。BIN文件的另一个特点是它只包含实际的数据内容没有校验和没有记录类型也没有地址间隔标记。如果固件里有大段的“空洞”比如Bootloader占32KB、App占64KB中间其实有留白BIN文件不会自动帮你填充那段空白它只是老老实实地把你编译出来的连续数据按顺序排布。这既是它的优点干净、纯粹、体积最小也是它的坑点你怎么控制烧写位置空白区域怎么办。1.2 HEX是“带地址和校验”的文本化固件HEX文件这里特指Intel HEX格式也是Keil、IAR、GCC工具链默认输出的那种则是另一种思路。它用ASCII文本记录每一段数据每一行都带着地址信息相当于给每个数据块都贴上了坐标。拿一行常见的Intel HEX记录来看:10010000214601360121470136007EFE09D2190140这行可以拆成几段来看字段示例含义起始符:每行以冒号开始长度10本行数据字节数0x10即16字节地址0100数据在存储空间中的偏移地址高16位类型00记录类型00表示数据记录01表示文件结束04表示扩展线性地址数据2146...0140实际固件内容校验和40前面所有字节累加后取补码用于校验行数据是否完整这里有个细节要注意HEX文件里“0100”这个地址不是完整地址。Intel HEX通过“扩展线性地址记录”类型04来指定高16位地址。比如STM32的Flash基地址是0x08000000HEX文件开头会有一条类似:020000040800F2的记录告诉烧录器“接下来的数据地址高16位是0x0800”。两条记录配合起来才能定位到真实的0x08000000处。所以说HEX文件自带完整的地址映射关系烧录器拿到HEX后不需要你额外输入起始地址它会按照内部记录一条一条把数据写到指定位置。这就是为什么很多烧录工具里HEX是“打开就能烧”的格式而BIN通常要弹窗让你填地址。1.3 什么时候必须转换、什么时候必须合并理解了这两种格式的本质后实际工作中遇到的情况就清晰了跨工具链协作有的同事用的是IAR生成的是HEX你这边用Keil生成的是BIN。两者要对比或合并时就得统一格式。OTA升级包制作很多OTA方案要求固件以BIN形式分发因为体积最小、解析最简单而且IoT设备的内存有限解析HEX文本反而费资源。这时候就需要把HEX转成BIN。产线量产烧录工厂一般希望一次烧录就能完成整个Flash的写入。如果Bootloader和App分开烧不仅浪费时间还容易出现“漏烧Bootloader导致设备无法升级”的批量事故。最好把多个固件合并成一个文件让产线只烧一次。Bootloader App合并这是最典型的合并场景。Bootloader放在低地址区App放在高地址区。两个文件格式还可能不一样一个是HEX一个是BIN。这时候就得做互转和合并。说实话我第一次处理“HEX和BIN互转”这个问题时是手动写了一个Python脚本硬解析HEX文本后来才发现业界早就有专门工具。下面这篇实操篇就把我踩过的坑和最终沉淀下来的方案一起讲清楚。2. 互转实操命令行工具和脚本两套方案做固件互转常用方法无非三种专门命令行工具SRecord、GCC工具链自带脚本objcopy、自己写Python解析脚本。我的建议是优先掌握前两种Python脚本了解原理即可——毕竟在工程环境里能用一条命令解决的事情没必要为它维护一段代码。2.1 SRecord嵌入式老牌瑞士军刀SRecord是SourceForge上老牌的开源嵌入式工具集其中的srec_cat和srec_info两个命令基本能覆盖BIN/HEX互转、合并、裁剪、地址偏移等所有需求。Windows和Linux都有对应版本下载解压后把路径加到系统PATH里就能用。先说最常见的BIN转HEX。假设你手上有一个app.bin需要把它转成烧录器能识别的、带地址信息的HEX并且起始地址是0x08008000srec_cat app.bin -Binary -offset 0x08008000 -o app.hex -Intel这条命令的意思是把app.bin当作二进制文件读入给它偏移0x08008000然后以Intel HEX格式输出到app.hex。-offset这个参数其实就是在生成HEX时给所有记录加上这个地址偏移。反过来HEX转BIN更简单。比如你手上有一个firmware.hex想转成纯BIN用于OTA或者暴力比对srec_cat firmware.hex -Intel -o firmware.bin -Binary这里有一个非常容易被坑的点如果HEX文件里的地址不是从0开始的比如从0x08000000到0x08001000那转出来的BIN文件大小是0x1000字节而不是你编译输出文件的实际大小。原因是BIN没有地址概念它把所有数据都当作从最低地址开始连续排列HEX中高地址部分与最低地址之间的“空洞”会被填充成0x00。这就导致一个现象你用boot.hex转出来的boot.bin可能比编译出的boot.bin大很多——因为HEX记录里有大量地址间隙这些间隙被“填零”补齐了。如果遇到这种情况要么用-crop参数裁剪地址范围要么先确认HEX文件的真实数据区间。我的习惯是转换前先用srec_info看一眼文件的地址分布srec_info firmware.hex -Intel它会输出类似Starting Address: 08000000、Execution Start Address: 08000000、Data: 08000000 - 08001000这样的汇总信息。先看清楚数据范围再决定怎么转。2.2 用Python写一个极简转换脚本有些时候公司内网环境装不了新工具或者你只是临时确认一下HEX里的某段数据那自己写个解析脚本是最快的。Python标准库就够用不需要第三方依赖。下面这个脚本实现BIN转HEX适合固件地址跨度不超过64KB的场景比如一些Cortex-M0的小项目import struct import sys def bin2hex(bin_path, hex_path, base_addr0x08000000): with open(bin_path, rb) as f: data f.read() lines [] # 每行16字节这是Intel HEX最常见的记录长度 for i in range(0, len(data), 16): chunk data[i:i16] record_len len(chunk) record_addr (base_addr i) 0xFFFF record_type 0x00 payload struct.pack(B, record_len) struct.pack(H, record_addr) payload bytes([record_type]) chunk checksum (-sum(payload)) 0xFF line : payload.hex().upper() f{checksum:02X} lines.append(line) # 文件结束记录 lines.append(:00000001FF) with open(hex_path, w) as f: f.write(\n.join(lines) \n) if __name__ __main__: bin2hex(sys.argv[1], sys.argv[2])注意上面这个脚本的局限它没有生成扩展线性地址记录0x04所以当地址超过base_addr 0xFFFF范围时会出错。真实做量产固件时我不建议用这种简化脚本但如果只是想快速查看某个小BIN的内容或者临时生成一个测试HEX它是完全够用的。反过来HEX转BIN的Python脚本其实更常用因为有时候OTA服务器只接受BIN而IDE只输出了HEX。解析逻辑就是逐行读取提取地址和数据写入一个bytearray最后统一写到文件。更重要的是它能把HEX中分散在各地址段的数据按BIN的连续地址映射关系整理出来——这个过程能帮你“看见”固件地址区间里的空洞到底有多大。2.3 转换时的地址对齐和填充问题做互转时最容易踩的坑不是命令用错而是“地址理解错”。我在一个项目里吃过亏同事给了我一版app.bin说是从0x08000000起始地址生成的App固件我直接用srec_cat转成HEX后烧进板子结果Bootloader跳转后直接卡死。排查了半天才发现那版BIN实际上是用--change-addresses偏移过的真正的App起始地址是0x08008000只是他口头跟我说的地址是“链接地址”不是“烧录地址”。这里要补充一个关键概念链接地址Link Address和烧录地址Load Address。BIN文件本身不区分这两个概念它只是数据地址信息完全依赖你转换时给的参数。HEX文件则明明白白写了每条记录要烧到哪儿。所以从BIN转HEX时-offset参数必须跟你的烧录规划一致——一般来说App的烧录地址就是链接地址也就是链接脚本里FLASH (rx) : ORIGIN 0x08008000里的那个地址。另外BIN转HEX时如果数据长度不是16字节的整数倍SRecord会在末尾用0xFF补齐到下一个16字节边界。这本身没问题但如果你拿这个HEX和别的HEX做差分比较会发现尾部多了几个0xFF属于正常现象别被吓到。3. 合并场景与方法从BootloaderApp到OTA升级包合并固件是我认为这整个主题里最有价值的部分。单文件转来转去无非是格式适配但当你需要把Bootloader、App、甚至配置字比如MAC地址、序列号合并成一个量产文件时才是真正考验功力的地方。3.1 为什么需要合并固件一个典型的量产场景是这样的产品用的是Cortex-M0内核芯片Bootloader负责引导和OTA升级放在Flash起始地址0x08000000占用前32KB0x08000000到0x08007FFFApp是实际的应用固件放在0x08008000开始的区域。产线烧录时如果烧录器只能操作整片Flash而你给产线的是两个独立文件那操作工就要手工设置两个地址区间、烧两遍不仅效率低还特别容易出错。合并的优势很明显一次烧录省时省力产线只需要选一个文件点一次烧录。地址关系固化合并好的文件里地址信息已经明确不存在人为填错地址的风险。便于发版管理一个版本号对应一个合并文件出问题好回溯。3.2 HEX合并靠地址说话srec_cat一行搞定如果两个文件都是HEX格式合并操作是最简单的因为HEX自带了地址信息srec_cat会按照地址自动排布srec_cat boot.hex -Intel app.hex -Intel -o merged.hex -Intel这条命令会把boot.hex和app.hex的数据都读进来按地址拼接后输出到merged.hex。只要两个文件的数据区间不重叠合并结果就是正确的。但注意这里有一个隐藏的检查动作确认两个文件的数据区间真的不重叠。我在实际项目中就遇到过两个HEX地址区间接壤处的边界重叠问题——Bootloader的HEX末尾地址是0x08007FFFApp的HEX起始地址是0x08008000这是正确的但如果Bootloader的Flash占用定义是0x08000000到0x08008000而App的起始地址恰好也是0x08008000此时最后一个字节和第一个字节虽然刚好相邻不冲突但Flash占用量Overlap算法在某些老版本SRecord里可能会误报。稳妥的办法是合并后马上用srec_info检查srec_info merged.hex -Intel确认输出里有类似Data: 08000000 - 08007FFF和Data: 08008000 - 0800FFFF这样的独立区间只要两个区间不交叉就没问题。3.3 BIN合并偏移计算与0xFF填充BIN文件的合并就要手算地址偏移了因为BIN本身没有地址。我的做法是先确定每一段的烧录地址然后写一个简单的拼接逻辑。假设boot.bin长度正好32KB0x8000字节app.bin从0x08008000开始。最终合并出来的量产BIN假设整体起始地址为0x08000000应该这样构造前0x8000字节是boot.bin的内容。从0x08008000往前算boot.bin占用了0x08000000到0x08007FFF所以App区前面没有空洞直接拼接app.bin即可。如果boot.bin实际只有28KB那从boot.bin结束到0x08008000之间还空着4KB。这个空洞应该填充什么我建议填0xFF——因为Flash擦除后本来就是0xFF这样和生产出来的芯片状态一致也方便后续用烧录器做整片校验。手工拼BIN太繁琐我一般直接用SRecord一步到位srec_cat boot.bin -Binary -offset 0x08000000 app.bin -Binary -offset 0x08008000 \ -fill 0xFF -within boot.bin -Binary -range-padding 0x08008000 \ -o merged.bin -Binary这条命令里-fill 0xFF -within ... -range-padding的意思是把Bootloader数据结束到0x08008000之间的空洞填充为0xFF。如果你不想记这么复杂的参数也可以分开两步操作先把boot.bin转换成HEX再把app.bin转换成HEX然后用3.2节的方式合并。两种方式结果一样SRecord这种方式更适合写进批处理脚本里固化下来。3.4 合并后的验证合并完成后千万别直接丢给产线。我见过太多人合并完也不验证烧几十片才发现Bootloader和App的地址区间接反了。我自己的标准流程是地址范围检查用srec_info查看合并文件的Data区间确认是连续且封闭的没有意外的空洞。文件头尾检查用十六进制编辑器比如HxD打开合并文件手动确认文件开头是Bootloader的向量表ARM芯片第一个32位数据是初始SP值第二个是Reset_Handler地址App区开头同样能看到向量表特征。回读校验挑一片样板烧录后用调试器读回Flash和合并后的文件做二进制diff。这一步能发现某些烧录器自动做了奇偶校验、字节对调等问题。这个验证流程看着繁琐但一旦养成习惯能省下后面大量排障时间。4. 工具链配合实战Keil生成、烧录器下载中的高频问题格式转换和合并本身不难真正折磨人的是把这些操作嵌入到日常开发工具链里。下面我把用户群里问的最多的问题集中整理一下。4.1 在Keil/MDK里配置自动生成BINKeil MDK默认只生成HEX和AXF要让它顺手把BIN也吐出来得自己在工程配置里加一句fromelf命令。步骤是打开工程进入Options for Target-User标签页。在After Build/Rebuild下面的Run #1勾选框里打勾输入框填入fromelf --bin --output.\Output\app.bin .\Output\app.axf这里的app.axf路径要改成你自己工程的实际输出路径。注意fromelf是Keil安装目录下ARMCC或ARMCLANG自带的小工具如果你的PATH环境变量没配置可以用完整路径调用例如C:\Keil_v5\ARM\ARMCLANG\bin\fromelf.exe。编译后去Output目录就能看到app.bin了。踩过的坑有两个。一是路径里有空格时必须加引号否则命令行解析会断开二是--bin生成的文件默认从AXF里的加载地址开始输出如果工程用了分散加载文件、分区布局比较复杂生成的BIN可能不是你想要的完整地址区间这时候要么改用--i32输出HEX再转BIN要么用SRecord做二次处理。4.2 HEX行长度与MDK/CCS的特殊设置有不少人在问“MDK怎么让HEX文件每一行都是16字节”。这其实与HEX记录类型的读取器兼容性有关。Keil默认生成的HEX每行数据长度就是0x1016字节这是Intel HEX最标准的记录长度绝大多数烧录器都支持。如果你遇到某些烧录器读不了长行格式的HEX或者你自己用脚本解析HEX时希望固定行长度可以用SRecord强制统一srec_cat input.hex -Intel -line-length16 -o output16.hex -Intel还有朋友用TI的CCS开发环境想在CCS里生成HEX文件。CCS默认不直接输出HEX需要在编译后处理步骤里调用hex2000C2000系列或者tiobj2bin等工具。以最常见的C2000为例在工程的Build - Steps里添加类似这样的命令${CG_TOOL_HEX} --intel --mapoutput.map -o output.hex output.out${CG_TOOL_HEX}是CCS自动注入的hex工具路径变量。不同芯片系列的HEX工具名不同但用法思路一致先把编译生成的.outELF格式通过hex工具转换成Intel HEX或TI-tagged HEX再去配合烧录器使用。4.3 J-Flash和Cortex-M0 SWD下载BIN的坑用J-Flash下载HEX文件非常省事File - Open data file选择HEXJ-Flash会自动识别地址区间并显示在窗口里然后在Target菜单里点Connect和Program就能烧录。但下载BIN文件时J-Flash会弹一个对话框让你填起始地址而且填完它还会标红提醒你这个地址是否在有效Flash范围内。Cortex-M0芯片用SWD下载BIN时最容易出问题的就是起始地址。M0内核没有指令预取缓存也没有复杂的MMU地址写错会直接导致程序跑飞。比如你给nRF52840烧录一个App固件时如果一开始地址填了0x00000000可能会覆盖掉SoftDevice保护区轻则App起不来重则把协议栈刷没了、整片Flash要重新恢复。我自己的做法是在烧录前用调试器读一下目标芯片Flash的前几个字确认Bootloader/SoftDevice实际占用的大小再用这个实际地址去烧App BIN。另外J-Flash打开BIN时要手动选择Raw Data文件类型如果你选了Intel HEX去开BIN界面会显示乱码或者解析失败。这个操作顺序很多人搞错值得单独记一笔。4.4 常见问题速查表现象可能是原因排查方法Keil生成了AXF但没有BINfromelf命令没配置或路径错误检查User标签页的After Build命令手动在命令行执行一次看报错烧HEX正常烧BIN后程序不运行BIN的起始地址填错确认烧录地址与链接地址一致用仿真器读取PC指针位置HEX转BIN后文件体积异常大HEX地址区间不连续空洞被0填充用-crop限制输出范围或改用带地址的HEX分发合并后的文件烧录后只能运行其中一个程序两个HEX地址区间重叠用srec_info查看区间重做合并烧录nRF52840的HEX后SoftDevice丢失烧录地址覆盖了SoftDevice保护区用nrfjprog查看保护区改烧录起始地址或取消全片擦除MDK生成的HEX每行长度超过16字节工具链默认输出长行记录用SRecord统一为--line-length16做固件格式互转和合并这件事说白了就是和“地址”打交道。BIN没带地址HEX带地址合并的本质就是把地址关系理清楚。我个人的建议是在开发调试阶段尽量用HEX因为自带地址和校验、不易出错到了量产和OTA阶段再统一转成BIN减小体积、方便传输。最后再分享一个小习惯不管是用SRecord还是脚本我都会把命令写进一个批处理或Makefile里连同版本号一起提交到代码仓库。这样每次发版只需要跑一条命令既能保证所有人都拿到同样的合并规则也方便回溯“某个版本的量产固件到底是怎么拼出来的”。本文还有配套的精品资源点击获取