AVR 微控制器中的 CALL 指令和堆栈

2025年03月17日 | 阅读 9 分钟

CALL指令可以描述为一种控制转移指令。借助CALL指令,我们可以调用特定的子程序。子程序包含一组指令。这些类型的指令将通过子程序频繁执行。由于这种子程序特性,程序将变得更加结构化,并且还将节省大量内存空间。AVR(Alf and Vegard's RISC processor)微控制器包含四种调用子程序的指令,即CALL(调用子程序)、RCALL(相对调用子程序)、ICALL(间接调用Z)和EICALL(扩展间接调用Z)。

CALL指令

CALL指令是4字节的。**操作码**由10位表示,**目标子程序**的地址由22位表示,与JMP指令一样。对于AVR,它提供了000000-$3FFFFF的4M地址空间。它能够调用地址范围内任意位置的子程序。

子程序执行后,AVR不一定知道返回地址。因此,微控制器将指令地址保存在堆栈(STACK)中,堆栈位于CALL指令正下方。当子程序执行完成时,将通过**RET**指令将控制权交还给调用者。因此,在子程序的末尾,每个子程序都必须放置RET指令。

堆栈

堆栈是一种常用的内存区域,用于临时存储寄存器的信息。当我们调用子程序时,它将返回地址。它是CPU(中央处理单元)RAM(随机存取存储器)的一部分。寄存器数量有限,因此CPU始终需要此存储空间。当我们调用函数时,值将被存储在此临时存储(堆栈)中。借助暂存寄存器,我们无法存储值,因为它们可能会被函数更改。

还有一个堆栈指针(SP)寄存器可用于访问堆栈。我们可以通过两个寄存器S​​PH(堆栈指针的高字节)和SPL(堆栈指针的低字节)在I/O内存空间中实现堆栈指针(SP)。堆栈遵循LIFO(后进先出)原则。这意味着最后压入的值将首先从堆栈中弹出。当有东西压入堆栈时,堆栈将增长到更高的地址。当有东西从堆栈中弹出时,它会减小。

CALL Instructions and Stack in AVR Microcontroller

如果AVR(Alf and Vegard's RISC processor)的内存超过256字节,则会有两个8位寄存器。如果AVR的内存小于256字节,则堆栈指针仅由SPL构成,因为8位寄存器只能寻址256字节的内存。还有一个PUSH操作,用于将CPU的信息存储到堆栈中。还有一个POP操作,用于将堆栈的内容加载回CPU。这些PUSH和POP操作是使用堆栈最常见和最简单的方法。现在我们将分别描述Push和Pop操作。

压栈(Pushing onto the STACK)

压栈可以称为“将某物放置在堆栈顶部”。开始时,SP能够指向堆栈的顶部。它基本上将64位寄存器或常量存储到堆栈中。“rax”或“r8”寄存器被称为64位寄存器,“eax”或“r8d”被称为32位寄存器。当我们尝试将数据压入堆栈时,它将始终保存在堆栈指针指向的位置。之后,堆栈指针将递减1。

例如:在下面的示例中,我们将使用PUSH指令将一个寄存器压入堆栈。执行此操作的命令如下:


CALL Instructions and Stack in AVR Microcontroller

出栈(Popping from the STACK)

出栈可以称为“从堆栈中取出顶部的东西”。出栈的功能与压栈的功能完全相反。这里,堆栈的内容从堆栈中弹出并放回寄存器。当我们尝试从堆栈中弹出数据时,堆栈的顶部位置将复制到寄存器,并且堆栈指针将递增1。调用POP时,SP会自动递增1。POP操作使用LIFO(后进先出)原则。这意味着最后压入的值将首先从堆栈中弹出。

例如:在此示例中,我们将使用POP指令将堆栈的内容弹出并放入寄存器。执行此操作的命令如下:

当我们把寄存器压入堆栈时,寄存器的内容不会被擦除。在这种情况下,数据将被简单地放置或复制到SRAM中。当我们从堆栈中弹出值时,该地址上存在的内容不会被堆栈擦除。

CALL Instructions and Stack in AVR Microcontroller

PUSH和POP操作示例

在此示例中,我们将把30加载到rax,然后把45加载到rex。执行此操作的命令如下:

执行第一次压栈后,堆栈将只包含一个值:

执行第二次压栈后,堆栈将包含两个值:

执行第一次出栈后,它将首先取出值45并将其放入寄存器rax。之后,堆栈将只剩一个值:

执行第二次出栈后,它将取出值17并将其放入寄存器rcx。之后,堆栈将变为空。最后一个指令“ret”如果堆栈不为空,将无法完美工作。在这种情况下,除了“ret”之外,所有其他指令都能正常工作。该指令将跳转到堆栈的顶部,而不管堆栈顶部包含什么数据。

注意:如果我们压入和弹出的项目数量不相等,我们的程序将会崩溃。因此,我们应该注意我们的压栈和出栈操作。

当我们尝试将一个以上的寄存器压入堆栈时,我们应该按相反的顺序调用POP指令,以便恢复其原始寄存器值,如下所示:

正如我们所见,前、第二个和第三个Push指令分别将r0、r1和r3的内容存储到堆栈中。在Pop操作中,第一个Pop操作是对r2执行的,因为它最后输入,并且Pop操作基于LIFO操作。这就是为什么我们先Pop r2,然后Pop r1和r0。

如果我们错误地以错误的顺序调用POP指令,在这种情况下,值将被恢复到错误的寄存器中。因此,我们在调用POP时应谨慎,如下所示:

正如我们所见,POP指令的调用顺序与Push的调用顺序不相反。因此,结果中r20和r21的数据已交换。但如果我们想在不使用第三个寄存器的情况下交换两个寄存器的内容,这个特性将非常有用。

注意:寄存器必须以与压栈时相反的顺序从堆栈中弹出,以便恢复其原始值。

栈指针

堆栈指针可以定义为输入/输出内存中的一个特殊寄存器,用于指向SRAM中分配的空间。SP包含一个16位寄存器,其中还包含SPL和SP​​H。如果微控制器拥有非常少量的SRAM,则不需要SP​​H。在这种情况下,仅使用SPL。堆栈通常从SRAM的末端开始。当我们向堆栈存储数据时,它将从较高的地址值向较低的地址值增长。堆栈的顶部始终由堆栈指针指向。

初始化堆栈指针

不同的AVR拥有不同数量的RAM(随机存取存储器)。最后一个RAM位置的地址可以通过AVR汇编器中的RAMEND来指定。因此,如果我们需要将堆栈指针初始化为指向最后一个内存位置,可以将RAMEND加载到SP中。正如我们上面学到的,堆栈指针包含两个寄存器,即SP​​H(堆栈指针的高字节)和SPL(堆栈指针的低字节)。因此,RAMEND的高字节将加载到SP​​H,RAMEND的低字节将加载到SPL。

正如我们上面学到的,SP在输入/输出内存中。因此,我们可以使用out指令将其值加载进去。对于新的AVR,SRAM的最后一个值将在上电时通过堆栈指针进行初始化。对于较旧的AVR,必须在任何程序开始时手动设置。其示例如下:

包含文件用于以SRAM的最后一个地址的形式包含常量RAMEND。许多微控制器拥有16位地址,该地址通过LOW函数和HIGH函数分为两个8位组件。这两个函数将被加载到一个工作寄存器中。对于一些小型微控制器,常量RAMEND可能小于16位。在这种情况下,将不使用SP​​H,我们只初始化SPL寄存器。

注意:虽然SP在上电时会被新微控制器自动初始化为RAMEND。如果在任何程序的开始时初始化堆栈指针,这对我们来说将非常好。在软件重置的情况下,这种做法将非常有用,因为它将保护SP免于从其他错误的位置开始。

CALL指令、RET指令和堆栈的作用

在执行CALL指令时,CALL指令下方指令的地址将被压入堆栈。CALL指令下方指令将被加载到PC,并在子程序执行完成并执行RET指令时执行。