大家好,我是学电子的小白白。
熟悉单片机开发的朋友,应该经常见到*.hex后缀的文件,它是单片机和嵌入式工程编译输出的一种常见的目标文件格式(比如keil就能编译输出hex文件),通过烧写工具把它下载到单片机中,程序就能在芯片中运行。
有些时候,比如我们在自己实现IAP时,又需要把编译后的目标文件转成*.bin文件的格式,才能往单片机中传输,之后bootloader程序会将接收到的bin文件固化到芯片中。
那么,hex文件和bin文件有什么区别呢,为什么有时用hex格式有时用bin格式?本篇文章就带大家来了解一下。
1)hex文件格式
这里,我在keil中编写了一个极简的LED闪烁程序,在keil中需要勾选输出hex设置:
如果不勾选,只会输出默认的axf格式目标文件。
编译之后,就可以输出hex格式的目标文件了,我们用普通的文本编辑器就可以打开hex目标文件,可以看到如下内容:
我们解释一下文件的内容:
a)hex文件内部是以文本格式来存储内容的,每行以冒号(:)起始,后面每两个字母是一个8bit的16进制数;
b)每行的格式:BBAAAATTD……DCC;
BB表示本行数据的长度;AAAA表示本行数据存储的地址;TT表示数据类型;DD的长度可长可短,是实际的数据;CC是校验和;
我们以第一行为例:020000040800F2
02是数据长度,也就是后面的实际数据“0800”的长度,2字节;
0000表示地址0;
04是数据类型,后面在详细解释;
0800是实际的数据;
F2是校验和,计算方法是,本行所有字节累加和(累加和只用低8位)取反再加1,即计算过程为0xF2=0x01 + not(0x02+0x04+0x08);
c)这里需要补充说明的是TT表示的数据类型
数据类型一共有6种形式:
'00'数据记录:用来记录数据,HEX文件的大部分记录都是数据记录;
'01'文件结束记录:用来标识文件结束,放在文件的最后,标识HEX文件的结尾;
'02'扩展段地址记录:用来标识扩展段地址的记录;
'03'开始段地址记录:开始段地址记录;
'04'扩展线性地址记录:用来标识扩展线性地址的记录;
'05'开始线性地址记录:开始线性地址记录;
这里仍然举几个例子说明,第一行:020000040800F2的数据类型是04,即扩展线性地址记录,表示的是这样一行的内容是地址的高位,也即DD区域表示地址高位为0x0800;当地址长度超过16bit时,就需要扩展线性地址记录来声明高位地址;
第二行:10000000000400200900000800F009F800F01CF8C6,数据类型是00,也就是数据记录,那么它的DD区域就是数据;同时,它的地址区域是0000,那么就表示,这一行的数据应该存在0000地址中;再结合上一行的扩展线性地址记录,它实际存储的地址应该是0x08000000。熟悉stm32单片机的朋友可能清楚,flash起始地址就是0x08000000:
第三行:1000100000F026F800F01EF800F022F8F6E703B52D,格式与第二行一样,但是地址不同,表示这一行的数据存在0x08000010地址中;我们发现这两地址相隔0x10,刚好是第一行的数据内容实际长度。
倒数第三行,格式与第二、三行也是一样的,只是长度不足0x10,只有0x08,这是已经到了实际内容的末尾。
Hex文件内大部分都是这种数据记录。
d)最后两行
:0400000508000009E6
:00000001FF
我们先解释末尾一行,:00000001FF,这行是表示文件结尾,所有的hex文件最后一行都可以是这个。
倒数第二行,数据类型是05,开始线性地址记录,其实它表示的是一个函数入口地址,但是这个函数地址并不会影响实际烧写到flash中的内容,我们可以不管它,MDK官方的解释是大多数情况下可以忽略它:
The Start Linear Address specifies the address of the __main (pre-main) function but not the address of the startup code which usually calls __main after calling SystemInit(). An odd linear start address specifies that __main is compiled for the Thumb instruction set.
The Start Linear Address Record can appear anywhere in hex file. In most cases this record can be ignored because it does not contain information which is needed to program flash memory.
到这里,hex文件中常见的一些格式就介绍完了,如果需要研究更深入,可以通过http://www.keil.com/support/docs/1584/
找到keil官方的解析(这个链接好像已经404了,可以在文末关注公*众*号找到保存的原文档)。
我们在做有IAP功能的项目时,有时需要把boot和app两段代码合并以后烧写,这样可以大大简化操作步骤,此时,可以把两个hex文件手动合并。
操作方法是,把其中一个hex文件最后的两行(开始线性地址记录、文件结束记录)删除:
再把另一个hex文件的所有内容都复制粘贴到其后,就可以了。
但是要注意,两个文件的地址区不能有重叠!
2)bin文件格式
相比hex文件,bin文件格式就简单得多了,它直接就是保存的需要烧写的目标文件内容,是没有任何附加格式的原始二进制文件。
我们把上一个工程设置为输出bin文件:
再编译输出,可以同时输出bin格式的目标文件。
使用十六进制文件编辑器打开,可以看到如下内容:
对比之前的hex文件格式,发现这些内容正好对应了hex文件中所有“数据记录”行中的实际数据内容。没有地址、校验、文件结尾等等一些附加内容。
3)hex文件和bin文件的对比和转换
通过前述的解释,我们了解了,bin文件只有最原始的程序数据,它与保存在单片机flash中的原始值是一样的;hex文件中,则除了程序数据外,还包含了地址、校验等等一些信息。
所以,一般在自己设计IAP程序时,直接传输bin文件可以简化bootloader软件的设计,减少一些处理过程,缩小boot程序的体积,但是要注意需要事先规定写入的地址;而通过上位机软件和下载器烧写时,使用hex文件,可以简化操作、增加可靠性。
一般软件IDE都可以生成hex文件,有的也能生成bin文件,如上文中提到的keil;st公司出品的STM32CubeProgrammer也可以将hex转bin。
如果软件没有这个功能,我们也不用重复造轮子,有很多前辈大佬已经做了转换工具,我们可以使用现成的。
我试用了多种,选了比较好用的一种hex转bin文件工具推荐给大家:
这个软件由“不咸不要钱”大佬编写,该大佬的软件开源地址:GitHub - mian2018/CSharp_Hex2Bin: 嵌入式 hex转bin
如果打不开,我在网盘里保存了一份,可以在我的公众号里找到网盘下载地址。
想要把bin文件转换为hex文件,就需要增加一些内容了,因为hex文件中比bin文件多的内容主要就是地址信息。
这里我也找了一个软件来转换,需要敲命令行,输入地址和文件名:
源码如下,也可在我的公众号里找到网盘下载地址。
/*
使用方法 : bin2hex -b adress filename output
-b : 指示hex文件起始地址
address : hex文件的起始地址(输入4字节十六进制地址)
filename : 待转换的文件名
output : 输出文件名
示例 : bin2hex -b 0x08000000 rom.bin out.hex
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
FILE *fp_read; /* 待读取文件句柄 */
FILE *fp_write; /* 待写入文件句柄 */
unsigned long start_adr; /* 转换成Hex格式的起始地址 */
unsigned short cur_base; /* 转换成Hex格式的当前地址高16位 */
unsigned short cur_offset; /* 转换成Hex格式的当前地址低16位 */
unsigned char read_buf[16];
unsigned char write_buf[48];
void calc_start_adr (char *buf)
{
unsigned int len,i;
char *str;
char c;
len = strlen(buf);
if ((buf[0] == '0') && ((buf[1] == 'x')||(buf[1] == 'X')))
{
buf[1] = '0';
}
for(i=0; i<len; i++)
{
c = buf[i];
if ((c >= '0') && (c <= '9'))
{}
else if((c >= 'A') && (c <= 'F'))
{}
else if((c >= 'a') && (c <= 'f'))
{}
else
{
printf ("Invalid argument.\n");
exit (-1);
}
}
/*十六进制字符串转换为地址*/
start_adr = strtol(buf, &str, 16);
cur_base = (unsigned short)(start_adr >> 16);
cur_offset = (unsigned short)start_adr;
}
void start_convert (void)
{
unsigned char cnt;
unsigned char read_num;
unsigned char cksum, highc, lowc;
/* 设置当前地址高16位 */
highc = cur_base >> 8;
lowc = (unsigned char)cur_base;
cksum = 2 + 4 + highc + lowc;
cksum = 0xFF - cksum;
cksum = cksum + 1;
sprintf (write_buf, ":02000004%04X%02X", cur_base, cksum);
write_buf[15] = 0x0D;
write_buf[16] = 0x0A;
fwrite (write_buf, 1, 17, fp_write);
read_num = fread (read_buf, 1, 16, fp_read);
while (read_num == 16)
{
/* 写入读取的16字节 */
highc = cur_offset >> 8;
lowc = (unsigned char)cur_offset;
cksum = 0x10 + highc + lowc;
for (cnt=0; cnt<16; cnt++)
{
cksum += read_buf[cnt];
}
cksum = 0xFF - cksum;
cksum = cksum + 1;
sprintf (write_buf, ":10%02X%02X00%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
highc, lowc,
read_buf[0], read_buf[1], read_buf[2], read_buf[3],
read_buf[4], read_buf[5], read_buf[6], read_buf[7],
read_buf[8], read_buf[9], read_buf[10], read_buf[11],
read_buf[12], read_buf[13], read_buf[14], read_buf[15],
cksum);
write_buf[43] = 0x0D;
write_buf[44] = 0x0A;
fwrite (write_buf, 1, 45, fp_write);
/* 计算当前地址低16位,当越限时写入当前地址高16位 */
if (cur_offset == 65520)
{
cur_offset = 0;
cur_base ++;
highc = cur_base >> 8;
lowc = (unsigned char)cur_base;
cksum = 2 + 4 + highc + lowc;
cksum = 0xFF - cksum;
cksum = cksum + 1;
sprintf (write_buf, ":02000004%04X%02X", cur_base, cksum);
write_buf[15] = 0x0D;
write_buf[16] = 0x0A;
fwrite (write_buf, 1, 17, fp_write);
}
else
{
cur_offset += 16;
}
read_num = fread (read_buf,1,16,fp_read);
}
/* 写入剩余的字节 */
if (read_num)
{
highc = cur_offset >> 8;
lowc = (unsigned char)cur_offset;
cksum = read_num + highc + lowc;
for (cnt=0; cnt<read_num; cnt++)
{
cksum += read_buf[cnt];
}
cksum = 0xFF - cksum;
cksum = cksum + 1;
sprintf (write_buf, ":%02X%02X%02X00", read_num, highc, lowc);
for (cnt=0; cnt<read_num; cnt++)
{
sprintf (&write_buf[9 + cnt * 2], "%02X", read_buf[cnt]);
}
sprintf (&write_buf[9 + cnt * 2], "%02X", cksum);
write_buf[11 + read_num * 2] = 0x0D;
write_buf[12 + read_num * 2] = 0x0A;
fwrite (write_buf, 1, 13 + read_num * 2, fp_write);
}
/* 写入终止序列 */
sprintf (write_buf, ":00000001FF");
write_buf[11] = 0x0D;
write_buf[12] = 0x0A;
fwrite (write_buf, 1, 13, fp_write);
}
int main (int argc, char *argv[])
{
if (argc != 5)
{
printf ("Usage : %s -b address filename output\n", argv[0]);
printf ("-b : indicate the starting address convert to.\n");
printf ("address : starting address.\n");
printf ("filename : file to be converted.\n");
printf ("output : file to output\n");
printf ("example : %s -b 0x08003000 input.bin output.hex\n", argv[0]);
return -1;
}
if (strcmp (argv[1], "-b"))
{
printf ("Invalid argument.\n");
return -1;
};
fp_read = fopen (argv[3], "rb");
if (fp_read == NULL)
{
printf ("Can't open file %s", argv[3]);
return -1;
}
fp_write = fopen (argv[4], "wb");
if (fp_write == NULL)
{
printf ("Can't create file %s",argv[4]);
return -1;
}
calc_start_adr (argv[2]);
start_convert ();
fclose (fp_read);
fclose (fp_write);
printf("Convert Seccessfully!\n");
return 0;
}
好了,本节内容就分享到这里了。
如果觉得有用,欢迎大家关注我,下载相关的学习资源和软件代码: