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 这个地址本身!

    作者:基本昵称

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32 BootLoader代码跳转机制详解

    发表回复