干了这么多年嵌入式,经常有新人拿着工程问我:“我编译完到底该烧哪个文件?hex、bin、axf长得差不多,为什么有时候用这个有时候用那个?”说实话,这三类文件放在一起确实容易让人犯迷糊。它们本质上是同一份程序在不同阶段的三种“载体”,但结构、用途、调试能力完全不同。你要是能把它们的区别和来龙去脉彻底弄明白,从Keil编译到量产烧录、再到在线调试,整个流程里的很多坑都会提前避开。
这篇文章就围绕hex、bin、axf三类文件,从格式原理、生成过程到实际工程里的转换与排错,一条线讲清楚。每个环节我都会附上实际项目里的操作细节和踩坑记录,适合刚入门的新手,也适合已经在用Keil/IAR但从来没深究过文件格式的工程师。
1. 先搞清楚本质:bin是“裸数据”,hex是“带地址的裸数据”,axf是“带调试信息的完整映像”
1.1 为什么bin文件必须烧录到指定地址才能运行
bin文件最直观,它就是纯粹的程序机器码,一个字节挨着一个字节,没有任何附加信息。你用十六进制编辑器打开一个bin文件,看到的全是连续的指令和数据,但里面没有告诉烧录器“该放到哪里”。所以烧录bin时必须手动指定起始地址,比如STM32的0x08000000、许多Cortex-M3/M4平台的0x00000000。地址错了,程序要么根本跑不起来,要么跑起来完全乱套。
我做过一个挺典型的失误:一块板子的Flash起始地址在链接脚本里从0x08000000改成0x08010000,结果只改了代码里的偏移,忘了烧录工具里的下载地址,直接把bin烧回了0x08000000。BootLoader跳过去之后野指针满天飞,查了好几个小时。所以说,bin虽然简单,但地址信息全靠“外部约定”,一旦工程结构复杂,很容易在烧录环节埋雷。
1.2 hex为什么自带地址信息
hex文件的通用叫法是Intel HEX,本质是ASCII文本,按行存储,每一行都包含“起始地址 + 数据长度 + 数据内容 + 校验和”。烧录工具读到hex后,会按行解析,自动把数据写入对应地址,所以不需要你手动指定起始地址。这也是为什么很多工程师说“hex比bin更不容易烧错”,因为地址信息就在文件里躺着。
但需要注意一点:hex的地址机制是“局部地址”。比如STM32的Flash地址是0x08000000,hex里每一行的地址通常会写成0x08000000附近的地址,靠一个特殊记录来扩展高位。这就引出了另一个关键技术点,后面我专门开一节讲Intel HEX的记录类型,尤其是“扩展线性地址记录”,因为网上很多搜索词都在问“hex start linear address record到底干什么用的”。
1.3 axf才是“完全体”,但烧录和量产通常不用它
axf是ARM编译器(ARMCC或AC6)生成的可执行映像文件,内部其实是ELF结构的变种——你可以理解为带调试信息、带符号表、带重定位信息、甚至带局部变量类型描述的“超级bin”。调试器(Keil的ULINK/J-Link、IAR的I-jet)加载axf后,能看到函数名、行号、局部变量,直接单步、打断点、查看内存。这是bin和hex完全做不到的。
但是axf不适合直接烧录到量产产线,原因在于:
- 它的体积通常比bin大很多,携带大量调试信息。
- 它不是纯Flash镜像,头部有ELF文件头、段表、符号表。
- 部分烧录器不直接识别axf作为烧录镜像,需要先用工具转换成bin或hex。
所以在我的日常流程里,axf是“开发阶段的主角”,hex是“中试和小批量烧录的常用格式”,bin是“量产、OTA、BootLoader差分升级时最常用的格式”。三者不是互斥关系,而是按场景依次切换。
2. Intel HEX格式深度解析:从记录类型到校验和
2.1 HEX文件的记录类型,一张表讲明白
很多人用hex文件用了一年,也没真正打开看过内容。其实你用记事本打开hex,第一眼会被吓到:“这啥玩意?”我来把它彻底拆开。
Intel HEX每一行格式为:
:LLAAAATTHHHH...HHCC各段含义:
| 字段 | 长度 | 说明 |
|---|---|---|
: | 1字符 | 起始标志 |
LL | 2字符 | 本行数据字节数,一个字节是2个十六进制字符 |
AAAA | 4字符 | 本行数据的起始地址(16位) |
TT | 2字符 | 记录类型 |
HH...HH | 可变 | 数据区,LL个字节 |
CC | 2字符 | 校验和 |
记录类型TT最常用的是:
| 类型码 | 名称 | 作用 |
|---|---|---|
00 | Data Record | 普通数据记录,存储程序内容 |
01 | End of File Record | 文件结束标记 |
02 | Extended Segment Address Record | 扩展段地址记录,用于8086模式地址扩展 |
03 | Start Segment Address Record | 起始段地址记录,一般少见 |
04 | Extended Linear Address Record | 扩展线性地址记录,设置高16位地址 |
05 | Start Linear Address Record | 起始线性地址记录,CPU入口地址 |
这里重点说04和05。04每条记录后面的数据只有两个字节,比如:
:020000040800F2解析一下:LL=02表示后面有2个数据字节;AAAA=0000表示本行数据区起始地址为0;TT=04表示这是扩展线性地址记录;数据是0800,意味着接下来所有00类型记录的AAAA都要加上0x08000000的高位。也就是说,后面的数据实际地址是0x08000000 + AAAA。这正是为了覆盖STM32这类基地址在0x08000000的芯片而设的。
05记录则标记程序入口地址,比如:
:040000050800019D表示入口地址是0x0800019D。有些烧录器或者调试工具会参考它确定复位后PC的初始值,不过大多数Cortex-M平台的烧录流程里,这个入口地址并不是决定性因素,真正起作用的是向量表里偏移0x04处的复位向量。
2.2 校验和怎么算:一行一行破译hex
校验和CC的计算方式倒不算难:把:后面的所有字节(LL、AAAA、TT、数据区)求和,取低8位,然后取补码,使得总和(含校验和)的低8位等于零。
以这条记录为例:
:0300300002337A1E- LL=03
- AAAA=0030
- TT=00
- 数据=02 33 7A
- 求和:03 + 00 + 30 + 00 + 02 + 33 + 7A = 0xE2
- 校验和:0x100 - 0xE2 = 0x1E,正好是记录末尾的
1E
所以如果哪一行hex被不小心改坏一格,烧录器会直接报校验错误。遇到这个问题别慌,多半不是烧录器坏了,而是hex文件被文本编辑器或者传输过程中改了字符。
2.3 为什么搜索热词里有“hex文件反编译成c语言”
网友经常搜“hex转C语言”“hex反编译”,我要提醒一句:hex文件里只有机器码,没有源码结构。即使你把它加载进IDA Ghidra这类反汇编工具,也只能还原汇编,无法还原你写的那个printf、那个while循环、那个结构体赋值。所谓“反编译成C语言”,在嵌入式领域更多是指“反汇编后人工分析逻辑”,顶多能帮你逆向出大概的时序和寄存器操作,绝不是像变魔术一样把源码变回来。
这背后的原因是编译过程本身是信息丢失的:变量名、注释、宏定义、编译优化后的语句重组,都在生成axf之前就已经被处理掉了。axf里还保留符号和行号,所以拿到axf还能反推一部分;hex是纯净物,连符号都没有,反编译难度高好几个量级。所以不要指望有什么“百分百还原C代码”的工具,这个搜索热词背后其实是不少人对文件格式的信息量差异有误解。
3. axf文件的结构与工程价值:为什么调试离不开它
3.1 你写的代码,是怎么一步步变成axf的
我们常说的“编译”,在嵌入式工具链里其实包含四个阶段:
- 预处理:展开头文件、宏替换、条件编译。常见问题比如头文件路径配错、宏没定义、条件编译分支选错。
- 编译:C/C++源码变成汇编文件(.s或.asm),这个阶段做语法分析、类型检查、优化。
- 汇编:汇编文件变成目标文件(.o),每个.o里都是相对地址的机器码和符号。
- 链接:把多个.o和启动文件、库文件链接起来,解析符号引用,分配绝对地址,生成axf。
axf本质上是一个ELF文件,它包含:
- ELF头:文件类型、目标架构、入口点、程序头表和节头表偏移。
- 节头表:列出所有节(.text、.data、.bss、.rodata),每个节有地址、大小、对齐方式。
- 程序头表:描述如何把节加载到内存里,烧录器其实主要看程序头表来拿“烧录区域”。
- 调试节:.debug_info、.debug_line等,行号、变量类型、调用栈信息都在这。
- 符号表:全局函数名、全局变量名、静态变量名,全都以字符串形式保存在里面。
所以说,你用J-Link下载程序时,如果选择的是axf,那么J-Link会从ELF的程序头里找到.loadable段,把数据提取出来写入Flash;调试时再根据.debug_line把当前PC映射到具体源码行。烧录和调试,一个文件全搞定。
3.2 为什么keil生成的axf会报错,常见的三种坑
很多人在搜索“axf文件报错”,我就把自己踩过的三种坑列出来:
- 路径和文件名带中文或空格。Keil安装目录、工程目录只要出现非ASCII字符,链接器生成的axf路径就可能带乱码。出错信息往往是
cannot open file 'xxx.axf'。解决办法就是全英文路径,干净利落。 - 内存溢出导致链接失败。如果代码量超过芯片Flash或RAM大小,链接器会直接报
L6220E: Region ... overflowed by ... bytes。这时候axf不会被生成。但很多人不看Build Output的完整日志,只看到“AXF: Error”就慌了。其实滚动上去看,真正的失败原因是溢出、未定义符号或重复定义。 - 调试器版本太老或芯片型号选错。有时候axf本身没问题,但调试器加载时报
RDDI-DAP Error或者Mismatched Debug Architecture。这类问题多半出在芯片型号配置和CMSIS-DAP/ST-Link版本不匹配上,先检查Debug设置里选的型号是不是和实际芯片一致。
3.3 axf、map、启动文件三者的关系
除了axf本身,链接过程还会生成一个.map文件,它记录每个符号、每个节的最终地址,以及内存占用情况。排查“怎么ram用这么多”“为什么这个变量地址落在诡异区域”时,直接看.map比看axf更直接。而启动文件(startup_xxx.s)的作用是设置初始栈指针、调用SystemInit、然后跳转到main。它也是在链接阶段决定向量表布局的关键。
我一般拿到一个陌生工程,会先打开.map确认三件事:
- 向量表复位向量是不是0x08000000附近。
.data段是否有合适的加载地址和运行地址(涉及初始化拷贝)。- 栈顶
__initial_sp是否落在RAM合法范围内。
如果这三项都正确,烧录后程序往往能跑;如果跑飞,多数是启动文件或分散加载文件(sct)配置有误。这些在axf生成阶段不会报错,但运行起来就露馅。
4. 实操环节:Keil里怎么生成bin文件,ISE 14.7又怎么生成bin
4.1 Keil中生成bin的几种方法与参数选择
Keil MDK默认只生成hex和axf,bin文件默认不生成。想在Keil里输出bin,最通用的方式是在User选项卡的After Build/Rebuild里加一句fromelf命令。我实际在工程里用的命令是:
fromelf --bin --output=.\Output\project.bin .\Output\project.axf解释一下参数:
--bin:告诉fromelf输出bin格式,这是最核心的开关。--output=xxx.bin:输出文件路径。- 最后一个参数是要转换的axf文件的相对路径。
如果希望输出带CRC校验的镜像、或者要去掉某些段,还可以加--bin --base之类,但常规裸机量产,上面这句足够了。
这里要注意:Keil的fromelf支持一个很有用的变体,叫--bincombined。它在多核芯片工程里会把多个核的镜像合成一个bin,Zynq的双核开发就会用到这个功能。如果你在做Zynq或者带M4核+其他核的芯片,直接搜“zynq双核生成bin文件”,最后落地方案大概率就是fromelf --bincombined。
关于生成bin的时机,一定要选After Build/Rebuild,不要放在Before Build。因为bin是axf的“衍生品”,必须在链接完成后才会出现。
4.2 fromelf命令的坑:分隔符和路径
fromelf的命令行参数在不同MDK版本里写法的容错程度不一样。我试过MDK 5.36之后,路径最好用正斜杠/,反斜杠\有时候会被转义。另外,如果输出路径的目录还没有创建,fromelf不会自动创建目录,所以最好先确认Output目录存在,否则报错再说找不到文件,其实是路径问题。
还有一种情况,有些工程会勾选Create HEX File,然后hex正常生成,但bin一直不出现。这有可能是因为fromelf的输出路径写的是绝对路径,而编译机换了电脑或目录结构改变,路径失效。我的建议是工程内所有文件生成路径都写成相对工程文件的路径,工程移动后重新编译也不会挂。
4.3 ISE 14.7怎么生成bin文件(FPGA方向)
热词里有“ISE14.7怎么生成bin文件”,这个我简单带一句。Xilinx ISE里工程默认生成的是bit文件,要在Generate Programming File阶段用iMPACT或PromGen工具生成bin,或者直接把配置过程保存成二进制镜像。
实际操作里,ISE 14.7下可以用以下方式生成bin:
- 在Process窗口里双击
Generate Programming File,先在Process Properties里把Programming File Format设为Binary。 - 重新运行Generate Programming File,输出目录会出现
.bin。 - 如果要用烧写器直接烧SPI Flash,建议再检查字节顺序(Byte Order)和比特位顺序(Bit Order),FPGA配置需要MSB First。
不过FPGA的bin和ARM的bin不是一个概念。ARM bin烧的是程序执行指令,FPGA bin烧的是配置比特流,是硬件逻辑的“装配图”。别混着理解就行。
4.4 十六进制和十进制的转换:为什么hex转十进制也常被搜
很多新人在看hex文件、调试内存窗口或者分析协议时,需要把hex数值转成十进制。比如你在调试一个传感器,读到的寄存器值是0x1A3C,心里得立刻知道等于6700左右(1A3C = 6716),才能判断采集的数据合不合理。
我的常用姿势是:
- 计算器程序员模式直接切HEX/DEC。
- 命令行里用
printf("%d", 0x1A3C),但注意嵌入式C里printf格式化是%d,如果值是0xFFFFFFFF会输出-1而不是4294967295,要看清楚数据类型。 - Python一行
print(int('0x1A3C', 16))。
这里提醒一下:十六进制转十进制本身不复杂,难的是“地址换算”。比如hex文件里某一行的地址是0800开头,你该知道这是扩展地址的高16位,不是真实的物理地址。老老实实读Type 04记录,把高位拼上,才是真地址。
5. 烧录、调试、量产阶段:文件选型和常见问题排查
5.1 下载算法和烧录文件的关系
用Keil调试时,你选hex还是axf烧录,其实都行,但背后调用的Flash算法不一样。Keil的Flash Download页面里配置了Programming Algorithm,这个算法是烧录器把数据写入Flash的驱动代码。axf和hex最终都会被转换成纯Flash数据后进入算法,所以这块的坑不在于文件格式,而在于算法选错。
举个真实案例:有人把STM32F103C8T6的工程当成F103RCT6来做,Flash算法选成了512KB的型号,结果下载能过,程序能跑一部分,但在Flash超过128KB的位置读回全是0。这跟你用hex还是bin没有关系,纯粹是Flash算法型号不匹配。排查到后来看.map里的代码量并不大,但算法区域的地址范围写错,一样会导致写入失败或写后校验失败。
5.2 典型报错:AXF文件无法打开、找不到AXF,怎么排查
我总结了四个排查顺序,新手可以直接照抄:
- 看Build Output窗口最底部的完整错误信息。很多所谓的“axf文件报错”其实是“上一行的链接错误导致axf没有生成”。
- 检查Output目录下是否存在axf。如果存在但调试器打不开,多半是路径/文件名问题。
- 检查调试器配置。J-Link或ST-Link在Settings里选的是不是“Application”而不是“CPU DLL”或者“Flash”。
- 重新编译一次,确定axf的时间戳是新生成的。有时候Debug配置和Release配置共用输出目录,旧axf匹配不上新源码,也会出诡异问题。
5.3 烧录bin文件时地址偏移的实战记录
我在做BootLoader + App结构的项目时有一段很深刻的经历。BootLoader占0x08000000开始的32KB,App从0x08008000开始。App工程里,链接起始地址要改成0x08008000,同时要把向量表偏移寄存器(SCB->VTOR)设置成0x08008000。这是两个独立的操作,漏掉任何一个都不行。
我最初只改了工程Option里的Target标签页ROM起始地址,忘了在代码里设置VTOR,结果App跑到一半中断就进不了正确回调。后来我在SystemInit或者main开头加上:
SCB->VTOR = 0x08008000;并把中断向量表有真实意义的数据也重映射一遍,整个程序才稳定。
对应到文件层面,每个固件版本我都会同时产出两份bin:
- App工程生成的bin(含地址偏移,直接烧到0x08008000)。
- BootLoader工程生成的bin(烧到0x08000000)。
量产时先用烧录器烧BootLoader+App合成镜像,或者分开两步烧。OTA时只下发App的bin,BootLoader负责接收并写入App区域,然后跳转。bin没有地址信息,所以OTA协议里必须约定好目标地址和长度,这是设计OTA协议时特别要注意的。
5.4 常见问题速查表:hex/bin/axf搜出来的高频问题
| 现象 | 原因 | 处理方案 |
|---|---|---|
| Keil无法生成bin文件 | 没有配置fromelf命令或路径不对 | 在After Build里加fromelf命令并检查输出目录存在 |
| hex烧录成功但程序不运行 | 地址记录被误改、芯片型号不符、Flash算法错误 | 用文本方式检查Type 04记录和Type 00记录地址范围 |
| axf调试时无法单步、断点无效 | 优化等级太高导致代码行与源码错位 | 把Optimization改成-O0或-Og,重新编译 |
| 内存窗口看到的值和源码变量不符 | 访问了外部外设寄存器或volatile缺失 | 给寄存器指针加volatile,确认类型宽度 |
| bin文件直接烧录后跑飞 | 烧录地址错误或向量表没有二次偏移 | 确认链接地址和SCB->VTOR一致 |
| hex转bin后大小不对 | hex里包含非连续段的填充字节 | 使用support分散加载的转换工具,或检查Type 05入口 |
5.5 关于OTA里bin文件“加壳加校验”的经验
这里单独分享一个我认为特别实用的工程技巧:量产OTA下发bin时,不要直接发裸bin。我通常会在固件bin前面追加一个自定义头部,包含魔数、版本号、固件长度、CRC32校验、时间戳。BootLoader收到后先校验魔数和CRC,再检查版本号合法性,最后才搬运到Flash。
为什么这么做?因为裸bin没有边界信息,传输中丢一个字节,接收端根本不知道。加上头部后,哪怕丢包,也能快速判断固件不完整并请求重传。这不是文件格式本身的要求,而是实际工程可靠性需要。很多人只是把hex或bin文件丢给上位机,结果OTA成功率只有七成,加上校验和头部之后,轻松提到99.9%。
6. 我在实际工程里的习惯和小建议
说实话,做嵌入式开发这么多年,文件格式问题表面上看起来小,实际上在关键时刻能卡你三天。我现在的习惯是,每个固件工程在编译完成后,都会自动生成axf、hex、bin三个文件,并且统一放到Output目录下按版本号命名。开发调试用axf,小批量烧录用hex,量产和OTA用bin。这样无论生产那边用什么工具,都能找到合适的镜像文件。
另外我会在版本发版前做一次“bin文件回读校验”:用烧录器把Flash内容读出来,和原始bin逐字节比对。别觉得这一步多余,有一次我发现量产烧录工具把bin的高位地址忽略了,导致3块板子中1块程序启动异常。回读校验救回了一整批货的时间。
以上这些都是我在真实项目中踩过、查过、并最终沉淀下来的经验。文件格式看着基础,但越是基础的东西,越值得我们彻底搞透。希望这篇文章能帮你少走一些弯路,尤其是在从开发环境向生产环境交接的时候,文件选型和格式理解会直接决定你加不加班。