什么是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(程序计数器)的值 然后取出第一条指令执行
![](https://img-blog.csdnimg.cn/img_convert/ccc4f49671af020930bd85cc6d8fa063.png)
启动文件源代码分析
;******************** (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.
;*
;*******************************************************************************
; ------------------分配栈空间----------------
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,这个标号就是栈顶的地址;在向量表里面会使用到
; Heap Configuration
; Heap Size (in Bytes)
;
; ------------------分配堆空间----------------
;和分配栈空间一样不过大小只是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程序
调研了一下群里的小伙伴,下面这个流程比较通用一些,大概是下图所示升级流程:
升级流程图
![](https://img-blog.csdnimg.cn/img_convert/09c4cc211c5aec98d4e75f40191efe10.png)
Flash分区
是以STM32F103RET6为主控做的flash分区,主要功能:
![](https://img-blog.csdnimg.cn/img_convert/332bee91e486675963c8db1ba20a70ab.png)
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 |