什么是IAP升级?
IAP,即In Application Programming,IAP是用户自己的程序在运行过程中对User Flash的部分区域进行烧写。简单来说,就是开发者代码出bug了或者添加新功能了,能够利用预留的通讯接口,对代码进行升级
UART、SPI、IIC、USB等等,当然还有wifi、4G、蓝牙等无线通讯手段,都可以作为IAP升级的方式,今天主要介绍如何使用串口对固件进行升级
STM32的代码启动过程
要想设计IAP,首先需要对MCU的代码启动过程有个了解,先来看看STM32的代码启动过程是怎样的吧
此部分参考:https://www.cnblogs.com/gulan-zmc/p/12248509.html
在《Cortex-M3权威指南》有讲述:芯片复位后首先会从向量表里面取出两个值(下图来自Cortex-M3权威指南):
启动文件源代码分析
;********************?(C)?COPYRIGHT?2011?STMicroelectronics?********************
;*?File?Name??????????:?startup_stm32f10x_hd.s
;*?Author?????????????:?MCD?Application?Team
;*?Version????????????:?V3.5.0
;*?Date???????????????:?11-March-2011
;*?Description????????:?STM32F10x?High?Density?Devices?vector?table?for?MDK-ARM?
;*??????????????????????toolchain.?
;*??????????????????????This?module?performs:
;*??????????????????????(上电复位后会做下面的几件事情)
;*??????????????????????-?Set?the?initial?SP(设置堆栈,就是设置MSP的值)
;*??????????????????????-?Set?the?initial?PC?==?Reset_Handler(设置PC的值)
;*??????????????????????-?Set?the?vector?table?entries?with?the?exceptions?ISR?address(设置中断向量表的地址)
;*??????????????????????-?Configure?the?clock?system?and?also?configure?the?external?(设置系统时钟;如果芯片外部由挂载SRAM,还需要配置SRAM,默认是没有挂外部SRAM的)
;*????????????????????????SRAM?mounted?on?STM3210E-EVAL?board?to?be?used?as?data?
;*????????????????????????memory?(optional,?to?be?enabled?by?user)
;*??????????????????????-?Branches?to?__main?in?the?C?library?(which?eventually??????(调用C库的__main函数,然后调用main函数执行用户的)
;*????????????????????????calls?main()).
;*??????????????????????After?Reset?the?CortexM3?processor?is?in?Thread?mode,
;*??????????????????????priority?is?Privileged,?and?the?Stack?is?set?to?Main.
;*?<<<?Use?Configuration?Wizard?in?Context?Menu?>>>???
;*******************************************************************************
;?------------------分配栈空间----------------
Stack_Size??????EQU?????0x00000400??????;EQU指令是定义一个标号;标号名是Stack_Size;?值是0x00000400(有点类似于C语言的#define)。Stack_Size标号用来定义栈的大小
????????????????AREA????STACK,?NOINIT,?READWRITE,?ALIGN=3??;AREA指令是定义一个段;这里定义一个?段名是STACK,不初始化,数据可读可写,2^3=8字节对齐的段(详细的说明可以查看指导手册)
Stack_Mem???????SPACE???Stack_Size???;SPACE汇编指令用来分配一块内存;这里开辟内存的大小是Stack_Size;这里是1K,用户也可以自己修改
__initial_sp??????;在内存块后面声明一个标号__initial_sp,这个标号就是栈顶的地址;在向量表里面会使用到
???????????????????????????????????????????
;?<h>?Heap?Configuration
;???<o>??Heap?Size?(in?Bytes)?<0x0-0xFFFFFFFF:8>
;?</h>
;?------------------分配堆空间----------------
;和分配栈空间一样不过大小只是512字节
Heap_Size???????EQU?????0x00000200
????????????????AREA????HEAP,?NOINIT,?READWRITE,?ALIGN=3
__heap_base????????;__heap_base堆的起始地址
Heap_Mem????????SPACE???Heap_Size??????;分配一个空间作为堆空间,如果函数里面有调用malloc等这系列的函数,都是从这里分配空间的
__heap_limit???????;__heap_base堆的结束地址
????????????????PRESERVE8?;PRESERVE8?指令作用是将堆栈按8字节对齐
????????????????THUMB;THUMB作用是后面的指令使用Thumb指令集
;?------------------设置中断向量表----------------
;?Vector?Table?Mapped?to?Address?0?at?Reset
????????????????AREA????RESET,?DATA,?READONLY??????;定义一个段,段名是RESET的只读数据段
????????????????;EXPORT声明一个标号可被外部的文件使用,使标号具有全局属性
????????????????EXPORT??__Vectors??????????;声明一个__Vectors标号允许其他文件引用??????????
????????????????EXPORT??__Vectors_End??????;声明一个__Vectors_End标号允许其他文件引用
????????????????EXPORT??__Vectors_Size?????;声明一个__Vectors_Size标号允许其他文件引用
????????????????
;DCD?指令是分配一个或者多个以字为单位的内存,并且按四字节对齐,并且要求初始化
;__Vectors?标号是?0x0000?0000?地址的入口,也是向量表的起始地址
__Vectors???????DCD?????__initial_sp???????????????;*?Top?of?Stack?????定义栈顶地址;单片机复位后会从这里取出值给MSP寄存器,
???????????????????????????????????????????????????;*?也就是从0x0000?0000?地址取出第一个值给MSP寄存器?(MSP?=?__initial_sp)?
???????????????????????????????????????????????????;*?__initial_sp的值是链接后,由链接器生成
????????????????DCD?????Reset_Handler??????????????;*?Reset?Handler????定义程序入口的值;单片机复位后会从这里取出值给PC寄存器,
???????????????????????????????????????????????????;*?也就是从0x0000?0004?地址取出第一个值给PC程序计数器(pc?=?Reset_Handler)
???????????????????????????????????????????????????;*?Reset_Handler是一个函数,在下面定义
????????????????;后面的定义是中断向量表的入口地址了这里就不多介绍了,想要了解的可以参考《STM32中文手册》和《Cortex-M3权威指南》
????????????????DCD?????NMI_Handler????????????????;?NMI?Handler??????
????????????????DCD?????HardFault_Handler??????????;?Hard?Fault?Handler
????????????????DCD?????MemManage_Handler??????????;?MPU?Fault?Handler
????????????????DCD?????BusFault_Handler???????????;?Bus?Fault?Handler
????????????????DCD?????UsageFault_Handler?????????;?Usage?Fault?Handler
????????????????.....由于文件太长这里省略了部分向量表的定义,完整的可以查看工程里的启动文件
????????????????DCD?????DMA2_Channel1_IRQHandler???;?DMA2?Channel1
????????????????DCD?????DMA2_Channel2_IRQHandler???;?DMA2?Channel2
????????????????DCD?????DMA2_Channel3_IRQHandler???;?DMA2?Channel3
????????????????DCD?????DMA2_Channel4_5_IRQHandler?;?DMA2?Channel4?&?Channel5
__Vectors_End????????????????????????????????????;__Vectors_End向量表的结束地址
__Vectors_Size??EQU??__Vectors_End?-?__Vectors???;定义__Vectors_Size标号,值是向量表的大小
????????????????AREA????|.text|,?CODE,?READONLY??;定义一个代码段,段名是|.text|,属性是只读
;PROC指令是定义一个函数,通常和ENDP成对出现(标记程序的结束)???????????????
;?Reset?handler
Reset_Handler???PROC??????????????????????????????????????;定义?Reset_Handler函数;复位后赋给PC寄存器的值就是Reset_Handler函数的入口地址值。也是系统上电后第一个执行的程序
????????????????EXPORT??Reset_Handler?????????????[WEAK]??;*[WEAK]指令是将函数定义为弱定义。所谓的弱定义就是如果其他地方有定义这个函数,
??????????????????????????????????????????????????????????;*编译时使用另一个地方的函数,否则使用这个函数
????????????????????????????????????????????????
??????????????????????????????????????????????????????????;*IMPORT???表示该标号来自外部文件,跟?C?语言中的?EXTERN?关键字类似
????????????????IMPORT??__main????????????????????????????;*__main?和?SystemInit?函数都是外部文件的标号
????????????????IMPORT??SystemInit????????????????????????;*?SystemInit?是STM32函数库的函数,作用是初始化系统时钟
????????????????LDR?????R0,?=SystemInit
????????????????BLX?????R0??????????????
????????????????LDR?????R0,?=__main???????????????????????;*?__main是C库的函数,主要是初始化堆栈和代码重定位,然后跳到main函数执行用户编写的代码
????????????????BX??????R0
????????????????ENDP
????????????????
;?Dummy?Exception?Handlers?(infinite?loops?which?can?be?modified)
;下面定义的都是异常服务函中断服务函数
NMI_Handler?????PROC
????????????????EXPORT??NMI_Handler????????????????[WEAK]
????????????????B???????.
????????????????ENDP
.....由于文件太长这里省略了部分函数的定义,完整的可以查看工程里的启动文件
SysTick_Handler?PROC
????????????????EXPORT??SysTick_Handler????????????[WEAK]
????????????????B???????.
????????????????ENDP
Default_Handler?PROC
????????????????EXPORT??WWDG_IRQHandler????????????[WEAK]
????????????????EXPORT??PVD_IRQHandler?????????????[WEAK]
????????????????.....由于文件太长这里省略了部分中断服务函数的定义,完整的可以查看工程里的启动文件
????????????????EXPORT??DMA2_Channel2_IRQHandler???[WEAK]
????????????????EXPORT??DMA2_Channel3_IRQHandler???[WEAK]
????????????????EXPORT??DMA2_Channel4_5_IRQHandler?[WEAK]
WWDG_IRQHandler
PVD_IRQHandler
TAMPER_IRQHandler
.....由于文件太长这里省略了部分标号的定义,完整的可以查看工程里的启动文件
DMA2_Channel1_IRQHandler
DMA2_Channel2_IRQHandler
DMA2_Channel3_IRQHandler
DMA2_Channel4_5_IRQHandler
????????????????B???????.
????????????????ENDP
????????????????ALIGN????;四字节对齐
;*******************************************************************************
;?User?Stack?and?Heap?initialization
;*******************************************************************************
;下面函数是初始化堆栈的代码
?????????????????IF??????:DEF:__MICROLIB?????
?????????????????;如果定义了__MICROLIB宏编译下面这部分代码,__MICROLIB在MDK工具里面定义
?????????????????;这种方式初始化堆栈是由?__main?初始化的
?????????????????EXPORT??__initial_sp???;栈顶地址?(EXPORT将标号声明为全局标号,供其他文件引用)
?????????????????EXPORT??__heap_base????;堆的起始地址
?????????????????EXPORT??__heap_limit???;堆的结束地址
????????????????
?????????????????ELSE
?????????????????;由用户初始化堆
?????????????????;否则编译下面的
?????????????????IMPORT??__use_two_region_memory??????;__use_two_region_memory?由用户实现
?????????????????EXPORT??__user_initial_stackheap
?????????????????
__user_initial_stackheap
?????????????????
?????????????????LDR?????R0,?=??Heap_Mem??????????????;堆的起始地址
?????????????????LDR?????R1,?=(Stack_Mem?+?Stack_Size);栈顶地址
?????????????????LDR?????R2,?=?(Heap_Mem?+??Heap_Size);堆的结束地址
?????????????????LDR?????R3,?=?Stack_Mem??????????????;栈的结束地址
?????????????????BX??????LR
?????????????????ALIGN
?????????????????ENDIF
?????????????????END
;*******************?(C)?COPYRIGHT?2011?STMicroelectronics?*****END?OF?FILE*****
STM32的启动步骤如下:
-
1、上电复位后,从 0x0000 0000 地址取出栈顶地址赋给MSP寄存器(主堆栈寄存器),即MSP = __initial_sp。这一步是由硬件自动完成的 -
2、从0x0000 0004 地址取出复位程序的地址给PC寄存器(程序计数器),即PC = Reset_Handler。这一步也是由硬件自动完成调用SystemInit函数初始化系统时钟 -
3、跳到C库的__main函数初始化堆栈(初始化时是根据前面的分配的堆空间和栈空间来初始化的)和代码重定位(初始RW 和ZI段),然后跳到main函数执行应用程序
IAP设计思路
大体分为两部分设计,bootloader、APP代码设计,bootloader用于检查APP区代码是否需要更新,以及跳转到APP区执行APP程序
调研了一下群里的小伙伴,下面这个流程比较通用一些,大概是下图所示升级流程:
升级流程图
Flash分区
是以STM32F103RET6为主控做的flash分区,主要功能:
-
boot区:0x0800 0000 到 0x0800 b7FF 地址的flash块划分给bootloader,用于升级固件,大小是46kb -
用户参数区:0x0800 B800 到 0x0800 BFFF 的flash块划分为用户参数区(parameters),用于存储用户的一些参数,大小是2Kb -
APP区:0x0800 C000 到 0x0804 3FFF 的flash块划分为APP区 ,(application)用于存放用户功能应用代码,大小是224Kb -
APP缓存区:0x0804 4000 到 0x0807 BFFF 的flash块划分为APP缓存区 (update region),用于暂存下发的固件,大小跟应用程序区一样 224kb -
未定义:0x0807 C000 到 0x0807 FFFF 的flash块划分未定义区,可以根据具体用途定义,大小是16Kb
代码实现
硬件:
软件:
内部flash读写操作
这部分比较简单,直接上代码:
读flash操作:
/************************************************************
??*?@brief???读取2字节数据
?*?@param[in]???uint32_t?faddr
??*?@return??NULL
??*?@github??
??*?@date????2021-xx-xx
??*?@version?v1.0
??*?@note????NULL
??***********************************************************/
uint16_t?BSP_FLASH_ReadHalfWord(uint32_t?raddr)
{
?return?*(__IO?uint16_t*)raddr;?
}
/************************************************************
??*?@brief??????读取n(uint16_t)字节数据
?*?@param[in]???uint32_t?ReadAddr
?*?@param[out]??uint16_t?*pBuffer
?*?@param[in]???uint16_t?len
??*?@return??NULL
??*?@github??
??*?@date????2021-xx-xx
??*?@version?v1.0
??*?@note????NULL
??***********************************************************/
void?BSP_FLASH_Read?(uint32_t?ReadAddr,?uint16_t?*pBuffer,?uint16_t?len?)????
{
?uint16_t?i;
?
?for(i=0;i<len;i++)
?{
??pBuffer[i]=BSP_FLASH_ReadHalfWord(ReadAddr);???//读取2个字节.
??ReadAddr+=2;???????????????????//偏移2个字节.?
?}
}
写操作,注意写之前要保证是没有写过的区域即可:
/************************************************************
??*?@brief???写入n(uint16_t)字节数据
?*?@param[in]???uint32_t?ReadAddr
?*?@param[out]???uint16_t?*pBuffer
?*?@param[in]???uint16_t?len
??*?@return??NULL
??*?@github??
??*?@date????2021-xx-xx
??*?@version?v1.0
??*?@note????NULL
??***********************************************************/
void?BSP_FLASH_Write_NoCheck?(?uint32_t?WriteAddr,?uint16_t?*?pBuffer,?uint16_t?len?)???
{????????
?uint16_t?i;?
?
?for(i=0;i<len;i++)
?{
??HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD,WriteAddr,pBuffer[i]);
???WriteAddr+=2;????????????????????????????????????//地址增加2.
?}??
}?
/************************************************************
??*?@brief?????写入n(uint16_t)字节数据
?*?@param[in]??uint32_t?WriteAddr
?*?@param[in]??uint16_t?*pBuffer
?*?@param[in]??uint16_t?len
??*?@return??NULL
??*?@github??
??*?@date????2021-xx-xx
??*?@version?v1.0
??*?@note????NULL
??***********************************************************/
void?BSP_FLASH_Write(uint32_t?WriteAddr,uint16_t?*?pBuffer,uint16_t?len?)?
{
??uint32_t?SECTORError?=?0;
?uint16_t?sector_off;????//扇区内偏移地址(16位字计算)
?uint16_t?sector_remain;?//扇区内剩余地址(16位字计算)????
??uint16_t?i;????
?uint32_t?secor_pos;????//扇区地址
?uint32_t?offaddr;???//去掉0X08000000后的地址
?
?if(WriteAddr<FLASH_BASE||(WriteAddr>=(FLASH_BASE+1024*STM32_FLASH_SIZE)))return;//非法地址
?
?HAL_FLASH_Unlock();?????????//解锁
?
?offaddr=WriteAddr-FLASH_BASE;????//实际偏移地址.
?secor_pos=offaddr/STM_SECTOR_SIZE;???//扇区地址??0~127?for?STM32F103RBT6
?sector_off=(offaddr%STM_SECTOR_SIZE)/2;??//在扇区内的偏移(2个字节为基本单位.)
?sector_remain=STM_SECTOR_SIZE/2-sector_off;??//扇区剩余空间大小???
?if(len<=sector_remain)sector_remain=len;//不大于该扇区范围
?
?while(1)?
?{?
??BSP_FLASH_Read(secor_pos*STM_SECTOR_SIZE+FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//读出整个扇区的内容
????
??for(i=0;i<sector_remain;i++)//校验数据
??{
???if(STMFLASH_BUF[sector_remain+i]!=0XFFFF)
????break;//需要擦除?????
??}
??if(i<sector_remain)//需要擦除
??{
???//擦除这个扇区
??????/*?Fill?EraseInit?structure*/
??????EraseInitStruct.TypeErase?????=?FLASH_TYPEERASE_PAGES;
??????EraseInitStruct.PageAddress???=?secor_pos*STM_SECTOR_SIZE+FLASH_BASE;
??????EraseInitStruct.NbPages???????=?1;
??????HAL_FLASHEx_Erase(&EraseInitStruct,?&SECTORError);
???for(i=0;i<sector_remain;i++)//复制
???{
????STMFLASH_BUF[i+sector_off]=pBuffer[i];???
???}
???BSP_FLASH_Write_NoCheck(secor_pos*STM_SECTOR_SIZE+FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//写入整个扇区??
??}
??else?
???BSP_FLASH_Write_NoCheck(WriteAddr,pBuffer,sector_remain);//写已经擦除了的,直接写入扇区剩余区间.????????
??if(len==sector_remain)
???break;//写入结束了
??else//写入未结束
??{
???secor_pos++;????//扇区地址增1
???sector_off=0;????//偏移位置为0???
????pBuffer+=sector_remain;???//指针偏移
???WriteAddr+=sector_remain;?//写地址偏移????
????len-=sector_remain;?//字节(16位)数递减
???
???if(len>(STM_SECTOR_SIZE/2))
????sector_remain=STM_SECTOR_SIZE/2;//下一个扇区还是写不完
???else?
????sector_remain=len;//下一个扇区可以写完了
??}??
?};?
?HAL_FLASH_Lock();//上锁
}
串口DMA+空闲中断接收不定长数据
麻烦小伙伴移步:
串口DMA+空闲中断接收不定长数据
在上篇文章的基础上,我们对结构体做点修改,增加bin文件数据总长度记录,以及bin文件接收完成标志(小伙伴们可以采用其他办法,不必拘泥于我教程中的方式):
#define?Max_RecLen?1024*3
typedef?struct{
?uint8_t?RxBuffer[Max_RecLen];??//DMA接收缓冲区
?uint16_t?RecDat_len;??????//单包数据长度
?uint32_t?Cur_WriteAddr;?????//APP缓冲区地址
?uint16_t?BinLen;????????//bin文件数据长度
?uint8_t?rec_endFlag;??????//单包数据接收结束标志
?uint8_t?Binrec_endFlag;?????????//bin文件接收结束标志?
?uint16_t?DMA_TIMCNT;???????//bin文件下发超时计数器
?
}UserUartDMA_Typedef;
然后在串口中断中:
/**
??*?@brief?This?function?handles?USART1?global?interrupt.
??*/
void?USART1_IRQHandler(void)
{
??/*?USER?CODE?BEGIN?USART1_IRQn?0?*/
?uint32_t?idle_flag_temp?=?0;
?uint16_t?len_temp?=?0;
??/*?USER?CODE?END?USART1_IRQn?0?*/
??HAL_UART_IRQHandler(&huart1);
??/*?USER?CODE?BEGIN?USART1_IRQn?1?*/
?idle_flag_temp?=?__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE);
?
?if(idle_flag_temp)
?{
??__HAL_UART_CLEAR_FLAG(&huart1,UART_FLAG_IDLE);
??HAL_UART_DMAStop(&huart1);
??
??UserUartDma.DMA_TIMCNT?=?0;
??UserUartDma.Binrec_endFlag=0;
??len_temp?=?__HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
??
??UserUartDma.RecDat_len?=?Max_RecLen?-?len_temp;??
??UserUartDma.BinLen+=UserUartDma.RecDat_len;
??UserUartDma.rec_endFlag?=?1;
??
?}
??__HAL_UART_ENABLE_IT(&huart1,UART_IT_RXNE);
??HAL_UART_Receive_DMA(&huart1,UserUartDma.RxBuffer,Max_RecLen);?
??/*?USER?CODE?END?USART1_IRQn?1?*/
}
IAP代码设计
扇区擦除,用于写入前擦除相应扇区
/******************************************************
*?Brief?????:?擦除APP区
*?Parameter?:?
*???????????*startaddr:APP起始地址
*??????*pages??:要擦除的page?=?APPSIZE/PAGESIZE
*?Return????:None.
*******************************************************/
void?APPReigion_Erase(uint32_t?startaddr,uint16_t?pages)
{
??uint32_t?SECTORError?=?0;
??//擦除APP区
??/*?Fill?EraseInit?structure*/
??EraseInitStruct.TypeErase?????=?FLASH_TYPEERASE_PAGES;
??EraseInitStruct.PageAddress???=?startaddr;
??EraseInitStruct.NbPages???????=?pages;
??HAL_FLASHEx_Erase(&EraseInitStruct,?&SECTORError);
}
升级标志获取与擦除
/******************************************************
*?Brief?????:?写入升级标志
*?Parameter?:?
*???????????*addr:标志存放地址
*??????*pdata:写入的数据
*?Return????:None.
*******************************************************/
void?APP_UpdateFlag_Write(uint32_t?addr,uint16_t?*pdata)
{
??BSP_FLASH_Write(addr,pdata,1);
}
/******************************************************
*?Brief?????:?获取升级标志
*?Parameter?:?
*???????????*addr:标志存放地址
*?Return????:None.
*******************************************************/
uint16_t?APP_UpdateFlag_Read(uint32_t?addr)
{
??uint16_t?flag_temp;
??BSP_FLASH_Read(addr,&flag_temp,1);
?
??return?flag_temp;
}
接下来就是APP缓冲区数据写入,APP区与APP缓冲区数据倒腾了
/******************************************************
*?Brief?????:?Bin文件写入app缓冲区
*?Parameter?:?
*???????????StartAddr:?起始地址
*???????????*pBin_DataBuf:?要传输的数据
*???????????packBufLength:单包数据长度
*?Return????:?None.
*******************************************************/
void?IAP_WriteBin(uint32_t?StartAddr,uint8_t?*?pBin_DataBuf,uint32_t?packBufLength)
{
?uint16_t?pack_len,?packlen_Ctr=0,?dataTemp;
?uint8_t?*?pData?=?pBin_DataBuf;
??
?for?(pack_len?=?0;?pack_len?<?packBufLength;?pack_len?+=?2?)
?{??????????
??dataTemp?=??(?uint16_t?)?pData[1]<<8;
??dataTemp?+=?(?uint16_t?)?pData[0];???
??pData?+=?2;??????????????????????????????????????????????????????//偏移2个字节
??ulBuf_Flash_App?[?packlen_Ctr?++?]?=?dataTemp;?????
?}?
???BSP_FLASH_Write?(?UserUartDma.Cur_WriteAddr,?ulBuf_Flash_App,?packlen_Ctr?);?
???UserUartDma.Cur_WriteAddr?+=?(packlen_Ctr*2);???????????????????????????????????????????//偏移packlen_Ctr??16=2*8.所以要乘以2.
???packlen_Ctr?=?0;
}
/******************************************************
*?Brief?????:?Bin文件从app缓冲区写入app区
*?Parameter?:?
*???????????SrcStartAddr:?app缓冲区起始地址
*???????????DstStartAddr:?APP区起始地址
*???????????BinLength:bin文件长度
*?Return????:?None.
*******************************************************/
void?IAP_WriteBinToAPPReigon(uint32_t?SrcStartAddr,uint32_t?DstStartAddr,uint32_t?BinLength)
{
?uint16_t?data_temp?=?0;
?uint32_t?count=0;
?
?
?HAL_FLASH_Unlock();?????????//解锁
?
?APPReigion_Erase(APP_START_ADDR,APPSIZE/PAGESIZE);
?HAL_Delay(10);
?
?for(count=0;count<BinLength;count=count+2)
?{
???BSP_FLASH_Read?(SrcStartAddr,?&data_temp,?1);
???BSP_FLASH_Write_NoCheck(DstStartAddr,?&data_temp,1);
???SrcStartAddr+=2;
???DstStartAddr+=2;
?}
?//BSP_FLASH_Write_NoCheck(DstStartAddr,?,1);
?HAL_FLASH_Lock();//上锁
}
最后,要设置APP程序的运行地址
/******************************************************
*?Brief?????:?设置栈顶指针
*?Parameter?:?
*???????????ulAddr:?
*?Return????:?None.
*******************************************************/
__asm?void?MSR_MSP?(?uint32_t?ulAddr?)?
{
????MSR?MSP,?r0???????????????????????//set?Main?Stack?value
????BX?r14
}
/******************************************************
*?Brief?????:?IAP执行
*?Parameter?:?
*???????????ulAddr_App:?APP起始地址
*?Return????:?None.
*******************************************************/
void?IAP_ExecuteApp?(?uint32_t?ulAddr_App?)
{
?pIapFun_TypeDef?pJump2App;?
?
?if?(?(?(?*?(?__IO?uint32_t?*?)?ulAddr_App?)?&?0x2FFE0000?)?==?0x20000000?)???//检查栈顶地址是否合法.
?{?
??pJump2App?=?(?pIapFun_TypeDef?)?*?(?__IO?uint32_t?*?)?(?ulAddr_App?+?4?);?//用户代码区第二个字为程序开始地址(复位地址)??
??MSR_MSP(?*?(?__IO?uint32_t?*?)?ulAddr_App?);?//初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
??pJump2App?();?????????????????????????????????????????????//跳转到APP.
?}
}??
IAP相关的代码就这些,主要是数据的倒腾,其他倒也没什么复杂的
目前小飞哥对整包bin文件传输完成判断,单包采用DMA+空闲中断的方式,整包文件传输采用的是串口中断在5s内没有数据过来,认为一包数据接收完成,置位接收标志,这部分小伙伴可以自行判断
static?void?Systick_Config(void)
{
??HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()?/?1000);?//1ms
??HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
??HAL_NVIC_SetPriority(SysTick_IRQn,?0,?0);
}
void?HAL_SYSTICK_Callback(void)
{
?UserUartDma.DMA_TIMCNT++;
?if(UserUartDma.DMA_TIMCNT>5000)
?{
??UserUartDma.Binrec_endFlag?=?1;
??UserUartDma.DMA_TIMCNT?=?0;
?}
??TIMX_IRQHandler_user();?
}
bin文件传输完成后,写入需要更新标志,重新跳转至boot区,检查是否APP代码需要更新:
uint32_t?Task_02()
{
?
?if(UserUartDma.rec_endFlag)
?{
??PRINT_INFO("update?firmware\n");
??PRINT_INFO("APP?长度:%d字节\n",?UserUartDma.RecDat_len);??
??
??UserUartDma.rec_endFlag?=?0;
??IAP_WriteBin(UserUartDma.Cur_WriteAddr,UserUartDma.RxBuffer,UserUartDma.RecDat_len);
??
??memset(UserUartDma.RxBuffer,0,UserUartDma.RecDat_len);
??UserUartDma.RecDat_len?=?0;
?}
?if(UserUartDma.BinLen!=0)
?{
??if(UserUartDma.Binrec_endFlag)
??{
????UserUartDma.Binrec_endFlag?=?0;
????
????APP_UpdateFlag_Write(APP_Len_ADDR,&UserUartDma.BinLen);???//写入升级标志
????APP_UpdateFlag_Write(APP_UpdateFlag_ADDR,&APP_UPDATE_FLAG);???//写入升级标志
????UserUartDma.BinLen?=?0;??
????IAP_ExecuteApp(FLASH_BASE);???//跳转8000000,重新启动
????HAL_Delay(2000);
??}
?
?}
}
MCU复位之后,初始化执行过程中,对APP升级标志进行检测:
PRINT_INFO("-----IAP?Menu--------------\n");
?PRINT_INFO("-----Download?APP?BIN------\n");
?PRINT_INFO("-----Restart?To?RUN?APP----\n");?
?PRINT_INFO("\n\n\n");
?
?if(0x5aa5==APP_UpdateFlag_Read(APP_UpdateFlag_ADDR))
?{
??PRINT_INFO("-----Restart.....5-------\n");
??HAL_Delay(1000);
??PRINT_INFO("-----Restart.....4-------\n");
??HAL_Delay(1000);
??PRINT_INFO("-----Restart.....3-------\n");
??HAL_Delay(1000);
??PRINT_INFO("-----Restart.....2-------\n");
??HAL_Delay(1000);
??PRINT_INFO("-----Restart.....1-------\n");
??HAL_Delay(1000);
??
??PRINT_INFO("the?device?need?update\n");
??IAP_WriteBinToAPPReigon(APP_Temp_ADDR,APP_START_ADDR,APP_UpdateFlag_Read(APP_Len_ADDR));?
??PRINT_INFO("firmware?write?OK\n");
??PRINT_INFO("please?double?click?the?button?the?execute?the?application\n");
?
??HAL_FLASH_Unlock();?????????//解锁
??APPReigion_Erase(APP_Temp_ADDR,APPSIZE/PAGESIZE);??//擦除APP缓冲区
??APPReigion_Erase(Flash_UNUSED,1);??????????//清除APP更新标志?????
??HAL_FLASH_Lock();??????????//上锁
??
??PRINT_INFO?(?"开始执行?APP\n"?);?
??//执行FLASH?APP代码
??IAP_ExecuteApp(APP_START_ADDR);
?}?
倒计时5秒后,代码更新,并执行
?PRINT_INFO("-----IAP?Menu--------------\n");
?PRINT_INFO("-----Download?APP?BIN------\n");
?PRINT_INFO("-----Restart?To?RUN?APP----\n");?
?PRINT_INFO("\n\n\n");
?
?if(0x5aa5==APP_UpdateFlag_Read(APP_UpdateFlag_ADDR))
?{
??PRINT_INFO("-----Restart.....5-------\n");
??HAL_Delay(1000);
??PRINT_INFO("-----Restart.....4-------\n");
??HAL_Delay(1000);
??PRINT_INFO("-----Restart.....3-------\n");
??HAL_Delay(1000);
??PRINT_INFO("-----Restart.....2-------\n");
??HAL_Delay(1000);
??PRINT_INFO("-----Restart.....1-------\n");
??HAL_Delay(1000);
??
??PRINT_INFO("the?device?need?update\n");
??IAP_WriteBinToAPPReigon(APP_Temp_ADDR,APP_START_ADDR,APP_UpdateFlag_Read(APP_Len_ADDR));?
??PRINT_INFO("firmware?write?OK\n");
??PRINT_INFO("please?double?click?the?button?the?execute?the?application\n");
?
??HAL_FLASH_Unlock();?????????//解锁
??APPReigion_Erase(APP_Temp_ADDR,APPSIZE/PAGESIZE);??//擦除APP缓冲区
??APPReigion_Erase(Flash_UNUSED,1);??????????//清除APP更新标志?????
??HAL_FLASH_Lock();??????????//上锁
??
??PRINT_INFO?(?"开始执行?APP\n"?);?
??//执行FLASH?APP代码
??IAP_ExecuteApp(APP_START_ADDR);
?}?
?else//不需要更新,执行APP
?{
??IAP_ExecuteApp(APP_START_ADDR);
?}
bin文件生成
输入:
C:\Keil_v5\ARM\ARMCC\bin\fromelf.exe --bin -o IAP.bin UART_CircleQueueTest\UART_CircleQueueTest.axf
参数意义:
输出bin文件名称
axf文件目录及文件
要注意boot区的地址范围,根据前面的设计,46K,拿出计算器....
APP代码是用户功能代码,实现业务逻辑,本次测试用的比较简单,接收到APP代码之后,会自动重启,更新APP区代码,如下:
注意APP BIN文件的flash地址设置
OK,至此,设计部分就完成了,涉及的内容还是非常多的
本文仅仅用于原理性介绍及IAP功能演示,与工程中实际使用的还是有很大区别的,后面会继续给大家出工程应用方便的教程,会涉及到签名、验签、加解密、校验固件完整性等等
特别感谢
感谢群友大帅比“肥嘟嘟左卫门”特为小伙伴们写的上位机测试软件,非常的nice,上位机软件还在更新中,欢迎小伙伴们进群聊,催更!!!
资料获取
欢迎关注公众号,后台回复“IAP”即可获取测试boot、APP源码,欢迎添加小飞哥好友,进群一起交流!
|