事件处理的背后--中断

前言

中断是计算机中的一种常用的机制,也是计算机中发展中里程碑式的一步,本文将以AT89S51单片机为例来讲解中断的产生和处理过程。下面以一个按键的处理程序为例来说明,在下面的例子中,我们要实现这样一个功能:当按下按钮S1时,我让红灯点亮100ms,然后让绿灯亮,如果没有按下S1,那么一直是绿灯亮。
单片机中断的例子

没有中断以前

如果没有中断,我们的做法应该是这样的:不停地扫描P3.3口,如果发现P3.3口是低电平,那么就让红灯亮100ms,然后熄灭红灯,让绿灯亮。在这个简单的功能里是没有什么问题的,但是如果单片机要处理的事情很多,而它为了处理S1按键这一个事情就被占据,不能脱身,对CPU来说就是非常浪费的,因为CPU的执行速度是非常之快的,而按键的按下和松开对CPU来说是非常慢的,也就是说再两次按键被按下的间隙,CPU可以执行非常多的指令,而这种轮询端口的方式大大降低了CPU的效率。

中断是什么?

在上面的例子中,我们看到,如果没有按钮事件到来,CPU的效率是非常低的。为了解放CPU,我们引入了中断的概念,那么究竟什么是中断?举个例子,下班回家我们想用微波炉做个鸡蛋羹,如果没有中断,我们就必须在微波炉旁边时刻等着,等到预计的时间到了,然后把微波炉关掉。在等待的过程中我们不能做其它的事情,因为超过了需要的时间,鸡蛋羹就会失去最佳味道。如果做鸡蛋羹需要5分钟的时间,那么我们(CPU)就傻等了五分钟时间,我们原本可以用这五分钟时间背几个单词或者看会儿书。我们知道CPU处理指令的速度非常之快,大约在10^9的数量级。等待的这五分钟时间原本可以用来处理很多指令。那么怎么办呢?这时我们可以给微波炉加一个定时的功能,有了这个功能,我们把打好的鸡蛋放到碗里,然后放到微波炉中就不用管了,这时我们可以去做其它的事情,在做事情的过程中,我们听到了微波炉发出了“嘀–嘀–嘀”的声音,我们知道是时间到了,我们关了微波炉,然后把做好的鸡蛋取出来。这个到了一定的条件发出“嘀–嘀–嘀”的声音就是产生了一个中断,我们去取出来鸡蛋就是对一个中断的处理。我们可以看到使用了中断可以提高CPU的效率,让它可以再外部设备进行某些操作的时候去做其它的事,而在触发中断时候再让CPU对中断进行相应的处理。

单片机的中断处理过程

通过上面的分析,我们知道中断可以提高CPU的执行效率,那么使用中断,我们可以怎样实现上图中所示实例的程序呢?

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
    ORG  00H ; 起始地址00H
; 主程序段,点亮绿色发光二极管D1

MAIN:
MOV IE, #84H ; 使能外部中断1
GREEN:
CLR P0.0 ; 点亮绿色发光二极管D1
JMP GREEN ; 循环
; 中断服务子程序,熄灭绿色发光二极管D1,点亮红色D2

ORG 13H ; 外部中断1的中断服务子程序起始地址为13H
EXT1_RED:
SETB P0.0 ; 熄灭绿色发光二极管D1
CLR P0.1 ; 点亮红色D2

D1:
MOV R4, #200; 延时100ms

D2: MOV R5, #248
DJNZ R5, $
DJNZ R4, D2
SETB P0.1 ;熄灭红色发光二极管D2
RET1 ;中断服务子程序结束

END ;程序结束

下面我们来详细分析下这段代码。ORG伪指令指明该段代码所在的内存地址是00,计算机执行到这里会将PC指针指向00处,这样就可以执行该段代码了。其中MOV IE, #84,将8:4赋值给IE寄存器,IE寄存器是一个中断使能寄存器,为什么要设置中断使能寄存器呢?在上图中我们可以看到P3.0–P3.5这六个端口后面都有个括号,括号中分别是(RXD,TXD,INT0,INT1,T0,T1),这说明这些端口要么作为普通的IO口,要么作为中断源。默认情况下,单片机上电以后是不会对任何中断产生响应的,相应的中断端口是做为普通的IO口使用的,只有使能了以后才可以接收中断。这六个端口对应五个中断源,RXD和TXD对应串行口中断,INT0,INT1,T0,T1分别对应于外部中断0,外部中断1,Timer0中断和Timer1中断。下面我们看下IE寄存器各个位的含义:
中断使能寄存器
从中我们可以看到IE的最高位是EA,它管理着所有的中断源,如果该为置零,那么所有的中断都不能响应了,其中第6位和第5位是保留位,第四位管理串行口中断,ET1管理Timer1中断,EX1管理外部中断1,第1位和第0位分别管理Timer0中断和外部中断0。我们写的84换算成十六进制刚好是10000100,对应于中断使能寄存器,也就是开启了外部中断1。而外部中断1的接口刚好和键盘S1的相连接。所以如果键盘被按下,就会触发单片机的一次中断。中断被使能以后,我们进入GREEN程序段,在这里面调用CLR P0.0;这条指令可以将P0.0口置成低电平,因为P0.0口上的绿色发光二极管的正极连接的是高电平,所以它会被点亮。然后调用JMP GREEN;指令一直循环。接下来就是中断服务子程序,也就是当中断发生的时候CPU需要执行的指令。ORG 13H;指明接下来的指令发到13H地址开始的指令处。为什么偏偏要是13H呢?因为单片机在设计生产出来的时候都有一个中断向量表,这个中断向量表就说明了每个中断如果发生那么CPU会去哪个地址处开始执行。AT89S51单片机的中断向量表如下:

AT89S51单片机的中断向量表
我们可以看到外部中断1的向量地址是0013H,也就是说CPU如果遇到了外部中断1就会去0013H地址处执行。我们可以看到每两个中断向量的地址相隔只有8位,所以通常情况下,从向量地址处开始的第一条指令是JMP 指令,让程序跳转到其它的地址处执行,不然会影响其后面中断向量的执行。因为我们这里只有一个中断,所以不存在相互影响。

在绿灯亮着的过程中,我们按下按键,这时就会触发一次中断,CPU找到13H开始的地址,然后取指,执行。执行EXT1_RED:,在这里先执行SETB P0.0;将P0.0口置为高电平,让绿灯熄灭,进而执行CLR P0.1;将P0.1口置为低电平然后点亮红色发光二极管。那么下面的延时程序段是怎么计算的呢?

1
2
3
4
5
6
7
8
D1:
MOV R4, #200; 延时100ms

D2: MOV R5, #248
DJNZ R5, $
DJNZ R4, D2
SETB P0.1 ;熄灭红色发光二极管D2
RET1 ;中断服务子程序结束

我们看上面单片机的电路,我们发现一个Y1,12MHz的元器件,这个器件就是晶振,一个机器周期等于晶振频率的倒数乘以12,所以一个机器周期的时间为:

$$
12 \times \frac{1}{12MHz} = 1 \mu s
$$

这个程序D1中先将200移动到寄存器R4中,D2中先将248移动到R5中,然后将R5减1,如果不等于0,则持续执行这条指令(DJNZ R5, $),所以:

1
2
MOV  R5,  #248
DJNZ R5, $

因为执行一个DJNZ指令需要花费两个时钟周期,所以内层循环总共的耗时是:

$$
1 + 2 \times 248 = 497 \mu s
$$

一条MOV指令花费一个时钟周期,所以加上外层循环总共的耗时时间是:

$$
200 \times (497 + 2) + 1 = 99801 \mu s \approx 100 ms
$$

到此为止一个没有操作系统的逻辑处理事件的流程已经结束了,那么操作系统,这个在硬件上面的第一层软件,它是怎样来管理中断的呢?

旁注

其实中断使能IE寄存器,在操作系统中也扮演者重要的角色,在进行临界区保护的时候,我们需要加锁,加锁可以有很多方式,比如用纯粹软件的方式:面包店算法。也可以使用硬件的方式,硬件的方式就是关闭中断,关闭中断之后,其它进程就不能进入操作系统内核了,不能进入操作系统内核,也就不会对其进行相应的调度,其它线程也就不能进入临界区了。通过这种硬件的方式,可以提高程序的执行速度,与此同时也可以减少软件方法书写的复杂性。在Linux中使用cli()sti()的系统调用来关闭和打开中断。同时如果有了操作系统,那么响应的中断往往会和一个驱动相连接,比如:网卡的中断号为X,那么在中断处理程序中就会将中断号X和网卡驱动程序绑定起来。这样,如果遇到了中断X,就会调用响应的驱动程序执行其操作。

参考资料

《实例解读51单片机完全学习与应用》
操作系统:临界区保护