三、03 OTA-BootLoader前置-flash擦除写入-跳转函数编写 flash擦除写入-串口发包前置HAL库默认是加锁状态下需要优先解锁使用完之后重新加锁。要先擦除后写入F407是以扇区擦除的。只支持8、16、32、64位写入默认是小端序。设置B区是32K也就是扇区0、1那么A区起始地址就是0x080080001.flash扇区擦除flash擦除APP区域怎么写主要有俩步要先写一个辅助函数获取对应起始地址的扇区号。根据数据手册设计对应扇区大小返回对应扇区。/* *根据起始地址返回对应扇区号 * * */uint32_tGetSectorFromAddress(uint32_taddress){if(address0x08004000)returnFLASH_SECTOR_0;//扇区0elseif(address0x08008000)returnFLASH_SECTOR_1;elseif(address0x0800C000)returnFLASH_SECTOR_2;elseif(address0x08010000)returnFLASH_SECTOR_3;elseif(address0x08020000)returnFLASH_SECTOR_4;elseif(address0x08040000)returnFLASH_SECTOR_5;elseif(address0x08060000)returnFLASH_SECTOR_6;elseif(address0x08080000)returnFLASH_SECTOR_7;elseif(address0x080A0000)returnFLASH_SECTOR_8;elseif(address0x080C0000)returnFLASH_SECTOR_9;elseif(address0x080E0000)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_addrAPP_START_ADDR;//起始扇区地址uint32_tend_addrAPP_START_ADDRMAX_APP_SIZE-1;//结束扇区地址uint32_tstart_sectorGetSectorFromAddress(start_addr);//起始扇区号uint32_tend_sectorGetSectorFromAddress(end_addr);//结束扇区号uint32_tnb_sectorsend_sector-start_sector1;//要擦除的扇区数FLASH_EraseInitTypeDef erase_init;//配置擦除参数erase_init.TypeEraseFLASH_TYPEERASE_SECTORS;//选择扇区擦除erase_init.Sectorstart_sector;//擦除开始扇区erase_init.NbSectorsnb_sectors;//擦除扇区数erase_init.VoltageRangeFLASH_VOLTAGE_RANGE_3;//3.3V供电uint32_tSectorError0;//擦除失败扇区号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_flagtrue;//接收到更新标志就可以判断是否要更新了。/* *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_lenSize;//实际接收数据长度receive_full_lenreceive_len;receive_flagtrue;if(receive_len6memcmp(receive_buff,CMD_UPDATE,6)0)erase_flagtrue;//接收到更新标志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_len0){printf(LEN:%d\r\n,receive_full_len);//打印累加大小在主循环中没有其他阻塞操作时会每个包都打印但是有阻塞操作就不一定了仅作为查看接收数据是否匹配receive_full_len0;//重置}if(erase_flag){EraseAppArea();//执行擦除erase_flagfalse;//重置擦除标志flash_write_offset0;//重置偏移量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_ti0;ireceive_len;i2)//16位写入(半字){uint32_tflash_addrAPP_START_ADDRiflash_write_offset;//写入的地址uint16_tdata16;if(i1receive_len)data16receive_buff[i]|(receive_buff[i1]8);//将缓冲区拼接成16位elsedata16receive_buff[i]|(0xff8);//如果剩下单独一位小端拼接成16位if(HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD,flash_addr,data16)!HAL_OK)//半字写入数据{//写入失败后续处理}}flash_write_offsetreceive_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_ADDR4);//复位中断这是APP区的栈顶地址开始就是0x20000000,最大也是到0x2001BFF,所以前三位校验就一定是200所以app_stack_ptr0xFFF00000)0x20000000,如果不相等就肯定是写入错误了。还有就是复位中断校验复位中断地址是肯定要在APP起始地址到APP区域末尾地址之间的。也就是APP_START_ADDRapp_reset_handleAPP_START_ADDRMAX_APP_SIZE-1注销bootloader程序这个也是为了程序健壮性编写的__disable_irq();//关全局中断for(inti0;i8;i){NVIC-ICPR[i]0xFFFFFFFF;// 清除所有挂起标志NVIC-ICER[i]0xFFFFFFFF;// 禁用所有中断}SysTick-CTRL0;//关闭SysTick定时器SysTick-LOAD0;SysTick-VAL0;HAL_DeInit();//注销HAL库外设配置不会注销内核最重要的是设置堆栈指针以及中断向量表了__set_MSP(app_stack_ptr);//设置堆栈指针SCB-VTORAPP_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_ADDR4);//复位中断//校验if((app_stack_ptr0xFFF00000)!STACK_ADDR){printf(stack addr error!);//栈顶地址错误return;}if((app_reset_handleAPP_START_ADDR)||(app_reset_handleAPP_START_ADDRMAX_APP_SIZE-1)){printf(reset handle error!);return;}//注销bootloader程序,健壮性__disable_irq();//关全局中断for(inti0;i8;i){NVIC-ICPR[i]0xFFFFFFFF;// 清除所有挂起标志NVIC-ICER[i]0xFFFFFFFF;// 禁用所有中断}SysTick-CTRL0;//关闭SysTick定时器SysTick-LOAD0;SysTick-VAL0;HAL_DeInit();//注销HAL库外设配置不会注销内核__set_MSP(app_stack_ptr);//设置堆栈指针SCB-VTORAPP_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-VTORAPP_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_#includestm32f4xx_hal.h#includeUSART_driver.h#includestdbool.h#defineRECEIVE_SIZE512//缓冲区大小#defineAPP_START_ADDR0x08008000//A区起始地址#defineMAX_APP_SIZE0x00038000//预留最大APP空间224KB,扇区2~5#defineSTACK_ADDR0x20000000//栈顶地址#defineCMD_UPDATEUpdate// 6 字节握手协议擦除并准备接收#defineJUMP_TO_APPJump//跳转测试voidbootloader_Init(void);voidbootloader_ReceiveHandle(void);voidbootloader_Jump_To_App(void);#endif#includeInt_bootloader.huint8_treceive_buff[RECEIVE_SIZE]{0};//串口缓冲区uint16_treceive_len0;//接收数据长度uint16_treceive_full_len0;//接收完整文件的长度uint32_tflash_write_offset0;//记录当前写入程序的偏移量bool receive_flagfalse;//接收数据标志bool erase_flagfalse;//扇区擦除标志bool jump_flagfalse;//跳转标志/* *param 根据起始地址返回对应扇区号 *param address起始地址 *return 返回对应扇区 */staticuint32_tGetSectorFromAddress(uint32_taddress){if(address0x08004000)returnFLASH_SECTOR_0;//扇区0elseif(address0x08008000)returnFLASH_SECTOR_1;elseif(address0x0800C000)returnFLASH_SECTOR_2;elseif(address0x08010000)returnFLASH_SECTOR_3;elseif(address0x08020000)returnFLASH_SECTOR_4;elseif(address0x08040000)returnFLASH_SECTOR_5;elseif(address0x08060000)returnFLASH_SECTOR_6;elseif(address0x08080000)returnFLASH_SECTOR_7;elseif(address0x080A0000)returnFLASH_SECTOR_8;elseif(address0x080C0000)returnFLASH_SECTOR_9;elseif(address0x080E0000)returnFLASH_SECTOR_10;elsereturnFLASH_SECTOR_11;}/* *param 擦除APP区flash擦除区域固定APP最大224KB *param 在接收到Updata时擦除会耗时几十毫秒会耗时几十毫秒 *return */staticvoidEraseAppArea(void){uint32_tstart_addrAPP_START_ADDR;//起始扇区地址uint32_tend_addrAPP_START_ADDRMAX_APP_SIZE-1;//结束扇区地址uint32_tstart_sectorGetSectorFromAddress(start_addr);//起始扇区号uint32_tend_sectorGetSectorFromAddress(end_addr);//结束扇区号uint32_tnb_sectorsend_sector-start_sector1;//要擦除的扇区数FLASH_EraseInitTypeDef erase_init;//配置擦除参数erase_init.TypeEraseFLASH_TYPEERASE_SECTORS;//选择扇区擦除erase_init.Sectorstart_sector;//擦除开始扇区erase_init.NbSectorsnb_sectors;//擦除扇区数erase_init.VoltageRangeFLASH_VOLTAGE_RANGE_3;//3.3V供电uint32_tSectorError0;//擦除失败扇区号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_ti0;ireceive_len;i2)//16位写入(半字){uint32_tflash_addrAPP_START_ADDRiflash_write_offset;//写入的地址uint16_tdata16;if(i1receive_len)data16receive_buff[i]|(receive_buff[i1]8);//将缓冲区拼接成16位elsedata16receive_buff[i]|(0xff8);//如果剩下单独一位小端拼接成16位if(HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD,flash_addr,data16)!HAL_OK)//半字写入数据{//写入失败后续处理}}flash_write_offsetreceive_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_ADDR4);//复位中断//校验if((app_stack_ptr0xFFF00000)!STACK_ADDR){printf(stack addr error!);//栈顶地址错误return;}if((app_reset_handleAPP_START_ADDR)||(app_reset_handleAPP_START_ADDRMAX_APP_SIZE-1)){printf(reset handle error!);return;}//注销bootloader程序,健壮性__disable_irq();//关全局中断for(inti0;i8;i){NVIC-ICPR[i]0xFFFFFFFF;// 清除所有挂起标志NVIC-ICER[i]0xFFFFFFFF;// 禁用所有中断}SysTick-CTRL0;//关闭SysTick定时器SysTick-LOAD0;SysTick-VAL0;HAL_DeInit();//注销HAL库外设配置不会注销内核__set_MSP(app_stack_ptr);//设置堆栈指针SCB-VTORAPP_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.RxStateHAL_UART_STATE_READY;HAL_UARTEx_ReceiveToIdle_DMA(huart1,receive_buff,RECEIVE_SIZE);//空闲中断接收}/* *param main函数执行 *param *return */voidbootloader_ReceiveHandle(void){if(receive_full_len0){printf(LEN:%d\r\n,receive_full_len);//打印累加大小在主循环中没有其他阻塞操作时会每个包都打印但是有阻塞操作就不一定了仅作为查看接收数据是否匹配receive_full_len0;//重置}if(erase_flag){EraseAppArea();//执行擦除erase_flagfalse;//重置擦除标志flash_write_offset0;//重置偏移量printf(\r\nERASE OK,Please send file\r\n);}if(jump_flag){jump_flagfalse;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_lenSize;//实际接收数据长度receive_full_lenreceive_len;receive_flagtrue;if(receive_len6memcmp(receive_buff,CMD_UPDATE,6)0)erase_flagtrue;//接收到更新标志elseif(receive_len4memcmp(receive_buff,JUMP_TO_APP,4)0)jump_flagtrue;//接收到跳转标志elseWrite_Current_Packet_To_Flash();//将包写入到flashmemset(receive_buff,0,RECEIVE_SIZE);//清空缓存区HAL_UARTEx_ReceiveToIdle_DMA(huart1,receive_buff,RECEIVE_SIZE);//下一个空闲中断接收}到这里flash擦除写入、跳转函数就写完了。三丶04 篇承接该篇