当前位置:   article > 正文

设计自己的Bootloader(2)-------- Bootloader与应用APP的设计_bootloader和app的代码

bootloader和app的代码

设计自己的Bootloader(2)-------- Bootloader与应用APP的设计

本次所采用的开发平台如下:

MCU :AT32F421K8U7

SRAM:16KB

Flash:64KB

软件开发平台:keil AC6

如果使用别的平台的单片机,原理上也是大同小异,明白原理就可以去设计自己的Bootloader和应用APP

Bootloader与应用APP的空间划分

我们知道Bootloader与应用APP是两个独立的程序被放置在MCU的Flash当中,系统复位之后先去执行Bootloader在再去跳转应用APP,故我们要对Bootlaoder的空间和应用APP的空间进行划分,使有足够的空间去存放两个程序代码。

image-20230904174523545

这个图是AT32F421的地址映射,而实际上其他M3 和 M4芯片的地址映射也是差不多,无非是Flash空间和SRAM空间和外设空间的大小不一样罢了,但是还是对整个0x0000_0000到0xffff_ffff的4GB地址空间进行寻址。

当系统确定从Flash启动后,那么中断向量表就会从0x0000_0000处映射到0x0800_0000处,如果不知道中断量表是啥,就来看看篇 (设计自己的Bootloader(1)-------- Cortex M3与M4的上电启动过程_三石君啊的博客-CSDN博客)。

当确定这点以后,我们知道,代码都是从0x0800_0000里开始执行,那么Bootloader代码就从0x0800_0000处开始存放。

我这里对Bootloader和应用APP的空间划分就如下:

  • Bootloader:0x0800_0000到0x0800_3fff
  • APP:0x0800_4000到0x0800_c000

Bootloader设计

image-20230904180551926

这个是程序大体上的流程,但是其中有很多要点,比如如何划分地址区域,Flash读写的设计,通信协议的设计,如何跳转应用APP,下来我们依次来写。

地址划分

打开keil,点击魔术棒,对flash区域进行设计

image-20230904180856477

接着对一下两个选项进行设置,根据自己划分的大小来设计

image-20230904181141997

这样就做好了地址划分的操作。

Flash读写设计

通过官方的寄存器手册可以看到,AT32F421K8U7的Flash扇区大小是1KB,那么我每次就是对一个扇区的起始地址写入一个扇区的数据。

image-20230904181617382

这个写入的操作很简单,对Flash解锁,擦除扇区,写入数据,对Flash上锁,相关的程序可以在各个平台的BSP的例程中找到。

以下就是我的flash写入操作,不是那么的严谨,比如没有对地址合法性进行判断。

void flash_8bit_write(uint32_t write_addr, uint8_t *pbuffer)
{
  uint32_t index,write_data;
  flash_unlock();
  flash_sector_erase(write_addr);
  for(index = 0; index < FLASH_SECTOR_SIZE; index++)
  {
    write_data = pbuffer[index];
    flash_byte_program(write_addr, write_data);
    write_addr += sizeof(uint8_t);
  }
  flash_lock();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

通信协议的设计

image-20230904182343123

这个是我的通信流程,很简单。

数据的组成如下:

typedef struct
{
  uint8_t cmd_head;//帧头
  uint8_t cmd_addr[4];//扇区首地址
  uint8_t cmd_buf[FLASH_SECTOR_SIZE];//一个扇区的大小
  uint8_t cmd_check[2];//crc16校验
} iap_data_group_type;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

我的通信接口选择的串口,接收方式是 串口DMA空闲中断

在实际设计的时候要注意加入对接收信息的应答,超时机制,让上位机明白是哪里出错了,方便Debug排除错误。

应答数据:

typedef enum 
{
	back_ok,//请求下一帧
	back_end,//IAP结束
	back_error_cmd_head,//帧头错误
	back_error_data,//数据内容错误
	back_error_timeout,//超时错误
}iap_back_type;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在串口中对数据的长度进行判断

void USART1_IRQHandler()
{
  if(usart_flag_get(USART1, USART_IDLEF_FLAG))
  {
    	usart_data_receive(USART1);
		dma_channel_enable(DMA1_CHANNEL3 , FALSE);//关闭dma通道
		int length = sizeof(iap_data_group_type) - DMA1_CHANNEL3->dtcnt;//计算接收长度
		if(length == sizeof(iap_data_group_type))
		{
			iap_receive = 1;
		}
		else//接收长度有问题
		{
			usart_set_dma((uint32_t)(&iap_data),sizeof(iap_data_group_type));//开启DMA接收
			usart_back_data(back_error_data);
		}
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

超时机制

void TMR3_GLOBAL_IRQHandler(void)
{
  if(tmr_flag_get(TMR3, TMR_OVF_FLAG) == SET)
  {
    	tmr_flag_clear(TMR3, TMR_OVF_FLAG);
		timeout_cnt++;
		if(timeout_cnt > 5)//超时判断
		{
			tmr_counter_enable(TMR3, FALSE);
			tmr_counter_value_set(TMR3,0);
		}
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

Bootloader的通信

void iap_handle()
{
	while(iap_receive == 0)
	{
		if(timeout_cnt > 5)//超时判断
		{
			timeout_cnt = 0;
			tmr_counter_enable(TMR3, TRUE);
			usart_set_dma((uint32_t)(&iap_data),sizeof(iap_data_group_type));//开启DMA接收
			usart_back_data(back_error_timeout);
		}
	}
	iap_receive = 0;//清空标志位
	timeout_cnt = 0;//超时清空
	uint8_t check_sum = 0;
	if(iap_data.cmd_head == CMD_HEAD)//帧头是否正确
	{
		/* 检查数据内容 */
		uint16_t crc = CalculateCRC16((uint8_t*)(&iap_data));
		uint16_t check_data = (uint16_t)(iap_data.cmd_check[0]<<8) | iap_data.cmd_check[1];
		if(check_data == crc)//数据正确
		{
			uint32_t iap_address = 0;
			iap_address = (uint32_t)iap_data.cmd_addr[0]<<24 ;
			iap_address = iap_address | (((uint32_t)iap_data.cmd_addr[1]<<16) & 0xffff0000);
			iap_address = iap_address | (((uint32_t)iap_data.cmd_addr[2]<<8) & 0xffffff00);
			iap_address = iap_address | (uint32_t)iap_data.cmd_addr[3];
			if(iap_address != 0xffffffff)//非结束地址
			{
				memcpy((void*)src_flash_buf , iap_data.cmd_buf , FLASH_SECTOR_SIZE);//拷贝到flash缓冲
				memset((void*)(&iap_data),0,sizeof(iap_data_group_type));//清空iap_data
				usart_set_dma((uint32_t)(&iap_data),sizeof(iap_data_group_type));//开启DMA接收
				usart_back_data(back_ok);//返回正确,请求下一帧
				flash_8bit_write(iap_address,src_flash_buf);//写入flash
			}
			else//为0xffffffff,IAP结束,跳转APP
			{
				memset((void*)src_flash_buf , 0 , FLASH_SECTOR_SIZE);//拷贝到flash缓冲
				flash_8bit_write_sector(14,src_flash_buf);
				usart_back_data(back_end);
				crm_reset();
				app_load(APP_START_ADDRESS);
			}
		}
		else//数据内容错误
		{
			memset((void*)(&iap_data),0,sizeof(iap_data_group_type));//清空iap_data
			usart_set_dma((uint32_t)(&iap_data),sizeof(iap_data_group_type));//开启DMA接收
			usart_back_data(back_error_data);//返回错误,重新请求该帧
		}
	}
	else//帧头错误
	{
		memset((void*)(&iap_data),0,sizeof(iap_data_group_type));//清空iap_data
		usart_set_dma((uint32_t)(&iap_data),sizeof(iap_data_group_type));//开启DMA接收
		usart_back_data(back_error_cmd_head);//返回错误,重新请求该帧
	}

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59

应用APP的跳转

当IAP结束后,就是要跳转到我们的应用代码,这个时候就要思考,怎么跳转呢?

从汇编的角度思考,就是将应用APP起始地址的指令执行就可以了,就是将起始地址传递给 PC程序计数器 就可以了,但是要用到汇编,很麻烦?有无更加简单的方法呢?

回想一下,我们在调用函数的时候,其实也是做了类似的操作,当函数调用时候,就是对相关的寄存器进行压栈,然后将调用函数的地址传递给 PC程序计数器 ,所以我们就可以设置一个函数指针,将应用APP的地址传递给这个指针,然后调用即可。

但是还需要注意,由于我们跳转的应用APP,而应用APP的中断向量表仍然是Bootloader程序的中断向量表,一旦有相关中断发生,程序必然会跳回Bootloader中,带来难以想象的后果。所以我们在跳转前要关闭所有的中断,同时把堆栈指针的地址设置到应用APP中,才能放心跳转。

以下是app跳转的代码

void app_load(uint32_t app_addr)
{
	/* 关闭时钟 */
	crm_periph_clock_enable(CRM_TMR3_PERIPH_CLOCK, FALSE);
	crm_periph_clock_enable(CRM_USART1_PERIPH_CLOCK, FALSE);
	crm_periph_clock_enable(CRM_GPIOA_PERIPH_CLOCK, FALSE);

	/* 关闭使用到的中断 */
	nvic_irq_disable(USART1_IRQn);
	nvic_irq_disable(TMR3_GLOBAL_IRQn);
	__NVIC_ClearPendingIRQ(USART1_IRQn);
	__NVIC_ClearPendingIRQ(TMR3_GLOBAL_IRQn);

  /* 设置应用APP的Reset_Handler地址 */
	jump_to_app = (iapjmp)*(uint32_t*)(app_addr + 4);
	/* 设置应用程序的栈顶指针地址 */
	__set_MSP(*(uint32_t*)app_addr);
	jump_to_app();//跳转
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

应用APP

到应用APP中已经非常简单了,总共操作如下:

  • 设置Flash的地址和大小
  • 设置自动化脚本,将.axf文件转化为.bin文件
  • 偏移中断向量表
  • 检测是否要升级

设置Flash的地址和大小

操作和在Bootloader中差不多,也是点击魔法棒设置

image-20230904185416783

设置自动化脚本,将.axf文件转化为.bin文件

在编译生成文件后,系统是产生axf,axf文件是编译默认生成的文件,不仅包含代码数据,而且还包含着调试信息,在MDK里进行debug调试用的就是这个文件,但是没法直接用于下载。

hex文件是包含地址信息,但是需要解析,也是比较麻烦。

bin文件最简单,就是一个二进制文件,只用将其中的二进制数据直接写入到对应的flash地址就可以了。

综上所述,我们直接选择bin文件就可以了,当然axf和hex也是可以,用上位机解析成bin文件就可以了。

但是keil无法直接生成bin文件,需要使用一个应用 fromelf.exe,这个是keil自带的一个应用,使用它和keil自己的自动化操作,就可以在编译结束后,把axf转化成bin了。

具体操作如下,点击魔法棒中的User选项卡。

image-20230904190155027

After Build/Rebuild勾选,在后面加入以下指令:

fromelf.exe --bin --output .\Listings\@L.bin !L
  • 1

完成后,在你编译结束后,Listings 目录下就会出现bin文件了。

偏移中断向量表

当Bootloader跳转到应用APP,假如响应中断,会响应Bootloader中的还是应用APP中的呢?

必然是Bootloader中。因为中断向量表的起始地址是0x0800_0000处,如果要响应APP中的中断,就必须要对中断向量表偏移一个Bootloader的程序大小的地址,这样就可以响应APP中的中断。

如何偏移呢?

在M3,M4内核中有个寄存器是专门负责中断向量的偏移。

image-20230904191216202

当对这个寄存器设置后,那么所有的中断响应都是在0x0800_0000处的中断向量加上这个偏移寄存器的值作为地址,就是该地址上的存放的4字节值就是该中断函数的入口,这样就可以放心响应APP中的中断了!

但是在CMSIS中已经对该函数进行封装,我们直接调用就行

nvic_vector_table_set(NVIC_VECTTAB_FLASH, 0x4000);
  • 1

第二个参数就是我们要偏移的地址。

检测是否要升级

这个就很简单了,我的做法是检测串口是否有收到符合条件的一组指令,有则在Bootloader的最后一个扇区进行相关代码的置位,然后软件复位,重新进入Bootloader进行升级

相关的代码:

void iap_communication_handle()
{
	if(iap_receive == 1)
	{
		if(iap_buf[0] == 0xff && iap_buf[1] == 0xfe && iap_buf[2] == 0xfd && iap_buf[3] == 0xfc)
		{
			iap_flash_buf[0] = UPDATA;
			flash_8bit_write_sector(14 , iap_flash_buf);
			nvic_system_reset();
		}
		else 
		{
			iap_receive = 0;
			usart_set_dma((uint32_t)(&iap_buf),4);
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号