STM32 BootLoader代码跳转机制详解
文章目录
概要
在Bootoader中,我们将0X08000000作为B区起始地址,将0X0800A000作为A区起始地址,B区负责监测是否需要更新A区程序,若不更新程序,则跳转至A区代码进行运行。
在STM32中,由于我们无法直接对PC指针进行赋值跳转,所以这里我们使用函数指针来完成对PC指针的赋值。
寄存器
在Corte-M3中,共有R0-R15个寄存器组,其中R13是堆栈指针SP,R14是链接寄存器,R15是PC。

R0-R12是通用寄存器,用于数据操作。
R13是两个堆栈指针,
主指针MSP:复位后缺省使用的堆栈指针,用于异常操作,包括中断
进程堆栈指针PSP;由用户应用到的程序代码使用。
注:在跳转A区之后,我们需要让A区认为自己是复位后重启直接运行的,
在下面启动文件的向量表中,我们可以可以看到,当系统启动时,第一块访问的地址为__initial_sp,那么在跳转至A区之后,我们首先要做的就是让SP默认值为0x0800A000.在这里SP的大小我们是不知道的,因为这是程序所定义的,但是这个初始值是必须要为A区起始地址处的值,即 *A区起始地址。为什么?
因为我们知道,如果不进行分区,即当程序上电就执行,那么SP默认为0x08000000处的值,因为STM32给我们下载程序的flash的起始地址就是0x080000000。所以我们的A区程序他的sp就需要指向A区的起始地址的值

R14:链接寄存器,当呼叫一个子程序是,由R14存储返回地址。中断,函数跳转结束都由该寄存器进行跳转回来。
R15 PC指针寄存器:指向当前的程序地址,如果修改它,那么我们就能改变程序的执行流。
注:程序跳转至A区执行靠的就是将PC指针指向A区代码来实现
在上述向量表中,可以看到Reset_Handler,即为复位向量,它位于向量表的第二位,那么他的初始值就为 A区起始地址+4。(因为寄存器都是32位,4*8=32,所以+4)。
但是,我们说过,我们需要让A区认为自己是复位之后立即执行的,那么我们就需要让PC指针来指向 Reset_Handler的地址,即A区起始地址+4
程序
typedef void (*load_a)(void); //指向函数的指针
load_a load_A;
__asm void MSR_SP( uint32_t addr )
{
MSR MSP,R0
BX R14
}
//MSR MSP,R0 将addr赋予MSSP
// BX R14 子函数之间跳转保存返回值
//设置PC
void LOAD_A(uint32_t addr)
{
if( (*(uint32_t *)addr >= 0x20000000) && ( *(uint32_t *)addr <= 0x20004fff ) )
{
MSR_SP( *(uint32_t *)addr ); //SP初始化
load_A = ( load_a )*(uint32_t *)(addr+4); //将复位地址,即0X08005000+4给到PC
load_A();
}
}
代码详解
typedef void (*load_a)(void); // 定义一个函数指针类型,指向无参、无返回值的函数
在这里,定义一个函数指针,令返回值为void;
load_a load_A; // 声明一个函数指针变量 load_A
在后续,load_A将会赋值为具体函数地址,在本文中,即将PC指针指向A区进行运行。
__asm void MSR_SP( uint32_t addr )
{
MSR MSP,R0
BX R14
}
该代码主要功能:设置MSP为A区起始地址,即在前文所述,将SP进行设置。然后返回继续执行C代码。
在汇编中,某个函数,如果他传入了参数,那么参数的存储会按照R0 R1 …的顺序存储,因此这里是将R0的值赋予MSP。
BX R14实际上是返回到调用该汇编函数的位置。
if( (*(uint32_t *)addr >= 0x20000000) && ( *(uint32_t *)addr <= 0x20004fff ) )
{
MSR_SP( *(uint32_t *)addr ); //SP初始化
load_A = ( load_a )*(uint32_t *)(addr+4); //将复位地址,即0X08005000+4给到PC
load_A();
}
if行的作用,判断addr地址处存的值是否在RAM地址范围,即0x2000000-0x20004fff处,
*(uint32_t *)addr:首先(uint32_t *)addr将addr类型定义为32位地址,之后再利用*进行数据读取,
MSR_SP( *(uint32_t *)addr ); addr 地址处存的值(应用程序的堆栈初值)传给 MSR_SP
注:如果不先设置好SP,直接跳转到应用程序,程序会跑飞。
load_A = ( load_a )*(uint32_t *)(addr+4); //将复位地址,即0X08005000+4给到PC
取 addr+4 地址的内容(向量表第二项),这是复位中断处理程序的入口地址(也就是应用程序真正执行的起点)。然后强制类型转换为 load_a 类型(也就是一个无参无返回值的函数指针)。最后把它赋值给 load_A。
此时,load_A处存上了应用程序真正执行的起点。
但是,我们的PC指针还是指在这个函数中,并没有指向应用程序真正执行的起点。
load_A();
调用函数 load_A(),注意,前面的load_A 本身是一个函数指针,指向一个函数,它本身只是一个指针而已,这个指针指到了程序起点而已。此时可以认为我PC看到load_A拿到了起点地址,但我的PC不知道我要去起始地址。
而load_A(),加上括号之后,他是一个无返回,无参数的函数,当PC指向这个函数之后,PC进入了这个函数,而这个函数一进去他就是A区程序。意味着PC开始指向了A区程序的起点,也就是Reset_Handler复位向量,
load_A(); 是通过函数指针调用对应的函数,相当于真正执行跳转到那个地址开始跑代码
大家可能会有一个疑惑,为什么我们的PC指针不指向(uint32_t *)(addr+4)这个地址
而是指向(uint32_t *)(addr+4)这个地址里面的值?即*(uint32_t *)(addr+4)
我们先看Cortex-M架构的启动流程,先看向量表

注意:
0x08005000他并不是程序入口,他是堆栈指针初始值
0x08005004:存在真正要跳转执行的函数入口地址,这个值给PC。
所以在启动程序时,
取*addr,给SP
取*addr+4,给PC
假设:
如果直接跳到 (addr+4),那只是跳到地址 0x08005004,而那里只是放了一个数据(程序入口地址),而不是代码本体。跳过去,程序一定会异常或跑飞!
所以在 Cortex-M 芯片中,向量表中的第2个元素(也就是 (addr + 4) 处的内容)才是应用程序的真正入口地址(要跳过去执行),而不是直接跳到 addr+4 这个地址本身!
作者:基本昵称