STM32学习笔记-CortexM3的函数跳转

STM32F1是CortexM3内核,这里记录一下其函数跳转与中断跳转中的寄存器与堆栈变换情况。

CortexM3的寄存器和堆栈

首先,得知道M3各个寄存器。

image-20240308203057070

这里主要需要关注R13栈堆寄存器(保存堆栈地址),R14连接寄存器(储存跳转前地址),R15程序计数寄存器(指向下一条要执行的指令地址)。

除此之外还有一些特殊功能寄存器。只能被MSR、MRS指令访问。

image-20240308203247182

这里只需要关注xPSR寄存器(储存程序状态),CONTROL寄存器(主要是其控制堆栈指针的功能)。

SP栈指针

SP指针指向的是栈的地址,最开始指向栈底,使用压栈出栈指令的时候会自动进行管理地址的增减。栈堆一个很重要的作用就是在跳转中储存寄存器的值

堆栈的 PUSH 与 POP

堆栈是一种存储器的使用模型。它由一块连续的内存和一个栈顶指针组成,用于实 现“后进先出”的缓冲区。其最典型的应用,就是在数据处理前先保存寄存器的值,再 在处理任务完成后从中恢复先前保护的这些值。

image-20240308205855773

在执行 PUSH 和 POP 操作时,那个通常被称为 SP 的地址寄存器,会由硬件自动调整它的值,以避免后续操作破坏先前的数据。

栈寄存器分为**MSP(主栈)和PSP(线程栈)**,由CONTROL寄存器决定。系统复位后或处于处理模式时为MSP。MSP初地址在向量表的第一项。同一时间SP只能读到一个栈,但可以通过MRS/MSR 指令来指名道姓地访问具体的堆栈指针。

LR链接寄存器

LR寄存器在使用跳转指令BL,BLX时,内核会将返回地址保存在LR中,方便之后直接使用BX LR返回。如果发生函数嵌套,需要在跳转前将BL的值压入栈,防止再次被覆盖。

在发生中断跳转到中断处理函数时,LR寄存器会储存一个叫EXC_RETURN的值,用来进行中断跳转流程。

PC寄存器

这个不用多说,储存了下一条要执行指令的地址,程序往下运行的核心(也是程序跑飞的核心)。CortexM3只支持Thumb模式运行,因此其最低位必须为1,表示在Thumb模式,否则会产生一个fault异常。

xPSR寄存器

程序状态寄存器,三合一,主要保存了一些状态(比如计算溢出、进位之类)。

CONTROL寄存器

用来设定是使用MSP还是PSP以及设定是程序是特权级(可以访问全部地址和功能)还是用户级(受限的地址和寄存器访问)。

APCS标准

ARM公司给ARM框架上的程序都规定了一套标准,所有编译器的编译结果都必须按照这个标准,这样编译出来的程序才能进行正确链接。APCS定义了以下几个方面:

  • 对寄存器使用的限制。
  • 使用栈的惯例。
  • 在函数调用之间传递/返回参数。
  • 可以被‘回溯’的基于栈的结构的格式,用来提供从失败点到程序入口的函数(和给予的参数)的列表。

这里给出它的几个要点:

  1. 参数和返回值传递,对于简单的情况,输入参数由R0-R3分别用来记录第1到第4个参数。当传递的参数超过4个时,就需要借助栈来保存参数。函数的返回值通常保存在R0中,若返回值为64位的,R1也用来保存返回值。

  2. 函数调用中的寄存器用法。函数或子程序应该保持R4-R11、R13(SP)和R14(LR)的数值。若这些寄存器在函数或子程序执行期间被修改,则其函数应该保持在栈中并在返回调用代码前恢复。这几个寄存器也被称作“被调用者保存寄存器”,也就是需要被调用者(例如子函数,中断等)进行保存的寄存器。而对于R0-R3、R12、则属于调用者保存寄存器,这几个寄存器是需要调用者做保存工作。在发生异常或中断啊时,R0-R3、R12、SP、PC会硬件自动进程压栈。

  3. 链接寄存器LR用于函数或子程序调用时返回地址的保存,若某函数需要调用另外一个函数或子程序,则它需要首先将LR的数值保存到栈中,否则,当执行了函数调用后,LR的当前值就会丢失。

总结一下,就是参数值传递按顺序存放在寄存器r0,r1,r2,r3里,超过4个参数值传递则放栈里。返回值放在r0,r1中。然后就是R4-R11、R13(SP)和R14(LR)的数值不能被改变。

函数跳转过程

函数的跳转不涉及特权模式,其过程还是比较简单的(相对于中断处理来说)。

这篇文章给出了一个具体的例子。

  1. 当程序需要进行函数跳转时,会将函数的参数放到R0,R1,R2,R3寄存器(一共4*32位),如果需要更大空间,需要将参数压到栈中。如果这其中还有有用的数据,需要先将其也压入栈中(比如中断嵌套时,这其中还有要使用的参数)。

  2. 之后执行跳转指令,该指令会跳转到一个地址并将跳转前的地址存到LR寄存器中。

  3. 进行跳转后,子函数需要保存R4-R11、R14(LR)的值到栈中(如果不改动就不需要保存)。

  4. 执行子程序,将要返回的结果存放在R0、R1

  5. 执行完成跳转回去前,将保存的寄存器值恢复出来。

  6. 利用LR的值进行跳转指令,回到原程序。

中断的跳转流程

中断的跳转与函数跳转不同,中断会直接打断程序的运行,不会等程序准备好跳转(此时R0-R3等寄存器还没有准备好,可能存有重要数据)。另外,中断使用的栈是MSP和程序可能使用PSP,栈的位置不同,会涉及到栈地址的保存。

因此对于中断,有一个专门中断跳转流程。

1,入栈

触发中断后,处理器要做的第一件事是保存现场。不像函数调用,此时无法保证APCS标准中的寄存器做好了跳转准备,因此处理器会依次把xPSR, PC, LR, R12以及R3-R0由硬件自动压入适当的堆栈中。如果此时正在使用PSP则压入PSP,否则压入MSP。进入中断后使用MSP

2,取向量

函数跳转指令需要地址,中断也需要。中断的地址储存在中断向量表中。在数据总线入栈时,指令总线(I-Code)会从向量表中读取中断处理函数的地址。

3,更新寄存器

完成上述操作后,处理器会将SP变成MSP,同时在IPSR相应位置设置异常编号,将地址放进PC,更新LR值。

此时,填入LR寄存器的值是一个特殊的名为EXC_RETURN的值,该值标志了中断返回后使用线程模式还是处理模式,使用PSP还是MSP。同时还标志了栈帧类型(结束后会设置到CONTROL寄存器的FPCA位)。

4,进入中断

进入中断函数后,和其他函数一样,软件去保存其他寄存器。之后执行自己的服务。结束后软件恢复保存的栈。

5,中断返回

在中断函数中执行触发中断返回的指令(BX LR,POP,LDR,LDM)改变PC的值为EXC_RETURN,会触发异常返回。

根据EXC_RETURN确定返回后处理器的状态。之后进行出栈,恢复之前被硬件压栈的寄存器。

引入一个EXC_RETURN的作用:使编译器能够以编译普通函数的方式编译中断函数,而不必使用标识进行注明(比如51中的__interrupt)。

参考文章

CM3权威指南

APCS,ARM 过程调用标准(ARM Procedure Call Standard)