中断的产生背景

请设想这样一个场景:此刻我正在厨房用煤气烧一壶水,而烧开一壶水刚好需要 10 分钟,我是一个主体,烧水是一个目的,而且我只能时时刻刻在这里烧水,因为一旦水开了,溢出来浇灭煤气的话,有可能引发一场灾难。但就在这个时候呢,我又听到了电视里传来《天龙八部》的主题歌,马上就要开演了,我真想夺门而出,去看我最喜欢的电视剧。然而,听到这个水壶发出的“咕嘟”的声音,我清楚:除非等水烧开了,否则我是无法享受我喜欢的电视剧的。

这里边主体只有一个我,而我要做的有两件事情,一个是看电视,一个是烧水,而电视和烧水是两个独立的客体,它们是同时进行的。其中烧水需要 10 分钟,但不需要了解烧水的过程,只需要得到水烧开的这样一个结果就行了,提下水壶和关闭煤气只需要几秒的时间而已。所以我们采取的办法就是:烧水的时候,定上一个闹钟,定时 10 分钟,然后我就可以安心看电视了。当 10 分钟时间到了,闹钟响了,此刻水也烧开了,我就过去把煤气灭掉,然后继续回来看电视就可以了。

这个场景和单片机有什么关系呢?

在单片机的程序处理过程中也有很多类似的场景,当单片机正在专心致志的做一件事情(看电视)的时候,总会有一件或者多件紧迫或者不紧迫的事情发生,需要我们去关注,有一些需要我们停下手头的工作去马上去处理(比如水开了),只有处理完了,才能回头继续完成刚才的工作(看电视)。这种情况下单片机的中断系统就该发挥它的强大作用了,合理巧妙的利用中断,不仅可以使我们获得处理突发状况的能力,而且可以使单片机能够“同时”完成多项任务。

定时器中断的应用

在第五章我们学过了定时器,而实际上定时器一般用法都是采取中断方式来做的,我是故意在第五章用查询法,就是使用 if(TF0==1)这样的语句先用定时器,目的是明确告诉同学们,定时器和中断不是一回事,定时器是单片机模块的一个资源,确确实实存在的一个模块,而中断,是单片机的一种运行机制。尤其是初学者们,很多人会误以为定时器和中断是一个东西,只有定时器才会触发中断,但实际上很多事件都会触发中断的,除了“烧水”,还有“有人按门铃”,“来电话了”等等。

标准 51 单片机中控制中断的寄存器有两个,一个是中断使能寄存器,另一个是中断优先级寄存器,这里先介绍中断使能寄存器,如表 6-1 和表 6-2 所示。随着一些增强型 51 单片机的问世,可能会有增加的寄存器,大家理解了我们这里所讲的,其它的通过自己研读数据手册就可以理解明白并且用起来了。


表 6-1 IE——中断使能寄存器的位分配(地址 0xA8、可位寻址)

7
6
5
4
3
2
1
0
符号
EA
--
ET2
ES
ET1
EX1
ET0
EX0
复位值
0
--
0
0
0
0
0
0


表 6-2 IE——中断使能寄存器的位描述

符号
描述
7
EA
总中断使能位,相当于总开关
6
--
--
5
ET2
定时器 2 中断使能
4
ES
串口中断使能
3
ET1
定时器 1 中断使能
2
EX1
外部中断 1 使能
1
ET0
定时器 0 中断使能
0
EX0
外部中断 0 使能

中断使能寄存器 IE 的位 0~5 控制了 6 个中断使能,而第 6 位没有用到,第 7 位是总开关。总开关就相当于我们家里或者学生宿舍里的那个电源总闸门,而 0~5 位这 6 个位相当于每个分开关。那么也就是说,我们只要用到中断,就要写 EA = 1 这一句,打开中断总开关,然后用到哪个分中断,再打开相对应的控制位就可以了。

我们现在就把前面的数码管动态显示的程序改用中断再实现出来,同时数码管显示抖动和“鬼影”也一并处理掉了。程序运行的流程跟图 6-1 所示的流程图是基本一致的,但因为加入了中断,所以整个流程被分成了两部分,秒计数和转换为数码管显示字符的部分还留在主循环内,而动态扫描部分则移到了中断函数内,并加入了消隐的处理。下面来看程序:

  • #include <reg52.h>


  • sbit ADDR0 = P1^0;

  • sbit ADDR1 = P1^1;

  • sbit ADDR2 = P1^2;

  • sbit ADDR3 = P1^3;

  • sbit ENLED = P1^4;


  • unsigned char code LedChar[] = { //数码管显示字符转换表

  •     0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,

  •     0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E

  • };

  • unsigned char LedBuff[6] = { //数码管显示缓冲区,初值 0xFF 确保启动时都不亮

  •     0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF

  • };


  • unsigned char i = 0;  //动态扫描的索引

  • unsigned int cnt = 0;  //记录 T0 中断次数


  • void main(){

  •     unsigned long sec = 0;  //记录经过的秒数

  •     EA = 1;  //使能总中断

  •     ENLED = 0;  //使能 U3,选择控制数码管

  •     ADDR3 = 1;  //因为需要动态改变 ADDR0-2 的值,所以不需要再初始化了

  •     TMOD = 0x01;  //设置 T0 为模式 1

  •     TH0 = 0xFC;  //为 T0 赋初值 0xFC67,定时 1ms

  •     TL0 = 0x67;

  •     ET0 = 1;  //使能 T0 中断

  •     TR0 = 1;  //启动 T0


  •     while (1){

  •         if (cnt >= 1000){  //判断 T0 溢出是否达到 1000 次

  •             cnt = 0;  //达到 1000 次后计数值清零

  •             sec++;  //秒计数自加 1


  •             //以下代码将 sec 按十进制位从低到高依次提取并转为数码管显示字符

  •             LedBuff[0] = LedChar[sec%10];

  •             LedBuff[1] = LedChar[sec/10%10];

  •             LedBuff[2] = LedChar[sec/100%10];

  •             LedBuff[3] = LedChar[sec/1000%10];

  •             LedBuff[4] = LedChar[sec/10000%10];

  •             LedBuff[5] = LedChar[sec/100000%10];

  •         }

  •     }

  • }

  • /* 定时器 0 中断服务函数 */

  • void InterruptTimer0() interrupt 1{

  •     TH0 = 0xFC;  //重新加载初值

  •     TL0 = 0x67;

  •     cnt++;  //中断次数计数值加 1


  •     //以下代码完成数码管动态扫描刷新

  •     P0 = 0xFF;

  •     //显示消隐

  •     switch (i){

  •         case 0: ADDR2=0; ADDR1=0; ADDR0=0; i++; P0=LedBuff[0]; break;

  •         case 1: ADDR2=0; ADDR1=0; ADDR0=1; i++; P0=LedBuff[1]; break;

  •         case 2: ADDR2=0; ADDR1=1; ADDR0=0; i++; P0=LedBuff[2]; break;

  •         case 3: ADDR2=0; ADDR1=1; ADDR0=1; i++; P0=LedBuff[3]; break;

  •         case 4: ADDR2=1; ADDR1=0; ADDR0=0; i++; P0=LedBuff[4]; break;

  •         case 5: ADDR2=1; ADDR1=0; ADDR0=1; i=0; P0=LedBuff[5]; break;

  •         default: break;

  •     }

  • }




大家可以先把程序抄下来,编译下载到单片机里运行,看看实际效果。是否可以看到,近乎完美的显示效果经过我们的努力终于做成功了。下面我们还要再来解析一下这个程序。

在这个程序中,有两个函数,一个是主函数,一个是中断服务函数。主函数 main()我们就不用说了,重点强调一下中断服务函数,它的书写格式是固定的,首先中断函数前边 void表示函数返回空,即中断函数不返回任何值,函数名是 InterruptTimer0(),这个函数名在符合函数命名规则的前提下可以随便取,我们取这个名字是为了方便区分和记忆,而后是 interrupt这个关键字,一定不能错,这是中断特有的关键字,另外后边还有个数字 1,这个数字 1 怎么来的呢?我们先来看表 6-3。


表 6-3 中断查询序列
中断
函数编号
中断名称
中断
标志位
中断
使能位
中断
向量地址
默认
优先级
0
外部中断 0
IE0
EX0
0x0003
1(最高)
1
T0 中断
TF0
ET0
0x000B
2
2
外部中断 1
IE1
EX1
0x0013
3
3
T1 中断
TF1
ET1
0x001B
4
4
UART 中断
TI/RI
ES
0x0023
5
5
T2 中断
TF2/EXF2
ET2
0x002B
6

这个表格同样不需要大家记住,需要的时候过来查就可以了。我们现在看第二行的 T0中断,要使能这个中断那么就要把它的中断使能位 ET0 置 1,当它的中断标志位 TF0 变为 1时,就会触发 T0 中断了,那么这时就应该来执行中断函数了,单片机又怎样找到这个中断函数呢?靠的就是中断向量地址,所以 interrupt 后面中断函数编号的数字 x 就是根据中断向量得出的,它的计算方法是 x*8+3=向量地址。当然表中都已经给算好放在第一栏了,我们可以直接查出来用就行了。到此为止,中断函数的命名规则我们就都搞清楚了。

中断函数写好后,每当满足中断条件而触发中断后,系统就会自动来调用中断函数。比如我们上面这个程序,平时一直在主程序 while(1)的循环中执行,假如程序有 100 行,当执行到 50 行时,定时器溢出了,那么单片机就会立刻跑到中断函数中执行中断程序,中断程序执行完毕后再自动返回到刚才的第 50 行处继续执行下面的程序,这样就保证了动态显示间隔是固定的 1ms,不会因为程序执行时间不一致的原因导致数码管显示的抖动了。

中断的优先级
中断优先级的内容,大家先通过我的介绍大概了解一下即可,后边实际应用的时候我们再详细理解。

在讲中断产生背景的时候,我们仅仅讲了看电视和烧水的例子,但是实际生活当中还有更复杂的,比如我正在看电视,这个时候来电话了,我要进入接电话的“中断”程序当中去,就在接电话的同时,听到了水开的声音,水开的“中断”也发生了,我们就必须要放下手上的电话,先把煤气关掉,然后再回来听电话,最后听完了电话再看电视,这里就产生了一个优先级的问题。

还有一种情况,我们在看电视的时候,这个时候听到水开的声音,水开的“中断”发生了,我们要进入关煤气的“中断”程序当中,而在关煤气的同时,电话声音响了,而这个时候,我们的处理方式是先把煤气关闭,再去接听电话,最后再看电视。

从这两个过程中,我们可以得到一个结论,就是最最紧急的事情,一旦发生后,我们不管当时处在哪个“程序”当中,我们必须先去处理最最紧急的事情,处理完毕后再去解决其它事情。在我们的单片机程序当中有时候也是这样的,有一般紧急的中断,有特别紧急的中断,这取决于具体的系统设计,这就涉及到中断优先级和中断嵌套的概念,在本章节我们先简单介绍一下相关寄存器,不做例程说明。

中断优先级有两种,一种是抢占优先级,一种是固有优先级,先介绍抢占优先级。来看表 6-4 和表 6-5。

表 6-4 IP——中断优先级寄存器的位分配(地址 0xB8、可位寻址)位76543210
符号----PT2PSPT1PX1PT0PX0
复位值----000000
表 6-5 IP——中断优先级寄存器的位描述位符号描述
7--保留
6--保留
5PT2定时器 2 中断优先级控制位
4PS串口中断优先级控制位
3PT1定时器 1 中断优先级控制位
2PX1外部中断 1 中断优先级控制位
1PT0定时器 0 中断优先级控制位
0PX0外部中断 0 中断优先级控制位
IP 这个寄存器的每一位,表示对应中断的抢占优先级,每一位的复位值都是 0,当我们把某一位设置为 1 的时候,这一位的优先级就比其它位的优先级高了。比如我们设置了 PT0位为 1 后,当单片机在主循环或者任何其它中断程序中执行时,一旦定时器 T0 发生中断,作为更高的优先级,程序马上就会跑到 T0 的中断程序中来执行。反过来,当单片机正在 T0中断程序中执行时,如果有其它中断发生了,还是会继续执行 T0 中断程序,直到把 T0 中的中断程序执行完毕以后,才会去执行其它中断程序。

当进入低优先级中断中执行时,如又发生了高优先级的中断,则立刻进入高优先级中断执行,处理完高优先级级中断后,再返回处理低优先级中断,这个过程就叫做中断嵌套,也称为抢占。所以抢占优先级的概念就是,优先级高的中断可以打断优先级低的中断的执行,从而形成嵌套。当然反过来,优先级低的中断是不能打断优先级高的中断的。

那么既然有抢占优先级,自然就也有非抢占优先级了,也称为固有优先级。在表 6-3 中的最后一列给出的就是固有优先级,请注意,在中断优先级的编号中,一般都是数字越小优先级越高。从表中可以看到一共有 1~6 共 6 级的优先级,这里的优先级与抢占优先级的一个不同点就是,它不具有抢占的特性,也就是说即使在低优先级中断执行过程中又发生了高优先级的中断,那么这个高优先级的中断也只能等到低优先级中断执行完后才能得到响应。既然不能抢占,那么这个优先级有什么用呢?

答案是多个中断同时存在时的仲裁。比如说有多个中断同时发生了,当然实际上发生这种情况的概率很低,但另外一种情况就常见的多了,那就是出于某种原因我们暂时关闭了总中断,即 EA=0,执行完一段代码后又重新使能了总中断,即 EA=1,那么在这段时间里就很可能有多个中断都发生了,但因为总中断是关闭的,所以它们当时都得不到响应,而当总中断再次使能后,它们就会在同时请求响应了,很明显,这时也必需有个先后顺序才行,这就是非抢占优先级的作用了——如表 6-3 中,谁优先级最高先响应谁,然后按编号排队,依次得到响应。

抢占优先级和非抢占优先级的协同,可以使单片机中断系统有条不紊的工作,既不会无休止的嵌套,又可以保证必要时紧急任务得到优先处理。在后续的学习过程中,中断系统会与我们如影随形,处处都有它的身影,随着学习的深入,相信你对它的理解也会更加的深入。