在调用函数时,也即当执行LCALL、ACALL等汇编指令时,硬件首先把PC指针压入到SP指向的系统栈(PC不可寻址,无法用程序出入栈,只能由硬件自动完成),随着函数调用链条不断加长,SP指向的系统栈被占用的空间越来越多。
下面看一下执行ACALL和LCALL时,硬件为我们自动做了什么? (这些信息都能从帮助文件中找到,keil->Help->uVision Help打开帮助文件,依次展开8051 instruction set指令集->Instructions)
ACALL等价于:
(下述代码由硬件自动完成)
PC = PC + 2
SP = SP + 1
(SP) = PC[7-0]
SP = SP + 1
(SP) = PC[15-8]
PC[10-0] = A[10-0]
LCALL等价于:
PC = PC + 3
SP = SP + 1
(SP) = PC[7-0]
SP = SP + 1
(SP) = PC[15-8]
PC = addr16
PC存放的是下一条要执行的指令,在调用子函数前,要把紧跟ACALL/LCALL指令后面的一条指令的ROM地址入栈,因为ACALL指令本身占用2字节,LCALL占用3字节,所以上面
的代码才一个PC+2,一个PC+3。
这里顺便再提一下函数(子程序)的返回指令:RET和RETI,RET用于普通子程序返回,这两个指令对应的硬件自动代码如下(来自于keil的帮助文件):
RET:
PC[15-8] = (SP)
SP = SP - 1
PC[7-0] = (SP)
SP = SP - 1
|
|
|
RETI:
PC[15-8] = (SP)
SP = SP - 1
PC[7-0] = (SP)
SP = SP - 1
执行完上述伪代码之后,
恢复中断逻辑,
以便接收下一个中断。
可以看到,RET和RETI在对PC和SP的处理上一模一样,区别在于,RETI还额外地要恢复一下中断逻辑,(因为同优先级的中断不可嵌套,不可嵌套的实现机制就是靠的这个中断逻辑,如果我们在中断服务函数中使用了RET来返回,中断逻辑得不到恢复的话,那么同优先级以及更低优先级的中断将无法响应)
对于51单片机,SP指向的系统栈的内容就两部分:
(1)是上面所述的CALL指令压入的PC指针;
(2)是中断服务函数为保护现场而手动压入的A、B、PSW、DPTR等,(keil编译生成的汇编不会把Rn入栈,只会使用using指令切换工作寄存器组,也即切换BANK,但是如果中断服务代码我们使用汇编自己写的话,也可以不切换BANK,而把Rn入栈)。可参考keil帮助文件,依次展开CX51 Compiler User‘s Guide-> Language Extensions -> Function Declarations -Interrupt > Interrupt Functions
对于STM32等单片机,在仅使用MSP的情况下,MSP的栈中除了上述2个部分以外,还会有第3部分:函数的形参和局部变量(Rn不足时);若Rn足以容纳形参和局部变量,那么形参和局部变量就不占用栈了,但是如果本函数func1( )中又需要调别另一个函数func2( )的话,func2( )函数传参等也是需要使用Rn的,这样本函数func1()在调用func2( )之前,func1()就必须得把刚才存在Rn中的形参和局部变量压栈,以便腾出Rn供func2( )传参和导出返回值用)。