Bootstrap

关于MCU中函数调用和中断处理的一些理解

前言

在做MCU的芯片开发时,虽然经常用的函数调用和中断处理,也知道在处理这两件事情时,会做现场保护,但是关于现场保护的细节是什么,入栈什么寄存器,出栈哪些寄存器一直没有太做关心。最近在面试时经常有些面试官会问到一些细节的东西,所以对此做一些整理。

函数调用

在早期的理解当中,认为函数调用和中断处理一样,会将芯片的所有寄存器进行入栈和出栈操作,实现现场的保护和恢复。目前通过查阅资料和芯片仿真,发现并不是这样。先说结论吧,在函数调用时会将部分有用的寄存器且在函数调用时会发生改变的寄存器进行入栈,并不是所有的寄存器都会入栈,而且出入栈的操作都是编译器来完成,非芯片硬件自动完成。
之所以会这样,是因为函数调用时预料范围内的代码执行,是完全可控的,当前执行的函数调用另外一个函数时,是从当前代码段通过跳转指令主动跳转到另外一个代码段,只需要保存一部分寄存器,比如保存跳转指令的下一条指令的地址到lr寄存器,有可能根据多级调用,还会将lr寄存器入栈等。无需保存所有寄存器的值。

下面根据一段代码看一下汇编代码:

int multip(int a, int b)
{
	return a * b;
}

int add_mult(int a, int b)
{	
		int c = a + b;
		c = multip(a, b);
		return c;
}
int test_fun(int a, int b)
{	
	return add_mult(a, b);
	
}
int main(void)
{
	return test_fun(3, 4);
}

以下是Keil汇编代码:

0x08001974 F000F812  BL.W     0x0800199C test_fun
0x0800199C B580      PUSH     {r7,lr}			//第一次函数调用时会入栈r7, lr
0x0800199E B082      SUB      sp,sp,#0x08
0x080019A0 9001      STR      r0,[sp,#0x04]
0x080019A2 9100      STR      r1,[sp,#0x00]
    70:         return add_mult(a, b); 
0x080019A4 9801      LDR      r0,[sp,#0x04]
0x080019A6 9900      LDR      r1,[sp,#0x00]
0x080019A8 F7FFFFCE  BL.W     0x08001948 add_mult

0x08001948 B580      PUSH     {r7,lr}			//第二次函数调用时会入栈r7, lr
0x0800194A B084      SUB      sp,sp,#0x10
0x0800194C 9003      STR      r0,[sp,#0x0C]
0x0800194E 9102      STR      r1,[sp,#0x08]
    64:                 int c = a + b; 
0x08001950 9803      LDR      r0,[sp,#0x0C]
0x08001952 9902      LDR      r1,[sp,#0x08]
0x08001954 4408      ADD      r0,r0,r1
0x08001956 9001      STR      r0,[sp,#0x04]
    65:                 c = multip(a, b); 
0x08001958 9803      LDR      r0,[sp,#0x0C]		//第三次函数调用时并没有入栈操作
0x0800195A 9902      LDR      r1,[sp,#0x08]
0x0800195C F000F816  BL.W     0x0800198C multip

0x0800198C B082      SUB      sp,sp,#0x08
0x0800198E 9001      STR      r0,[sp,#0x04]
0x08001990 9100      STR      r1,[sp,#0x00]

0x08001992 9801      LDR      r0,[sp,#0x04]
0x08001994 9900      LDR      r1,[sp,#0x00]
0x08001996 4348      MULS     r0,r1,r0
0x08001998 B002      ADD      sp,sp,#0x08
0x0800199A 4770      BX       lr			//第一次返回

0x080019A0 9001      STR      r0,[sp,#0x04]
0x08001962 9801      LDR      r0,[sp,#0x04]
0x08001964 B004      ADD      sp,sp,#0x10
0x08001966 BD80      POP      {r7,pc}		//第二次返回,现场恢复

0x080019AC B002      ADD      sp,sp,#0x08
0x080019AE BD80      POP      {r7,pc}		//第三次返回,现场恢复

以下是arm-none-eabi-gcc汇编的结果

	.cpu arm7tdmi
	.eabi_attribute 20, 1
	.eabi_attribute 21, 1
	.eabi_attribute 23, 3
	.eabi_attribute 24, 1
	.eabi_attribute 25, 1
	.eabi_attribute 26, 1
	.eabi_attribute 30, 6
	.eabi_attribute 34, 0
	.eabi_attribute 18, 4
	.file	"a.c"
	.text
	.align	2
	.global	multip
	.arch armv4t
	.syntax unified
	.arm
	.fpu softvfp
	.type	multip, %function
multip:
	@ Function supports interworking.
	@ args = 0, pretend = 0, frame = 8
	@ frame_needed = 1, uses_anonymous_args = 0
	@ link register save eliminated.
	str	fp, [sp, #-4]!			//不需要入栈
	add	fp, sp, #0
	sub	sp, sp, #12
	str	r0, [fp, #-8]
	str	r1, [fp, #-12]
	ldr	r3, [fp, #-8]
	ldr	r2, [fp, #-12]
	mul	r1, r2, r3
	mov	r3, r1
	mov	r0, r3
	add	sp, fp, #0
	@ sp needed
	ldr	fp, [sp], #4
	bx	lr					//第一次返回,未压栈所以也不出栈
	.size	multip, .-multip
	.align	2
	.global	add_mult
	.syntax unified
	.arm
	.fpu softvfp
	.type	add_mult, %function
add_mult:
	@ Function supports interworking.
	@ args = 0, pretend = 0, frame = 16
	@ frame_needed = 1, uses_anonymous_args = 0
	push	{fp, lr}			//入栈部分寄存器
	add	fp, sp, #4
	sub	sp, sp, #16
	str	r0, [fp, #-16]
	str	r1, [fp, #-20]
	ldr	r2, [fp, #-16]
	ldr	r3, [fp, #-20]
	add	r3, r2, r3
	str	r3, [fp, #-8]
	ldr	r1, [fp, #-20]
	ldr	r0, [fp, #-16]
	bl	multip
	str	r0, [fp, #-8]
	ldr	r3, [fp, #-8]
	mov	r0, r3
	sub	sp, fp, #4
	@ sp needed
	pop	{fp, lr}		//出栈部分寄存器
	bx	lr
	.size	add_mult, .-add_mult
	.align	2
	.global	test_fun
	.syntax unified
	.arm
	.fpu softvfp
	.type	test_fun, %function
test_fun:
	@ Function supports interworking.
	@ args = 0, pretend = 0, frame = 8
	@ frame_needed = 1, uses_anonymous_args = 0
	push	{fp, lr}		//入栈部分寄存器
	add	fp, sp, #4
	sub	sp, sp, #8
	str	r0, [fp, #-8]
	str	r1, [fp, #-12]
	ldr	r1, [fp, #-12]
	ldr	r0, [fp, #-8]
	bl	add_mult
	mov	r3, r0
	mov	r0, r3
	sub	sp, fp, #4
	@ sp needed
	pop	{fp, lr}		//出栈部分寄存器
	bx	lr
	.size	test_fun, .-test_fun
	.align	2
	.global	main
	.syntax unified
	.arm
	.fpu softvfp
	.type	main, %function
main:
	@ Function supports interworking.
	@ args = 0, pretend = 0, frame = 0
	@ frame_needed = 1, uses_anonymous_args = 0
	push	{fp, lr}
	add	fp, sp, #4
	mov	r1, #4
	mov	r0, #3
	bl	test_fun
	mov	r3, r0
	mov	r0, r3
	sub	sp, fp, #4
	@ sp needed
	pop	{fp, lr}
	bx	lr
	.size	main, .-main
	.ident	"GCC: (GNU Tools for Arm Embedded Processors 8-2019-q3-update) 8.3.1 20190703 (release) [gcc-8-branch revision 273027]"

中断处理

在CM3的权威指南中关于中断/异常的相应序列中有这样的描述:

当CM3开始响应一个中断时,会在它看不见的体内奔涌起三股暗流:

  • 入栈: 把8个寄存器的值压入栈
  • 取向量:从向量表中找出对应的服务程序入口地址
  • 选择堆栈指针MSP/PSP,更新堆栈指针SP,更新连接寄存器LR,更新程序计数器PC

入栈
响应异常的第一个行动,就是自动保存现场的必要部分:依次把xPSR, PC, LR, R12以及R3‐R0由硬件自动压入适当的堆栈中:如果当响应异常时,当前的代码正在使用PSP,则压入PSP,即使用线程堆栈;否则压入MSP,使用主堆栈。一旦进入了服务例程,就将一直使用主堆栈。
在这里插入图片描述
取向量
当数据总线(系统总线)正在为入栈操作而忙的团团转时,指令总线(I-Code总线)可不是凉快地坐着看热闹——它正在为响应中断紧张有序地执行另一项重要的任务:从向量表中找出正确的异常向量,然后在服务程序的入口处预取指。由此可以看到各自都有专用总线的好处:入栈和取指这两项工作能同时进行。
更新寄存器
在入栈和取向量的工作都完毕之后,执行服务例程之前,还要更新一系列的寄存器:

  • SP: 在入栈中会把堆栈指针(PSP或MSP)更新到新的位置。在执行服务例程后,将由MSP负责对堆栈的访问。
  • PSR: IPSR位段(地处PSR的最低部分)会被更新为新响应的异常编号。
  • PC:在向量取出完毕后, PC将指向服务例程的入口地址
  • LR: LR的用法将被重新解释,其值也被更新成一种特殊的值,称为“EXC_RETURN”,并且在异常返回时使用。EXC_RETURN的二进制值除了最低4位外全为1,而其最低4位则有另外的含义。

异常返回
当异常服务例程执行完毕后,需要很正式地做一个“异常返回”动作序列,从而恢复先前的系统状态,才能使被中断的程序得以继续执行。从形式上看,有3种途径可以触发异常返回序列,如表9.2所示;不管使用哪一种,都需要用到先前储的LR的值。
在这里插入图片描述
异常返回值 在这里插入图片描述
在这里插入图片描述

进程切换

在一些RTOS中所有的任务切换本质都是通过触发PendSV异常来实现,所以进程切换的过程中对现场的保护有一部分依赖于异常/中断对现场的保护,即入栈xPSR, PC, LR, R12以及R3‐R0等寄存器,还有一些需要在PendSV的异常服务程序中进行手动的入栈和出栈操作(比如对R4~R11的入栈出栈操作)。以freeRTOS中CM3的任务切换为例,下面代码中已添加注释:

__asm void xPortPendSVHandler( void )
{
	extern uxCriticalNesting;
	extern pxCurrentTCB;            /* 指向当前激活的任务 */
	extern vTaskSwitchContext;      
	 
	PRESERVE8
  
	mrs r0, psp                   /* PSP内容存入R0 */    
	isb                           /* 指令同步隔离,清流水线 */
 
 
	ldr	r3, =pxCurrentTCB     /* 当前激活的任务TCB指针存入R2 */
	ldr	r2, [r3]
 
 
	stmdb r0!, {r4-r11}          /* 保存剩余的寄存器,异常处理程序执行前,硬件自动将xPSR、PC、LR、R12、R0-R3入栈 */
	str r0, [r2]		     /* 将新的栈顶保存到任务TCB的第一个成员中 */
 
 
	stmdb sp!, {r3, r14}         /* 将R3和R14临时压入堆栈,因为即将调用函数vTaskSwitchContext,调用函数时,返回地址自动保存到R14中,所以一旦调用发生,R14的值会被覆盖,因此需要入栈保护; R3保存的当前激活的任务TCB指针(pxCurrentTCB)地址,函数调用后会用到,因此也要入栈保护*/
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY   /* 进入临界区 */
	msr basepri, r0
	dsb                         /* 数据和指令同步隔离 */
	isb
	bl vTaskSwitchContext        /* 调用函数,寻找新的任务运行,通过使变量pxCurrentTCB指向新的任务来实现任务切换 */
	mov r0, #0                   /* 退出临界区*/
	msr basepri, r0
	ldmia sp!, {r3, r14}         /* 恢复R3和R14*/
 
 
	ldr r1, [r3]
	ldr r0, [r1]		     /* 当前激活的任务TCB第一项保存了任务堆栈的栈顶,现在栈顶值存入R0*/
	ldmia r0!, {r4-r11}	     /* 出栈*/
	msr psp, r0
	isb
	bx r14                      /* 异常发生时,R14中保存异常返回标志,包括返回后进入线程模式还是处理器模式、使用PSP堆栈指针还是MSP堆栈指针,当调用 bx r14指令后,硬件会知道要从异常返回,然后出栈,这个时候堆栈指针PSP已经指向了新任务堆栈的正确位置,当新任务的运行地址被出栈到PC寄存器后,新的任务也会被执行。*/
	nop
}
;