51单⽚机interrupt和using使⽤详解
注:下⽂的“BANK”和“⼯作寄存器组”,这两个名词是⼀个概念。
⾸先推荐⼀篇⽂章,
这篇⽂章⼤部分是翻译软件直接翻译的,有些翻译的不通顺的地⽅、有歧义的地⽅,直接读读英⽂原版就清晰了。
如果链接挂了,⾃⾏搜索:关于如何利⽤Keil C实现51单⽚机中断功能(interrupt、using关键字的⽤法
官⽅⽂档⾥关于using的说明可参阅2个地⽅,(1)keil软件菜单栏->Help->uVision Heip,打开帮助⽂件,然后依次展开Ax51
Asmbler Ur Guide -> Control Statement -> Reference -> USING,(2)帮助⽂件依次展开CX51 Compiler Ur‘s Guide-
> Language Extensions -> Function Declarations -> Register Banks
下⾯是我对interrupt和 using使⽤理解,
⾸先看interrupt,这个⽐较简单,直接看⼀个外部中断0服务函数的例⼦
void ext_int0_src() interrupt 0 using 2//
{
/*外部中断0的服务函数*/
}
interrupt 0,这⾥的数字0是外部中断的中断向量号,每⼀个中断向量都有对应的⼀个⼊⼝地址。如果对中断向量表的概念⽐较熟悉的话,这⾥很好理解,不再赘述。
下⾯主要看using的使⽤⽅法,这个⽐较复杂,我们从头说起。
因为51的RAM空间极其有限,keil在编译51的C程序时,C语⾔函数中的局部变量、形参、返回值、返回地址会优先使⽤⼯作寄存器
R0~R7(因为⼯作寄存器读写速度最快),如果这8个字节不够⽤,keil会为其余的局部变量分配RAM空间,这个空间在编译完成后就固定下来了,假如某个函数中的局部变量a编译后位于RAM的0x5d位
置,但是keil也有可能为另⼀个函数的中的局部变量b分配空间时,把b也分配到RAM的0x5d位置,相当于a和b分时复⽤同⼀个RAM空间,这种编译⼿段也就导致了函数的不可重⼊特性。51的可重⼊函数由仿真栈来实现,可参阅本博客的另⼀篇⽂章。
对于STM32等RAM较为充⾜的单⽚机来说,keil分配局部变量就不采⽤这种分时复⽤RAM的⽅法了,⽽是通过栈,具体地说就是,在调⽤函数前,先把实参添加到⼯作寄存器(⼯作寄存器并不是全部⽤来传参,还有⼀些⽤来传递函数的返回值、函数的返回地址等),若⼯作寄存器满⾜不了传参需求,那么其余实参将被压栈,这样,在进⼊函数之后,通过读取⼯作寄存器和出栈,即可令形参获得实参的值;另外函数中的局部变量⼀般也先从⼯作寄存器开始分配空间,若空间不⾜,会从栈中申请空间。这种编译⽅法是最常见的,⽽上⼀段keil针对51的编译⼿段⽐较另类。
51的⼯作寄存器R0~R7共有4组(分别是BANK 0、1、2、3),在任何时刻,都只有1个⼯作组⽣效!这4个组(BANK)在RAM中的位置分别是[00H, 07H]、[08H, 0FH]、[10H, 17H]、[18H, 1FH],换句话说,RAM中的00H地址、08H地址、10H地址、18H地址,这四个地址的名字都叫R0,那么在汇编程序中我们经常看到类似MOV R0,#07这样的语句,这个#07到底被放到了RAM的哪个地址中去了呢?00H?08H?10H?还是18H?到底是这4个地址中的哪⼀个,取决于51的PSW寄存器的RS1和RS0两个位,若PSW.RS=2,就意味着第2组⼯作寄存器⽣效,R0的地址就是10H。
上⾯提到了51不可重⼊函数的局部变量分时共享同⼀RAM空间的情况,需要注意的是,未使⽤的⼯作寄存器组所在的RAM地址(从
00H~1FH共32字节)不会被复⽤,换句话说,如果我们在程序中没有⼿动切换⼯作寄存器组,那么有些寄存器组就在程序的⾃始⾄终从不被使⽤,⽩⽩浪费了很多RAM。举个例⼦,普通函数只使⽤了BANK 0,且中断函数通过声明using 1只使⽤了bank 1,那么bank 2和bank3不会被程序的任何地⽅给⽤到(唯⼀的例外:仿真栈指针,下⾯有讲到);再举个例⼦,假设整个51程序中都没有使⽤中断,也即只使⽤了BANK 0,那么BANK 1、2、3所在的空间在整个程序运⾏过程中都是闲着的(唯⼀的例外:仿真栈指针)。
51在上电后,PSW的RS两个位默认为0,也即51默认使⽤⼯作寄存器组BANK 0,在默认状态下,对于普通的C语⾔函数,其传参、申请局部变量、导出函数的返回值等功能,keil将其翻译成汇编以后,肯定要使⽤R0~R7;对于51的中断服务函数,它没有形参,也不⽤返回值,但是⼀般肯定有局部变量,这时就需要⽤到R0~R7了;试想,在执⾏普通函数时,R0~R7已经被使⽤了,在执⾏普通函数时,⼀旦发⽣中断,⽽中断函数也需要使⽤R0~R7,那怎么办?我们最先想到的是,在执⾏中断服务函数前先把R0~R7⼊栈(像累加器A、状态PSW等也要⼊栈这个不⽤说⼤家也知道),在中断服务完成后把R0~R7出栈,然后就能恢复现场,回到普通函数中去了,但是这8个Rn不能直接⼊栈,PUSH R0这样的语句是不允许的,要想R0⼊栈只能⽤两句:MOV A R0; PUSH A;这样的后果是,每次⼯作
寄存器⼊栈都需要2*8=16条汇编语句才能完成,再加上A、B、PSW等寄存器⼊栈等,相当于每次中断都要消耗⼤量的时间来出栈⼊栈,影响程序速度。如何解决这⼀问题呢?51提供了这样⼀种机制,切换⼯作寄存器组,过程如下:
普通函数的执⾏过程中正在使⽤BANK0的R0~R7,执⾏过程中突然发⽣了中断,⽽中断函数也想使⽤R0~R7,在执⾏中断服务函数前,我们切换⼯作寄存器组,切换的具体⽅法就是直接修改PSW的RS两个⽐特位,⽽不必把BANK 0⼊栈,本⽂开头的例⼦中using 2,就是说,在进⼊外部中断0的服务函数前,先⼊栈CPU寄存器,再把⼯作寄存器组由0切换成2,在退出中断服务后,先由BANK2切换回BANK0,并弹出CPU寄存器,由于BANK0和BANK2处在不同的RAM空间,互相不⼲扰,切换回BANK0之后就把那个普通函数的现场给恢复了。
还有两个问题点需要说明:
问题1、对于同⼀优先级的中断,可以using同⼀个寄存器组,因为同⼀优先级的中断不会互相打断,也就是说,例如:我把同⼀优先级的中断函数都using 2,这些函数也不会冲突地使⽤R0~R7,他们只会分时复⽤BANK 2。但是对于不同优先级的中断函数,必须⼿动设定为让他们使⽤不同的BANK,因为⾼优先级的中断会打断低优先级的中断,如果使⽤相同的BANK,⼀旦发⽣中断嵌套,低优先级服务函数正在使⽤的R0~R7将会被覆盖。这些东西都是说的在⽤C编程时的情况,如果⽤的是汇编编程,
那我们可以⾃由的任意压栈出栈CPU寄存器,任意的压栈出栈BANKn,只要程序员⾃⼰⼼⾥清楚地知道哪些Rn正在被使⽤,那他就能写出安全的程序。⽤C编程的话,程序员⽐较省⼼,只要使⽤了using指令,就能轻易地保护R0~R7。当然,在汇编中使⽤USING也是可以的,也不⽤压栈出栈BANK,也很省⼼。
问题2、如果我们使⽤了keil为51构造的仿真栈,以⼤编译模式为例,keil会给仿真栈指针?C_XBP分配⼀个RAM空间(2个字节),这个空间在哪呢?通过查看map⽂件(后缀名为.m51)我们发现,若我们的中断函数只使⽤了BANK 1(普通函数默认已把BANK 0给⽤了,除⾮程序员为所有的普通函数都⽤using 1或2或3来修饰,如果不是闲的蛋疼没⼈会这么做),这时仿真栈指针?C_XBP将会被分配到BANK 2的R0和R1所在的位置上,也即RAM的10H、11H地址。如果普通函数使⽤了BANK0,中断函数只使⽤了using 2,那么?
C_XBP将会被分配到BANK 1的R0和R1所在的位置上;如果普通函数使⽤了BANK0,中断函数使⽤了BANK1和BANK2,那么?C_XBP 将会被分配到BANK 3的R0和R1所在的位置上。
综上所述,在进⼊中断服务程序后,要么通过修改PSW切换BANK 号码来保护R0~R7,要么把R0~R7⼊栈来保护R0~R7。使⽤using n编写的C语⾔中断服务函数,keil就给翻译成了PSW切换PSW切换的⽅式。