一、实验目的
1、深入了解51单片机中断原理。掌握外部中断、定时器/计数器中断的编程方式;
2、了解中断响应、中断优先级、中断标志位复位等原理,掌握二级中断、中断嵌套的具体应用编程;
3、了解定时器/计数器的工作原理,掌握计数器初值计算方法,定时/计数中断服务的编程开发。
二、AT89C51中断技术概述
中断技术主要用于实时监测与控制,要求单片机能及时地响应中断请求源提出的服务请求,并快速响应与及时处理。
当中断请求源发出中断请求时,如中断请求被允许,单片机暂时中止当前正在执行的主程序,转到中断服务处理程序处理中断服务请求,处理完中断服务请求后,再回到原来被中止的程序之处(断点),继续执行被中断的主程序。
如果没有中断系统,单片机大量时间可能会浪费在查询是否有服务请求的定时查询操作上,即不论是否有服务请求,都必须去查询。
采用中断技术完全消除查询方式的等待,大大提高单片机工作效率和实时性。
如下为单片机对外设中断服务请求整个中断响应和处理过程。
三、AT89C51中断系统结构
中断系统有5个中断请求源(简称中断源),2个中断优先级,可实现2级中断服务程序嵌套。每一中断源可用软件独立控制为允许中断或关闭中断状态;每一个中断源的优先级均可用软件设置。
中断系统结构图如下:
1、中断请求源
参考上面的中断系统结构图,中断系统共有5个中断请求源,它们是:
(1)INT0*———外部中断请求0,外部中断请求信号(低电平或负跳变有效)由INT0引脚输入,中断请求标志为IE0。
(2)INT1———外部中断请求1,外部中断请求信号(低电平或负跳变有效)由INT1*引脚输入,中断请求标志为IE1。
(3)定时器/计数器T0计数溢出的中断请求,标志为TF0。
(4)定时器/计数器T1计数溢出的中断请求,标志为TF1。
(5)串行口中断请求,标志为发送中断TI或接收中断RI。
2、中断请求标志寄存器
5个中断请求源的中断请求标志分别由特殊功能寄存器TCON和SCON相应位锁存。(见下图)
TCON寄存器
为定时器/计数器的控制寄存器,字节地址为88H,可位寻址。既包括定时器/计数器T0、T1溢出中断请求标志位TF0和TF1,也包括两个外部中断请求的标志位IE1与IE0,还包括两个外部中断请求源的中断触发方式选择位。
TCON格式见下图:
TCON寄存器中与中断系统有关各标志位功能如下:
(1)TF1—定时器/计数器T1的溢出中断请求标志位。当启动T1计数后,T1从初值开始加1计数,当最高位产生溢出时,硬件 置TF1为“1”,向CPU申请中断,响应TF1中断时,TF1标志硬件自动清“0”,TF1也可由软件清“0”。
(2)TF0—定时器/计数器T0溢出中断请求标志位,与TF1类似。
(3)IE1—外部中断请求1中断请求标志位。
(4)IE0—外部中断请求0中断请求标志位,与IE1类似。
(5)IT1—选择外中断请求1为跳沿触发还是电平触发方式。
0–电平触发方式,加到INT0脚上的外中断请求输入信号为低电平有效,并把IE1置“1”。转向中断服务程序时,则由硬件自动把IE1清“0”。
1–跳沿触发方式,加到INT1脚上的外中断请求输入信号从高到低的负跳变有效,并把IE1置“1”。转向中断服务程序时,则由硬件自动把IE1清“0”。
(6)IT0—选择外中断请求0为跳沿触发方式还是电平触发方式,与IT1类似。
当AT89S51复位后,TCON被清“0”,5个中断源的中断请求标志均为0。
TR1(D6位)、TR0(D4位)这2位与中断系统无关,仅与定时器/计数器T1和T0有关。
SCON寄存器
SCON寄存器为串行口控制寄存器,字节地址为98H,可位寻址。SCON的低二位锁存串口的发送中断和接收中断的中断请求标志TI和RI。
格式见下图:
SCON标志位功能:
(1)TI—串口发送中断请求标志位。CPU将1字节的数据写入串口的发送缓冲器SBUF时,就启动一帧串行数据的发送,每发送完一帧串行数据后,硬件使TI自动置“1”。CPU响应串口发送中断时,并不清除TI中断请求标志,TI标志必须在中断服务程序中用指令对其清“0”。
(2)RI—串行口接收中断请求标志位。在串口接收完一个串行数据帧,硬件自动使RI中断请求标志置“1”。CPU在响应串口接收中断时,RI标志并不清“0”,须在中断服务程序中用指令对RI清“0”。
3、中断允许与中断优先级的控制
实现中断允许控制和中断优先级控制分别中断允许寄存器IE和中断优先级寄存器IP实现。下面介绍两个特殊功能寄存器。
中断允许寄存器IE
各中断源开放或屏蔽,是由片内中断允许寄存器IE控制。IE字节地址为A8H,可进行位寻址。
格式见下图:
IE对中断开放和关闭实现两级控制。两级控制就是有一个总的中断开关控制位EA(IE.7位),当EA=0,所有中断请求被屏蔽,CPU对任何中断请求都不接受;当EA=1时,CPU开中断,但5个中断源的中断请求是否允许,还要由IE中的低5位所对应的5个中断请求允许控制位的状态来决定。
IE中各位的功能如下:
(1)EA—中断允许总开关控制位。
EA=0,所有的中断请求被屏蔽。
EA=1,所有的中断请求被开放。
(2)ES—串行口中断允许位。
ES=0,禁止串行口中断。
ES=1,允许串行口中断。
(3)ET1—定时器/计数器T1溢出中断允许位。
ET1=0,禁止T1溢出中断。
ET1=1,允许T1溢出中断。
(4)EX1—外部中断1中断允许位。
EX1=0,禁止外部中断1中断。
EX1=1,允许外部中断1中断。
(5)ET0—定时器/计数器T0的溢出中断允许位。
ET0=0,禁止T0溢出中断。
ET0=1,允许T0溢出中断。
(6)EX0—外部中断0中断允许位。
EX0=0,禁止外部中断0中断。
EX0=1,允许外部中断0中断。
AT89S51复位后,IE被清“0”,所有中断请求被禁止。IE中与各个中断源相应位可用指令置“1”或清“0”,即可允许或禁止各中断源的中断申请。若使某一个中断源被允许中断,除了IE相应位被置“1”外,还必须使EA位置“1”。
中断优先级寄存器IP
中断请求源有两个中断优先级,每一个中断请求源可由软件设置为高优先级中断或低优先级中断,也可实现两级中断嵌套。
所谓两级中断嵌套,就是AT89S51正在执行低优先级中断的服务程序时,可被高优先级中断请求所中断,待高优先级中断处理完毕后,再返回低优先级中断服务程序。
两级中断嵌套见下图:
各中断源的中断优先级关系,可归纳为下面两条基本规则:
(1)低优先级可被高优先级中断,高优先级不能被低优先级中断。
(2)任何一种中断(不管是高级还是低级)一旦得到响应,不会再被它的同级中断源所中断。如果某一中断源被设置为高优先级中断,在执行该中断源的中断服务程序时,则不能被任何其他的中断源的中断请求所中断。
AT89S51片内有一个中断优先级寄存器IP,字节地址为B8H,可位寻址。只要用程序改变其内容,即可进行各中断源中断优先级设置,IP寄存器格式见下图。
中断优先级寄存器IP各位含义:
(1)PS—串行口中断优先级控制位,1—高级;0—低级。
(2)PT1—T1中断优先级控制位,1—高级;0—低级。
(3)PX1—外部中断1中断优先级控制位,1—高级;0—低级。
(4)PT0—T0中断优先级控制位,1—高级;0—低级。
(5)PX0—外部中断0中断优先级控制位,1—高级;0—低级。
中断优先级控制寄存器IP各位都可由程序置“1”和清“0”,用位操作指令或字节操作指令可更新IP的内容,改变各中断源的中断优先级。
AT89S51复位后,各中断源均为低优先级中断。IP内容为00。
下面介绍AT89S51的中断优先级结构。
中断系统有两个不可寻址的“优先级激活触发器”,其中一个指示某高优先级中断正在执行,所有后来中断均被阻止;另一个触发器指示某低优先级中断正在执行,所有同级中断都被阻止,但不阻断高优先级的中断请求。
在同时收到几个同优先级的中断请求时,哪一个中断请求能优先得到响应,取决于内部查询顺序。这相当于在同一个优先级还存在另一辅助优先级结构,其查询顺序见表6-1:
由表6-1,各中断源在同一优先级条件下,外部中断0中断优先权最高,串行口中断的优先权最低。
四、响应中断请求的条件
一个中断源中断请求被响应,须满足以下必要条件:
(1)总中断允许开关接通,即IE寄存器中的中断总允许位EA=1。
(2)该中断源发出中断请求,即该中断源对应的中断请求标志为“1”。
(3)该中断源的中断允许位=1,即该中断被允许。
(4)无同级或更高级中断正在被服务。
中断响应就是CPU对中断源提出的中断请求的接受,当查询到有效的中断请求时,满足上述条件时,紧接着就进行中断响应。
中断响应过程
首先由硬件自动生成一条长调用指令“LCALL addr16”。即程序存储区中相应的中断入口地址。例如,对于外部中断1的响应,硬件自动生成的长调用指令为:LCALL 0013H;
生成LCALL指令后,紧接着就由CPU执行该指令。首先将程序计数器PC内容压入堆栈以保护断点,再将中断入口地址装入PC,使程序转向响应中断请求的中断入口地址。各中断源服务程序入口地址是固定的,见表6-2。
其中两个中断入口间只相隔8字节,一般情况下难以安放一个完整的中断服务程序。
因此,通常总是在中断入口地址处放置一条无条件转移指令,使程序执行转向在其他地址存放的中断服务程序入口。
中断响应是有条件的,并不是查询到的所有中断请求都能被立即响应,当遇到下列3种情况之一时,中断响应被封锁:
(1)CPU正在处理同级或更高优先级的中断。因为当一个中断被响应时,要把对应的中断优先级状态触发器置“1”(该触发器指出CPU所处理的中断优先级别),从而封锁了低级中断请求和同级中断请求。
(2)所查询的机器周期不是当前正在执行指令的最后一个机器周期。设定这个限制的目的是只有在当前指令执行完毕后,才能进行中断响应,以确保当前指令执行的完整性。
(3)正在执行的指令是RETI或是访问IE或IP的指令。因为按中断系统的规定,在执行完这些指令后,需再执行完一条指令,才响应新的中断请求。
如存在上述3种情况之一,CPU将丢弃中断查询结果,不能对中断进行响应。
五、外部中断的响应时间
在使用外部中断时,有时需考虑从外部中断请求有效(外部中断请求标志置“1”)到转向中断入口地址所需要的响应时间,即外部中断响应的实时性问题。下面就来讨论这个问题。
外中断最短响应时间为3个机器周期。其中中断请求标志位查询占1个机器周期,而这个机器周期恰好处于指令的最后一个机器周期。在这个机器周期结束后,中断即被响应,CPU接着执行1条硬件子程序调用指令LCALL以转到相应的中断服务程序入口,这需要2个机器周期。
外部中断响应最长时间为8个机器周期。这种情况发生在CPU进行中断标志查询时,刚好才开始执行RETI或访问IE或IP的指令,则需把当前指令执行完再继续执行一条指令后,才能响应中断。
执行上述的RETI或访问IE或IP的指令,最长需要2个机器周期。而接着再执行1条指令,我们按最长的指令(乘法指令MUL和除法指令DIV)来算,也只有4个机器周期。再加上硬件子程序调用指令LCALL的执行,需要2个机器周期,所以,外部中断响应的最长时间为8个机器周期。
如已在处理同级或更高级中断,外部中断请求响应时间取决于正在执行的中断服务程序的处理时间,此情况下,响应时间无法计算。
这样,在一单一中断系统,AT89S51 对外部中断请求响应时间总是在3~8个机器周期间。
六、外部中断的触发方式选择
外部中断有两种触发方式:电平触发方式和跳沿触发方式。
电平触发方式
若外部中断定义为电平触发方式,外部中断申请触发器状态随着 CPU 在每个机器周期采样到的外部中断输入引脚电平变化而变化,这能提高CPU对外部中断请求的响应速度。当外部中断源被设定为电平触发方式时,在中断服务程序返回之前,外部中断请求输入必须无效(即外部中断请求输入已由低电平变为高电平),否则CPU返回主程序后会再次响应中断。所以电平触发适合于外部中断以低电平输入且中断服务程序能清除外部中断请求源(即外部中断输入电平又变为高电平)的情况。
跳沿触发方式
外部中断若定义为跳沿触发方式,外部中断申请触发器能锁存外部中断输入线上的负跳变。即便是CPU暂时不能响应,中断请求标志也不会丢失。在这种方式下,如果相继连续两次采样,一个机器周期采样到外部中断输入为高,下一机器周期采样为低,则中断申请触发器置“1”,直到CPU响应此中断时,该标志才清“0”。这样就不会丢失中断,但输入的负脉冲宽度至少要保持1个机器周期(若晶振频率为6MHz,则为2s),才能被CPU采样到。外部中断的跳沿触发方式适合于以负脉冲形式输入的外部中断请求。
七、中断请求的撤销
某中断请求被响应后,就存在着一个中断请求撤销问题。下面按中断请求源的类型分别说明中断请求的撤销方法。
1、定时器/计数器中断请求的撤销
定时器/计数器中断的中断请求被响应后,硬件会自动把中断请求标志位(TF0或TF1)清“0”,因此定时器/计数器中断请求是自动撤销的。
2、外部中断请求的撤销
(1)跳沿方式外部中断请求的撤销
中断请求撤销两项内容:中断标志位清“0”和外中断信号的撤销。其中,中断标志位(IE0或IE1)清“0”是在中断响应后由硬件自动完成的。而外中断请求信号的撤销,由于跳沿信号过后也就消失了,所以跳沿方式的外部中断请求也是自动撤销的。
(2)电平方式外中断请求撤销
中断请求标志自动撤销,但中断请求信号低电平可能继续存在,在以后的机器周期采样时,又会把已清“0”的IE0或IE1标志位重新置“1”。要彻底解决电平方式外部中断请求撤销,除标志位清“0”之外,还需在中断响应后把中断请求信号输入引脚从低电平强制改变为高电平。为此,可增加如下所示电路:
由上图,D触发器锁存外来的中断请求低电平,并通过其输出端Q接到(INT0或INT1)。所以,增加的D触发器不影响中断请求。中断响应后,为撤销中断请求,可利用D触发器直接置“1”SD端实现,即把SD端接AT89S51的P1.0。因此,只要P1.0端输出一个负脉冲就可以使D触发器置“1”,从而就撤销低电平的中断请求信号。负脉冲可在中断服务程序中先P1.0置1,再让P1.0为0,再把P1.0置1。
3、串行口中断请求的撤销
只有标志位清“0”的问题。串行口中断标志位是TI和RI,但对这两个中断标志CPU不自动清“0”。因为响应串口中断后,CPU无法知道是接收中断还是发送中断,还需测试这两个中断标志位来判定,然后才清除。所以串口中断请求撤销只能使用软件在中断服务程序中把串行口中断标志位TI、RI清0。
八、中断函数
为直接使用C51编写中断服务程序,C51中定义了中断函数。由于C51编译器在编译时对声明为中断服务程序的函数自动添加相应现场保护、阻断其他中断、返回时自动恢复现场等处理的程序段,因而在编写中断函数时可不必考虑这些问题,减小编写中断服务程序烦琐程度。
中断服务函数的一般形式为:
函数类型 函数名(形式参数表)interrupt n using n
关键字interrupt后面的 n是中断号,对于8051单片机,n的取值为0~4,编译器从8×n+3处产生中断向量。AT89S51中断源对应的中断号和中断向量见下表。
AT89S51内部RAM中可使用4个工作寄存器区,每个工作寄存器区包含8个工作寄存器(R0~R7)。关键字using后面的n用来选择4个工作寄存器区。using是一选项,如不选,中断函数中的所有工作寄存器内容将被保存到堆栈中。
关键字using对函数目标代码的影响如下:
在中断函数的入口处将当前工作寄存器区内容保护到堆栈中,函数返回前将被保护的寄存器区内容从堆栈中恢复。使用using在函数中确定一个工作寄存器区须十分小心,要保证任何工作寄存器区的切换都只在指定的控制区域中发生,否则将产生不正确的函数结果。
例如,外中断1()中断服务函数如下:
void int1( ) interrupt 2 using 0//中断号n=2,选择0区工作寄存器区
中断调用与标准C的函数调用是不一样的,当中断事件发生后,对应的中断函数被自动调用,即没有参数,也没有返回值,会带来如下影响。
(1)编译器会为中断函数自动生成中断向量。
(2)退出中断函数时,所有保存在堆栈中的工作寄存器及特殊功能寄存器被恢复。
(3)在必要时特殊功能寄存器Acc、B、DPH、DPL以及PSW的内容被保存到堆栈中。
编写中断程序,应遵循以下规则:
(1)中断函数没有返回值,如果定义一个返回值,将会得到不正确结果。建议将中断函数定义为void类型,明确说明无返回值。
(2)中断函数不能进行参数传递,如果中断函数中包含任何参数声明都将导致编译出错。
(3)任何情况下都不能直接调用中断函数,否则会产生编译错误。因为中断函数的返回是由汇编语言指令RETI完成的。RETI指令会影响AT89S51硬件中断系统内的不可寻址的中断优先级寄存器的状态。如没有实际中断请求情况下,直接调用中断函数,也就不会执行RETI指令,其操作结果有可能产生一个致命错误。
(4)如在中断函数中再调用其他函数,则被调用的函数所用的寄存器区必须与中断函数使用的寄存器区不同。
九、中断系统的应用——在Proteus和普中单片机板上分别完成按键中断控制流水灯的实验
(1)在Proteus上完成采用定时计数器控制LED灯每隔1s周期性亮灭的实验
Keil代码实现:
#include <reg51.h>
// 函数声明
void delay(unsigned int time_ms);
void initExternalInterrupt();
void initTimer();
void ledBlink();
// 中断服务程序
void externalInterrupt0() interrupt 0 {
// 检查按钮是否按下
if (K1 == 0) {
// 等待按钮释放
while (K1 == 0);
// 交替闪烁LED 5次
for (int i = 0; i < 5; i++) {
ledBlink();
}
// 关闭所有LED
P1 = 0xFF;
}
}
// 主函数
void main() {
// 初始化外部中断0
initExternalInterrupt();
// 初始化定时器
initTimer();
// 使能全局中断
EA = 1;
// 主循环
while (1);
}
// 初始化外部中断0
void initExternalInterrupt() {
IT0 = 1; // 设置为电平触发
EX0 = 1; // 使能外部中断0
}
// 初始化定时器
void initTimer() {
TMOD |= 0x01; // 设置定时器0为模式1(16位定时器)
TH0 = 0xFC; // 加载高8位
TL0 = 0x67; // 加载低8位
ET0 = 1; // 使能定时器0中断
TR0 = 1; // 启动定时器0
}
// 定时器中断服务程序
void timer0Interrupt() interrupt 1 {
static unsigned int count = 0;
TH0 = 0xFC; // 加载高8位
TL0 = 0x67; // 加载低8位
count++;
if (count >= 1000) { // 等待1秒
count = 0;
ledBlink(); // 闪烁LED
}
}
// LED交替闪烁函数
void ledBlink() {
static bit state = 0;
if (state == 0) {
P1 = 0x0F; // 低4位亮,高4位灭
} else {
P1 = 0xF0; // 高4位亮,低4位灭
}
state = ~state; // 切换状态
delay(500); // 延时500ms
}
// 延时函数
void delay(unsigned int time_ms) {
unsigned int i, j;
for (i = 0; i < time_ms; i++) {
for (j = 0; j < 120; j++); // 粗略延时1ms
}
}
(2)采用计数器中断,实现按4次按钮开关后,P1口的8只LED闪烁不停
Keil代码实现:
#include <reg51.h>
// 函数声明
void delay(unsigned int time_ms);
void initTimer();
void ledBlink();
// 全局变量
unsigned int buttonPressCount = 0;
// 主函数
void main() {
// 初始化定时器
initTimer();
// 使能全局中断
EA = 1;
// 主循环
while (1);
}
// 初始化定时器
void initTimer() {
TMOD |= 0x01; // 设置定时器0为模式1(16位定时器)
TH0 = 0xFC; // 加载高8位
TL0 = 0x67; // 加载低8位
ET0 = 1; // 使能定时器0中断
TR0 = 1; // 启动定时器0
}
// 定时器中断服务程序
void timer0Interrupt() interrupt 1 {
static unsigned int count = 0;
TH0 = 0xFC; // 加载高8位
TL0 = 0x67; // 加载低8位
count++;
if (count >= 1000) { // 等待1秒
count = 0;
if (buttonPressCount >= 4) {
ledBlink(); // 闪烁LED
}
}
}
// 按钮中断服务程序
void externalInterrupt0() interrupt 0 {
// 检查按钮是否按下
if (K1 == 0) {
// 等待按钮释放
while (K1 == 0);
// 按钮按下次数加1
buttonPressCount++;
}
}
// LED闪烁函数
void ledBlink() {
static bit state = 0;
if (state == 0) {
P1 = 0x00; // 所有LED亮
} else {
P1 = 0xFF; // 所有LED灭
}
state = ~state; // 切换状态
}
// 延时函数
void delay(unsigned int time_ms) {
unsigned int i, j;
for (i = 0; i < time_ms; i++) {
for (j = 0; j < 120; j++); // 粗略延时1ms
}
}
(3)不在中断函数使用延时循环实现同样的功能
一般来说,中断函数中要尽量避免使用执行时间较长(耗时)的代码,以避免中断服务影响到主程序代码的执行效率。但是在上面外部中断的实验中,中断函数采用了软件延时函数去控制LED亮灭的间隔周期。这是一种不好的编程。因此,我们换一种更合理的方式,不在中断函数使用延时循环,实现同样的功能。
Keil代码实现:
#include <reg51.h>
// 函数声明
void initExternalInterrupt();
void ledPattern1();
void ledPattern2();
// 全局变量
bit modeFlag = 0; // 模式标志位,0表示模式1,1表示模式2
// 主函数
void main() {
// 初始化外部中断0
initExternalInterrupt();
// 主循环
while (1) {
if (modeFlag == 0) {
ledPattern1(); // 模式1:低4位LED交替闪烁
} else {
ledPattern2(); // 模式2:高4位LED交替闪烁
}
}
}
// 初始化外部中断0
void initExternalInterrupt() {
IT0 = 1; // 设置为电平触发
EX0 = 1; // 使能外部中断0
EA = 1; // 使能全局中断
}
// 按钮中断服务程序
void externalInterrupt0() interrupt 0 {
// 检查按钮是否按下
if (K1 == 0) {
// 等待按钮释放
while (K1 == 0);
// 切换模式标志位
modeFlag = ~modeFlag;
}
}
// 模式1:低4位LED交替闪烁
void ledPattern1() {
LED0 = 0; LED1 = 1; LED2 = 0; LED3 = 1;
LED4 = 0; LED5 = 1; LED6 = 0; LED7 = 1;
}
// 模式2:高4位LED交替闪烁
void ledPattern2() {
LED0 = 1; LED1 = 0; LED2 = 1; LED3 = 0;
LED4 = 1; LED5 = 0; LED6 = 1; LED7 = 0;
}
小结:学习51单片机中断原理是深入理解嵌入式系统开发中重要概念的关键一步。我自己也是在学习当中,在学习的过程中,我意识到中断是一种重要的机制,它允许单片机在执行主程序的同时,响应外部事件或特定条件的发生。