51单片机入门——定时器与外部中断

1. 定时器

1.1. 定时器的初步认识

在认识定时器之前我们先了解两个基本概念。

时钟周期:时钟周期 T 是时序中最小的时间单位,具体计算方法就是 1 / 时钟源频率,一般情况下单片机的晶振都是 11.0592 MHz 的,对于这个单片机系统来说时钟周期就是 1 / 11059200 秒。

机器周期:我们的单片机完成一个操作的最短的时间。机器周期主要针对于汇编语言,在汇编语言下程序的每一条语句所使用的时间都是机器语言的整数倍,而且语句占用的时间是可以计算出来的,而 C 语言一条语句所占用的时间是不确定的,受诸多因数的影响。51 单片机系列,在其标准架构下一个机器语言是 12 个时钟周期,也就是 12 / 11059200 秒。现在有不少增强型的 51 单片机,其速度都比较快,有的 1 个 机器周期等于 4 个时钟周期,有的 1 个机器周期就等于 1 个时钟周期,也就是说大体上其速度可以达到标准 51 架构的 3 倍 或 12 倍。

上述概念了解后就可以开始我们的重头戏了,定时器和计数器。定时器计数器是单片机的同一个模块,通过配置 SFR(特殊功能寄存器)可以实现的两种不同的功能,我们大多数情况下都使用的是定时器,故本文主要讲述定时器功能。

顾名思义,定时器就是用来定时的。定时器内有一个寄存器,我们让它开始计数后,这个寄存器的值每经过过一个机器周期就会自动加 1,因此,我们可以把机器周期理解为定时器的计数周期。就像我们的钟表,每经过一秒,数字自动加 1,而这个定时器就是每过一个机器周期的时间,也就是 12/11059200 秒,数字自动加 1。还有一个特别注意的地方,就是钟表是加到 60 后,秒就自动变成 0 了,这种情况在单片机或计算机里我们称之为溢出。那定时器加到多少才会溢出呢?后面会讲到定时器有多种工作模式,分别使用不同的位宽(指使用多少个二进制位),假如是 16 位的定时器,也就是 2 个字节,最大值就是 65535,那么加到 65535 后,再加 1 就算溢出,如果有其他位数的话,道理是一样的,对于 51 单片机来说,溢出后,这个值会直接变成 0。从某一个初始值开始,经过确定的时间后溢出,这个过程就是定时的含义。

1.2. 定时器的寄存器

标准的 51 单片机内部有 T0 和 T1 这两个定时器,T 就是 Timer 的缩写,现在很多 51 系列单片机还会增加额外的定时器,在这里我们先讲定时器 0 和 1。前边提到过,对于单片机的每一个功能模块,都是由它的 SFR,也就是特殊功能寄存器来控制。与定时器有关的特殊功能寄存器,有以下几个,大家不需要去记忆这些寄存器的名字和作用,你只要大概知道就行,用的时候,随时可以查手册,找到每个寄存器的名字和每个寄存器所起到的作用。

下图的寄存器是存储定时器的计数值的。TH0/TL0 用于 T0,TH1/TL1 用于 T1。

在这里插入图片描述

下图是定时器控制寄存器 TCON 的位分配(地址 0x88、可位寻址)

在这里插入图片描述

下图为定时器控制寄存器的位描述

在这里插入图片描述
对于上图的描述中,只要写到硬件置 1 或者清 0 的,就是指一旦符合条件,单片机将自动完成的动作,只要写软件置 1 或者清 0 的,是指我们必须用程序去完成这个动作,后续遇到此类描述就不再另做说明了。

对于 TCON 这个 SFR,其中有 TF1、TR1、TF0、TR0 这 4 位需要我们理解清楚,它们分别对应于 T1 和 T0,我们以定时器 1 为例讲解,那么定时器 0 同理。先看 TR1,当我们程序中写 TR1 = 1 以后,定时器值就会每经过一个机器周期自动加 1,当我们程序中写 TR1 = 0以后,定时器就会停止加 1,其值会保持不变化。TF1,这个是一个标志位,他的作用是告诉我们定时器溢出了。比如我们的定时器设置成 16 位的模式,那么每经过一个机器周期,TL1加 1 一次,当 TL1 加到 255 后,再加 1,TL1 变成 0,TH1 会加 1 一次,如此一直加到 TH1和 TL1 都是 255(即 TH1 和 TL1 组成的 16 位整型数为 65535)以后,再加 1 一次,就会溢出了,TH1 和 TL1 同时都变为 0,只要一溢出,TF1 马上自动变成 1,告诉我们定时器溢出了,仅仅是提供给我们一个信号,让我们知道定时器溢出了,它不会对定时器是否继续运行产生任何影响。

定时器有多种工作模式,工作模式的选择就由 TMOD 来控制。

下图为定时器模式寄存器的位分配(地址 0x89、不可位寻址)

在这里插入图片描述

下图为定时器模式寄存器的位描述

在这里插入图片描述

下图为定时器模式寄存器 M1 / M0 工作模式

在这里插入图片描述
在定时器控制寄存器 TCON 的位分配最后标注了“可位寻址”,而定时器模式寄存器的位分配标注的是“不可位寻址”。意思就是说:比如 TCON 有一个位叫 TR1,我们可以在程序中直接进行 TR1 = 1 这样的操作。但对 TMOD 里的位比如 ( T1 ) M1 = 1 这样的操作就是错误的。我们要操作就必须一次操作这整个字节,也就是必须一次性对 TMOD 所有位操作,不能对其中某一位单独进行操作,那么我们能不能只修改其中的一位而不影响其它位的值呢?当然可以,在后续文章中你就会学到方法的,现在就先不关心它了。

上图列出的就是定时器的 4 种工作模式,其中模式 0 是为了兼容老的 8048 系列单片机而设计的,现在的 51 几乎不会用到这种模式,而模式 3 根据我的应用经验,它的功能用模式 2 完全可以取代,所以基本上也是不用的,那么我们就重点来学习模式 1 和模式 2。

模式 1,是 THn 和 TLn 组成了一个 16 位的定时器,计数范围是 0~65535,溢出后,只要不对 THn 和 TLn 重新赋值,则从 0 开始计数。模式 2,是 8 位自动重装载模式,只有 TLn做加 1 计数,计数范围 0~255,THn 的值并不发生变化,而是保持原值,TLn 溢出后,TFn就直接置 1 了,并且 THn 原先的值直接赋给 TLn,然后 TLn 从新赋值的这个数字开始计数。这个功能可以用来产生串口的通信波特率,我们讲串口的时候要用到,本篇上半部分我们重点学习模式 1。为了加深大家理解定时器的原理,我们来看一下他的模式 1 的电路示意图。
在这里插入图片描述
OSC 框表示时钟频率,因为 1 个机器周期等于 12 个时钟周期,所以那个 d 就等于 12。下边 GATE 右边的那个门是一个非门电路,再右侧是一个或门,再往右是一个与门电路。

图上可以看出来,下边部分电路是控制了上边部分,那我们先来看下边是如何控制的,我们以定时器 0 为例。

1、TR0 和下边或门电路的结果要进行与运算,TR0 如果是 0 的话,与运算完了肯定是 0,所以如果要让定时器工作,那么 TR0 就必须置 1。

2、这里的与门结果要想得到 1,那么前面的或门出来的结果必须也得是 1 才行。在 GATE位为 1 的情况下,经过一个非门变成 0,或门电路结果要想是 1 的话,那 INT0 即 P3.2 引脚必须是 1 的情况下,这个时候定时器才会工作,而 INT0 引脚是 0 的情况下,定时器不工作,这就是 GATE 位的作用。

3、当 GATE 位为 0 的时候,经过一个非门会变成 1,那么不管 INT0 引脚是什么电平,经过或门电路后都肯定是 1,定时器就会工作。

4、要想让定时器工作,就是自动加 1,从图上看有两种方式,第一种方式是那个开关打到上边的箭头,就是 C/T = 0 的时候,一个机器周期 TL 就会加 1 一次,当开关打到下边的箭头,即 C/T =1 的时候,T0 引脚即 P3.4 引脚来一个脉冲,TL 就加 1 一次,这也就是计数器功能。

1.3. 定时器的应用

了解了定时器相关的寄存器,那么我们下面就来做一个定时器的程序,巩固一下我们学到的内容。我们这节课的程序先使用定时器 0,在使用定时器的时候,需要以下几个步骤:

第一步:设置特殊功能寄存器 TMOD,配置好工作模式。

第二步:设置计数寄存器 TH0 和 TL0 的初值。

第三步:设置 TCON,通过 TR0 置 1 来让定时器开始计数。

第四步:判断 TCON 寄存器的 TF0 位,监测定时器溢出情况。

写程序之前,我们要先来学会计算如何用定时器定时时间。我们的晶振是 11.0592M,时钟周期就是 1/11059200,机器周期是 12/11059200,假如要定时 20ms,就是 0.02 秒,要经过x 个机器周期得到 0.02 秒,我们来算一下 x*12/11059200=0.02,得到 x= 18432。16 位定时器溢出值是 65536(因 65535 再加 1 才是溢出),于是我们就可以这样操作,先给 TH0 和 TL0一个初始值,让它们经过 18432 个机器周期后刚好达到 65536,也就是溢出,溢出后可以通过检测 TF0 的值得知,就刚好是 0.02 秒。那么初值 y = 65536 - 18432 = 47104,转成 16 进制就是 0xB800,也就是 TH0 = 0xB8,TL0 = 0x00。

#include <reg52.h>

sbit LED = P1^4;
		
void main()
{
 	unsigned char cnt = 0; //定义一个计数变量,记录 T0 溢出次数
 	LED = 0; 
 	TMOD = 0x01; //设置 T0 为模式 1
 	TH0 = 0xB8; //为 T0 赋初值 0xB800
 	TL0 = 0x00;
 	TR0 = 1; //启动 T0
 	
 	while (1)
 	{
 		if (TF0 == 1) //判断 T0 是否溢出
 		{
 			TF0 = 0; //T0 溢出后,清零中断标志
 			TH0 = 0xB8; //并重新赋初值
 			TL0 = 0x00;
 			cnt++; //计数值自加 1
 			if (cnt >= 50) //判断 T0 溢出是否达到 50 次
 			{
 				cnt = 0; //达到 50 次后计数值清零
 				LED = ~LED; //LED 取反:0-->1、1-->0
 			}
 		}
 	} 
 }

在这里插入图片描述

2. 外部中断

2.1. 中断的背景

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

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

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

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

2.2. 定时器中断的应用

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

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

下表为:IE——中断使能寄存器的位分配(地址 0xA8、可位寻址)

在这里插入图片描述

下表为:IE——中断使能寄存器的位描述

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

我们现在就把前面的数码管动态显示改用中断再实现出来。

下图为:数码管动态显示秒表程序流程图

在这里插入图片描述

//***************使用74HC138辅助控制数码管************//
#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 中断次数
unsigned char flag1s = 0; //1 秒定时标志

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 (flag1s == 1) //判断 1 秒定时标志
 		{
 			flag1s = 0; //1 秒定时标志清零
 			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
 	if (cnt >= 1000) //中断 1000 次即 1 秒
 	{
 		cnt = 0; //清零计数值以重新开始下 1 秒计时
 		flag1s = 1; //设置 1 秒定时标志为 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 怎么来的呢?我们看看下面的表格

下表为:中断查询序列

在这里插入图片描述
我们现在看第二行的 T0中断,要使能这个中断那么就要把它的中断使能位 ET0 置 1,当它的中断标志位 TF0 变为 1时,就会触发 T0 中断了,那么这时就应该来执行中断函数了,单片机又怎样找到这个中断函数呢?靠的就是中断向量地址,所以 interrupt 后面中断函数编号的数字 x 就是根据中断向量得出的,它的计算方法是 x*8+3=向量地址。当然表中都已经给算好放在第一栏了,我们可以直接查出来用就行了。到此为止,中断函数的命名规则我们就都搞清楚了。中断函数写好后,每当满足中断条件而触发中断后,系统就会自动来调用中断函数。比如我们上面这个程序,平时一直在主程序 while(1)的循环中执行,假如程序有 100 行,当执行到 50 行时,定时器溢出了,那么单片机就会立刻跑到中断函数中执行中断程序,中断程序执行完毕后再自动返回到刚才的第 50 行处继续执行下面的程序,这样就保证了动态显示间隔是固定的 1ms,不会因为程序执行时间不一致的原因导致数码管显示的抖动了。

2.3. 中断的优先级

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

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

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

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

中断优先级有两种,一种是抢占优先级,一种是固有优先级,先介绍抢占优先级。

下表为:IP——中断优先级寄存器的位分配(地址 0xB8、可位寻址)

在这里插入图片描述

下表为:IP——中断优先级寄存器的位描述

在这里插入图片描述
IP 这个寄存器的每一位,表示对应中断的抢占优先级,每一位的复位值都是 0,当我们把某一位设置为 1 的时候,这一位的优先级就比其它位的优先级高了。比如我们设置了 PT0位为 1 后,当单片机在主循环或者任何其它中断程序中执行时,一旦定时器 T0 发生中断,作为更高的优先级,程序马上就会跑到 T0 的中断程序中来执行。反过来,当单片机正在 T0中断程序中执行时,如果有其它中断发生了,还是会继续执行 T0 中断程序,直到把 T0 中的中断程序执行完毕以后,才会去执行其它中断程序。

当进入低优先级中断中执行时,如又发生了高优先级的中断,则立刻进入高优先级中断执行,处理完高优先级级中断后,再返回处理低优先级中断,这个过程就叫做中断嵌套,也称为抢占。所以抢占优先级的概念就是,优先级高的中断可以打断优先级低的中断的执行,从而形成嵌套。当然反过来,优先级低的中断是不能打断优先级高的中断的。那么既然有抢占优先级,自然就也有非抢占优先级了,也称为固有优先级。在表中断查询序列中的最后一列给出的就是固有优先级,请注意,在中断优先级的编号中,一般都是数字越小优先级越高。从表中可以看到一共有 1~6 共 6 级的优先级,这里的优先级与抢占优先级的一个不同点就是,它不具有抢占的特性,也就是说即使在低优先级中断执行过程中又发生了高优先级的中断,那么这个高优先级的中断也只能等到低优先级中断执行完后才能得到响应。既然不能抢占,那么这个优先级有什么用呢?

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

抢占优先级和非抢占优先级的协同,可以使单片机中断系统有条不紊的工作,既不会无休止的嵌套,又可以保证必要时紧急任务得到优先处理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:深蓝海洋 设计师:CSDN官方博客 返回首页
评论

打赏作者

倾晨灬雨曦

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值