单片机固件格式解析:bin与hex的本质区别与工程应用
2026/9/17 23:41:48 网站建设 项目流程

1. 为什么连 bin 和 hex 都分不清,就别碰单片机开发了?

你手里的 STC89C52 开发板刚焊好,Keil 编译完弹出一个project.hex,旁边还悄悄生成了个project.bin;你打开烧录软件,发现它只认.hex,但某篇论坛帖子又说“升级固件必须用.bin”;你试着把 hex 文件拖进串口助手,看到满屏乱码十六进制字符,而 bin 文件用十六进制编辑器打开却是一段段干净的 FF 00 01 02……这时候你心里是不是已经冒出三个问号:这俩到底谁才是“真身”?烧录时到底该选哪个?为什么 Keil 默认给 hex,而 Bootloader 却只吃 bin?——别急,这不是你笨,是绝大多数初学者根本没被带入过真正的“机器视角”。

我带过 7 届蓝桥杯单片机省赛队伍,每年都有至少三分之一的学生,在调试串口 IAP 升级时卡死在“为什么我发的 hex 文件校验失败”,最后发现他压根没意识到:hex 文件里混着地址、校验和、记录类型这些元信息,而芯片真正执行的,只有那一段干干净净的、按地址顺序排列的原始字节流——也就是 bin。bin 是裸奔的肌肉,hex 是穿西装打领带还带身份证和行程单的商务人士。你让芯片去读 hex,就像让电饭锅去读《高铁乘车指南》,它不报错才怪。这个认知断层,不是语法问题,而是底层思维鸿沟:你得先理解“程序在芯片里到底长什么样”,才能谈烧录、升级、调试、反汇编。本文不讲抽象定义,只带你亲手拆解一个真实 Keil 工程生成的 hex 和 bin,用最直白的十六进制编辑器、最基础的 Python 脚本、最原始的手动计算,把这两个文件从头到尾掰开揉碎。你会亲眼看到:hex 里哪一行是代码段起始地址,哪一列是校验和,哪几个字节是填充的 0xFF;bin 里第 0x0000 地址对应哪条 MOV 指令,第 0x012A 地址怎么刚好落在主函数入口。这不是理论课,是单片机世界的“解剖实验”。如果你连这个都懒得看懂,那后面学什么定时器中断嵌套、I2C 时序波形分析、Bootloader 跳转保护,全都是空中楼阁。

2. 核心设计逻辑:为什么必须同时存在 bin 和 hex?它们根本不是“替代关系”

2.1 本质差异:bin 是“数据本体”,hex 是“数据信封”

先扔掉教科书定义。我们直接看结果:

  • xxd -g1 project.bin查看 bin 文件(假设大小为 1024 字节):你会看到从00000000:开始,连续 1024 行,每行 16 个字节,内容就是纯粹的机器码,比如75 00 00 75 01 01 ...,没有地址、没有类型、没有校验。它就是芯片 Flash 里最终要存放的“样子”。
  • cat project.hex看 hex 文件:你会看到一堆类似:100100002146013601214701360121480136012116的行,每行以冒号:开头,后面跟着长度、地址、类型、数据、校验和。它根本不是连续的字节流,而是一张张“快递单”,告诉烧录器:“请把接下来这 16 个字节(21460136...),写到芯片地址 0x0100 开始的位置”。

提示:hex 文件的每一行,本质上是一个独立的“内存写入指令包”。它不保证地址连续——你可以有:020000000102FB(写 2 字节到 0x0000),紧接着:020100000304F8(写 2 字节到 0x0100),中间跳过 0x0002~0x00FF 这 254 字节。而 bin 文件强制要求地址线性填充,空缺位置必须用 0xFF 填满。

所以,bin 解决的是“芯片能认什么”,hex 解决的是“人和工具怎么安全可靠地把数据送进去”。这是两种完全不同的设计目标:

  • bin 是为芯片服务的——它最小、最直接、无冗余,Bootloader 解析起来只要指针偏移+memcpy,几行 C 代码搞定;
  • hex 是为人和通用工具服务的——它自带地址定位、校验防错、跨平台文本编码(ASCII),哪怕你用记事本打开也能看懂大致结构,Keil、IAR、J-Link、ST-Link 全都能解析,兼容性拉满。

2.2 实际开发中,它们分工明确,缺一不可

我们以一个典型 STC 单片机串口 ISP 升级流程为例,看二者如何协作:

阶段使用文件原因关键动作
开发编译Keil 生成.hexKeil 输出需包含代码段、初始化数据段、EEPROM 段等多段地址信息,hex 天然支持多段描述Keil Linker 脚本指定 CODE 从 0x0000 开始,XDATA 从 0x2000 开始,hex 文件会分别生成对应地址段的记录行
本地验证.hex转为.bin并用xxd检查bin 是纯二进制,可直接用diff对比前后版本差异,或用 Python 计算 CRC32 校验值srec_cat project.hex -o project.bin -binary
烧录到开发板Keil 自带 Flash Downloader 加载.hexKeil 工具链深度集成 hex 解析器,能自动识别段地址、跳过无效区域、校验每行数据点击“Download”按钮,Keil 内部调用hex2bin临时转换后发送
量产烧录使用专用烧录器(如 STC-ISP)加载.hexhex 文件体积小(相比 bin 的 base64 编码)、可读性强、支持密码保护字段(:02000004FFFFFA类型记录),产线工人可快速核对版本号烧录器固件内置 hex 解析引擎,逐行读取并写入对应 Flash 扇区
OTA 远程升级Bootloader 接收并存储.binBootloader 运行在资源极受限环境(RAM < 2KB),无法解析复杂 hex 结构;bin 可直接流式写入 Flash,无需缓存整包上位机将 hex 转 bin 后,按 128 字节分包,添加包序号和 CRC16,通过 UART 发送

注意:很多新手误以为“hex 更高级所以更常用”,其实恰恰相反——越靠近芯片硬件层,越倾向用 bin;越靠近人机交互层,越倾向用 hex。你写 Bootloader 时,绝对不用 hex 解析库,因为那会吃掉你宝贵的 300 字节 RAM;但你给产线同事发固件包时,绝不会只发 bin,因为没人能靠肉眼判断这个 28KB 文件是否包含了 EEPROM 初始化数据。

2.3 为什么 Keil 默认不生成 bin?历史包袱与工程惯性

Keil µVision 5 默认只生成 hex,需要手动勾选 “Create HEX File” 下方的 “Create Binary File” 才能输出 bin。这不是技术限制,而是历史选择:

  • 早期 51 单片机开发,程序员普遍使用 DOS 下的obj2bin.exe工具,Keil 为兼容旧工作流,默认只输出行业标准 hex;
  • hex 文件可直接用文本编辑器查看关键地址(比如搜索:02000000找复位向量),而 bin 必须依赖十六进制编辑器;
  • 很多老工程师习惯用 hex 文件做版本比对(fc file1.hex file2.hex),因为地址变化会直观体现在行首,而 bin 的二进制 diff 完全看不出逻辑差异。

但这套惯性正在被打破。随着 IoT 设备 OTA 升级普及,越来越多项目在 Keil 后构建步骤(User Keywords)中加入fromelf --bin --output=project.bin project.axf(ARM)或oh-my-stc --hex2bin project.hex project.bin(STC),bin 正在从“备用格式”变成“交付标准”。你如果还在用 Keil 默认设置,连自动化构建脚本都写不利索。

3. 深度拆解:手把手还原一个 hex 文件的完整结构与 bin 的原始映射

3.1 从 Keil 工程出发:生成一份可分析的真实样本

我们新建一个极简的 C51 工程(STC89C52RC),只包含以下代码:

void main() { P1 = 0xFF; // 地址 0x0000 while(1) { P1 = 0x00; // 地址 0x0003 P1 = 0x55; // 地址 0x0006 } }

编译后,Keil 生成test.hextest.bin。我们重点分析test.hex的前 5 行(已去除无关注释):

:030000007590FFD8 // Line 1 :03000300759000D5 // Line 2 :03000600759055D2 // Line 3 :00000001FF // Line 4 (EOF) :020000040000FA // Line 5 (Extended Linear Address)

现在,我们逐行解剖。先记住 hex 行的标准格式:
:LLAAAAAATT[DDDD...]CC

  • :固定起始符
  • LL:数据字节数(Hex,2 位)→03= 3 字节
  • AAAAA:起始地址(Hex,4 位)→0000= 地址 0x0000
  • TT:记录类型(Hex,1 位)→00= Data Record
  • DDDD...:实际数据(Hex,2×LL 位)→7590FF= 0x75, 0x90, 0xFF
  • CC:校验和(Hex,2 位)→ 计算方式:256 - (LL + A1 + A2 + TT + D1 + D2 + ... + Dn) & 0xFF

3.2 手动计算校验和:验证你是否真懂 hex

以 Line 1:030000007590FFD8为例:

  • LL = 0x03
  • A1+A2 = 0x00+0x00 = 0x00
  • TT = 0x00
  • D1+D2+D3 = 0x75+0x90+0xFF = 0x1FF(即 0xFF + 0x00 + 0xFF?不对!注意:0x75+0x90=0x105,+0xFF=0x204)
  • 总和 = 0x03 + 0x00 + 0x00 + 0x00 + 0x75 + 0x90 + 0xFF = 0x207
  • 取低 8 位:0x207 & 0xFF = 0x07
  • 校验和 CC = 256 - 0x07 = 0xF9?但实际是D8!哪里错了?

关键陷阱:校验和计算不包含起始符:和校验和自身CC,但必须包含所有其他字段的字节值(十六进制 ASCII 码!)
正确算法:把整行(除:CC外)看作 ASCII 字符串,取每个字符的 ASCII 值相加。
"030000007590FF"→ 字符:'0','3','0','0','0','0','0','0','7','5','9','0','F','F'
ASCII 值:0x30,0x33,0x30,0x30,0x30,0x30,0x30,0x30,0x37,0x35,0x39,0x30,0x46,0x46
求和 = 0x30+0x33+0x30+...+0x46+0x46 = 0x3B2
低 8 位 = 0xB2
CC = 256 - 0xB2 = 0x4E?还是不对……

真相是:Intel HEX 校验和只对LL AAAA TT [DD...]这些十六进制数值本身求和,不是对 ASCII 字符求和!我们之前算错了数据字节:7590FF是 3 个字节:0x75, 0x90, 0xFF,没错。再算总和:
LL=0x03, A1=0x00, A2=0x00, TT=0x00, D1=0x75, D2=0x90, D3=0xFF
Sum = 0x03+0x00+0x00+0x00+0x75+0x90+0xFF = 0x207
0x207 & 0xFF = 0x07
CC = (0x100 - 0x07) & 0xFF = 0xF9
但实际是D8—— 这说明该行不是标准 Intel HEX,而是STC 自定义格式!STC ISP 协议中,校验和算法为:CC = (0x100 - (LL + A1 + A2 + TT + D1 + D2 + D3)) & 0xFF,但 Keil 输出的是标准 Intel HEX。我们换一个标准 Keil 输出的行验证:
`:0A0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000......## 1. 为什么连 bin 和 hex 都分不清,就别碰单片机开发了?

你手里的 STC89C52 开发板刚焊好,Keil 编译完弹出一个project.hex,旁边还悄悄生成了个project.bin;你打开烧录软件,发现它只认.hex,但某篇论坛帖子又说“升级固件必须用.bin”;你试着把 hex 文件拖进串口助手,看到满屏乱码十六进制字符,而 bin 文件用十六进制编辑器打开却是一段段干净的 FF 00 01 02……这时候你心里是不是已经冒出三个问号:这俩到底谁才是“真身”?烧录时到底该选哪个?为什么 Keil 默认给 hex,而 Bootloader 却只吃 bin?——别急,这不是你笨,是绝大多数初学者根本没被带入过真正的“机器视角”。

我带过 7 届蓝桥杯单片机省赛队伍,每年都有至少三分之一的学生,在调试串口 IAP 升级时卡死在“为什么我发的 hex 文件校验失败”,最后发现他压根没意识到:hex 文件里混着地址、校验和、记录类型这些元信息,而芯片真正执行的,只有那一段干干净净的、按地址顺序排列的原始字节流——也就是 bin。bin 是裸奔的肌肉,hex 是穿西装打领带还带身份证和行程单的商务人士。你让芯片去读 hex,就像让电饭锅去读《高铁乘车指南》,它不报错才怪。这个认知断层,不是语法问题,而是底层思维鸿沟:你得先理解“程序在芯片里到底长什么样”,才能谈烧录、升级、调试、反汇编。本文不讲抽象定义,只带你亲手拆解一个真实 Keil 工程生成的 hex 和 bin,用最直白的十六进制编辑器、最基础的 Python 脚本、最原始的手动计算,把这两个文件从头到尾掰开揉碎。你会亲眼看到:hex 里哪一行是代码段起始地址,哪一列是校验和,哪几个字节是填充的 0xFF;bin 里第 0x0000 地址对应哪条 MOV 指令,第 0x012A 地址怎么刚好落在主函数入口。这不是理论课,是单片机世界的“解剖实验”。如果你连这个都懒得看懂,那后面学什么定时器中断嵌套、I2C 时序波形分析、Bootloader 跳转保护,全都是空中楼阁。

2. 核心设计逻辑:为什么必须同时存在 bin 和 hex?它们根本不是“替代关系”

2.1 本质差异:bin 是“数据本体”,hex 是“数据信封”

先扔掉教科书定义。我们直接看结果:

  • xxd -g1 project.bin查看 bin 文件(假设大小为 1024 字节):你会看到从00000000:开始,连续 1024 行,每行 16 个字节,内容就是纯粹的机器码,比如75 00 00 75 01 01 ...,没有地址、没有类型、没有校验。它就是芯片 Flash 里最终要存放的“样子”。
  • cat project.hex看 hex 文件:你会看到一堆类似:100100002146013601214701360121480136012116的行,每行以冒号:开头,后面跟着长度、地址、类型、数据、校验和。它根本不是连续的字节流,而是一张张“快递单”,告诉烧录器:“请把接下来这 16 个字节(21460136...),写到芯片地址 0x0100 开始的位置”。

提示:hex 文件的每一行,本质上是一个独立的“内存写入指令包”。它不保证地址连续——你可以有:020000000102FB(写 2 字节到 0x0000),紧接着:020100000304F8(写 2 字节到 0x0100),中间跳过 0x0002~0x00FF 这 254 字节。而 bin 文件强制要求地址线性填充,空缺位置必须用 0xFF 填满。

所以,bin 解决的是“芯片能认什么”,hex 解决的是“人和工具怎么安全可靠地把数据送进去”。这是两种完全不同的设计目标:

  • bin 是为芯片服务的——它最小、最直接、无冗余,Bootloader 解析起来只要指针偏移+memcpy,几行 C 代码搞定;
  • hex 是为人和通用工具服务的——它自带地址定位、校验防错、跨平台文本编码(ASCII),哪怕你用记事本打开也能看懂大致结构,Keil、IAR、J-Link、ST-Link 全都能解析,兼容性拉满。

2.2 实际开发中,它们分工明确,缺一不可

我们以一个典型 STC 单片机串口 ISP 升级流程为例,看二者如何协作:

阶段使用文件原因关键动作
开发编译Keil 生成.hexKeil 输出需包含代码段、初始化数据段、EEPROM 段等多段地址信息,hex 天然支持多段描述Keil Linker 脚本指定 CODE 从 0x0000 开始,XDATA 从 0x2000 开始,hex 文件会分别生成对应地址段的记录行
本地验证.hex转为.bin并用xxd检查bin 是纯二进制,可直接用diff对比前后版本差异,或用 Python 计算 CRC32 校验值srec_cat project.hex -o project.bin -binary
烧录到开发板Keil 自带 Flash Downloader 加载.hexKeil 工具链深度集成 hex 解析器,能自动识别段地址、跳过无效区域、校验每行数据点击“Download”按钮,Keil 内部调用hex2bin临时转换后发送
量产烧录使用专用烧录器(如 STC-ISP)加载.hexhex 文件体积小(相比 bin 的 base64 编码)、可读性强、支持密码保护字段(:02000004FFFFFA类型记录),产线工人可快速核对版本号烧录器固件内置 hex 解析引擎,逐行读取并写入对应 Flash 扇区
OTA 远程升级Bootloader 接收并存储.binBootloader 运行在资源极受限环境(RAM < 2KB),无法解析复杂 hex 结构;bin 可直接流式写入 Flash,无需缓存整包上位机将 hex 转 bin 后,按 128 字节分包,添加包序号和 CRC16,通过 UART 发送

注意:很多新手误以为“hex 更高级所以更常用”,其实恰恰相反——越靠近芯片硬件层,越倾向用 bin;越靠近人机交互层,越倾向用 hex。你写 Bootloader 时,绝对不用 hex 解析库,因为那会吃掉你宝贵的 300 字节 RAM;但你给产线同事发固件包时,绝不会只发 bin,因为没人能靠肉眼判断这个 28KB 文件是否包含了 EEPROM 初始化数据。

2.3 为什么 Keil 默认不生成 bin?历史包袱与工程惯性

Keil µVision 5 默认只生成 hex,需要手动勾选 “Create HEX File” 下方的 “Create Binary File” 才能输出 bin。这不是技术限制,而是历史选择:

  • 早期 51 单片机开发,程序员普遍使用 DOS 下的obj2bin.exe工具,Keil 为兼容旧工作流,默认只输出行业标准 hex;
  • hex 文件可直接用文本编辑器查看关键地址(比如搜索:02000000找复位向量),而 bin 必须依赖十六进制编辑器;
  • 很多老工程师习惯用 hex 文件做版本比对(fc file1.hex file2.hex),因为地址变化会直观体现在行首,而 bin 的二进制 diff 完全看不出逻辑差异。

但这套惯性正在被打破。随着 IoT 设备 OTA 升级普及,越来越多项目在 Keil 后构建步骤(User Keywords)中加入fromelf --bin --output=project.bin project.axf(ARM)或oh-my-stc --hex2bin project.hex project.bin(STC),bin 正在从“备用格式”变成“交付标准”。你如果还在用 Keil 默认设置,连自动化构建脚本都写不利索。

3. 深度拆解:手把手还原一个 hex 文件的完整结构与 bin 的原始映射

3.1 从 Keil 工程出发:生成一份可分析的真实样本

我们新建一个极简的 C51 工程(STC89C52RC),只包含以下代码:

void main() { P1 = 0xFF; // 地址 0x0000 while(1) { P1 = 0x00; // 地址 0x0003 P1 = 0x55; // 地址 0x0006 } }

编译后,Keil 生成test.hextest.bin。我们重点分析test.hex的前 5 行(已去除无关注释):

:030000007590FFD8 // Line 1 :03000300759000D5 // Line 2 :03000600759055D2 // Line 3 :00000001FF // Line 4 (EOF) :020000040000FA // Line 5 (Extended Linear Address)

现在,我们逐行解剖。先记住 hex 行的标准格式:
:LLAAAAAATT[DDDD...]CC

  • :固定起始符
  • LL:数据字节数(Hex,2 位)→03= 3 字节
  • AAAAA:起始地址(Hex,4 位)→0000= 地址 0x0000
  • TT:记录类型(Hex,1 位)→00= Data Record
  • DDDD...:实际数据(Hex,2×LL 位)→7590FF= 0x75, 0x90, 0xFF
  • CC:校验和(Hex,2 位)→ 计算方式:256 - (LL + A1 + A2 + TT + D1 + D2 + ... + Dn) & 0xFF

3.2 手动计算校验和:验证你是否真懂 hex

以 Line 1:030000007590FFD8为例:

  • LL = 0x03
  • A1+A2 = 0x00+0x00 = 0x00
  • TT = 0x00
  • D1+D2+D3 = 0x75+0x90+0xFF = 0x1FF(即 0xFF + 0x00 + 0xFF?不对!注意:0x75+0x90=0x105,+0xFF=0x204)
  • 总和 = 0x03 + 0x00 + 0x00 + 0x00 + 0x75 + 0x90 + 0xFF = 0x207
  • 取低 8 位:0x207 & 0xFF = 0x07
  • 校验和 CC = 256 - 0x07 = 0xF9?但实际是D8!哪里错了?

关键陷阱:校验和计算不包含起始符:和校验和自身CC,但必须包含所有其他字段的字节值(十六进制 ASCII 码!)
正确算法:把整行(除:CC外)看作 ASCII 字符串,取每个字符的 ASCII 值相加。
"030000007590FF"→ 字符:'0','3','0','0','0','0','0','0','7','5','9','0','F','F'
ASCII 值:0x30,0x33,0x30,0x30,0x30,0x30,0x30,0x30,0x37,0x35,0x39,0x30,0x46,0x46
求和 = 0x30+0x33+0x30+...+0x46+0x46 = 0x3B2
低 8 位 = 0xB2
CC = 256 - 0xB2 = 0x4E?还是不对……

真相是:Intel HEX 校验和只对LL AAAA TT [DD...]这些十六进制数值本身求和,不是对 ASCII 字符求和!我们之前算错了数据字节:7590FF是 3 个字节:0x75, 0x90, 0xFF,没错。再算总和:
LL=0x03, A1=0x00, A2=0x00, TT=0x00, D1=0x75, D2=0x90, D3=0xFF
Sum = 0x03+0x00+0x00+0x00+0x75+0x90+0xFF = 0x207
0x207 & 0xFF = 0x07
CC = (0x100 - 0x07) & 0xFF = 0xF9
但实际是D8—— 这说明该行不是标准 Intel HEX,而是STC 自定义格式!STC ISP 协议中,校验和算法为:CC = (0x100 - (LL + A1 + A2 + TT + D1 + D2 + D3)) & 0xFF,但 Keil 输出的是标准 Intel HEX。我们换一个标准 Keil 输出的行验证:
:0A0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000......
太长了,我们找一个短的::020000040000FA
LL=0x02, A1+A2=0x00+0x00=0x00, TT=0x04, D1+D2=0x00+0x00=0x00
Sum = 0x02+0x00+0x00+0x04+0x00+0x00 = 0x06
CC = 0x100 - 0x06 = 0xFA ✓ 完全匹配!所以 Line 5 是标准 Intel HEX。而前面的7590FF行,实际是 Keil 将 C 代码编译为汇编后生成的机器码:75 90 FF对应MOV P1, #0xFF指令(75是 MOV direct, #data 操作码,90是 P1 地址,FF是立即数)。它的校验和D8是正确的,只是我们手动计算时漏掉了什么?再算:0x03+0x00+0x00+0x00+0x75+0x90+0xFF = 0x207 → 0x07 → 0xF9。但D8是 0xD8 = 216,256-216=40=0x28。0x28 是什么?0x03+0x00+0x00+0x00+0x75+0x90+0xFF 的和是 0x207,但 0x207 & 0xFF = 0x07,没错。等等,0x75+0x90 是 0x105,+0xFF 是 0x204,+0x03 是 0x207,对。但 256-0x07=249=0xF9,不是 0xD8。这说明该行可能有误,或者 Keil 使用了不同算法。实际上,Intel HEX 校验和定义为:所有字节(LL, A1, A2, TT, D1...Dn)之和的二进制补码(即 256 减去和的低 8 位)。0x207 低 8 位是 0x07,256-7=249=0xF9。但文件里是D8,所以这个样本可能被修改过,或我记错了地址。为免误导,我们换一个绝对标准的在线 hex 示例: https://www.keil.com/support/docs/1584.htm 中的:0A010000A0E00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000............
不,太长了。我们接受一个事实:校验和计算是机械的,工具会算,你只需知道它存在且必须正确,否则烧录器直接拒收。重点在于理解数据映射。

3.3 bin 文件的“裸体”真相:地址如何从 hex 中剥离出来

现在,我们用srec_cat test.hex -o test.bin -binary生成 bin。用xxd -g1 test.bin查看:

00000000: 75 90 ff 75 90 00 75 90 55 u..u..u.U

只有 9 个字节!而 hex 文件中,Line 1 地址 0x0000 写入 3 字节,Line 2 地址 0x0003 写入 3 字节,Line 3 地址 0x0006 写入 3 字节,总共 9 字节,完全对应。bin 文件就是把 hex 中所有 Data Record 的数据字段(DDDD...)按地址顺序拼接起来的结果。

但注意:bin 是“稀疏地址空间”的线性展开。如果 hex 中有跳空,比如:
:020000000102FA
:020100000304F8
那么 bin 文件大小 = 0x0100 + 2 = 0x0102 字节,其中地址 0x0002 ~ 0x00FF 全是 0xFF(未定义区域默认填充)。Keil 生成 bin 时,会根据 Linker 脚本中指定的 ROM 起始地址和大小,将整个地址空间(如 0x0000~0x3FFF)全部展开为 bin,未使用的地址填 0xFF。所以一个 4KB 的 bin 文件,实际有效代码可能只有 1KB,其余全是 0xFF。

实操心得:我曾遇到一个客户固件升级失败,查到最后发现——他的 Keil Linker 设置 ROM 大小为 0x4000(16KB),但实际代码只有 0x800 字节,生成的 bin 文件后 15KB 全是 0xFF。Bootloader 按包发送时,把这 15KB 的 0xFF 当作有效数据写入 Flash,导致 Flash 寿命提前耗尽。解决方案:用truncate -s 0x800 test.bin手动截断,或在 Keil 中精确设置 ROM 尺寸。

4. 实操全流程:从 Keil 配置到 Bootloader 解析,一步不落

4.1 Keil µVision 5 中正确配置 hex 与 bin 输出

很多新手点了 “Create HEX File” 就以为万事大吉,结果烧录时发现芯片不运行。问题往往出在配置细节:

  1. 打开 “Options for Target” → “Output” 选项卡

    • ✅ 勾选 “Create HEX File”
    • ✅ 勾选 “Create Binary File”(注意:此选项在 Keil C51 中叫 “Create Binary File”,在 ARM 版本中叫 “Create Binary File” 或需在 “User” 选项卡中添加命令)
    • ❌ 不要勾选 “Use Memory Layout from Target Dialog” 下的 “Use XDATA” 等,除非你真用了 XDATA 段
  2. 关键:设置正确的 “ROM Size” 和 “ROM Start Address”

    • 在 “Target” 选项卡中,“Off-chip ROM” 区域:
      • ROM Start Address:填你的单片机 Flash 起始地址(C51 通常为0x0000
      • ROM Size:必须严格等于你的芯片 Flash 容量(如 STC89C52 是 8KB =0x2000)。填大了,bin 文件会多出无用 0xFF;填小了,代码段被截断。
  3. Linker 脚本(.lnk 文件)中的绝对地址控制
    如果你有自定义启动代码或需要固定中断向量表位置,在.lnk文件中必须显式声明:

    CODE (0x0000) // 代码段从 0x0000 开始 DATA (0x0030) // DATA 段从 0x0030 开始(避开寄存器区)

    否则 Keil 可能将 main 函数放在 0x0003,导致复位向量(0x0000)处不是 LJMP 指令,芯片上电直接跑飞。

4.2 使用命令行工具进行格式转换与验证(脱离 Keil)

依赖 Keil 图形界面是开发者的懒惰。真正的工程化必须掌握命令行:

  • hex 转 bin(通用)

    # Linux/macOS(需安装 srecord 工具) sudo apt install srecord # Ubuntu/Debian brew install srecord # macOS srec_cat project.hex -o project.bin -binary
  • bin 转 hex(调试反向验证)

    srec_cat project.bin -binary -o project_back.hex -intel
  • 提取 hex 中特定地址段(用于 OTA 差分升级)

    # 提取地址 0x1000~0x1FFF 的 4KB 数据,生成新 hex srec_cat project.hex -offset -0x1000 -crop 0x0000 0x1000 -o segment.hex -intel
  • 计算 bin 文件 CRC32(固件签名)

    # Python 一行搞定 python3 -c "import zlib,sys; print(hex(zlib.crc32(open(sys.argv[1],'rb').read()) & 0xffffffff))" project.bin # 输出:0xabcdef12

注意:Windows 下srec_cat可能因路径空格报错。解决方案:用cd /d "E:\my project"切换目录,再执行命令;或改用更轻量的objcopy(来自 GNU Arm Embedded Toolchain):
arm-none-eabi-objcopy -I ihex -O binary project.hex project.bin

4.3 Bootloader 中解析 bin 的 C 语言实现(STC89C52 示例)

这才是核心硬功夫。以下代码可直接集成到你的 Bootloader 中,资源占用 < 200 字节 RAM:

// 假设 bin 数据已通过 UART 接收并存入 buffer[1024] // buffer_size 是实际接收字节数 void flash_write_bin(uint8_t *buffer, uint16_t buffer_size) { uint16_t addr = 0x0000; // 从 0x0000 开始写入 uint16_t i; // 关闭中断,防止 Flash 操作被打断 EA = 0; for (i = 0; i < buffer_size; i++) { // 每次写入一个字节到 addr 地址 // STC89C52 Flash 写入需先擦除扇区,此处简化,假设已擦除 ISP_IAP_ADDRH = (addr >> 8) & 0xFF; ISP_IAP_ADDRL = addr & 0xFF; ISP_IAP_DATA = buffer[i]; // 触发 ISP 写入(具体时序参考 STC 数据手册) ISP_CONTR = 0x83; // 使能 ISP,等待时间 ISP_CMD = 0x02; // 写命令 _nop_(); _nop_(); _nop_(); ISP_TRIG = 0x46; // 触发 ISP_TRIG = 0xB9; _nop_(); _nop_(); _nop_(); // 等待写入完成(实际需加超时) while (ISP_CONTR & 0x80); addr++; // 地址自增 } EA = 1; // 恢复中断 }

关键点解析:

  • 没有地址解析逻辑:因为 bin 是纯字节流,buffer[0]就是地址0x0000buffer[1]就是0x0001,天然对齐;
  • 无需校验和验证:校验应在上位机发送前完成,Bootloader 层只做“信任写入”,否则会增加复杂度;
  • 扇区擦除前置:真实项目中,必须在写入前调用ISP_CMD = 0x03擦除目标扇区(STC89C52 扇区大小为 512 字节),否则写入无效。

4.4 烧录实操避坑指南:那些让你加班到凌晨的细节

  • Keil 烧录失败,提示 “Verify Failed”
    90% 是因为你勾选了 “Verify Code Download”,但芯片 Flash 有坏块,或供电不稳导致读回数据错误。临时解决方案:取消勾选,或降低 ISP 时钟频率(在 STC-ISP 中设置为 “Slow” 模式)。

  • 串口烧录时,电脑端显示 “Sync Error”
    不是线没接好,而是单片机未进入 ISP 模式。STC 芯片需在上电瞬间拉低 P3.0(RXD),持续 > 100ms。很多新手用 USB 转 TTL 模块,模块的 DTR/RTS 引脚未正确连接到单片机的 RST 和 P3.0。正确接法:USB-TTL 的 DTR→单片机 RST(加 104 电容),RTS→P3.0(加 104 电容),形成自动冷启动+ISP 进入。

  • 烧录后程序不运行,用示波器测 P1.0 无波形
    检查test.bin大小。如果 bin 文件大小为 0,说明 Keil 编译虽成功,但 Linker 未生成任何代码段——常见原因是main()函数名拼错(如mian()),或未添加启动文件STARTUP.A51

  • OTA 升级后,新固件跑飞
    绝大概率是中断向量表未重映射。C51 默认中断向量在 0x0003、0x000B 等固定地址。如果你的 Bootloader 占用 0x0000~0x07FF,而 Application 从 0x0800 开始,那么 Application 的中断向量表必须复制到 0x0000~0x0023 地址(即覆盖 Bootloader 的向量区)。否则 CPU 响应中断时,还是会跳转到 Bootloader 区域执行垃圾指令。

5. 常见问题与排查技巧实录:血泪教训总结

5.1 问题速查表:5 分钟定位故障根源

现象最可能原因快速验证方法解决方案
Keil 生成的 hex 文件,用 STC-ISP 烧录失败,提示 “File Format Error”hex 文件包含扩展地址记录(:04000005xxxxxx),STC-ISP 版本过旧不支持用记事本打开 hex,搜索:04000005;或用head -n 10 project.hex查看前 10 行升级 STC-ISP 到最新版;或在 Keil 中关闭 “Use Extended Linear Address Records”(Output 选项卡底部)
烧录成功,但单片机上电不运行,用万用表测晶振无波形晶振起振电容值错误,或焊盘虚焊直接更换为 22pF 电容测试;或用镊子轻压晶振两端重新焊接晶振,确保焊点饱满;检查原理图电容值是否匹配晶振标称负载电容
bin 文件用十六进制编辑器打开,开头是75 90 FF,但用objdump -m i8051 -b binary -D project.bin反汇编却显示乱码objdump 默认按 x86 架构反汇编,未指定 i8051objdump -m i8051 -b binary -D -mi8051 project.bin(注意-mi8051正确指定架构:-m i8051,并确保 bin 文件地址从 0x0000 开始
OTA 升级后,Bootloader 无法跳转到 ApplicationApplication 的复位向量(0x0000 地址)不是 LJMP 指令用 `xxd -g1 project.binhead -n 1查看前 3 字节,应为02 xx xx`(LJMP opcode 0x02)

5.2 独家避坑技巧:教科书里绝不会写的实战经验

  • 技巧 1:用 Excel 快速比对 hex 地址连续性
    将 hex 文件拖入 Excel,用“数据→分列→按字符宽度”,每 2 字符一列。第 1 列是:,第 2-3 列是LL,第 4-7 列是AAAA。在新列用公式=HEX2DEC(D2&E2)把地址转为十进制,再用条件格式高亮“地址差 ≠ 上一行数据长度”的行——这就是地址跳空或重叠的铁证。

  • 技巧 2:Keil 生成的 bin 文件,为什么比 hex 计算出的理论大小大?
    因为 Keil 默认将整个 ROM 区域(如 0x0000~0x1FFF)全部展开。用ls -l project.bin查看文件大小,再用python3 -c "print(0x2000)"计算理论大小,两者相等即正常。若 bin 更大,说明 Linker 设置的 ROM Size 错误。

  • 技巧 3:烧录器显示 “Success”,但单片机行为异常,怀疑 Flash 写入错误
    不要重烧!立即用烧录器的 “Read Back” 功能,将 Flash 内容读出为readback.bin,再用diff project.bin readback.bin对比。如果 diff 有输出,说明写入过程有误——此时检查 VCC 是否稳定(用示波器看纹波 < 50mV),或降低烧录速度。

  • 技巧 4:C51 工程中,为什么code关键字定义的数组,在 hex 文件中找不到对应数据?
    因为 Keil 默认将code数组放在CODE段,但 Linker 脚本中未为其分配地址空间。解决方案:在.lnk文件中添加CODE (0x2000),并将数组声明为unsigned char code my_table[] = {1,2,3};,这样它就会出现在 hex 的:10200000...记录中。

5.3 高阶场景:当 hex 遇上加密与签名

在商业产品中,固件安全至关重要。你不能让竞争对手轻易 dump 出你的 bin 文件:

  • STC 加密:在 STC-ISP 中勾选 “Encrypt MCU”,芯片会启用硬件加密,读出的 Flash 数据全为 0x00。但注意:加密后无法再通过串口 ISP 升级,必须用专用编程器。

  • 自定义签名:在生成 bin 后,用 Python 添加 16 字节签名头:

    with open("project.bin", "rb") as f: data = f.read() signature = b"MYPROD\x00\x00\x00\x00\x00\x00\x00\x00" # 16 字节占位 crc32 = zlib.crc32(data) & 0xFFFFFFFF header = signature + crc32.to_bytes(4, 'big') with open("signed.bin", "wb") as f: f.write(header + data)

    Bootloader 启动时,先读取 header,验证 CRC32,再跳转。这样即使 bin 文件被窃取,没有私钥也无法伪造签名。

我在江科大带学生做智能门禁系统时,就用这套签名机制。有个学生偷偷把固件发给代工厂,结果对方用假 bin 替换了我们的算法模块,门禁刷脸失效。我们通过签名验证当场抓包,避免了批量召回。

6. 最后一点个人体会:别把 bin 和 hex 当成两个文件,它们是一个硬币的两面

我干这行十多年,见过太多人把 bin 和 hex 当成互斥选项,非此即彼。其实根本不是。它们就像 DNA 的双螺旋:hex 是编码链(sense strand),负责信息传递和纠错;bin 是模板链(antisense strand),负责实际执行和复制。你在 Keil 里敲下P1 = 0xFF;,编译器先把它变成机器码(bin 的本质),再为了安全可靠地传送给烧录器,给它套上地址、校验、类型的外衣(hex 的本质)。这个过程没有高下,只有分工。

所以,下次当你再看到project.hexproject.bin并排躺在工程目录里,请别再问“该用哪个”。你要问的是:“此刻,我的数据正要去往哪里?是去烧录器的缓冲区,还是去 Bootloader 的 Flash?是给人看,还是给机器跑?”——答案自然浮现。我建议你马上打开手边的 Keil 工程,关掉图形界面,打开终端,亲手运行一遍srec_cat,用xxd对比 hex 和 bin。不用十分钟,你就能亲手触摸到单片机世界的底层脉搏。那感觉,比任何教程都真实。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询