章节概述:
介绍什么是IAP、IAP的前置知识。
IAP
IAP(In Application Programming,在应用中编程);是用户自己的程序在运行过程中对User Flash 的部分区域进行烧写,目的是为了在产品发布后可以方便地通过预留的通信口对产品中的固件程序进行更新升级。
原理是:将代码区划分为两部分,两部分区域各存放一个程序,通过触发Bootloader对User application的擦除和重新写入即可完成用户应用的更换:
Bootloader
(引导加载程序),程序执行初始入口,根据定义好的条件(例如:按键是否被按下、串口是否接收到特定的数据、U盘是否插入等)选择并执行分支;出厂以后固定下来,一般不随便更改。(不建议开发人员提供更改Bootloader
的接口)User Application
(用户应用程序),允许在需要变更时,对这一区域的程序进行更换。
站在用户的角度来说,IAP做到了能让用户自己来更换设备里边的代码程序而厂家这边只需要提供给用户一个代码文件。
代码区:
BootLoader Application
┌─────┬─────────┐
└─────┴─────────┘
根据实际情况分配这两块空间的大小,BootLoader程序占用的空间越小越好。
通常实现IAP功能时,即用户程序运行中作自身的更新操作,需要在设计固件程序时编写两个项目代码:
- 第一个项目程序不执行正常的功能操作,而只是通过某种通信方式(如USB、USART)接收程序或数据,执行对第二部分代码的更新
- 第二个项目代码才是真正的功能代码。
后面我们还会利用多个分区完成更高级的操作。
IAP准备
为了完成IAP功能,以及明确理解各个步骤的意义,我们先对有关知识进行说明:
- STM32启动时程序执行顺序(涉及分区与跳转)
- 中断机制(为了做到中断重定向,最终的目的是使App程序的正确运行)
STM32启动步骤
程序执行入口
STM32的启动方式有3种(均是芯片内置的存储介质):
- 内置FLASH(用户闪存,一般默认是这个)
- 内置SRAM
- 系统存储器ROM
通过BOOT0
和BOOT1
引脚的设置可以选择启动方式:
状态 | 结果 |
---|---|
BOOT1=x BOOT0=0 |
从用户闪存(内置FLASH)启动,这是正常的工作模式。(x 代表任意) |
BOOT1=0 BOOT0=1 |
从系统存储器启动,这种模式启动的程序功能由厂家设置。 |
BOOT1=1 BOOT0=1 |
从内置SRAM启动,这种模式可以用于调试。 |
原理:决定哪个地址(
Main Flash
,System Flash
,SRAM
)映射到地址0x0000 0000
。
在《Cortex-M3权威指南》有讲述:芯片复位后首先会从向量表里面取出两个值:
- 从
0x0000 0000
地址取出MSP(主堆栈寄存器)
的值 - 从
0x0000 0004
地址取出PC(程序计数器)
的值,然后取出第一条指令执行
请注意,这与传统的ARM架构不同(其实也和其它大多数的单片机不同)。传统的ARM架构总是从0地址开始执行第一条指令,并且这是一条跳转指令。在CM3中,在0地址提供的是MSP的初始值,然后紧跟着的是向量表(向量表在以后还可以转移到其它位置)。向量表中的数值是32位的地址,而不是跳转指令。向量表的第一个条目指向复位后应执行的第一条指令。
因为CM3使用的是向下生长的满栈,所以MSP得初始值必须是堆栈内存的末地址加1,举例来说:
如果堆栈区域在
0x20007C00
~0x20007FFF
之间,那么MSP的初始值就必须是0x20008000
。
所以我们知道,在嵌入式中,main函数并不是程序执行的入口。
一般来说,芯片执行代码的入口地址是PC
指针在上电后的第一个值,也就是复位。通过复位后,指令在按顺序往下执行。那么,由我们编写的代码链接顺序就很重要了。
一般来说,执行顺序是由链接文件告诉编译器来决定的。当链接器进行链接的时候,首先决定各个目标文件在最终可执行文件里的位置。然后访问所有目标文件的地址重定义表,对其中记录的地址进行重定向(加上一个偏移量,即该编译单元在可执行文件上的起始地址)。然后遍历所有目标文件的未解决符号表,并且在所有的导出符号表里查找匹配的符号, 并在未解决符号表中所记录的位置上填写实现地址。最后把所有的目标文件的内容写在各自的位置上,再作一些另的工作,就生成一个可执行文件。所以,程序的链接地址必须等于运行地址。
那么,一般在arm中,都是以 *.s
文件作为初始入口,例如stm32的startup_stm32f10x_md.s
文件写得很清楚:
This module performs:
- Set the initial SP
- Set the initial PC == Reset_Handler
- Set the vector table entries with the exceptions ISR
- Branches to __main in the C library
也就是说,经过启动文件,通过一些设置堆栈和中断等最后引导至我们所说的main
函数,可以将main
函数视为应用层的入口。
启动文件分析
分析
startup_stm32f10x_hd.s
启动文件时会涉及到到一些汇编指令,如果不认识的指令可以到mdk
的help
->μVision Help
里面搜索。
如下:
;******************** (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 >>>
;*******************************************************************************
; THE PRESENT FIRMWARE WHICH IS FOR GUIDANCE ONLY AIMS AT PROVIDING CUSTOMERS
; WITH CODING INFORMATION REGARDING THEIR PRODUCTS IN ORDER FOR THEM TO SAVE TIME.
; AS A RESULT, STMICROELECTRONICS SHALL NOT BE HELD LIABLE FOR ANY DIRECT,
; INDIRECT OR CONSEQUENTIAL DAMAGES WITH RESPECT TO ANY CLAIMS ARISING FROM THE
; CONTENT OF SUCH FIRMWARE AND/OR THE USE MADE BY CUSTOMERS OF THE CODING
; INFORMATION CONTAINED HEREIN IN CONNECTION WITH THEIR PRODUCTS.
;*******************************************************************************
; Amount of memory (in bytes) allocated for Stack
; Tailor this value to your application needs
; <h> Stack Configuration
; <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>
; ------------------分配栈空间----------------
Stack_Size EQU 0x00000400 ;EQU指令是定义一个标号;标号名是Stack_Size; 值是0x00000400(有点类似于C语言的#define)。Stack_Size标号用来定义栈的大小,这里是1 Kb
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的值是链接后,由链接器生成
; 上电后根据boot引脚来决定PC位置,比如boot设置为flash启动,则启动后PC跳到0x08000000。此时CPU会先取2个地址,第一个是栈顶地址,第二个是复位异常地址
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 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD SVC_Handler ; SVCall Handler
DCD DebugMon_Handler ; Debug Monitor Handler
DCD 0 ; Reserved
DCD PendSV_Handler ; PendSV Handler
DCD SysTick_Handler ; SysTick Handler
; External Interrupts
DCD WWDG_IRQHandler ; Window Watchdog
DCD PVD_IRQHandler ; PVD through EXTI Line detect
DCD TAMPER_IRQHandler ; Tamper
DCD RTC_IRQHandler ; RTC
DCD FLASH_IRQHandler ; Flash
DCD RCC_IRQHandler ; RCC
.....由于文件太长这里省略了部分向量表的定义,完整的可以查看工程里的启动文件
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系统的整个启动流程:
- 系统复位,CPU在系统时钟的第4个上升沿根据
BOOT0
,BOOT1
的配置确定寄存器SYSCFG_CFGR1
的MEM_MODE
的值。 MEM_MODE
进一步决定哪个地址(Main Flash
,System Flash
,SRAM
)映射到地址0x0000 0000
。- CPU从
0x0000 0000
地址取出栈顶地址赋给MSP寄存器(主堆栈寄存器),即MSP = __initial_sp
。这一步是由硬件自动完成的。 - 从
0x0000 0004
地址取出复位程序的地址给PC寄存器(程序计数器),即PC = Reset_Handler
。这一步也是由硬件自动完成。 映射地址+4
对应着复位中断程序(如0x08000 0004
),也就是说,系统一开始就执行Reset_Handler
,进而运行SystemInit
。- 在
SystemInit
函数中初始化系统时钟。 - 跳到C库的
__main
函数初始化堆栈(初始化时是根据前面的分配的堆空间和栈空间来初始化的)和代码重定位(初始RW 和ZI段),然后跳到main
函数执行应用程序。
就这样,整个代码启动完成。接下来就是中断产生于中断响应了,中断响应在IAP方案中也是需要注意的地方。
中断机制
为了方便讨论,以程序的映射地址0x00000000
而不是类似0x08000000
这样的实际物理地址进行讨论。
当以内置flash作为启动,flash 的起始地址
0x0800 0000
被映射到0x0000 0000
。
没有IAP时的中断
在发生中断的过程为:发生中断(中断请求),到中断向量表查找中断函数入口地址,跳转到中断函数,执行中断函数,中断返回。
也就是说在STM32的内置的Flash中有一个中断向量表来存放各个中断服务函数的入口地址,内置Flash的分配情况大致如下图:
栈顶地址 | 中断向量表 | 中断服务函数 | 主程序 |
↑0x08000000,主程序 |
中断向量表存放在代码开始部分的后4个字节处(即0x00000004
);当发生中断后程序通过查找该表得到相应的中断服务程序入口地址,然后再跳到相应的中断服务程序中执行。
代码区开始的4个字节存放的是栈顶的地址(即0x00000000
中的内容是栈顶的位置)。
则程序的走向应为:
上电后从0x00000004
处取出复位中断向量的地址,然后跳转到复位中断程序的入口,执行结束后跳转到main函数中。
在执行main
函数的过程中发生中断,则STM32强制将PC
指针指回中断向量表处,从中断向量表中找到相应的中断函数入口地址,跳转到相应的中断服务函数,执行完中断函数后再返回到main
函数中来。
允许IAP时的中断
通过一开始的概述,我们知道IAP将分了2个区;了解了启动步骤的具体流程,就不难看出实际上IAP中的2个分区的各个部分应该是什么样子:
栈顶地址 | 中断向量表 | 中断服务函数 | IAP主程序 | 新栈顶地址 | 新中断向量表 | 中断服务函数 | 用户程序 |
↑0x00000000,Bootloader | ↑0x00000000+N+M,User Application |
BootLoader
程序和User application
各有一个中断向量表,假设BootLoader程序(IAP程序)占用的空间为N+M
字节(那么用户程序的首地址就是0x00000000+N+M
)。
则程序的走向应为:
上电初始程序依然从0x00000004
处取出复位中断向量地址,执行复位中断函数后跳转到IAP的main
。
在支持IAP模式的情况中,关于同时存在2个中断向量表的理解:
实际上,在
BootLoader
跳转到User
之前,需要对中断向量表进行处理(具体是通过中断向量表设置一个偏移量以设置向量表的重定义),改完偏移之后的中断异常都用新表(复位中断例外,APP通过软件reset跳转至BootLoader
)。简单地理解就是,整个向量表地址向后偏移了(直接改变),再也回不去第一个向量表了(复位中断例外);而不是像有些教程说的:先跳到旧的向量表,再跳到新的向量表。
在BootLoader
的main
函数执行完成后强制跳转到0x00000004+N+M
处,执行Reset_handler
,跳转到新的main
函数中来。
当发生中断请求后,程序跳转到新的中断向量表中取出新的中断函数入口地址,再跳转到新的中断服务函数中执行,执行完中断函数后再返回到main
函数中来。
在User App
的main
函数的执行过程中,如果CPU得到一个中断请求,PC指针本来应该跳转到0x00000004
处的中断向量表,由于我们设置了中断向量表偏移量为N+M
,因此PC指针被强制跳转到0x00000004+N+M
处的中断向量表中得到相应的中断函数地址,再跳转到相应新的中断服务函数,执行结束后返回到main
函数中来。
所以,如果没有处理中断向量表,那么在User App
下执行时,遇到中断以后会重新跳转IAP
中的中断处理函数,显然是不符合我们的期望的。
IAP流程总结
读到这里,我相信大家对于IAP需要做的事情有很清晰的认识。
对于 BootLoader
流程大致应该如下:
1、初始化时钟。
2、初始化中断向量表地址。
3、初始化用于交互的接口(如按键,可设计为:上电时如果按键被按下则进行用户程序更新操作)。
4、初始化通讯接口(如串口,用于读取升级程序)。
5、判断跳转条件,是则执行步骤6,否则执行步骤10。
6、擦除用户程序(例如:擦除0x08008000 ~ 0x0807ffff
地址空间Flash)。
7、从串口读取新的用户代码数据,把代码写入用户程序空间。
8、检测数据接收完毕?是则执行步骤9,否则跳回步骤7。
9、用户程序更新完毕,等待重新上电或硬件复位。
10、跳转到用户程序(强制将PC指针跳转到0x08008000+4
处)。
对于 App
流程大致应该如下:
1、初始化时钟。
2、修改中断向量表地址。
3、正常执行。