打印
[其他ST产品]

IAP固件升级经验分享

[复制链接]
1427|45
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
jcky001|  楼主 | 2023-4-3 10:24 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
什么是IAP升级?

IAP,即In Application Programming,IAP是用户自己的程序在运行过程中对User Flash的部分区域进行烧写。简单来说,就是开发者代码出bug了或者添加新功能了,能够利用预留的通讯接口,对代码进行升级


UART、SPI、IIC、USB等等,当然还有wifi、4G、蓝牙等无线通讯手段,都可以作为IAP升级的方式,今天主要介绍如何使用串口对固件进行升级


STM32的代码启动过程

要想设计IAP,首先需要对MCU的代码启动过程有个了解,先来看看STM32的代码启动过程是怎样的吧


在《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程序

调研了一下群里的小伙伴,下面这个流程比较通用一些,大概是下图所示升级流程:


升级流程图




使用特权

评论回复
沙发
jcky001|  楼主 | 2023-4-3 10:24 | 只看该作者
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


代码实现

硬件:

  • fallingstar-board(已开源,打板验证)

软件:

  • 内部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=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);
  •    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;
  •    }
  •    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+空闲中断接收不定长数据

在上篇文章的基础上,我们对结构体做点修改,增加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,拿出计算器....


使用特权

评论回复
板凳
jcky001|  楼主 | 2023-4-3 10:25 | 只看该作者





  • APP代码设计

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


  • boot区代码执行效果



  • APP1代码执行效果





  • 接下来再进行一次更新





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




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

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




使用特权

评论回复
地板
kkzz| | 2024-1-6 16:36 | 只看该作者
固件升级是一种在运行时更新设备应用程序的技术。

使用特权

评论回复
5
rosemoore| | 2024-1-6 17:37 | 只看该作者
在实际应用之前,对固件更新程序进行充分的测试和调试,确保其在各种情况下都能正确工作。

使用特权

评论回复
6
minzisc| | 2024-1-6 18:28 | 只看该作者
应用程序则执行实际的产品功能,且在正常运行时不会干扰Bootloader。

使用特权

评论回复
7
timfordlare| | 2024-1-6 20:14 | 只看该作者
实现固件映像的校验和验证,确保接收到的数据完整无误。

使用特权

评论回复
8
mikewalpole| | 2024-1-6 21:20 | 只看该作者
Bootloader部分通常较小,主要负责初始化必要的硬件,并通过串口、USB或其他通信接口接收新固件数据。

使用特权

评论回复
9
uytyu| | 2024-1-7 21:38 | 只看该作者
为用户设计一个简单易用的界面,以方便用户进行固件更新操作。可以使用图形界面,也可以使用命令行界面,具体取决于应用需求。

使用特权

评论回复
10
wilhelmina2| | 2024-1-7 22:15 | 只看该作者
写入操作应遵循STM32 Flash编程规范,包括页擦除、字节/半字/字编程,并在每个操作之后检查状态以确保成功。

使用特权

评论回复
11
sdCAD| | 2024-1-7 22:26 | 只看该作者
应在应用程序中实现适当的升级逻辑,以便在需要时自动触发升级操作。

使用特权

评论回复
12
sanfuzi| | 2024-1-7 22:46 | 只看该作者
在升级前,应备份重要数据,以防止因升级导致的数据丢失。

使用特权

评论回复
13
eefas| | 2024-1-8 12:42 | 只看该作者
考虑到不同硬件平台和软件版本之间的兼容性,设计灵活的升级策略。

使用特权

评论回复
14
primojones| | 2024-1-8 13:23 | 只看该作者
在STM32的FLASH内存中划分出足够的区域用于存储新的固件映像。

使用特权

评论回复
15
backlugin| | 2024-1-8 15:35 | 只看该作者
在每次升级后,应更新设备上的固件版本信息,以便于后续管理和故障排查。

使用特权

评论回复
16
xiaoyaodz| | 2024-1-8 16:44 | 只看该作者
可以参考ST官方提供的示例代码,或使用已有的开源项目作为参考。

使用特权

评论回复
17
updownq| | 2024-1-8 17:14 | 只看该作者
设计或采用已有的通信协议来传输固件文件,如YModem、XModem、STMOD+协议等。

使用特权

评论回复
18
plsbackup| | 2024-1-8 18:18 | 只看该作者
在升级过程中,应保证设备的供电稳定,以避免因电源波动导致升级失败或设备损坏

使用特权

评论回复
19
burgessmaggie| | 2024-1-9 19:39 | 只看该作者
校验机制必不可少,例如使用CRC校验码确保数据完整性。

使用特权

评论回复
20
minzisc| | 2024-1-9 21:29 | 只看该作者
在Flash中划分两个区域,一个用于存放Bootloader,另一个用于存放可更新的应用程序。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

1522

主题

4615

帖子

6

粉丝