1. 前言
我们前面已经完整的实现了所有的功能,而如果我们的客户在使用过程中,想加入新功能了,此时怎么办呢?----你可能会说那重新烧录一份不就好了?可是在现实的生产环境中,可能往往不会给你留物理接口供你烧录,你也许可能通过网络等下发文件,此时就用到我们今天的程序了!
2. 什么是bootloader?
bootloader也是一个程序,只不过这个这个程序比较特殊,他的作用是:
- 接收升级文件
- 跳转到指定区域执行APP。
1很好理解,但是2是什么呢?怎么跳转呢?跳转了之后怎么就知道怎么执行APP了,那这件事就说来话长了,且听我慢慢道来
2.1 STM32启动过程都做了什么?
相信你一定有个疑问?—为啥程序知道我们要从main函数中开始呢?让我们从STM32上电复位那一刻起,看看都发生了什么。
当我们上电或者复位后,我们的PC指针首先会根据BOOT0和BOOT1的设置,决定系统从哪里启动。
以我们最熟悉的从flash中(0x8000 0000)启动为例,此时到了这个位置之后,先做那几件事呢?–有这么两件事是Cortex-M3规定好的
- 从0x8000 0000中得到SP栈顶的值,此时有了栈才有了运行环境嘛
- 从0x8000 0004中取出值给PC指针,此时PC指针有了值时候,就可以一条条指令运行了
那么问题来了,0x8000 0004中的值到底是多少,怎么设置呢?
这时候就需要查看我们的startup_stm32xxx.s启动文件了!
我们来看这程序中,超级重要的一块内容!
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
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
意思就是我把这些东西会放在整个bin文件的最开始,那第一个果然就是给SP指针—栈顶的值,那看起来PC指针此时就指向了ResetHadndler,接着来看ResetHandler做了什么
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main
LDR R0, =0xE000ED88 ; 使能浮点运算 CP10,CP11
LDR R1,[R0]
ORR R1,R1,#(0xF << 20)
STR R1,[R0]
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
; Dummy Exception Handlers (infinite loops which can be modified)
看起来在 Reset_Handler 中,执行了SystemInit 和 __main两个函数。这里简单介绍一下这俩函数做什么?
- SystemInit:配置时钟
- __main:配置运行需要的环境,最终跳转到main函数
此时费劲千辛万苦,终于跳转到我们的main函数执行了!
所以我们总结一下启动过程
- 根据BOOT0和BOOT1,决定从哪里驱动
- 依次初始化栈顶指针SP 和 PC指针指向ResetHandler
- 执行ResetHadnler,依次执行SystemInit()和__main
- 跳转main函数开始执行
2.2 中断发生的时候都做了什么?
这里就不多讲了,还是想说前面提到的startup.s内个段(中断向量表)的重要性,因为他保证了中断发生之后,根据中断的类型,PC指针能快速找到对应中断服务程序的位置,然后跳转去执行。
那中断发生后,CPU是怎么知道这个中断向量表( AREA RESET, DATA, READONLY)的起始位置在哪呢,就是有一个vector寄存器保存了中断向量表的起始位置
SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET;
3. 实现我们的bootloader
我们在知道了启动流程和中断处理的时候的流程,我们就知道我们想要实现跳转APP的时候,需要做哪些工作了。
- 首先找到APP的bin文件在Flash中的起始位置
- 然后根据起始位置的值,初始化APP的SP指针
- 然后bootloader直接访问 起始地址 + 4,也就是APP的ResetHandler
- 这个ResetHandler执行完之后,就到了APP的main函数
- 在APP main函数第一件事:修改vector寄存器的值,指向APP对应的中断向量表起始地址(不修改的话还是bootloader的中断向量表的起始地址)(5的修改也可以放在4的前面)
3.1 bootloader 引导下的APP的注意事项
- 一定记得修改vector寄存器的值!
试想一下,如果不改的话,在APP中发生中断了,就会跑去bootloader的中断向量表去查中断服务程序了,此时的行为就不可预测了。 - 跳转过程要关中断
因为bootloader程序和APP程序是共享中断的使用了,在跳转过程中最好屏蔽所有的中断,然后在APP中再打开中断。
系统时钟sysTick也禁止 - 跳转前最好清理掉所有的中断寄存器
bootloader根据需要可能会使用中断,试想一下跳转过程前恰好触发了bootloader某个中断,此时跳转到APP,APP一开中断,可能会有意想不到的效果 - 关于MSP和PSP
这就涉及到RTOS和裸机环境了,在裸机环境中我们一般任何时候使用的MSP指针,而在RTOS时,使用的是PSP指针。但是奥,大伙注意,我们就算APP用的RTOS的环境,启动RTOS之前,还是裸机环境,用的MSP指针。
在这里不想分类讨论了,在跳转之前就做这两件事就好:- 设置PSP 为 0 :__set_PSP(0);
- 使用MSP指针:__set_CONTROL(0);
- 回顾
跳转之所以麻烦,问题的根源在哪呢?在CPU的寄存器对于APP和bootloader是共享的。试想只要由于bootloader修改了某个寄存器,导致寄存器状态和APP期望的初始状态不一致,就有可能出问题。
3.2 代码实现(仅跳转部分)
__set_PRIMASK(1);//bootloader在进入app之前使用__set_PRIMASK(1);函数关闭中断,在app中需要将中断打开__set_PRIMASK(0);
SysTick->CTRL = 0X00;//禁止SysTick
SysTick->LOAD = 0;
SysTick->VAL = 0;
//... Disable用到的中断....
//... 清空所有这些中断的位...
NVIC_DisableIRQ(USART1_IRQn);
NVIC_DisableIRQ(EXTI2_IRQn);
NVIC_ClearPendingIRQ(USART1_IRQn);
NVIC_ClearPendingIRQ(EXTI2_IRQn);
__set_PSP(0); // 清空PSP
__set_CONTROL(0);//使用操作系统后系统内核会使用PSP模式,跳转到APP后没有恢复到MSP模式就会导致内存异常从而进入到内存异常中断
jump2app = (iapfun) * (volatile uint32_t *)(appxaddr + 4); //APP的ResetHandler
/* 初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址) */
sys_msr_msp(*(volatile uint32_t *)appxaddr); // APP的SP指针
jump2app();
在APP中要做的
sys_nvic_set_vector_table(FLASH_BASE, 0x80000); // 设置中断向量表的偏移量
__set_PRIMASK(0);//解除中断屏蔽,打开中断