函数调用与中断调用浅析
前言
在编写单片机的代码时,调用函数以及调用中断总感觉二者之间有些莫名的联系,函数的调用是可预知的,在代码写好的时候,什么函数调什么函数就已经由编码者确定好了,而中断的调用是不可预知的,中断随时都可以打断正常程序的运行,但中断函数也是由普通C函数实现的,二者都有着“插入”正常程序的特点,既然有“插入”的动作,必然不能影响“被插入者”的正常执行,研究一下二者对于寄存器的保存和恢复还是有必要的。
1. C函数是如何在ARM架构上工作的
针对ARM的C编译器遵循规范AAPCS(ARM Architecture Procedure Call Standard),规范中提到:一个C函数中可以改变R0-R3,R12,LR(R14)以及PSR这几个寄存器的值,而不用想着恢复其原本的值,但如果用到R4-R11这些寄存器,则需要先将这些寄存器的值保存进栈中并在用完之后(即函数结尾)恢复到原来的值。
R0-R3,R12,LR以及PSR被叫做“caller saved register“”,即调用者保存寄存器,当调用子函数之前需要先保存这些寄存器,为什么需要提前保存呢?比如在A函数中调用B函数,R0-R3被规定用于保存B函数的形参,所以如果A函数用了R0-R3保存了中间数据,则需要在进入B函数前将R0-R3的值进行保存。对LR而言,在进入B函数之前,LR保存的是A函数的返回地址,一旦进入B函数以后,LR里的值就被替换为B函数的返回地址,因此在进入B函数前需要将LR的值存进栈里,否则A函数的返回地址就丢失了。
R4-R11被称为“callee-saved registers”,即被调用者寄存器,由于根据规定caller只保存上述一些容易被更改的寄存器的内容,那么剩下的寄存器就只能由被调用者保存恢复咯。
2. 中断处理机制
在前文中已经提到中断也是由C函数实现的,对于函数调用,编译器在编译的时候就知道什么时候会调用某个函数,所以提前就能把caller saved register给保存了,但是中断处理函数的发生对于编译器而言是不可预知的,那怎么在进入中断处理函数之前把caller saved register给保存了呢? 既然编译器做不了这份工作,那就只能让处理器硬件来做了。
2.1 栈帧
见上图所示,在处理器硬件的控制下,中断进入时就将R0-R3,R12,LR以及PSR自动保存到SP指向的栈中,并在中断退出时,自动恢复这些寄存器原本的值,这样的机制就实现了允许C函数被用作中断处理函数
但这和普通的函数调用仍然存在一个小区别,也就是上图中的蓝色部分PC(Return Address),这个值在中断退出时会直接送进PC,也就是指定中断退出后程序该运行的位置,在普通的函数调用中,函数的返回地址可以直接从LR寄存器中获取,那在中断调用中,为什么不直接从LR寄存器里获取返回地址呢?原因是LR寄存器被用来存放了其他东西。
在普通函数调用的过程中,始终处于Thread模式;而在中断调用的过程中,可能是应用程序进入中断,即从Thread模式转换到了Handler模式,也可能是中断嵌套,即从Handler模式到Handler模式,因此从中断退出时,是需要判断原先的状态是Thread模式还是Handler模式,而普通的函数调用并不需要判断。
在普通函数调用过程中,SP使用MSP还是PSP是不会改变的;而在中断调用过程中,应用程序的SP可能使用PSP,也可能使用MSP,而进入中断处理函数后SP固定使用MSP,这样就要求中断返回时需要判断后续是使用MSP还是PSP。
综上所述,中断返回时需要确定回到的Operation mode(Thread/Handler)以及使用的SP(MSP/PSP),而这个任务就交给了LR寄存器,在中断中,LR存放的内容不再是返回地址,而是EXC_RETURN,见下图:
可见BIT2和BIT3满足了上面的要求,当把EXC_RETURN放进PC后(例如BX LR),则会进入处理器中断返回的流程,从栈帧中恢复对应的寄存器值并将返回地址给到PC,这样就完成了中断的返回。
为了查询方便,再贴个xPSR的定义,一般用的少,此处不过多解释
3.总结
前文基本介绍了下中断进入时的处理器自动做的操作,想知道细节可以去看cortex m3/m4权威指南。知道这个有什么用呢?首先,RTOS的任务上下文切换就用到这部分知识,如果没有这部分知识,去看RTOS任务切换的PendSV汇编代码只会一头雾水。其次,当软件运行过程中出现HardFault时,通常程序会一直停在HardFault的while(1)里,可供用户使用的信息极少,但结合栈帧的知识,就能分析出代码出现HardFault前是运行到哪个地址出的问题。