8051模拟器汇编入门(5)子程序

有时,编写代码来完成特定任务可能会变得冗余。在汇编语言中,尤其如此,因为指令的数量有限,大多数任务都涉及用它们做非常相似的事情。子程序是一组旨在执行程序中频繁使用的操作的指令。从某种意义上说,这就像在C或Java这样的语言中编写函数,但层次稍微低一些—特别是我们必须自己做更多的切换工作。

假设你想要将所有寄存器设置为零。有八个寄存器,所以写出来需要八条指令。如果你想在多个地方这样做,每次都写出八条指令就会变得很痛苦。相反,我们可以使用子程序,只写一次。为此,我们使用CALL(实际上是ACALL或LCALL)和RET指令。例如,

JMP START
ALL_THE_ZEROS:
MOV R0, #0
MOV R1, #1
...
MOV R7, #7
RET ; 从子程序调用返回。
START:
MOV R3, #7
CALL ALL_THE_ZEROS ; 调用子程序。
MOV R7, #4
CALL ALL_THE_ZEROS ; 再次调用它。

CALL指令看起来类似于JMP指令,因为你使用标签来指定代码中的去哪里。CALL和JMP的区别在于CALL在执行跳转之前,会将CALL之后的下一条指令的PC地址推入堆栈。调用子程序跳转到标记的子程序,然后从那里继续。当子程序完成后,使用RET指令。RET将从堆栈中弹出最后一条CALL指令原本存储在那里的PC地址,然后跳转到该地址并从那里继续执行。

练习9:

在寄存器R0和R1中存储数字。编写一个子程序,将R0中的数字与R1中的数字的四倍相加。你可以使用乘法或通过四次添加R1来完成这个操作。结果应该在R1中。调用子程序。

MOV R0, #1
MOV R1, #2

CALC:   MOV     A, R1          ; Move R1 to accumulator
        MOV     B, #04H        ; Load B with 4
        MUL     AB             ; Multiply A by 4
        ADD     A, R0          ; Add R0 to the result
        MOV     R1, A          ; Store final result in R1
        RET                    ; Return from subroutine

ACALL CALC

还有一件事。调用子程序可能是危险的。当你调用子程序时,它可以改变你正在使用的寄存器中的值。如果你不小心,可能会得到意外的行为。为了解决这个问题,在调用之前保存寄存器中的值,然后在之后恢复它们。这通常是通过将寄存器的值推入堆栈,然后在之后弹出它们来完成的。

练习10:

制作一个子程序,以你希望的方式改变R0和R1的值。当程序开始时,将R0和R1初始化为某个值。将R0和R1的值推入堆栈。调用子程序以正确的顺序从堆栈中弹出R0和R1的值。

MOV R0, #1
MOV R1, #2

CALC:   MOV     A, R1          ; Move R1 to accumulator
        MOV     B, #04H        ; Load B with 4
        MUL     AB             ; Multiply A by 4
        ADD     A, R0          ; Add R0 to the result
        MOV     R1, A          ; Store final result in R1
        RET                    ; Return from subroutine

ACALL CALC

还有一件事。调用子程序可能是危险的。当你调用子程序时,它可以改变你正在使用的寄存器中的值。如果你不小心,可能会得到意外的行为。为了解决这个问题,在调用之前保存寄存器中的值,然后在之后恢复它们。这通常是通过将寄存器的值推入堆栈,然后在之后弹出它们来完成的。

练习10:

制作一个子程序,以你希望的方式改变R0和R1的值。当程序开始时,将R0和R1初始化为某个值。将R0和R1的值推入堆栈。调用子程序以正确的顺序从堆栈中弹出R0和R1的值。

        ; Initialize R0 and R1 with some values
        MOV     R0, #25H       ; Initialize R0 with 25H
        MOV     R1, #37H       ; Initialize R1 with 37H
        
        ; Push R0 and R1 onto stack
        ; Note: Push order is important for correct popping
        MOV     A, R0          ; Move R0 to accumulator
        PUSH    ACC            ; Push R0's value onto stack
        MOV     A, R1          ; Move R1 to accumulator
        PUSH    ACC            ; Push R1's value onto stack
        
        ; Call subroutine that changes R0 and R1
        ACALL   CHANGE_VALS
        
        ; Pop values back into R0 and R1
        ; Note: Pop in reverse order of push
        POP     ACC            ; Pop top value from stack
        MOV     R1, A          ; Restore R1
        POP     ACC            ; Pop next value from stack
        MOV     R0, A          ; Restore R0
        
        SJMP    $              ; Infinite loop after completion

; Subroutine to change register values
CHANGE_VALS:
        MOV     R0, #0FFH      ; Change R0 to FFH
        MOV     R1, #0AAH      ; Change R1 to AAH
        RET                    ; Return from subroutine

让我解释一下这个程序是如何工作的:

1.初始化:

  • R0 用 25H 初始化
  • R1 用 37H 初始化

2.推送到堆栈:

  • 由于我们不能直接推送寄存器,我们首先将每个寄存器移动到累加器
  • 首先推送 R0 的值,然后推送 R1 的值
  • 堆栈在内存中向下增长,因此最后推送的项目位于顶部

3.子程序:

  • CHANGE_VALS 将 R0 更改为 FFH,将 R1 更改为 AAH
  • 这表明原始值安全地存储在堆栈中

4.从堆栈弹出:

  • 以相反的顺序弹出值(LIFO – 后进先出)
  • 首先弹出 R1 的值,然后弹出 R0 的值
  • 值从累加器移回各自的寄存器

5.执行后:

  • 最初:R0 = 25H,R1 = 37H
  • CHANGE_VALS 之后:R0 = FFH,R1 = AAH
  • 弹出后:R0 = 25H,R1 = 37H(恢复为原始值)