1. 堆栈的概念
堆栈(Stack)是一段特殊的内部RAM区域。堆栈的特殊性在于其访问方式与普通的内部RAM单元不同。普通的内部RAM单元可以实现随机访问,在任何时刻给定一个单元地址,都可以读取或写入期望的数据。但是,堆栈中的单元只能按照“先进后出”或“后进先出”的原则进行访问。
对堆栈单元的访问有两种基本操作,即入栈和出栈。所谓入栈,就是将数据写入堆栈单元;所谓出栈,就是从堆栈单元读出数据,如图1所示。
在51单片机中,入栈和出栈操作分别用专门的指令PUSH和POP实现。这两条指令都只需要一个操作数,并且只能采用直接寻址。
例如,要将累加器A中的字节数据入栈,可以用如下指令
PUSH ACC
注意操作数不能写为A。这里的ACC汇编后将用累加器对应的特殊功能寄存器单元地址0E0H替换,也就是上述指令等价于如下指令
PUSH 0E0H
类似地,如果希望从堆栈单元中取出一个数据保存到工作寄存器R0中,可以用如下指令:
POP 00H
其中的操作数不能写为R0,即不能采用寄存器寻址,必须采用直接寻址。
2. 栈顶与堆栈指针
入栈和出栈操作实际上是一种数据的移动,因此通常将PUSH和POP指令归属为数据传送类指令。与普通的MOV指令不同的是,指令中只有一个操作数,那么数据传送过程中所需的另外一个操作数在哪里?为什么在指令中不给出?在执行过程中怎么找到该操作数?
上述问题最终决定于堆栈单元访问的特殊性。说到堆栈的入栈和出栈操作,默认一定是对堆栈中的栈顶单元进行操作和访问,因此不需要在指令中给出。
栈顶指的是堆栈中最后一个数据所存放的单元。在程序运行过程中,根据需要可能会连续或者穿插着执行若干次入栈或出栈操作,将多个数据存入堆栈单元或者从堆栈单元中取出,从而使得堆栈中存放的数据个数在不断变化,栈顶将不断上下浮动。
为了实现“先进后出”或者“后进先出”的访问规则,可以将堆栈比喻为一个容器,该容器只有一个出入口,从容器中取出数据或者将数据放入容器都只能通过该出入口进行。因此,如果当前需要执行一次入栈操作,将数据存入堆栈后,栈顶将向上(单元地址增大的方向)移动一个单元,该单元成为新的栈顶;如果当前需要执行一次出栈操作,则只能将当前栈顶单元中的数据取出,而无法访问到栈顶下面的单元,在取出数据后栈顶向下(单元地址减小的方向)移动一个单元,该单元成为新的栈顶。
由此可见,在程序执行过程中,每次进行入栈和出栈操作都将导致当前栈顶不断变化。在51单片机中,为了记录和指示当前栈顶单元的位置,专门提供了一个特殊功能寄存器,称为堆栈指针。
堆栈指针SP(Stack Pointer)是一个专用的8位寄存器,在内部RAM特殊功能寄存器区中的地址为81H。在程序运行过程中,每执行一次入栈操作,SP先递增1,然后将一个字节数据推入堆栈。反之,每执行一次出栈操作,将一个字节数据从堆栈中弹出后,SP再递减1。上述操作最终保证SP在任何时刻都指向当前栈顶,因此在当前需要执行新的入栈和出栈操作时,单片机只需要根据SP中的数据即可确定当前栈顶单元位置。
51单片机系统刚启动或者复位后,SP中的内容初始化为07H,意味着当前栈顶为07H单元。执行第一次入栈操作时,数据将保存到SP+1指向的08H单元,意味着系统默认将地址从08H开始的内部RAM单元作为堆栈。考虑到内部RAM开始的32个单元是工作寄存器区,后面紧跟着还有位寻址区,一般在需要用到堆栈时,在主程序最前面用如下指令
MOV SP,#dat
为SP重新赋值,以便将堆栈定位到其他内部RAM区域。例如,如下指令
MOV SP,#60H
3. 堆栈的应用
在调用子程序和响应中断的过程中,将断点(主程序中当前PC的值)推入堆栈。在子程序或中断服务程序执行完毕时,从堆栈中弹出断点,存入PC,从而控制程序返回原来被打断的主程序位置继续执行。
在程序中,每执行一次子程序调用指令ACALL或LCALL或者每响应一次中断请求,首先将SP的值加1,将16位断点的低8位入栈,之后再将SP加1,将断点的高8位入栈。在执行RET或RETI指令从子程序和中断服务程序返回时,首先从堆栈当前栈顶将断点的高8位出栈,再将SP减1,之后再将断点的低8位出栈,并与高8位一起存入PC,从而正确返回主程序。
由于断点指的是指令代码在ROM中存放的16位单元地址,因此每次断点入栈和出栈都分别需要连续执行两次,执行后SP将分别加2或减2,当前栈顶上下浮动两个单元。
在主程序调用子程序的过程中,经常需要将主程序中的某些数据传递给子程序,作为子程序的入口参数。在子程序返回时,子程序中处理后的某些数据也需要作为出口参数返回给主程序,以便在主程序访问和做进一步操作。
在汇编语言程序中,主程序和子程序中的不同数据可能都需要存放在工作寄存器中,而在51单片机中,工作寄存器数量有限,因此当数据较多时,工作寄存器不够用。例如,在主程序和子程序中的两个不同数据可能都需要存放到指定的某个工作寄存器,此时将出现数据冲突。
现场通常指的是在子程序中要修改和使用,但返回主程序时不希望发生变化的寄存器和存储单元。在开始执行子程序之前,先将这些寄存器和存储单元中的数据用PUSH指令推入堆栈,称为现场保护。在子程序中使用完该寄存器返回主程序之前,再利用POP指令将其从堆栈中取出来,称为恢复现场。
实现现场保护和恢复的PUSH和POP指令一般都在子程序中。需要注意的是,如果有多个现场(例如多个工作寄存器),在入栈保护现场和出栈恢复现场的过程中,必须按照先进后出和后进先出的顺序进行操作,否则不能正确恢复现场。
例如,在程序中最开始用如下指令将两个现场R0和R1入栈:
在子程序执行完毕恢复现场时,必须顺序执行如下两条指令:
堆栈单元不能随机访问,每次入栈和出栈操作都是对当前栈顶单元进行。如果有多个数据进行入栈和出栈操作,根据堆栈单元的特殊访问规则,还可以实现一些特殊的功能。
例如,将累加器A和30H内部RAM单元中的数据交换,可以用如下指令实现:
XCH A,30H
也可以通过堆栈,利用如下程序段实现:
配套教材