IAP固件升级原理及实现详解

什么是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权威指南):

  • 从0x0000 0000地址取出MSP(主堆栈寄存器)的值

  • 从0x0000 0004地址取出PC(程序计数器)的值

  • 然后取出第一条指令执行

启动文件源代码分析

;******************** (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读写

  • 串口DMA+空闲中断

内部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代码设计

  • bootloader代码设计

扇区擦除,用于写入前擦除相应扇区

/******************************************************
* 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

参数意义:

  • C:\Keil_v5\ARM\ARMCC\bin\fromelf.exe: fromelf.exe文件目录地址,一般keil下自带有

  • --bin -o IAP.bin :

输出bin文件名称

  • UART_CircleQueueTest\UART_CircleQueueTest.axf:

axf文件目录及文件

要注意boot区的地址范围,根据前面的设计,46K,拿出计算器....

  • APP代码设计

APP代码是用户功能代码,实现业务逻辑,本次测试用的比较简单,接收到APP代码之后,会自动重启,更新APP区代码,如下:

  • boot区代码执行效果

  • APP1代码执行效果

  • 接下来再进行一次更新

注意APP BIN文件的flash地址设置

OK,至此,设计部分就完成了,涉及的内容还是非常多的

本文仅仅用于原理性介绍及IAP功能演示,与工程中实际使用的还是有很大区别的,后面会继续给大家出工程应用方便的教程,会涉及到签名、验签、加解密、校验固件完整性等等

特别感谢

感谢群友大帅比“肥嘟嘟左卫门”特为小伙伴们写的上位机测试软件,非常的nice,上位机软件还在更新中,欢迎小伙伴们进群聊,催更!!!

资料获取

欢迎关注公众号,后台回复“IAP”即可获取测试boot、APP源码,欢迎添加小飞哥好友,进群一起交流!

  • 34
    点赞
  • 183
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
好的,我可以对STM32的IAP固件升级设计和U盘升级固件方案进行更详细的介绍。 在STM32中,IAP固件升级设计主要是通过在MCU内部实现自身的固件升级。具体实现方法包括: 1. 在Flash存储器中预留一定的存储空间,用于存放固件升级文件。 2. 在MCU的Bootloader程序中实现升级文件的接收和存储功能。 3. 在升级文件传输完成后,MCU会自动重启,并执行新的固件程序。 IAP固件升级设计需要注意以下几点: 1. 升级文件的格式需要符合固件升级设计的要求,在文件传输过程中需要进行校验,确保数据传输的正确性。 2. 在升级过程中需要保证固件程序的完整性,避免升级失败导致MCU无法正常工作。 3. 在实现IAP固件升级设计时,需要考虑到不同型号的MCU之间的差异,确保程序的兼容性和可移植性。 U盘升级固件方案是一种简单、灵活的固件升级方式。具体实现方法包括: 1. 将升级文件存储在U盘中,并将U盘连接到MCU的USB接口。 2. 在MCU中实现USB设备模式,接收U盘中的升级文件,并存储在Flash存储器中。 3. 在升级文件传输完成后,MCU会自动重启,并执行新的固件程序。 U盘升级固件方案需要注意以下几点: 1. 升级文件的格式需要符合固件升级设计的要求,在文件传输过程中需要进行校验,确保数据传输的正确性。 2. 在实现USB设备模式时,需要考虑到不同型号的MCU之间的差异,确保程序的兼容性和可移植性。 3. 在使用U盘升级固件方案时,需要保证U盘的兼容性和可靠性,避免升级失败导致MCU无法正常工作。 以上是对STM32的IAP固件升级设计和U盘升级固件方案的详细介绍,希望能够对你有所帮助。如有需要,可以进一步了解相关技术细节。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小飞哥玩嵌入式

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值