Featured image of post 编写中断服务程序时是否需要保存外部函数的形参?

编写中断服务程序时是否需要保存外部函数的形参?

答:不用!

前言

在学习8051中断时,我发现有这么一种写法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
unsigned int _n;
void delay(unsigned int n) {
    _n = n;
    while (_n--);
}

void isr_INT0() interrupt 0 {
    unsigned int n;
    EA = 0;
    n = _n;  // 保护现场
    EA = 1;
    
    doSomething();
    delay(5000);
    
    EA = 0;
    _n = n;  // 恢复现场
    EA = 1;
}

直觉告诉我,这种写法可能不必要…8051的参数传递类似x86里的__fastcall,参数是尽可能优先通过寄存器传递的。(文档)就算在delay函数中发生了中断,寄存器的值也应该会跟着断点一起保存…吧?口说无凭,我们得想个办法验证一下。

本次复现环境为μVision5.38.0.0 with Microchip AT89C51

尝试

我们仿照上面的代码,但是不对n进行保护现场:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <REGX51.H>

void delay(unsigned int n) {
    while (n--);
}

void isr_INT0() interrupt 0 {
    delay(5000);
}

void main() {
    delay(15000);
    while(1);
}

用Keil编译之前,我们先进行一些设置,让他输出带汇编的Listing文件:

  • 单击Project - Options for Target ‘Target 1’或图标
  • 进入Listing选项卡,勾选C Compiler Listing并勾选Conditional、Symbols和Assembly Code

完成后的设置应该差不多这样

修改好之后再编译,我们就会得到一些lst文件。用任意文本编辑器打开main.lst,观察一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
line level    source

   1          #include <REGX51.H>
   2          
   3          void delay(unsigned int n) {
   4   1          while (n--);
   5   1      }
   6          
   7          void isr_INT0() interrupt 0 {
   8   1          delay(5000);
   9   1      }
  10          
  11          void main() {
  12   1        delay(15000);
  13   1        while(1);
  14   1      }

ASSEMBLY LISTING OF GENERATED OBJECT CODE

             ; FUNCTION _delay (BEGIN)
                                           ; SOURCE LINE # 3
;---- Variable 'n' assigned to Register 'R6/R7' ----
0000         ?C0001:
                                           ; SOURCE LINE # 4
0000 EF                MOV     A,R7
0001 1F                DEC     R7
0002 AC06              MOV     R4,AR6
0004 7001              JNZ     ?C0008
0006 1E                DEC     R6
0007         ?C0008:
0007 4C                ORL     A,R4
0008 70F6              JNZ     ?C0001
                                           ; SOURCE LINE # 5
000A 22                RET     
             ; FUNCTION _delay (END)

             ; FUNCTION isr_INT0 (BEGIN)
0000 C0E0              PUSH    ACC
0002 C0D0              PUSH    PSW
0004 75D000            MOV     PSW,#00H
0007 C004              PUSH    AR4
0009 C006              PUSH    AR6
000B C007              PUSH    AR7
                                           ; SOURCE LINE # 7
                                           ; SOURCE LINE # 8
000D 7F88              MOV     R7,#088H
000F 7E13              MOV     R6,#013H
0011 120000      R     LCALL   _delay
                                           ; SOURCE LINE # 9
0014 D007              POP     AR7
0016 D006              POP     AR6
0018 D004              POP     AR4
001A D0D0              POP     PSW
001C D0E0              POP     ACC
001E 32                RETI    
             ; FUNCTION isr_INT0 (END)

             ; FUNCTION main (BEGIN)
                                           ; SOURCE LINE # 11
                                           ; SOURCE LINE # 12
0000 7F98              MOV     R7,#098H
0002 7E3A              MOV     R6,#03AH
0004 120000      R     LCALL   _delay
0007         ?C0005:
                                           ; SOURCE LINE # 13
0007 80FE              SJMP    ?C0005
             ; FUNCTION main (END)

看起来有点乱,稍微解释一下吧!

  • 20行开始,就是转换后方便阅读的汇编代码。
  • ;开头的句子是注释,相当于C语言中的//
  • 从左开始数的第一列是指令地址,你有没有发现从上往下看这一列是递增的呢?
  • 第二列是指令(Instruction),这是可以查表的,想了解可以看看官方文档或者本文末尾附录。操作码里包括了操作码(OpCode)和操作数(Operands)。
  • 第三列是助记符,顾名思义就是帮助你记住操作码对应功能的东西。
  • 最后一列就是操作数了。他们以逗号分隔开。

那么…

阅读汇编

delay函数

我们可以清楚地看到, C0001是delay函数的入口点,那我们就一行行分析一下。首先, 这里提到n被保存到了R6和R7里面。那么R6是啥,R7又是啥呢?

8051寄存器

没错,他们都是寄存器,数据范围是8位。我们的函数参数是unsigned int,是16位的,刚好需要两个寄存器。接下来往下看, MOV A, R7是什么意思呢?查阅官方文档可以看到,这其实就是赋值操作,相当于A = R7。那么A又是啥呢?

哈哈,还是寄存器。不过A是特殊的,叫做累加器(Accumulator)累加器的用处就是进行一些运算。和我们要找的东西没关系,我们快速过一遍吧。 DEC R7把R7减去1, MOV R4, AR6AR6(R6)赋值给R4。 JNZ是Jump if Not Zero,累加器不为0的时候跳转。在这里意思就是如果R7不为0,就跳转到 C0008ORL A,R4的意思是A |= R4,既按位逻辑或。这里的R4是上面的R6,而R6是我们一开始传进来的实参。反正带着源码看,虽然不能完全理解但也大概知道是做了些什么事情了。我们还是回到主题上吧。

经过刚刚的分析,我们可以发现delay函数至少使用了这些寄存器:

  • R4
  • R6
  • R7
  • A(累加器)

中断函数

该看下一个函数了。紧挨着delay的就是我们写的中断函数,因为这是中断发生后最先到达的地方,所以大概率编译器会在这里使用魔法吧。快速扫一眼,马上就发现和刚刚的函数不一样:第一行就有一个 PUSH操作。还是老样子,我们打开官方文档

The PUSH instruction increments the stack pointer and stores the value of the specified byte operand at the internal RAM address indirectly referenced by the stack pointer. No flags are affected by this instruction.

意思就是,PUSH指令会将寄存器中的值压入栈中。这好像有点接近我们要找的东西了,继续往下看,程序依次将 ACC、PSW、AR4、AR6、AR7入栈。

等等,好像看到什么熟悉的东西了?ACC、AR4、AR6、AR7,这不就是delay函数用到的寄存器吗!再往下看, LCALL完delay马上又把这些寄存器POP了回去。这个不就是我们开始手写的 保护现场吗!事已至此,我们已经找到了标题这个问题的答案了。编译器会自动帮我们进行一部分保护现场的工作,所以编写中断服务程序时无需保存外部函数的形参好耶可以少写一堆代码了!

官方文档

是的…做了这么多才发现另一个官方文档中有写。

  • When required, the contents of ACCBDPHDPL, and PSW are saved on the stack at function invocation time.
  • All working registers used in the interrupt function are stored on the stack if a register bank is not specified with the using attribute.
  • The working registers and special registers that were saved on the stack are restored before exiting the function.
  • The function is terminated by the 8051 RETI instruction.

User's Guides for Keil C51 Development Tools
这里的第二点就是上面得到的结论:C51编译器会自动将中断函数中调用的函数所使用的所有寄存器保存至栈上。知道了这点之后也有一些别的启发,比如不能在中断函数中调用太多函数导致爆栈等等。

碎碎念:其实也不能说做的都是无用功,能直接从最底层的汇编理解编译器做了些什么,然后自己找出答案的过程还是很有趣的!

附录:8051所有指令码

展开内容
指令码占用字节助记符操作数
001NOP
012AJMPaddr11
023LJMPaddr16
031RRA
041INCA
052INCdirect
061INC@R0
071INC@R1
081INCR0
091INCR1
0A1INCR2
0B1INCR3
0C1INCR4
0D1INCR5
0E1INCR6
0F1INCR7
103JBCbit, offset
112ACALLaddr11
123LCALLaddr16
131RRCA
141DECA
152DECdirect
161DEC@R0
171DEC@R1
181DECR0
191DECR1
1A1DECR2
1B1DECR3
1C1DECR4
1D1DECR5
1E1DECR6
1F1DECR7
203JBbit, offset
212AJMPaddr11
221RET
231RLA
242ADDA, #immed
252ADDA, direct
261ADDA, @R0
271ADDA, @R1
281ADDA, R0
291ADDA, R1
2A1ADDA, R2
2B1ADDA, R3
2C1ADDA, R4
2D1ADDA, R5
2E1ADDA, R6
2F1ADDA, R7
303JNBbit, offset
312ACALLaddr11
321RETI
331RLCA
342ADDCA, #immed
352ADDCA, direct
361ADDCA, @R0
371ADDCA, @R1
381ADDCA, R0
391ADDCA, R1
3A1ADDCA, R2
3B1ADDCA, R3
3C1ADDCA, R4
3D1ADDCA, R5
3E1ADDCA, R6
3F1ADDCA, R7
402JCoffset
412AJMPaddr11
422ORLdirect, A
433ORLdirect, #immed
442ORLA, #immed
452ORLA, direct
461ORLA, @R0
471ORLA, @R1
481ORLA, R0
491ORLA, R1
4A1ORLA, R2
4B1ORLA, R3
4C1ORLA, R4
4D1ORLA, R5
4E1ORLA, R6
4F1ORLA, R7
502JNCoffset
512ACALLaddr11
522ANLdirect, A
533ANLdirect, #immed
542ANLA, #immed
552ANLA, direct
561ANLA, @R0
571ANLA, @R1
581ANLA, R0
591ANLA, R1
5A1ANLA, R2
5B1ANLA, R3
5C1ANLA, R4
5D1ANLA, R5
5E1ANLA, R6
5F1ANLA, R7
602JZoffset
612AJMPaddr11
622XRLdirect, A
633XRLdirect, #immed
642XRLA, #immed
652XRLA, direct
661XRLA, @R0
671XRLA, @R1
681XRLA, R0
691XRLA, R1
6A1XRLA, R2
6B1XRLA, R3
6C1XRLA, R4
6D1XRLA, R5
6E1XRLA, R6
6F1XRLA, R7
702JNZoffset
712ACALLaddr11
722ORLC, bit
731JMP@A+DPTR
742MOVA, #immed
753MOVdirect, #immed
762MOV@R0, #immed
772MOV@R1, #immed
782MOVR0, #immed
792MOVR1, #immed
7A2MOVR2, #immed
7B2MOVR3, #immed
7C2MOVR4, #immed
7D2MOVR5, #immed
7E2MOVR6, #immed
7F2MOVR7, #immed
802SJMPoffset
812AJMPaddr11
822ANLC, bit
831MOVCA, @A+PC
841DIVAB
853MOVdirect, direct
862MOVdirect, @R0
872MOVdirect, @R1
882MOVdirect, R0
892MOVdirect, R1
8A2MOVdirect, R2
8B2MOVdirect, R3
8C2MOVdirect, R4
8D2MOVdirect, R5
8E2MOVdirect, R6
8F2MOVdirect, R7
903MOVDPTR, #immed
912ACALLaddr11
922MOVbit, C
931MOVCA, @A+DPTR
942SUBBA, #immed
952SUBBA, direct
961SUBBA, @R0
971SUBBA, @R1
981SUBBA, R0
991SUBBA, R1
9A1SUBBA, R2
9B1SUBBA, R3
9C1SUBBA, R4
9D1SUBBA, R5
9E1SUBBA, R6
9F1SUBBA, R7
A02ORLC, /bit
A12AJMPaddr11
A22MOVC, bit
A31INCDPTR
A41MULAB
A5 reserved 
A62MOV@R0, direct
A72MOV@R1, direct
A82MOVR0, direct
A92MOVR1, direct
AA2MOVR2, direct
AB2MOVR3, direct
AC2MOVR4, direct
AD2MOVR5, direct
AE2MOVR6, direct
AF2MOVR7, direct
B02ANLC, /bit
B12ACALLaddr11
B22CPLbit
B31CPLC
B43CJNEA, #immed, offset
B53CJNEA, direct, offset
B63CJNE@R0, #immed, offset
B73CJNE@R1, #immed, offset
B83CJNER0, #immed, offset
B93CJNER1, #immed, offset
BA3CJNER2, #immed, offset
BB3CJNER3, #immed, offset
BC3CJNER4, #immed, offset
BD3CJNER5, #immed, offset
BE3CJNER6, #immed, offset
BF3CJNER7, #immed, offset
C02PUSHdirect
C12AJMPaddr11
C22CLRbit
C31CLRC
C41SWAPA
C52XCHA, direct
C61XCHA, @R0
C71XCHA, @R1
C81XCHA, R0
C91XCHA, R1
CA1XCHA, R2
CB1XCHA, R3
CC1XCHA, R4
CD1XCHA, R5
CE1XCHA, R6
CF1XCHA, R7
D02POPdirect
D12ACALLaddr11
D22SETBbit
D31SETBC
D41DAA
D53DJNZdirect, offset
D61XCHDA, @R0
D71XCHDA, @R1
D82DJNZR0, offset
D92DJNZR1, offset
DA2DJNZR2, offset
DB2DJNZR3, offset
DC2DJNZR4, offset
DD2DJNZR5, offset
DE2DJNZR6, offset
DF2DJNZR7, offset
E01MOVXA, @DPTR
E12AJMPaddr11
E21MOVXA, @R0
E31MOVXA, @R1
E41CLRA
E52MOVA, direct
E61MOVA, @R0
E71MOVA, @R1
E81MOVA, R0
E91MOVA, R1
EA1MOVA, R2
EB1MOVA, R3
EC1MOVA, R4
ED1MOVA, R5
EE1MOVA, R6
EF1MOVA, R7
F01MOVX@DPTR, A
F12ACALLaddr11
F21MOVX@R0, A
F31MOVX@R1, A
F41CPLA
F52MOVdirect, A
F61MOV@R0, A
F71MOV@R1, A
F81MOVR0, A
F91MOVR1, A
FA1MOVR2, A
FB1MOVR3, A
FC1MOVR4, A
FD1MOVR5, A
FE1MOVR6, A
FF1MOVR7, A
Licensed under CC BY-NC-SA 4.0