[本文发布地址https://blog.youkuaiyun.com/Stack_/article/details/124683892,未经允许不得转载。转载须注明出处]
一、Hex格式
【Intel英特尔Hex格式官方文档】
【什么是Hex文件格式?】
除每行开头的 ‘:’ 外,其它均是2字节表示一个十六进制数。例如02表示0x02,F2表示0xF2
行开头:每行以ASCII的 : 为开头 |
长度:标识数据区域的字节数 |
地址:相对于基地址的偏移地址 |
类型标识:有6种,如下表 |
数据区域 |
行校验: 冒号后、校验和前的值累加,并对和值按位取反后加1 |
数据标识定义 | 指代本行数据类型 |
---|---|
00 | 数据 |
01 | 文件结束 |
02 | 扩展段地址。段地址始于8080CPU那个年代,现在段地址这个概念被弱化甚至尘封,忽略即可 |
03 | 起始段地址 |
04 | 扩展线性地址。 如上图中第1行标识了数据地址的高16位为0x0800即基地址为0x08000000,后面数据类型为数据的行的地址均为偏移地址,偏移地址都要加上基地址才能得出实际的地址,如出现下一个扩展线性地址,则表示指定新的基地址 |
05 | 起始线性地址。 告诉单片机从哪个地址开始运行程序。目前看来这个也可以忽略 |
在Keil MDK中更改程序起始地址 0x8000000 -> 0x8005000 后, hex文件的地址变化如下:
二、Hex数据域
@ 优快云 Tyrion.Mon
for (i = 0; i < 10; i++)
{
printf("%x, ", *((uint32_t *)0x8000000 + i)); //依次读取一个32位值进行打印
}
printf("\r\n");
for (i = 0; i < 20; i++)
{
printf("%x, ", *((uint16_t *)0x8000000 + i)); //依次读取一个16位值进行打印
}
printf("\r\n");
for (i = 0; i < 40; i++)
{
printf("%x, ", *((uint8_t *)0x8000000 + i)); //依次读取一个8位值进行打印
}
代码中加上以上代码以输出Flash地址的值,烧写程序之后,对比hex文件和打印结果
- Flash支持16位半字写入和32位整字写入,且为小端格式。
- 选择半字写入时,向地址0x08000000写入0x5B88,向地址0x08000002写入0x2000,向地址0x08000004写入0x0145,…
- 选择整字写入时,向地址0x08000000写入0x20005B88,向地址0x08000004写入0x08000145,向地址0x08000008写入0x0800014D,…
三、烧写地址设置
在Keil MDK中,对Cortex-M3/4已经默认了程序烧录起始地址是0x08000000,并且可以选择在烧写程序前进行全片擦除或是按需擦除 。那么就可以划分出两块区域分别放Bootloader程序和APP程序。
例如如下划分
Start | Size | |
---|---|---|
Bootloader | 0x8000000 | 0x5000 (20K) |
APP | 0x8005000 | 0x78000 (492K) |
或者 | ||
APP | 0x8000000 | 0x78000 (492K) |
Bootloader | 0x8078000 | 0x5000 (20K) |
Bootloader在前:单片机上电时先运行Bootloader,等待升级指令。无指令时计时,超时进入APP;有升级指令,则开始升级APP即接收升级数据写入Flash,写入完成后跳转APP。
Bootloader在后:单片机上电时直接进入APP,运行过程中接收到升级指令后跳转Bootloader进行APP升级。升级完成后跳转APP。
同样的,在APP中也可升级Bootloader,但一般不需要。
Bootloader的功能尽量单一,大小尽量小,且占用的地址不能与APP的重叠。
需要特别注意的是,如果不勾选“Use Memory Layout from Target Dialog”,则上面选项卡中设置的地址是不起作用的,需要自行修改分散加载文件(未研究)。
四、Bootloader跳转APP
跳转前需要检查APP的栈顶指针是否合法、关闭所有中断、重设栈顶指针、偏移中断向量表(可以在APP中设置)。
定义如下数据
@ 优快云 Tyrion.Mon
#define APP_LOADED_ADDR 0x5000 //APP加载地址
typedef void (*pAppFunction) (void);
pAppFunction application;
__IO uint32_t JumpAddress = NULL;
升级APP完成后,调用如下代码
@ 优快云 Tyrion.Mon
/* APP栈顶指针合法 */
if (0x20000000 == ((*(__IO uint32_t*)APP_LOADED_ADDR) & 0x2FFE0000))
{
__disable_irq(); //关闭所有中断
JumpAddress = *(__IO uint32_t*)(APP_LOADED_ADDR + 4); //Reset_Handler 入口地址
application = (pAppFunction) JumpAddress;
__set_MSP(*(__IO uint32_t*) APP_LOADED_ADDR); //APP程序堆栈指针起始
printf("APP地址%#x, \r\n", APP_LOADED_ADDR);
application(); //跳转到Reset_Handler即APP
}
- 宏 APP_LOADED_ADDR 是APP的起始地址,而APP程序的起始是栈顶指针。
- 为什么加载地址不是0x8005000?
因为选择了从Flash启动,所以Flash被映射到了0x00000000。所以访问0x080050000和0x5000效果是一样的。 - 所以如果正确写入了APP,则 地址 (__IO uint32_t *)APP_LOADED_ADDR 处的栈顶指针必然在该型号单片机的SRAM大小范围内。96K Sram地址范围为0x2000 0000 - 0x2001 7FFF,64K Sram地址范围为0x2000 0000 - 0x2000 FFFF。
- (0x20000000 == ((*(__IO uint32_t *)APP_LOADED_ADDR) & 0x2FFE0000)) 表示的是从地址0x5000取出栈顶指针,通过运算检查栈顶地址是否落在0x2000 0000 - 0x2001 7FFF。64K的检查为(0x20000000 == ((*(__IO uint32_t *)APP_LOADED_ADDR) & 0x2FFF0000))。相当于读出的值直接与Sram范围值比较。不少教程都用这样的复杂的写法,实际上这样写即可 if ( *(__IO uint32_t *)APP_LOADED_ADDR >= 0x2000 0000 && *(__IO uint32_t *)APP_LOADED_ADDR <= SRAM结束地址)。
- 跳转到APP不是跳转栈顶指针,而是跳转Reset_Handler,而Reset_Handler在栈顶指针后,所以需要偏移32位。
- application为一个void类型的函数指针,并将其指向Reset_Handler的地址。application()表示执行指针指向的函数,即Reset_Handler函数。
- 跳转前关闭中断,防止 1)已经跳转到APP了但来了一个中断并落在了Bootloader, 2)中断向量表已偏移但来了中断,就会落在错误的地方
需要特别注意的是,如果使能了Systick等内核级别的中断,用__disable_irq(); 之类的屏蔽中断的语句是无法关闭的,需要去停止Systick的运行。最优的是在bootloader跳转前关闭所有用到的资源以及中断,在APP中重新配置所需要的,以免在Bootloader中用到的中断在APP中没有用到,而缺少对应的中断处理导致中断死循环或HardFault。APP跳转Bootloader亦应如此。
五、Bootloader升级APP
从串口接收到hex文件的一行,校验后写入FLASH。以下是简单的一个写入流程(以GD32F303ZET6 为例)。
@ 优快云 Tyrion.Mon
#define _bytes2short(byte1, byte0) (((((uint16_t)byte1) << 8) & 0xff00) | \
(((uint16_t)byte0) & 0x00ff))
#define UPGRADE_OK 1
#define UPGRADE_FAIL 2
static uint16_t line_data_len;
static uint32_t base_addr;
static uint32_t offset_addr;
/**
* @brief main
* @note 传入hex文件的一行
* @param None
* @retval None
* @author PWH
* @date 2021/3
*/
static uint8_t app_Upgrade(uint8_t *Array, uint32_t len)
{
/* 传入数组:
0 : ':'
1 : 数据域长度
2-3 : 偏移地址
4 : 行数据类型
5 - n :数据域 (不一定存在)
last : 校验和
*/
uint16_t i = 0;
static uint16_t line_num_record = 0;
char line_head = Array[0];
line_data_len = Array[1];
offset_addr = 0x0000ffff & _bytes2short(Array[2], Array[3]); //偏移地址
uint16_t line_data_type = Array[4];
uint8_t data_check = Array[len - 1];
uint32_t write_addr = 0;
static int32_t has_erase_page = -1;
int32_t need_erase_page = 0;
static bool APP_StartAddr_OK = false;
uint8_t ret = UPGRADE_OK;
uint8_t sum = 0;
if (line_head != ':') //行首有误
{
ret = UPGRADE_FAIL;
}
if (line_data_len != (len - 6)) //标识长度和实际长度不对应
{
ret = UPGRADE_FAIL;
}
if (line_data_type > 5) //行类型标识符未知
{
ret = UPGRADE_FAIL;
}
for (i = 1; i < (len - 1); i++)
{
sum += Array[i];
}
sum = (~sum + 1);
if (data_check != sum) //校验不通过
{
ret = UPGRADE_FAIL;
}
if (UPGRADE_OK == ret)
{
switch (line_data_type)
{
case 4: //指定基地址
base_addr = _bytes2short(Array[5], Array[6]);
base_addr = (base_addr << 16) & 0xffff0000;
fmc_unlock(); 【解锁flash。fmc函数是我自己根据库函数重写的,下同。 大家调用库函数即可】
break;
case 0: //写入Flash的数据
write_addr = base_addr | offset_addr;
if (APP_StartAddr_OK == false && ((APP_LOADED_ADDR + FMC_BANK0_START_ADDRESS) == write_addr))
{
APP_StartAddr_OK = true; //APP Flash首地址正确
}
if (APP_StartAddr_OK == true)
{
for (i = 0; i < line_data_len; i += 4)
{
need_erase_page = ((write_addr + i) - FMC_BANK0_START_ADDRESS) / 2048; //算出当前地址所在页(每页2K字节,不同MCU每页字节数可能不一样)
if (need_erase_page != has_erase_page)
{
has_erase_page = need_erase_page;
FMC->FMC_STAT0.Bits.ENDF = 1; //操作结束标志位 操作成功执行后,此位被硬件置1, 软件写1清0
FMC->FMC_STAT0.Bits.WPERR = 1; //擦除/编程保护错误标志位, 在受保护的页上擦除/编程操作时,此位被硬件置1。软件写1清0。
FMC->FMC_STAT0.Bits.PGERR = 1; //编程错误标志位 当被编程区域状态不为0xFFFF时,对闪存编程,此位被硬件置1。软件写1清0。
fmc_erase_page(write_addr + i);
printf("擦除页%d\r\n\r\n", need_erase_page);
FMC->FMC_STAT0.Bits.ENDF = 1;
FMC->FMC_STAT0.Bits.WPERR = 1;
FMC->FMC_STAT0.Bits.PGERR = 1;
}
fmc_program(PROGRAM_MODE_WORD, write_addr + i, *((volatile uint32_t *)(Array + 5 + i)));
FMC->FMC_STAT0.Bits.ENDF = 1;
FMC->FMC_STAT0.Bits.WPERR = 1;
FMC->FMC_STAT0.Bits.PGERR = 1;
//写入后查询
if (*((volatile uint32_t *)(Array + 5 + i)) != (*((volatile uint32_t *)(write_addr + i))))
{
ret = UPGRADE_FAIL;
#if (PRINT_UPGRADE_ERR_INFO == 1)
printf("写入错误 %#x, %#x", *((volatile uint32_t *)(Array + 5 + i)), *((volatile uint32_t *)(write_addr + i)));
#endif
break;
}
#if (PRINT_UPGRADE_DATA_INFO == 1)
printf("写入值 %#x, ", *((volatile uint32_t *)(write_addr + i)));
#endif
}
#if (PRINT_UPGRADE_DATA_INFO == 1)
printf("\r\n");
#endif
}
else
{
ret = UPGRADE_FAIL;
}
break;
case 1: //文件结束
fmc_lock();
#if (PRINT_UPGRADE_DATA_INFO == 1)
printf("APP写入全部完成\r\n");
#endif
break;
default:
break;
}
}
return ret;
}
六、跳转APP后的处理
/* 基地址 */
#define NVIC_VECTTAB_RAM ((uint32_t)0x20000000) /*!< RAM */
#define NVIC_VECTTAB_FLASH ((uint32_t)0x08000000) /*!< Flash */
/* NVIC中断向量表偏移掩码 */
#define NVIC_VECTTAB_OFFSET_MASK ((uint32_t)0x1FFFFF80)
#define USER_BOOT_EXIST 1 //是否有bootloader
#define VECT_TAB_OFFSET 0x5000 //
@ 优快云 Tyrion.Mon
void nvic_vector_table_set(uint32_t nvic_vict_tab, uint32_t offset)
{
SCB->VTOR = nvic_vict_tab | (offset & NVIC_VECTTAB_OFFSET_MASK);
}
int main(void)
{
#if (USER_BOOT_EXIST == 1)
/* APP中断向量表地址偏移 需要在Linker选项卡勾选“USE Memory Layout from Target Dialog”,并修改Target选项卡中的ROM的Start地址 */
nvic_vector_table_set(NVIC_VECTTAB_FLASH, VECT_TAB_OFFSET);
__enable_irq(); //解除中断屏蔽
#endif
/*初始化*/
while(1)
{
}
}
- 如果在Bootloader跳转APP前就设置了中断向量偏移,则略过。
- NVIC_VECTTAB_OFFSET_MASK掩码为0x1FFFFF80是因为VTOR寄存器的格式
- APP地址偏移不能随意,CortexM3 M4权威指南中有如下描述。且还要关注该型号单片机的Flash每页大小,因为写Flash时需要整页擦除,须避免APP和Bootloader代码在同一页。