三、03 OTA-BootLoader前置-flash擦除写入-跳转函数编写

flash擦除写入-串口发包

前置

HAL库默认是加锁状态下,需要优先解锁,使用完之后重新加锁
先擦除后写入,F407是以扇区擦除的。
只支持8、16、32、64位写入,默认是小端序

设置B区是32K,也就是扇区0、1,那么A区起始地址就是0x08008000

1.flash扇区擦除

flash擦除APP区域怎么写?
主要有俩步,要先写一个辅助函数获取对应起始地址的扇区号。根据数据手册设计对应扇区大小返回对应扇区。

/* *@根据起始地址返回对应扇区号 * * */uint32_tGetSectorFromAddress(uint32_taddress){if(address<0x08004000)returnFLASH_SECTOR_0;//扇区0elseif(address<0x08008000)returnFLASH_SECTOR_1;elseif(address<0x0800C000)returnFLASH_SECTOR_2;elseif(address<0x08010000)returnFLASH_SECTOR_3;elseif(address<0x08020000)returnFLASH_SECTOR_4;elseif(address<0x08040000)returnFLASH_SECTOR_5;elseif(address<0x08060000)returnFLASH_SECTOR_6;elseif(address<0x08080000)returnFLASH_SECTOR_7;elseif(address<0x080A0000)returnFLASH_SECTOR_8;elseif(address<0x080C0000)returnFLASH_SECTOR_9;elseif(address<0x080E0000)returnFLASH_SECTOR_10;elsereturnFLASH_SECTOR_11;}

然后根据写入的起始扇区地址,也就是0x08008000,以及APP区的大小计算出要擦除几个扇区然后将对应扇区执行擦除。主要公式就是,尾地址=起始地址+APP大小-1,要擦除扇区数=尾扇区号-起始扇区号。起始扇区是0x08004000(也就是扇区2),起始扇区号、尾扇区号就是用GetSectorFromAddress算出。然后配置flash擦除的结构体FLASH_EraseInitTypeDef 进行扇区擦除。

/* *@param 擦除APP区flash,擦除区域固定(APP最大224KB) *@param 在接收到Updata时擦除,会耗时几十毫秒 *@return */staticvoidEraseAppArea(void){uint32_tstart_addr=APP_START_ADDR;//起始扇区地址uint32_tend_addr=APP_START_ADDR+MAX_APP_SIZE-1;//结束扇区地址uint32_tstart_sector=GetSectorFromAddress(start_addr);//起始扇区号uint32_tend_sector=GetSectorFromAddress(end_addr);//结束扇区号uint32_tnb_sectors=end_sector-start_sector+1;//要擦除的扇区数FLASH_EraseInitTypeDef erase_init;//配置擦除参数erase_init.TypeErase=FLASH_TYPEERASE_SECTORS;//选择扇区擦除erase_init.Sector=start_sector;//擦除开始扇区erase_init.NbSectors=nb_sectors;//擦除扇区数erase_init.VoltageRange=FLASH_VOLTAGE_RANGE_3;//3.3V供电uint32_tSectorError=0;//擦除失败扇区号HAL_FLASH_Unlock();//flash解锁if(HAL_FLASHEx_Erase(&erase_init,&SectorError)!=HAL_OK){printf("erase error!");Error_Handler();// 擦除失败,进入死循环//可以后续添加优化处理}HAL_FLASH_Lock();//锁flash}

flash写入的时候要先擦除,那么要在什么时候进行擦除呢?所以要自定义一个握手协议。这个握手协议就是串口先发送一个字符串,比如“Update”,接收字符出然后进行校验,校验通过表示要有OTA更新,那么就要将APP区域进行擦除。
在中断回调中加入if(receive_len == 6 && memcmp(receive_buff,CMD_UPDATE, 6) == 0) erase_flag=true;//接收到更新标志,就可以判断是否要更新了。

/* *@param 串口空闲中断接收数据回调 *@param DMA将缓冲区填满或者检测到空闲状态(电平转换时刻)就会触发回调 *@return */voidHAL_UARTEx_RxEventCallback(UART_HandleTypeDef*huart,uint16_tSize){if(huart->Instance!=USART1)return;HAL_UART_DMAStop(&huart1);//停止DMA,发包间隔是1ms,在开启下一个中断接收之前要保证中间所有流程能在1ms之内执行完receive_len=Size;//实际接收数据长度receive_full_len+=receive_len;receive_flag=true;if(receive_len==6&&memcmp(receive_buff,CMD_UPDATE,6)==0)erase_flag=true;//接收到更新标志elseWrite_Current_Packet_To_Flash();//将包写入到flashmemset(receive_buff,0,RECEIVE_SIZE);//清空缓存区HAL_UARTEx_ReceiveToIdle_DMA(&huart1,receive_buff,RECEIVE_SIZE);//下一个空闲中断接收}/* *@param main函数执行 *@param *@return */voidbootloader_receiveHandle(void){if(receive_full_len>0){printf("LEN:%d\r\n",receive_full_len);//打印累加大小,在主循环中没有其他阻塞操作时,会每个包都打印,但是有阻塞操作就不一定了,仅作为查看接收数据是否匹配receive_full_len=0;//重置}if(erase_flag){EraseAppArea();//执行擦除erase_flag=false;//重置擦除标志flash_write_offset=0;//重置偏移量printf("\r\nERASE OK,Please send file\r\n");}}

2.flash写入数据

因为后续串口写入bin文件,最小单位是俩字节,所以这里使用16位写入也就是半字(F407支持字节写入,如果用字节写入会更加简单)。那么设置的缓冲区是字节大小的,就需要将俩个字节拼接成半字,并且要判断最后剩余一个字节的情况单独拼接。主要公式是:当前写入地址=起始地址+写入数据下表i+偏移地址

当然还会有一些情况,比如说上一个包是5字节,下一个包是6字节,也就是发送的所有包都是不定长状态,那么就需要考虑更多的情况了,需要分情况来拼接半字而不是像下面这样只要判断最后一个包的最后一个字节了。(因为串口发送这里使用固定256字节+1ms空闲发送,也就是说只有最后一个包是不定长状态,所以这个情况这里没有优化)。

需要注意的是,这里flash写入是在中断中完成的写入一包数据加上中断中操作可能要耗时1ms左右(不同板子不同性能),而如果设置串口发送一个包间隔是1ms,可能会造成丢包,可以加大串口发送间隔或者用其他方法。这里经过测试,1ms发送间隔也能正常写入(当然是在实验室环境下),后续可以进一步优化该问题。
代码如下:

/* *@param 将当前收到数据包写入Flash *@param *@return */staticvoidWrite_Current_Packet_To_Flash(void){HAL_FLASH_Unlock();//解锁flashfor(uint16_ti=0;i<receive_len;i+=2)//16位写入(半字){uint32_tflash_addr=APP_START_ADDR+i+flash_write_offset;//写入的地址uint16_tdata16;if(i+1<receive_len)data16=receive_buff[i]|(receive_buff[i+1]<<8);//将缓冲区拼接成16位elsedata16=receive_buff[i]|(0xff<<8);//如果剩下单独一位,小端拼接成16位if(HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD,flash_addr,data16)!=HAL_OK)//半字写入数据{//写入失败后续处理}}flash_write_offset+=receive_len;//记录偏移HAL_FLASH_Lock();//锁flash}

3.跳转函数编写

到这里就是B区的程序差不多写完了,接下来就是写调转函数
主要是下面几个步骤:进行健壮性判断(数据校验)、注销bootloader程序、跳转到A区的复位中断、APP代码修改偏移量以及中断向量表
数据校验:
为了代码的健壮性,肯定要进行数据校验的,这里只是进行一个简单的校验,后续优化可以加入CRC校验等。
主要校验栈顶地址以及复位中断向量表是否正确,也就是写入到flash APP区域的前8个字节

uint32_tapp_stack_ptr=*(volatileuint32_t*)(APP_START_ADDR);//获取栈顶地址的值uint32_tapp_reset_handle=*(volatileuint32_t*)(APP_START_ADDR+4);//复位中断

这是APP区的,栈顶地址开始就是0x20000000,最大也是到0x2001BFF,所以前三位校验就一定是200,所以app_stack_ptr&0xFFF00000)=0x20000000,如果不相等,就肯定是写入错误了。还有就是复位中断校验,复位中断地址是肯定要在APP起始地址到APP区域末尾地址之间的。也就是APP_START_ADDR<app_reset_handle<APP_START_ADDR+MAX_APP_SIZE-1
注销bootloader程序
这个也是为了程序健壮性编写的

__disable_irq();//关全局中断for(inti=0;i<8;i++){NVIC->ICPR[i]=0xFFFFFFFF;// 清除所有挂起标志NVIC->ICER[i]=0xFFFFFFFF;// 禁用所有中断}SysTick->CTRL=0;//关闭SysTick定时器SysTick->LOAD=0;SysTick->VAL=0;HAL_DeInit();//注销HAL库,外设配置,不会注销内核

最重要的是设置堆栈指针以及中断向量表了:

__set_MSP(app_stack_ptr);//设置堆栈指针SCB->VTOR=APP_START_ADDR;//重定向中断向量表

然后使用函数指针跳转即可,完整跳转代码如下:

/* *@param 跳转到A程序 *@param *@return */staticvoidbootloader_Jump_To_App(void){typedefvoid(*pFunc)(void);//函数指针uint32_tapp_stack_ptr=*(volatileuint32_t*)(APP_START_ADDR);//获取栈顶地址的值uint32_tapp_reset_handle=*(volatileuint32_t*)(APP_START_ADDR+4);//复位中断//校验if((app_stack_ptr&0xFFF00000)!=STACK_ADDR){printf("stack addr error!");//栈顶地址错误return;}if((app_reset_handle<APP_START_ADDR)||(app_reset_handle>APP_START_ADDR+MAX_APP_SIZE-1)){printf("reset handle error!");return;}//注销bootloader程序,健壮性__disable_irq();//关全局中断for(inti=0;i<8;i++){NVIC->ICPR[i]=0xFFFFFFFF;// 清除所有挂起标志NVIC->ICER[i]=0xFFFFFFFF;// 禁用所有中断}SysTick->CTRL=0;//关闭SysTick定时器SysTick->LOAD=0;SysTick->VAL=0;HAL_DeInit();//注销HAL库,外设配置,不会注销内核__set_MSP(app_stack_ptr);//设置堆栈指针SCB->VTOR=APP_START_ADDR;//重定向中断向量表//跳转pFunc jump_to_app=(pFunc)app_reset_handle;jump_to_app();//跳转}

APP修改:
APP代码主要修改以下几点:
设置中断向量表:要在main函数相关初始化之前设置好中断向量表:

……………/* USER CODE BEGIN 0 */#defineAPP_START_ADDR0x08008000/* USER CODE END 0 *//** * @brief The application entry point. * @retval int */intmain(void){/* USER CODE BEGIN 1 */__disable_irq();SCB->VTOR=APP_START_ADDR;__enable_irq();/* USER CODE END 1 */…………

修改偏移量:
也就是将VECT_TAB_OFFSET修改成0x8000

修改ROM:
开始地址要设置为0x08008000也就是APP起始地址,Size设置为APP大小,这里设置的224KB。

然后编译得到.bin文件

为了方便跳转测试,这里写一个跳转的握手协议,当串口接收到发送的Jump就跳转APP区。如上面更新的握手协议一个原理。
完整代码:

#ifndef__INT_BOOTLOADER_#define__INT_BOOTLOADER_#include"stm32f4xx_hal.h"#include"USART_driver.h"#include"stdbool.h"#defineRECEIVE_SIZE512//缓冲区大小#defineAPP_START_ADDR0x08008000//A区起始地址#defineMAX_APP_SIZE0x00038000//预留最大APP空间(224KB,扇区2~5)#defineSTACK_ADDR0x20000000//栈顶地址#defineCMD_UPDATE"Update"// 6 字节握手协议,擦除并准备接收#defineJUMP_TO_APP"Jump"//跳转测试voidbootloader_Init(void);voidbootloader_ReceiveHandle(void);voidbootloader_Jump_To_App(void);#endif
#include"Int_bootloader.h"uint8_treceive_buff[RECEIVE_SIZE]={0};//串口缓冲区uint16_treceive_len=0;//接收数据长度uint16_treceive_full_len=0;//接收完整文件的长度uint32_tflash_write_offset=0;//记录当前写入程序的偏移量bool receive_flag=false;//接收数据标志bool erase_flag=false;//扇区擦除标志bool jump_flag=false;//跳转标志/* *@param 根据起始地址返回对应扇区号 *@param address起始地址 *@return 返回对应扇区 */staticuint32_tGetSectorFromAddress(uint32_taddress){if(address<0x08004000)returnFLASH_SECTOR_0;//扇区0elseif(address<0x08008000)returnFLASH_SECTOR_1;elseif(address<0x0800C000)returnFLASH_SECTOR_2;elseif(address<0x08010000)returnFLASH_SECTOR_3;elseif(address<0x08020000)returnFLASH_SECTOR_4;elseif(address<0x08040000)returnFLASH_SECTOR_5;elseif(address<0x08060000)returnFLASH_SECTOR_6;elseif(address<0x08080000)returnFLASH_SECTOR_7;elseif(address<0x080A0000)returnFLASH_SECTOR_8;elseif(address<0x080C0000)returnFLASH_SECTOR_9;elseif(address<0x080E0000)returnFLASH_SECTOR_10;elsereturnFLASH_SECTOR_11;}/* *@param 擦除APP区flash,擦除区域固定(APP最大224KB) *@param 在接收到Updata时擦除,会耗时几十毫秒,会耗时几十毫秒 *@return */staticvoidEraseAppArea(void){uint32_tstart_addr=APP_START_ADDR;//起始扇区地址uint32_tend_addr=APP_START_ADDR+MAX_APP_SIZE-1;//结束扇区地址uint32_tstart_sector=GetSectorFromAddress(start_addr);//起始扇区号uint32_tend_sector=GetSectorFromAddress(end_addr);//结束扇区号uint32_tnb_sectors=end_sector-start_sector+1;//要擦除的扇区数FLASH_EraseInitTypeDef erase_init;//配置擦除参数erase_init.TypeErase=FLASH_TYPEERASE_SECTORS;//选择扇区擦除erase_init.Sector=start_sector;//擦除开始扇区erase_init.NbSectors=nb_sectors;//擦除扇区数erase_init.VoltageRange=FLASH_VOLTAGE_RANGE_3;//3.3V供电uint32_tSectorError=0;//擦除失败扇区号HAL_FLASH_Unlock();//flash解锁if(HAL_FLASHEx_Erase(&erase_init,&SectorError)!=HAL_OK){printf("erase error!");Error_Handler();// 擦除失败,进入死循环//可以后续添加优化处理}HAL_FLASH_Lock();//锁flash}/* *@param 将当前收到数据包写入Flash *@param *@return */staticvoidWrite_Current_Packet_To_Flash(void){HAL_FLASH_Unlock();//解锁flashfor(uint16_ti=0;i<receive_len;i+=2)//16位写入(半字){uint32_tflash_addr=APP_START_ADDR+i+flash_write_offset;//写入的地址uint16_tdata16;if(i+1<receive_len)data16=receive_buff[i]|(receive_buff[i+1]<<8);//将缓冲区拼接成16位elsedata16=receive_buff[i]|(0xff<<8);//如果剩下单独一位,小端拼接成16位if(HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD,flash_addr,data16)!=HAL_OK)//半字写入数据{//写入失败后续处理}}flash_write_offset+=receive_len;//记录偏移HAL_FLASH_Lock();//锁flash}/* *@param 跳转到A程序 *@param *@return */staticvoidbootloader_Jump_To_App(void){typedefvoid(*pFunc)(void);//函数指针uint32_tapp_stack_ptr=*(volatileuint32_t*)(APP_START_ADDR);//获取栈顶地址的值uint32_tapp_reset_handle=*(volatileuint32_t*)(APP_START_ADDR+4);//复位中断//校验if((app_stack_ptr&0xFFF00000)!=STACK_ADDR){printf("stack addr error!");//栈顶地址错误return;}if((app_reset_handle<APP_START_ADDR)||(app_reset_handle>APP_START_ADDR+MAX_APP_SIZE-1)){printf("reset handle error!");return;}//注销bootloader程序,健壮性__disable_irq();//关全局中断for(inti=0;i<8;i++){NVIC->ICPR[i]=0xFFFFFFFF;// 清除所有挂起标志NVIC->ICER[i]=0xFFFFFFFF;// 禁用所有中断}SysTick->CTRL=0;//关闭SysTick定时器SysTick->LOAD=0;SysTick->VAL=0;HAL_DeInit();//注销HAL库,外设配置,不会注销内核__set_MSP(app_stack_ptr);//设置堆栈指针SCB->VTOR=APP_START_ADDR;//重定向中断向量表//跳转pFunc jump_to_app=(pFunc)app_reset_handle;jump_to_app();//跳转}/* *@param DMA空闲中断接收初始化 *@param *@return */voidbootloader_Init(void){//清空问题标志位__HAL_UART_CLEAR_OREFLAG(&huart1);//溢出错误__HAL_UART_CLEAR_IDLEFLAG(&huart1);//IDLE标志位huart1.RxState=HAL_UART_STATE_READY;HAL_UARTEx_ReceiveToIdle_DMA(&huart1,receive_buff,RECEIVE_SIZE);//空闲中断接收}/* *@param main函数执行 *@param *@return */voidbootloader_ReceiveHandle(void){if(receive_full_len>0){printf("LEN:%d\r\n",receive_full_len);//打印累加大小,在主循环中没有其他阻塞操作时,会每个包都打印,但是有阻塞操作就不一定了,仅作为查看接收数据是否匹配receive_full_len=0;//重置}if(erase_flag){EraseAppArea();//执行擦除erase_flag=false;//重置擦除标志flash_write_offset=0;//重置偏移量printf("\r\nERASE OK,Please send file\r\n");}if(jump_flag){jump_flag=false;bootloader_Jump_To_App();//跳转}}/* *@param 串口空闲中断接收数据回调 *@param DMA将缓冲区填满或者检测到空闲状态(电平转换时刻)就会触发回调 *@return */voidHAL_UARTEx_RxEventCallback(UART_HandleTypeDef*huart,uint16_tSize){if(huart->Instance!=USART1)return;HAL_UART_DMAStop(&huart1);//停止DMA,发包间隔是1ms,在开启下一个中断接收之前要保证中间所有流程能在1ms之内执行完receive_len=Size;//实际接收数据长度receive_full_len+=receive_len;receive_flag=true;if(receive_len==6&&memcmp(receive_buff,CMD_UPDATE,6)==0)erase_flag=true;//接收到更新标志elseif(receive_len==4&&memcmp(receive_buff,JUMP_TO_APP,4)==0)jump_flag=true;//接收到跳转标志elseWrite_Current_Packet_To_Flash();//将包写入到flashmemset(receive_buff,0,RECEIVE_SIZE);//清空缓存区HAL_UARTEx_ReceiveToIdle_DMA(&huart1,receive_buff,RECEIVE_SIZE);//下一个空闲中断接收}

到这里flash擦除写入、跳转函数就写完了。

三丶04 篇承接该篇