本章专门用来介绍51单片机的中断系统,为后面学习外部中断、 定时器中断、 串口中断做好铺垫。
中断是为使单片机具有对外部或内部随机发生的事件实时处理而设置的,中断功能的存在, 很大程度上提高了单片机处理外部或内部事件的能力。 它也是单片机最重要的功能之一, 是学习单片机必须要掌握的。
对于单片机来讲, 中断是指 CPU
在处理某一事件 A
时, 发生了另一事件 B
,请求 CPU
迅速去处理(中断发生); CPU
暂时停止当前的工作(中断响应), 转去处理事件 B
(中断服务); 待 CPU
将事件 B
处理完毕后, 再回到原来事件 A
被中断的地方继续处理事件 A(中断返回), 这一过程称为中断。
单片机在执行程序时, 中断也随时有可能发生, 但无论何时发生, 只要一旦发生, 单片机将立即暂停当前程序,赶去处理中断程序, 处理完中断程序后再返回刚才暂停处接着执行原来的程序。其流程图如下:
引起 CPU 中断的根源称为中断源。 中断源向 CPU 提出中断请求, CPU 暂时中断原来的事务 A, 转去处理事件 B, 对事件 B 处理完毕后, 再回到原来被中断的地方(即断点), 称为中断返回。 实现上述中断功能的部件称为中断系统(中断机构)。
当中央处理机 CPU 正在处理某件事的时候外界发生了紧急事件请求, 要求CPU 暂停当前的工作, 转而去处理这个紧急事件, 处理完以后, 再回到原来被中断的地方, 继续原来的工作, 这样的过程称为中断。 实现这种功能的部件称为中断系统, 请示 CPU 中断的请求源称为中断源。 微型机的中断系统一般允许多个中断源, 当几个中断源同时向 CPU 请求中断, 要求为它服务的时候, 这就存在CPU 优先响应哪一个中断源请求的问题。 通常根据中断源的轻重缓急排队, 优先处理最紧急事件的中断请求源, 即规定每一个中断源有一个优先级别。 CPU 总是先响应优先级别最高的中断请求。
当 CPU 正在处理一个中断源请求的时候(执行相应的中断服务程序), 发生了另外一个优先级比它还高的中断源请求。 如果 CPU 能够暂停对原来中断源的服务程序, 转而去处理优先级更高的中断请求源, 处理完以后, 再回到原低级中断服务程序, 这样的过程称为中断嵌套。 这样的中断系统称为多级中断系统, 没有中断嵌套功能的中断系统称为单级中断系统。
中断的开启与关闭、 设置启用哪一个中断等都是由单片机内部的一些特殊功能寄存器来决定的, 在前面章节的学习中我们仅对单片机 IO 口操作过(实际上操作 IO 口即操作 IO 口寄存器, 只不过编译器已经帮我们把 IO 口寄存器封装好直接操作 IO 即可, 这些可在 51 单片机头文件内查看) , 从本文开始就会介绍单片机内部更多的特殊功能寄存器以及如何配置它实现相应的功能。
STC89C5X 系列单片机提供了 8 个中断请求源, 它们分别是: 外部中断0(INT0)、 外部中断 1(INT1)、 外部中断 2(INT2)、 外部中断 3(INT3)、 定时器 0中断、 定时器 1 中断、 定时器 2 中断、 串口(UART)中断。(注意: 51 系列单片机一定有基本的 5 个中断, 但不全有 8 个中断, 需要查看芯片手册, 通常我们使用的都是基本的 5 个中断: INT0、 INT1、 定时器 0/1, 串口中断) 。 所有的中断都具有四个中断优先级(基本型只有两个) 。 用户可以通过关闭总中断允许位(EA/IE.7)或相应中断的允许位来屏蔽所有的中断请求, 也可以用打开相应的中断允许位来使 CPU 响应相应的中断申请。 其中有些中断源可以用软件独立地控制为开中断或关中断状态。 每一个中断的优先级别均可用软件设置。 高优先级的中断请求可以打断低优先级的中断, 反之, 低优先级的中断请求不可以打断高优先级及同优先级的中断。 当两个相同优先级的中断同时产生时, 将由查询次序来决定系统先响应哪个中断。 STC89C5X 系列单片机的各个中断查询次序表如下图所示:
通过设置新增加的特殊功能寄存器 IPH 中的相应位, 可将中断优先级设为四级, 如果只设置 IP 或 XICON, 那么中断优先级就只有两级, 与传统 8051 单片机两级中断优先级完全兼容。 上图中的中断查询次序即为中断号, 这个中断号在编程时非常重要, 当中断来临时, 只有中断号正确才能进入中断。
下面我们以 51 单片机均有的 5 个中断来介绍, 其内部结构框图如下所示:
①INT0
对应的是 P3.2
口的附加功能, 可由 IT0(TCON.0)
选择其为低电平有效还是下降沿有效。 当 CPU 检测到 P3.2
引脚上出现有效的中断信号时, 中断标志 IE0(TCON.1)
置 1, 向 CPU 申请中断。
②INT1
对应的是 P3.3
口的附加功能, 可由 IT1(TCON.2)
选择其为低电平有效还是下降沿有效。 当 CPU 检测到 P3.3
引脚上出现有效的中断信号时, 中断标志 IE1(TCON.3)
置 1,向 CPU 申请中断。
③T0
对应的是 P3.4
口的附加功能, TF0(TCON.5)
,片内定时/计数器 T0
溢出中断请求标志。 当定时/计数器 T0
发生溢出时, 置位 TF0
, 并向 CPU 申请中断
④T1
对应的是 P3.5
口的附加功能, TF1( TCON.7)
, 片内定时/计数器 T1
溢出中断请求标志。 当定时/计数器 T1
发生溢出时, 置位 TF1
, 并向 CPU 申请中断。
⑤RXD
和 TXD
对应的是 P3.0
和 P3.1
口的附加功能, RI(SCON.0)
或 TI(SCON.1)
, 串行口中断请求标志。 当串行口接收完一帧串行数据时置位 RI 或当串行口发送完一帧串行数据时置位 TI
, 向 CPU 申请中断。
(1)中断允许控制
CPU 对中断系统的所有中断以及某个中断源的开放和屏蔽是由中断允许寄存器 IE 控制的。
EX0(IE.0), 外部中断 0 允许位;
ET0(IE.1), 定时/计数器 T0 中断允许位;
EX1(IE.2), 外部中断 1 允许位;
ET1(IE.3), 定时/计数器 T1 中断允许位;
ES(IE.4), 串行口中断允许位;
EA (IE.7), CPU 中断允许(总允许) 位。
IT0(TCON.0) , 外部中断 0 触发方式控制位。
当 IT0=0 时, 为电平触发方式。
当 IT0=1 时, 为边沿触发方式(下降沿有效) 。
IE0(TCON.1) , 外部中断 0 中断请求标志位。
IT1(TCON.2) , 外部中断 1 触发方式控制位。
IE1(TCON.3) , 外部中断 1 中断请求标志位。
TF0(TCON.5) , 定时/计数器 T0 溢出中断请求标志位。
TF1(TCON.7) , 定时/计数器 T1 溢出中断请求标志位。
(3)中断优先级
同一优先级中的中断申请不止一个时, 则有中断优先权排队问题。 同一优先级的中断优先权排队, 由中断系统硬件确定的自然优先级形成, 其排列如所示:
(4)中断号
(5)中断响应条件
①中断源有中断请求;
②此中断源的中断允许位为 1;
③CPU 开中断(即 EA=1) 。
以上三条同时满足时, CPU 才有可能响应中断。 在使用中断时我们需要做什
么呢?
①你想使用的中断是哪个? 选择相应的中断号;
②你所希望的触发条件是什么?
③你希望在中断之后干什么?
我们以外部中断 0 为例, 主程序中需要有以下代码:
EA=1; //打开总中断开关
EX0=1; //开外部中断 0
IT0=0/1; //设置外部中断的触发方式
中断服务函数:
void int0() interrupt 0 using 1
{
//编写用户所需的功能代码
}
在中断函数中 int0 是函数名, 可自定义, 但必须符合 C 语言标识符定义规则, interrupt 是一个关键字, 表示 51 单片机中断。 后面的“0” 是中断号, 外部中断 0 中断号为 0, 这个可参考前面中断号的内容。 后面的 using 1 可省略不写。
上一节我们介绍了 51 单片机的中断系统, 这一节就来学习 51 单片机的外部中断, 通过上一节的介绍我们知道, 51 单片机外部中断有 2 个, 外部中断 0 和外部中断 1, 它们的使用方法是一样的, 所以只要学会一个即可掌握所有外部中断使用。 本节所要实现的功能是: 使用独立按键 K3 控制 LED 亮灭, K3 连接外部中断 0(P3.2) 管脚
要让 51 单片机发生中断必须要满足以下 3 个条件, 这 3 个条件的顺序可以任意:
①中断源有中断请求;
②此中断源的中断允许位为 1;
③CPU 开中断(即 EA=1) 。
比如我们配置外部中断 0, 对应的配置程序如下:
EA=1; //打开总中断开关
EX0=1; //开外部中断 0
IT0=0/1; //设置外部中断的触发方式
如果要配置的是外部中断 1, 只需将 EX0
改为 EX1
, IT0
改为 IT1
。
因为独立按键一端是共地的, 当按下后对应单片机 IO 口被拉低, 而默认单片机 IO 口是高电平, 这样就有一个下降沿过程, 所以通常使用外部中断都是配置为下降沿触发, 即 IT0=1;
在编写程序时通常我们会将外部中断的配置放到一个自定义函数内便于管理维护。 如下所示:
void exti0_init(void)
{
IT0=1;//跳变沿触发方式(下降沿)
EX0=1;//打开 INT0 的中断允许
EA=1;//打开总中断
}
当触发中断后即会进入中断服务函数 , 外部中断 0 中断服务函数如下:
void exti0() interrupt 0 //外部中断 0 中断函数
{
//执行所需的功能
}
在中断函数中 exti0 是函数名, 可自定义, 但必须符合 C 语言标识符定义规则, interrupt 是一个关键字, 表示 51 单片机中断。 后面的“0” 是中断号, 外部中断 0 中断号为 0, 如果是外部中断 1, 则中断号为 2, 这个可参考中断章节的内容。
本节所要实现的功能是: 使用独立按键 K3
控制 LED 亮灭。
ps:K3
键是连接在单片机 P3.2
口(外部中断 0) , K4
按键是连接在 P3.3
口(外部中断 1) 。
/**************************************************************************************
实验名称:外部中断0实验
接线说明:
实验现象:下载程序后,当按下K3键可控制D1指示灯亮灭
***************************************************************************************/
#include "reg52.h"
typedef unsigned int u16; //对系统默认数据类型进行重定义
typedef unsigned char u8;
//定义LED1管脚
sbit LED1=P2^0;
//定义独立按键K3控制脚
sbit KEY3=P3^2;
/*******************************************************************************
* 函 数 名 : delay_10us
* 函数功能 : 延时函数,ten_us=1时,大约延时10us
* 输 入 : ten_us
* 输 出 : 无
*******************************************************************************/
void delay_10us(u16 ten_us)
{
while(ten_us--);
}
/*******************************************************************************
* 函 数 名 : exti0_init
* 函数功能 : 外部中断0配置函数
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void exti0_init(void)
{
IT0=1;//跳变沿触发方式(下降沿)
EX0=1;//打开INT0的中断允许
EA=1;//打开总中断
}
void main()
{
exti0_init();//外部中断0配置
while(1)
{
}
}
void exti0() interrupt 0 //外部中断0中断函数
{
delay_10us(1000);//消抖
if(KEY3==0)//再次判断K3键是否按下
LED1=!LED1;//LED1状态翻转
}
程序代码比较简单, 首先定义 K3
键与 LED1
的控制管脚, 然后定义了外部中断 0
的配置函数 exti0_init
, 该函数内容是按照前面介绍的配置方法实现, 即开启总中断、 外部中断 0
功能, 设置外部中断 0
为下降沿触发。 然后进入 while循环, 在循环体内没有执行任何功能程序。
为什么在主函数中没有看到按键对 LED 的控制呢? 因为我们在exti0_init()
函数内就已经把按键管脚配置为外部中断 0 下降沿触发, 当有按键按下, 即会进入对应中断服务函数执行相应的功能程序, LED 的控制就在中断函数内完成的。
按下 K3
键, D1
指示灯亮, 再次按下 K
3 键, D1
指示灯灭, 如此循环。
上一节我们介绍了 51 单片机的外部中断, 学习了如何配置 51 单片机的外部中断。 这一节我们来学习 51 单片机的定时器中断。本章以定时器 0 为例进行讲解, 让大家学会 51 单片机定时器的使用, 定时器 1 的使用方法与定时器 0 一样。 本章要实现的功能是: 通过定时器 0 中断控制 D1 指示灯间隔 1 秒闪烁。
在介绍定时器之前先科普下几个知识:
1、CPU 时序的有关知识
①振荡周期: 为单片机提供定时信号的振荡源的周期(晶振周期或外加振荡周期)。
②状态周期: 2 个振荡周期为 1 个状态周期, 用 S 表示。 振荡周期又称 S 周期或时钟周期。
③机器周期: 1 个机器周期含 6 个状态周期, 12 个振荡周期。是执行一个基本指令所需的时间
④指令周期: 完成 1 条指令所占用的全部时间, 它以机器周期为单位。
例如: 外接晶振为 12MHz 时, 51 单片机相关周期的具体值为:
振荡周期=1/12us;
状态周期=1/6us;
机器周期=1us;
指令周期=1us~4us;
2、学习定时器前需要明白的几点
①51 单片机有两组定时器/计数器, 因为既可以定时, 又可以计数, 故称之为定时器/计数器。
②定时器/计数器和单片机的 CPU 是相互独立的。 定时器/计数器工作的过程是自动完成的, 不需要 CPU 的参与。
③51 单片机中的定时器/计数器是根据机器内部的时钟或者是外部的脉冲信号对寄存器中的数据加 1。
有了定时器/计数器之后, 可以增加单片机的效率, 一些简单的重复加 1 的工作可以交给定时器/计数器处理。 CPU 转而处理一些复杂的事情。 同时可以实现精确定时作用。
STC89C5X 单片机内有两个可编程的定时/计数器 T0
、 T1
和一个特殊功能定时器 T2
。 定时/计数器的实质是加 1 计数器(16 位) , 由高 8 位和低 8 位两个寄存器 THx
和 TLx
组成。 它随着计数器的输入脉冲进行自加 1, 也就是每来一个脉冲, 计数器就自动加 1, 当加到计数器为全 1 时, 再输入一个脉冲就使计数器回零, 且计数器的溢出使相应的中断标志位置 1, 向 CPU
发出中断请求(定时/计数器中断允许时) 。 如果定时/计数器工作于定时模式, 则表示定时时间已到;如果工作于计数模式, 则表示计数值已满。 可见, 由溢出时计数器的值减去计数初值才是加 1 计数器的计数值。
上图中的 T0
和 T1
引脚对应的是单片机 P3.4
和 P3.5
管脚。 51 单片机定时/计数器的工作由两个特殊功能寄存器控制。 TMOD
是定时/计数器的工作方式寄存器, 确定工作方式和功能; TCON
是控制寄存器, 控制 T0
、 T1
的启动和停止及设置溢出标志。
1、工作方式寄存器 TMOD
工作方式寄存器 TMOD
用于设置定时/计数器的工作方式, 低四位用于 T0
, 高
四位用于 T1
。 其格式如下:
GATE 是门控位, 用于控制定时器的启动是否受外部中断源信号的影响。GATE=0 时, 只要用软件使 TCON 中的 TR0 或 TR1 为 1, 就可以启动定时/计数器工作;GATA=1 时, 要用软件使 TR0 或 TR1 为 1, 同时外部中断引脚 INT0/1 也为高电平时, 才能启动定时/计数器工作。 即此时定时器的启动条件, 加上了 INT0/1 引脚为高电平这一条件。
C/T
:定时/计数模式选择位。 C/T
=0 为定时模式;C/T
=1 为计数模式。
M1M0
: 工作方式设置位。 定时/计数器有四种工作方式。
2、控制寄存器 TCON
TCON
的低 4 位用于控制外部中断(前面介绍过,与本节的定时/计数器无关),TCON
的高 4 位用于控制定时/计数器的启动和中断申请。 其格式如下:
1、方式0
方式 0 为 13 位计数, 由 TL0
的低 5 位(高 3 位未用) 和 TH0
的 8 位组成。TL0
的低 5 位溢出时向 TH0
进位, TH0
溢出时, 置位 TCON
中的 TF0
标志, 向 CPU发出中断请求。 其结构图如下所示:
门控位 GATE
具有特殊的作用。 当 GATE=0
时, 经反相后使或门输出为 1
, 此时仅由 TR0
控制与门的开启, 与门输出 1
时, 控制开关接通, 计数开始; 当 GATE=1
时, 由外中断引脚信号控制或门的输出, 此时控制与门的开启由外中断引脚信号
和 TR0
共同控制。 当 TR0=1
时, 外中断引脚信号引脚的高电平将启动计数, 外中断引脚信号引脚的低电平将停止计数。 这种方式常用来测量外中断引脚上正脉冲的宽度。 计数模式时, 计数脉冲是 T0引脚上的外部脉冲。 计数初值与计数个数的关系为: X=213-N。(想要准确的计N个数后结束,就必须先算出并设置好初值)
2、方式 1
方式 1 的计数位数是 16 位, 由 TL0
作为低 8 位, TH0
作为高 8 位, 组成了16 位加 1 计数器。 其结构图如下所示:
计数初值与计数个数的关系为: X=216-N。
3、方式 2
方式 2 为自动重装初值的 8 位计数方式。 工作方式 2 特别适合于用作较精确的脉冲信号发生器。 其结构图如下所示:
计数初值与计数个数的关系为: X=28-N。
4、方式 3
方式 3 只适用于定时/计数器 T0
, 定时器 T1
处于方式 3 时相当于 TR1=0
,停止计数。 工作方式 3 将 T0
分成为两个独立的 8 位计数器 TL0
和 TH0
。 其结构如下所示:
这几种工作方式中应用较多的是方式 1 和方式 2。 定时器中通常使用定时器方式 1, 串口通信中通常使用方式 2。
在使用定时器时, 应该如何配置使其工作? 其步骤如下( 各步骤顺序可任意):
①对 TMOD
赋值, 以确定 T0
和 T1
的工作方式, 如果使用定时器 0 即对 T0
配置, 如果使用定时器 1 即对 T1 配置。
②根据所要定时的时间计算初值,并将其写入 TH0
、 TL0
或 TH1
、 TL1
。
③如果使用中断, 则对 EA
赋值, 开放定时器中断。
④使 TR0
或 TR1
置位, 启动定时/计数器定时或计数。
上文中有一个定时/计数器初值的计算, 下面我们来看下如何计算定时/计数器初值。
前面我们介绍过机器周期的概念, 它是 CPU
完成一个基本操作所需要的时间。其计算公式是: 机器周期=1/单片机的时钟频率。 51 单片机内部时钟频率是外部时钟的 12 分频, 也就是说当外部晶振的频率输入到单片机里面的时候要进行 12分频。 比如说你用的是 12MHZ 晶振, 那么单片机内部的时钟频率就是 12/12MHZ,当你使用12MHZ 的外部晶振的时候, 机器周期=1/1M=1us。 如果我们想定时 1ms的初值是多少呢? 1ms/1us=1000。 也就是要计数 1000 个, 初值=65535-1000+1( 因为实际上计数器计数到 65536( 216) 才溢出, 所以后面要加 1)=64536=FC18H, 所以初值即为 THx
=0XFC, TLx
=0X18。
知道了如何计算定时/计数器初值, 那么想定时多长时间都可以计算出, 当然由于定时计数器位数有限, 我们不可能直接通过初值定时很长时间, 如果要实现很长时间的定时, 比如定时 1 秒钟。 可以通过初值设置定时1ms, 每当定时 1ms结束后又重新赋初值, 并且设定一个全局变量累计定时 1ms 的次数, 当累计到1000 次, 表示已经定时 1 秒了。 需要其他定时时间类似操作, 这样我们就可以使用定时器来实现精确延时来替代之前的 delay 函数。
这里以定时器 0 为例介绍配置定时器工作方式 1、 设定 1ms 初值, 开启定时器计数功能以及总中断, 如下:
void time0_init(void)
{
//按位或而不是直接给TMOD赋值是为了保留 TMOD 寄存器中的其他位的同时,
//将指定的位设置为 1。这样可以确保不影响其他位的状态
TMOD|=0X01;//选择为定时器 0 模式, 工作方式 1
TH0=0XFC; //给定时器赋初值, 定时 1ms
TL0=0X18;
ET0=1;//打开定时器 0 中断允许
EA=1;//打开总中断
TR0=1;//打开定时器
}
对于定时器 1 的使用方法是一样的, 只是将上述的 0 变为 1 即可,如下:
void time1_init(void)
{
TMOD|=0X10;//选择为定时器0模式,工作方式1
TH1=0XFC; //给定时器赋初值,定时1ms
TL1=0X18;
ET1=1;//打开定时器1中断允许
EA=1;//打开总中断
TR1=1;//打开定时器
}
本节使用到硬件资源如下:
(1) LED 模块(D1)
(2) 定时器 0
本章硬件电路非常简单, 只使用到开发板上 LED 模块的 D1。 至于定时器 0 ,它属于 51 单片机内部资源, 只需通过软件配置即可使用。 开发板上 LED 模块电路在前面已经介绍, 这里就不多说。
本节所要实现的功能是: 通过定时器 0 中断控制 D1 指示灯间隔 1 秒闪烁。
#include "reg52.h"
typedef unsigned int u16; //对系统默认数据类型进行重定义
typedef unsigned char u8;
//定义LED1管脚
sbit LED1=P2^0;
/*******************************************************************************
* 函 数 名 : time0_init
* 函数功能 : 定时器0中断配置函数,通过设置TH和TL即可确定定时时间
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void time0_init(void)
{
TMOD|=0X01;//选择为定时器0模式,工作方式1
TH0=0XFC; //给定时器赋初值,定时1ms
TL0=0X18;
ET0=1;//打开定时器0中断允许
EA=1;//打开总中断
TR0=1;//打开定时器
}
/*******************************************************************************
* 函 数 名 : main
* 函数功能 : 主函数
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void main()
{
time0_init();//定时器0中断配置
while(1)
{
}
}
void time0() interrupt 1 //定时器0中断函数
{
static u16 i;//定义静态变量i
TH0=0XFC; //给定时器赋初值,定时1ms
TL0=0X18;
i++;
if(i==1000)
{
i=0;
LED1=!LED1;
}
}
实验代码非常简单, 首先定义 LED1
指示灯控制管脚, 然后定义定时器 0 中断配置函数 time0_init
, 该函数配置内容就是按照前面介绍的配置方法所写,即选择定时器工作模式 0、 工作方式 1、 设置定时 1ms 初值、 打开定时器计数功能和开启总中断功能。 然后进入 while
循环, 在循环体内没有执行任何功能程序。当定时时间到达即会进入定时器 0 中断, 在中断服务函数内, 重新赋初值准备下次计数, 并且定义一个静态变量来累计定时 1ms 次数, 当变量等于 1000 时, 表示定时时间达 1 秒, 然后清零变量以及控制 LED
状态翻转。 执行完成后退出中断
返回主函数, 当时间到达又进入中断, 如此循环。
为什么要使用关键字 static 将 i 定义为静态变量呢? 我们希望每次进入中断函数时, i 保存的是上次累加值, 使用了 static 关键字, 就可以让变量 i 实现这种功能, 即不会每次进入中断函数后被初始化为 0。 假如去掉 static 关键字, 那么变量 i 就是一个局部变量, 每次进入中断函数后, 变量 i 初始值都是 0,也就是说它的值永远也不会递增到 1000, 从而实现不了 1s 定时。 可以这样理解,使用了 static 关键字就相当于将 i 变成了一个全局变量功能。
D1 指示灯间隔 1s闪烁。