☰
嵌入式开发必懂:hex、bin、axf三种固件文件的区别与选择
2026/9/30 23:17:38 网站建设 项目流程

干了这么多年嵌入式,经常有新人拿着工程问我:“我编译完到底该烧哪个文件?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字符起始标志
LL2字符本行数据字节数,一个字节是2个十六进制字符
AAAA4字符本行数据的起始地址(16位)
TT2字符记录类型
HH...HH可变数据区,LL个字节
CC2字符校验和

记录类型TT最常用的是:

类型码名称作用
00Data Record普通数据记录,存储程序内容
01End of File Record文件结束标记
02Extended Segment Address Record扩展段地址记录,用于8086模式地址扩展
03Start Segment Address Record起始段地址记录,一般少见
04Extended Linear Address Record扩展线性地址记录,设置高16位地址
05Start 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的

我们常说的“编译”,在嵌入式工具链里其实包含四个阶段:

  1. 预处理:展开头文件、宏替换、条件编译。常见问题比如头文件路径配错、宏没定义、条件编译分支选错。
  2. 编译:C/C++源码变成汇编文件(.s或.asm),这个阶段做语法分析、类型检查、优化。
  3. 汇编:汇编文件变成目标文件(.o),每个.o里都是相对地址的机器码和符号。
  4. 链接:把多个.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文件报错”,我就把自己踩过的三种坑列出来:

  1. 路径和文件名带中文或空格。Keil安装目录、工程目录只要出现非ASCII字符,链接器生成的axf路径就可能带乱码。出错信息往往是cannot open file 'xxx.axf'。解决办法就是全英文路径,干净利落。
  2. 内存溢出导致链接失败。如果代码量超过芯片Flash或RAM大小,链接器会直接报L6220E: Region ... overflowed by ... bytes。这时候axf不会被生成。但很多人不看Build Output的完整日志,只看到“AXF: Error”就慌了。其实滚动上去看,真正的失败原因是溢出、未定义符号或重复定义。
  3. 调试器版本太老或芯片型号选错。有时候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:

  1. 在Process窗口里双击Generate Programming File,先在Process Properties里把Programming File Format设为Binary。
  2. 重新运行Generate Programming File,输出目录会出现.bin。
  3. 如果要用烧写器直接烧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,怎么排查

我总结了四个排查顺序,新手可以直接照抄:

  1. 看Build Output窗口最底部的完整错误信息。很多所谓的“axf文件报错”其实是“上一行的链接错误导致axf没有生成”。
  2. 检查Output目录下是否存在axf。如果存在但调试器打不开,多半是路径/文件名问题。
  3. 检查调试器配置。J-Link或ST-Link在Settings里选的是不是“Application”而不是“CPU DLL”或者“Flash”。
  4. 重新编译一次,确定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块程序启动异常。回读校验救回了一整批货的时间。

以上这些都是我在真实项目中踩过、查过、并最终沉淀下来的经验。文件格式看着基础,但越是基础的东西,越值得我们彻底搞透。希望这篇文章能帮你少走一些弯路,尤其是在从开发环境向生产环境交接的时候,文件选型和格式理解会直接决定你加不加班。

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

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

立即咨询