任务五 定时器的初步认识
3.5.1 任务介绍
在动态数码管的学习中,我们已经用DelayMs()延时函数实现了数字钟的走时,但是这种方式下的数字钟走时不够准确。生活中,我们需要精确计量时间时,通常会借助于走时准确的秒表,51单片机内部也有一个类似于秒表的装置,我们称之为定时器,借助于定时器,我们可以实现走时准确的数字钟。
本节的任务是:利用单片机定时器完成走时准确的数字钟,另外在程序中,数码管扫描也由定时器来驱动。
3.5.2知识准备
1、定时器的引入
在讲述定时器的原理之前,我们先看一下图3.5.1中的水龙头向水盆滴水的画面。在画面中,水龙头由于没有关紧,水 一滴一滴地滴向脸盆,盆的容量是有限的,水会在某一个时刻
从水盆中溢出。假设一开始水盆没有水,65536个水滴恰好可
以把水盆装满,恰好是计数了65536次。如果我们计数1000 次怎么办呢?向水盆中预先装下了64536滴水,然后打开水龙 头,开始滴水,等到水盆中的水溢出了,自然是计数了100次。如果水滴的速度是恒定的,1滴/1秒,计数就变成了计时了。
图3.5.1 水盆滴水 实际古人计时装置“刻漏”的原理和水盆滴水的原理相似。
51单片机的定时器/计数器的原理与上面讲述的水盆滴水的例子类似,如表3.5.1所示。
3.5.1 定时/计数器和水盆滴水的类比
2、定时器的内部结构及工作原理
- 1 -
在51单片机内部有2个定时器,分别称为定时/计数器0、定时/计数器1,每个定时/计数器具有计数和定时两大功能,有4种工作方式。定时/计数器0和定时/计数器1配置上完全相同,现用定时/计数器1的工作方式1来说明定时器内部结构与工作原理。图3.5.2为定时/计数器1在工作方式1下的内部结构图。
(1)定时/计数器1的脉冲源有两种:当定时/计数器1工作于定时方式时,加1脉冲由振荡器的12分频提供(12M 晶振的12分频为1MHz )。当定时/计数器1工作于计数方式时,加1脉冲由外部脉冲源提供,P3.4是定时/计数器0的外部脉冲源输入端,P3.5是定时/计数器1的外接脉冲输入端。定时/计数器工作于定时还是计数方式,取决于选择开关C/T ,当C/T =0时工作于定时方式,C/T =1时工作于计数方式。
(2)脉冲要经过启动开关才能到达RAM 。启动开关=TR1 & (GATE | INT1) ,GATE 为门控位,INT1为外部中断1。当GATE=0时,GATE | INT1的结果为1,则启动开关仅有TR1决定。当GATA=1,则启动开关的不仅由TR1决定,还要由来自于外部中断1的信号决定是否开启中断。
(3)两个8位的RAM ,高八位的RAM 称为TH1,低八位的RAM 称为TL1,组合在一起共16位。每来一个脉冲,RAM 的值加1,当RAM 的数值超过65535时,计数器会溢出,溢出标志位TF1会由0变成1,同时TF1的变化会引发中断事件的发生。要设定不同的定时时间,在定时/计时器启动之前,向RAM 预先装入初值。
图3.5.2 定时/计数器1的内部结构(方式1)
3、定时器的方式控制字
51单片机的特殊功能寄存器(SFR)中有两个属于定时器的寄存器,分别为TMOD 和TCON 。 (1)TMOD 寄存器
表3.5.1 TMOD寄存器
————----------------————----------------————---——
-——
-——
- 2 -
从表3.5.1中我们可以看到,TMOD 被分成两份,每份四位,高四位用于定时/计数器1的控制,低四位用于定时/计数器0的控制。结合图3.5.2 定时/计数1的内部结构,GATE 参与了启动开关的选择,称为门控位。C/T是用来选择定时还是计数,M1M0是下面要介绍的4种工作方式的选择位。
(2)TCON 寄存器
表3.5.2 TCON寄存器
TCON 寄存器也被分成了两份,前四位用于定时/计数器,后四位用于外部中断,如表3.5.2所示。前四位中又被分成了两份,分别属于定时/计数器1和定时/计数器0。其中TF 是定时器溢出标志,没有溢出时为0,溢出后为1。TR 参与了定时器的启动。
需要说明的是,TMOD 寄存器中的每一个位不可以单独寻址,举例:定时/计数器0,定时模式,门控位为0,方式0(M1M0=01),可以写成TMOD=0x05。TCON 寄存器中的每一个位可单独寻址,举例:启动定时器0,TR0=1就可以了。
4、定时/计数器的四种工作方式
51单片机的定时器有四种工作方式,TMOD 寄存器中M1和M0位决定了使用哪种工作方式。
M1M0为的配置和定时器的工作方式的对应关系如表3.5.3所示。
表3.5.3 定时器工作方式
工作方式0和工作方式3很少使用,在这里不做专门的讲解,下面介绍工作在方式1和(1)工作方式1:
当定时/计数器处于工作方式1时,两个8位的RAM 都参与计数,即计数的范围是:0~
- 3 -
工作方式2的特点。
65535。需要注意,工作于方式1时,定时/计数器溢出后,需要手动装入初值。
(2)工作方式2
定时/计数器处于工作方式2时,相比于方式1,其计数范围缩小了,高8位的RAM 不参与计数,低8位的RAM 参与计数。其计数范围是0~255。高8位的RAM 闲置不用吗?实际上不是这样的,首先来分析一下定时/计数器在工作方式1下存在的问题。
开发板的晶振频率为12MHz ,12分频后为1MHz ,脉冲周期是1us ,选择定时模式时,每隔1us ,RAM 的值加1,16位的定时/计数模式,最大定时范围为65.536ms 。我们要实现数字钟的1秒定时,可以先定时50ms (RAM 初值=65536-50000=15536), 然后连续定时20次,就可以实现1秒的定时。在介绍工作方式1时,提到溢出后需要手动装入初值(15536),如果不手动装入初值,则溢出后,RAM 从0开始计数。
RAM 的初值重装是在中断服务函数中完成的。定时/计数器溢出后产生中断,到进入中断服务函数后装入初值,是需要耗费时间的。实际是,定时/计数器溢出后,从0开始计数,运行一段时间初值重装了,又从初值运行。尽管这段时间很小(几十个us ),但连续定时,日积月累,误差就大了,所以工作方式1下,定时的精度不高。
工作方式2下初值的重装不需要手动装入,一旦溢出,硬件自动完成初值的重装,中间没有时间耽搁,方式2比方式1定时要准确。定时/计数器初始化时,高8位RAM 也装入初值,当定时/计数器溢出后,硬件自动把高8位RAM 存放的初值导入到低8位RAM ,完成初值重装。
5、定时器/计数器的初始化和溢出标志位处理 定时器/计数器初始化设置如下:
(1)TMOD 寄存器的配置:定时/计数的选择,GATE 门的设置,工作方式的选择。 (2)装入初值,根据任务的要求,给THn 、TLn(n为0或1) 装入初值。 (3)开总中断:EA=1,开定时器中断:ETn(n为0或1)=1。
(4)如果还有别的中断,根据任务的重要性,需要确定中断优先级,配置IP 寄存器。 (5)启动定时器/计数器,TRn(n为0或1)=1。
定时器/计数器溢出可以采用查询和中断两种方式进行处理。查询方式效率低(主程序中不停的查询溢出标志位TFn(n为0或1) 是否置位),本书中采用中断方式。
6、定时/计数应用举例 例程1:计数的应用
自动化生产线上,12瓶易拉罐饮料为一箱,光电传感器检测到一瓶易拉罐就给送出一个脉冲,控制器连续接收到12个脉冲,则作为一个包装。本例用按键来模拟光电传感器发出的脉冲,按键接P3.4(定时/计数器0的外接脉冲输入端),用发光二极管的状态取反来模拟一个包装的动作,发光二极管接单片机的P0.0。连续按下12次按键,发光二极管状态取反一次。
- 4 -
根据任务要求,定时/计数器的配置如下:使用定时/计数器0,计数、工作方式1。程序如下:
#include
#define uchar unsigned char #define uint unsigned int
sbit LED=P0^0; //LED接口定义 //计数器0初始化 void Timer0Init() {
TMOD=0x05; //GATE=0,C/T=1,M1M0=01; TH0=(65536-12)/256; //RAM高8位赋值 TL0=(65536-12)%12; //RAM低8位赋值 ET0=EA=1;
//开定时器中断和总中断
TR0=1; //启动定时器
}
//主函数
void main(void) {
Timer0Init(); //定时/计数器初始化
while(1) { }
//计数器0中断服务函数
void Timer0Intr(void) interrupt 1 {
TH0=(65536-12)/256;
TL0=(65536-12)%12; //溢出后重新赋初值
LED=!LED; //中断每产生一次,发光二极管的状态取反一次 }
程序解释:
- 5 -
//执行别的任务 }
(1)Timer0Init()函数完成计数器的初始化。外部中断不参与计数器的开启,门控位:GATE=1;计数模式:C/T=1;工作方式1,M1M0=01。每12个脉冲到来,计数器溢出一次,初值为(65536-12),取出高8位和低8位,赋给TH0和TL0。最后开中断,启动计数器。
(2)计数器溢出后,CPU 响应中断服务函数,在中断服务函数中,重新赋予初值,并完成LED 的状态取反。
例程2:定时的应用
用定时的方式实现LED 闪烁,LED 闪烁的频率为1Hz 。LED 接单片机P0.0引脚,0.5秒亮,
0.5秒灭。根据任务要求,定时/计数器的配置如下:使用定时/计数器1,计时、工作方式2。 #include
#define uchar unsigned char #define uint unsigned int
sbit LED=P1^0; //LED接口定义 bit Flag500Ms=0; //500ms标志位 //定时器1的初始化 void Timer1Init() {
TMOD=0x02; //GATE=0,C/T=0,M1M0=10; TH1=256-200;
ET1=EA=1; } //主函数 void main(void) {
Timer1Init(); //定时器的初始化 while(1) {
if(Flag500Ms==1) //500ms标志位置1 {
Flag500Ms=0; //清空500ms 标志位 LED=!LED; //LED状态取反 }
- 6 -
//RAM高八位赋值
//开定时器中断和总中断 //启动定时器
TL1=256-200; //RAM低八位赋值,200us 的定时
TR1=1;
}
//定时器1的中断服务函数
void timer0_intr(void) interrupt 1 {
static uint Cnt200us=0; //200us计数变量
if(++Cnt200us>=2500) //200us*2500=500ms {
Cnt200us=0; //清空计数变量 Flag500Ms=1; //500ms标志位置1
} }
程序解释:
(1)Timer0Init()函数完成定时器1的初始化。门控位不参与定时:GATE=0;定时:C/T=0;工作方式2,自动重装模式,M1M0=10。所以TMOD=0x20。200us 定时器溢出一次,8位自动重装模式,所以TH1=TL1=(256-200)=56。
(2)由于采用了自动重装模式,在中断服务函数中,不需要重新赋初值。本任务的定时目标是500ms ,程序中定时器的定时长度是200us ,所以需要连续定时。每进入一次中断服务函数,200us 计数变量(Cnt200us )加1,Cnt200us 计数到2500,整个计时的时间为200us ×2500次=500ms。
(3)500ms 到来后,程序中并没有像上一个计数例子一样,在中断服务函数中完成LED 的闪烁,而是把500ms 标志位(Flag500Ms)置1,在主程序中,检测到该标志位置1,则完成LED 的闪烁。
(4)计数变量Cnt200us 在定义的时候,uint 前面多了个关键字static ,其作用是声明变量Cnt200us 为局部静态变量。什么是局部静态变量呢?C 语言变量根据定义的位置不同分为全局变量、局部变量,
全局变量是在函数外部定义的变量,又称外部变量。全局变量一般定义在程序的开始处,可以为多个函数共同使用,其有效的作用范围是从它定义的位置开始整个程序文件结束为止。
局部变量是在一个函数内部内部定义的变量,该变量只能在定义它的那个函数范围内有效,不同的函数可以使用相同的局部变量名。由于它们的作用范围不同,不会互相干扰,函数的形式参数也属于局部变量。
局部变量根据存储空间分配的不同又分为自动变量和静态变量。在函数内部定义的变量,如果不做专门说明,则为自动变量。自动变量的特点是只在定义它们的时候才分配存储空间,
- 7 -
}
在定义他们的函数返回时系统回收变量所占用的存储空间,对这些变量存储空间的分配和回收是系统自动完成的,
静态变量在定义是前面多了个关键字static ,局部静态变量仍然保留局部变量的特性,只能在定义的函数内部使用,但与自动变量不同的是,再次调用定义它的函数时,它又可继续使用,而且保存了前次被调用后留下的值。 因此,当多次调用一个函数且要求在调用之间保留某些变量的值时,可考虑采用静态局部变量。
弄明白了什么是局部静态变量,再来看中断函数中计数变量Cnt200us 的使用就简单多了。首次调用中断函数时,计数变量Cnt200us 定义且赋初值0,紧跟着在函数中Cnt200us 的值加1,退出出中断服务函数后,Cnt200us 的值不释放,仍然为1。
3.5.3任务实施
本节的任务程序实现如下:
#incldue
#define uchar unsigned char #define uint unsigned int
//段码 uchar code Seg7Code[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90}; uchar code Seg7Posit[]={0xfe,0xfd,0xfb,0xf7,0xef,0xdf,0xbf,0x7f}; //位码 uchar DispBuffer[6]; //缓冲区 bit SystemFlag1Ms=1; //1ms标志位
//6位数码管显示子函数,在第二位和第四位上显示小数点 void Seg7Display() {
static uchar i=0;
P1=0xff; //消隐 P0=Seg7Code[disp_buff[i]]; //送段码
if((i==1)||(i==3)) P0&=~0x80; //在第二位和第四位数码管上加小数点 P1=Posit[i]; //送位选 if(++i>=6) i=0; }
//把时间送入缓冲区
void TimerToBuffer(uchar nhour,nminute,nsecond) {
DispBuffer[0]=nhour/10;
DispBuffer[1]=nhour%10; //小时的个位和十位的分解
- 8 -
DispBuffer[1]=nhour%10; //小时的个位和十位的分解 DispBuffer[2]=nsecond/10;
DispBuffer[3]=nsecond%10; //分的个位和十位的分解 DispBuffer[4]=nminute/10;
DispBuffer[5]=nminute%10; //秒的个位和十位的分解 }
//定时器0初始化 void Timer0Init() {
TMOD=0x02; //GATE=0,C/T=0,M1M0=02; TH0=56; //高8位RAM 赋值
TL0=56; //低8位RAM 赋值,200us 定时 ET0=EA=1; //开定时器中断和总中断
TR0=1; //开启定时器 }
//主函数
void main(void) {
uchar Hour=9; //小时
uchar Minute=23; //分
uchar Second=37; //秒
uint Cnt1Ms=0; //1ms计数器
Timer0Init(); //定时器0初始化 while(1) {
if(FlagSystem1Ms==1) //定时器产生的1ms 的时标信号到 {
SystemFlag1Ms=0; //时标信号清零
TimerToBuffer(); //显示数据送往缓冲区
Seg7Display(); //6位数码管动态显示
if(++Cnt1Ms>=1000) //1秒到,1ms*1000=1000ms {
Cnt2Ms=0; //2ms计数变量清零
if(++Second>=60) //秒加1,到60秒,则分加1 {
Second=0; //秒清零
if(++Minute>=60) //分家1,到60分,则小时加1
{ - 9 -
Second=0; //秒清零 {
Minute=0; //分清零
if(++Hour>=24) //小时加1,到24小时 Hour=0; //小时清零
}
} } } } }
//定时器0中断服务函数,提供1ms 的时标信号
void timer0_intr(void) interrupt 1 {
static uchar Cnt200us=0; //200us计数变量 if(++Cnt200us>=5) //0.2ms*5=1ms {
Cnt200us=0; //清空计数变量 SystemFlag1Ms=1; //1ms标志位置1 } }
程序解释:
(1)程序中第一次提出了时标信号这个概念,主程序中的任务是在时标信号的驱动下完成的。本程序的“时标信号”是标志位(Flag1ms ), 每隔1ms ,时标信号被置1,主程序中检测到时标信号置1后,再清零。1ms 的是通过定时器定时来完成,定时器200us 溢出一次,连续5次,置位1次时标信号。
(2)主程序中的任务有3个:缓冲区数据刷新(TimerToBuffer())、6位数码管的扫描(Seg7Display())和数字钟计时。缓冲区数据刷新和之前的函数一样,不需解释。数码管扫描的程序中和之前的程序相比,做了一些改动。在之前的Seg7Display()函数中,数码管的逐位扫描是通过延时1ms 来实现的,6位数码管延时需要6ms 的延时,在扫描数码管这段时间内,别的任务只能等待。扫描完数码管后,再执行别的任务,数码管的最后一位点亮的时间并非是1ms ,而是1ms+别的任务的执行时间,这将导致数码管最后一位和其余5位分配时间不一致,最后1位的亮度高于其它5位。
本程序中,利用定时器产生的1ms 时标信号,每间隔1ms 执行一次Seg7Display()函数, 执行一次Seg7Dislplay()只完成一位数码管的扫描,根据静态变量i 的值送入段码和位码,
- 10 -
变量i 加1,下一个1ms 的时标信号到来后,送入下一位数码管的段码和位码,依次循环。
(3)时间的计时也是通过1ms 的时标信号来完成的。1ms 时标信号每到来一次,计数变量Cnt2Ms 加1,计数500次,则为1秒,其余的和以前的程序一样。
总结:在本节之前,程序中任务的时间分配是通过延时函数来实现的,单个任务中有延时函数看不出问题,但当程序中的任务多了,任务之间会相互堵塞,运行效率较低。本节的例程给出了LED 闪烁、数码管扫描等任务采用定时器时标信号轮询的方法,没有延时语句,任务之间不会堵塞,提高了运行效率。
- 11 -
任务五 定时器的初步认识
3.5.1 任务介绍
在动态数码管的学习中,我们已经用DelayMs()延时函数实现了数字钟的走时,但是这种方式下的数字钟走时不够准确。生活中,我们需要精确计量时间时,通常会借助于走时准确的秒表,51单片机内部也有一个类似于秒表的装置,我们称之为定时器,借助于定时器,我们可以实现走时准确的数字钟。
本节的任务是:利用单片机定时器完成走时准确的数字钟,另外在程序中,数码管扫描也由定时器来驱动。
3.5.2知识准备
1、定时器的引入
在讲述定时器的原理之前,我们先看一下图3.5.1中的水龙头向水盆滴水的画面。在画面中,水龙头由于没有关紧,水 一滴一滴地滴向脸盆,盆的容量是有限的,水会在某一个时刻
从水盆中溢出。假设一开始水盆没有水,65536个水滴恰好可
以把水盆装满,恰好是计数了65536次。如果我们计数1000 次怎么办呢?向水盆中预先装下了64536滴水,然后打开水龙 头,开始滴水,等到水盆中的水溢出了,自然是计数了100次。如果水滴的速度是恒定的,1滴/1秒,计数就变成了计时了。
图3.5.1 水盆滴水 实际古人计时装置“刻漏”的原理和水盆滴水的原理相似。
51单片机的定时器/计数器的原理与上面讲述的水盆滴水的例子类似,如表3.5.1所示。
3.5.1 定时/计数器和水盆滴水的类比
2、定时器的内部结构及工作原理
- 1 -
在51单片机内部有2个定时器,分别称为定时/计数器0、定时/计数器1,每个定时/计数器具有计数和定时两大功能,有4种工作方式。定时/计数器0和定时/计数器1配置上完全相同,现用定时/计数器1的工作方式1来说明定时器内部结构与工作原理。图3.5.2为定时/计数器1在工作方式1下的内部结构图。
(1)定时/计数器1的脉冲源有两种:当定时/计数器1工作于定时方式时,加1脉冲由振荡器的12分频提供(12M 晶振的12分频为1MHz )。当定时/计数器1工作于计数方式时,加1脉冲由外部脉冲源提供,P3.4是定时/计数器0的外部脉冲源输入端,P3.5是定时/计数器1的外接脉冲输入端。定时/计数器工作于定时还是计数方式,取决于选择开关C/T ,当C/T =0时工作于定时方式,C/T =1时工作于计数方式。
(2)脉冲要经过启动开关才能到达RAM 。启动开关=TR1 & (GATE | INT1) ,GATE 为门控位,INT1为外部中断1。当GATE=0时,GATE | INT1的结果为1,则启动开关仅有TR1决定。当GATA=1,则启动开关的不仅由TR1决定,还要由来自于外部中断1的信号决定是否开启中断。
(3)两个8位的RAM ,高八位的RAM 称为TH1,低八位的RAM 称为TL1,组合在一起共16位。每来一个脉冲,RAM 的值加1,当RAM 的数值超过65535时,计数器会溢出,溢出标志位TF1会由0变成1,同时TF1的变化会引发中断事件的发生。要设定不同的定时时间,在定时/计时器启动之前,向RAM 预先装入初值。
图3.5.2 定时/计数器1的内部结构(方式1)
3、定时器的方式控制字
51单片机的特殊功能寄存器(SFR)中有两个属于定时器的寄存器,分别为TMOD 和TCON 。 (1)TMOD 寄存器
表3.5.1 TMOD寄存器
————----------------————----------------————---——
-——
-——
- 2 -
从表3.5.1中我们可以看到,TMOD 被分成两份,每份四位,高四位用于定时/计数器1的控制,低四位用于定时/计数器0的控制。结合图3.5.2 定时/计数1的内部结构,GATE 参与了启动开关的选择,称为门控位。C/T是用来选择定时还是计数,M1M0是下面要介绍的4种工作方式的选择位。
(2)TCON 寄存器
表3.5.2 TCON寄存器
TCON 寄存器也被分成了两份,前四位用于定时/计数器,后四位用于外部中断,如表3.5.2所示。前四位中又被分成了两份,分别属于定时/计数器1和定时/计数器0。其中TF 是定时器溢出标志,没有溢出时为0,溢出后为1。TR 参与了定时器的启动。
需要说明的是,TMOD 寄存器中的每一个位不可以单独寻址,举例:定时/计数器0,定时模式,门控位为0,方式0(M1M0=01),可以写成TMOD=0x05。TCON 寄存器中的每一个位可单独寻址,举例:启动定时器0,TR0=1就可以了。
4、定时/计数器的四种工作方式
51单片机的定时器有四种工作方式,TMOD 寄存器中M1和M0位决定了使用哪种工作方式。
M1M0为的配置和定时器的工作方式的对应关系如表3.5.3所示。
表3.5.3 定时器工作方式
工作方式0和工作方式3很少使用,在这里不做专门的讲解,下面介绍工作在方式1和(1)工作方式1:
当定时/计数器处于工作方式1时,两个8位的RAM 都参与计数,即计数的范围是:0~
- 3 -
工作方式2的特点。
65535。需要注意,工作于方式1时,定时/计数器溢出后,需要手动装入初值。
(2)工作方式2
定时/计数器处于工作方式2时,相比于方式1,其计数范围缩小了,高8位的RAM 不参与计数,低8位的RAM 参与计数。其计数范围是0~255。高8位的RAM 闲置不用吗?实际上不是这样的,首先来分析一下定时/计数器在工作方式1下存在的问题。
开发板的晶振频率为12MHz ,12分频后为1MHz ,脉冲周期是1us ,选择定时模式时,每隔1us ,RAM 的值加1,16位的定时/计数模式,最大定时范围为65.536ms 。我们要实现数字钟的1秒定时,可以先定时50ms (RAM 初值=65536-50000=15536), 然后连续定时20次,就可以实现1秒的定时。在介绍工作方式1时,提到溢出后需要手动装入初值(15536),如果不手动装入初值,则溢出后,RAM 从0开始计数。
RAM 的初值重装是在中断服务函数中完成的。定时/计数器溢出后产生中断,到进入中断服务函数后装入初值,是需要耗费时间的。实际是,定时/计数器溢出后,从0开始计数,运行一段时间初值重装了,又从初值运行。尽管这段时间很小(几十个us ),但连续定时,日积月累,误差就大了,所以工作方式1下,定时的精度不高。
工作方式2下初值的重装不需要手动装入,一旦溢出,硬件自动完成初值的重装,中间没有时间耽搁,方式2比方式1定时要准确。定时/计数器初始化时,高8位RAM 也装入初值,当定时/计数器溢出后,硬件自动把高8位RAM 存放的初值导入到低8位RAM ,完成初值重装。
5、定时器/计数器的初始化和溢出标志位处理 定时器/计数器初始化设置如下:
(1)TMOD 寄存器的配置:定时/计数的选择,GATE 门的设置,工作方式的选择。 (2)装入初值,根据任务的要求,给THn 、TLn(n为0或1) 装入初值。 (3)开总中断:EA=1,开定时器中断:ETn(n为0或1)=1。
(4)如果还有别的中断,根据任务的重要性,需要确定中断优先级,配置IP 寄存器。 (5)启动定时器/计数器,TRn(n为0或1)=1。
定时器/计数器溢出可以采用查询和中断两种方式进行处理。查询方式效率低(主程序中不停的查询溢出标志位TFn(n为0或1) 是否置位),本书中采用中断方式。
6、定时/计数应用举例 例程1:计数的应用
自动化生产线上,12瓶易拉罐饮料为一箱,光电传感器检测到一瓶易拉罐就给送出一个脉冲,控制器连续接收到12个脉冲,则作为一个包装。本例用按键来模拟光电传感器发出的脉冲,按键接P3.4(定时/计数器0的外接脉冲输入端),用发光二极管的状态取反来模拟一个包装的动作,发光二极管接单片机的P0.0。连续按下12次按键,发光二极管状态取反一次。
- 4 -
根据任务要求,定时/计数器的配置如下:使用定时/计数器0,计数、工作方式1。程序如下:
#include
#define uchar unsigned char #define uint unsigned int
sbit LED=P0^0; //LED接口定义 //计数器0初始化 void Timer0Init() {
TMOD=0x05; //GATE=0,C/T=1,M1M0=01; TH0=(65536-12)/256; //RAM高8位赋值 TL0=(65536-12)%12; //RAM低8位赋值 ET0=EA=1;
//开定时器中断和总中断
TR0=1; //启动定时器
}
//主函数
void main(void) {
Timer0Init(); //定时/计数器初始化
while(1) { }
//计数器0中断服务函数
void Timer0Intr(void) interrupt 1 {
TH0=(65536-12)/256;
TL0=(65536-12)%12; //溢出后重新赋初值
LED=!LED; //中断每产生一次,发光二极管的状态取反一次 }
程序解释:
- 5 -
//执行别的任务 }
(1)Timer0Init()函数完成计数器的初始化。外部中断不参与计数器的开启,门控位:GATE=1;计数模式:C/T=1;工作方式1,M1M0=01。每12个脉冲到来,计数器溢出一次,初值为(65536-12),取出高8位和低8位,赋给TH0和TL0。最后开中断,启动计数器。
(2)计数器溢出后,CPU 响应中断服务函数,在中断服务函数中,重新赋予初值,并完成LED 的状态取反。
例程2:定时的应用
用定时的方式实现LED 闪烁,LED 闪烁的频率为1Hz 。LED 接单片机P0.0引脚,0.5秒亮,
0.5秒灭。根据任务要求,定时/计数器的配置如下:使用定时/计数器1,计时、工作方式2。 #include
#define uchar unsigned char #define uint unsigned int
sbit LED=P1^0; //LED接口定义 bit Flag500Ms=0; //500ms标志位 //定时器1的初始化 void Timer1Init() {
TMOD=0x02; //GATE=0,C/T=0,M1M0=10; TH1=256-200;
ET1=EA=1; } //主函数 void main(void) {
Timer1Init(); //定时器的初始化 while(1) {
if(Flag500Ms==1) //500ms标志位置1 {
Flag500Ms=0; //清空500ms 标志位 LED=!LED; //LED状态取反 }
- 6 -
//RAM高八位赋值
//开定时器中断和总中断 //启动定时器
TL1=256-200; //RAM低八位赋值,200us 的定时
TR1=1;
}
//定时器1的中断服务函数
void timer0_intr(void) interrupt 1 {
static uint Cnt200us=0; //200us计数变量
if(++Cnt200us>=2500) //200us*2500=500ms {
Cnt200us=0; //清空计数变量 Flag500Ms=1; //500ms标志位置1
} }
程序解释:
(1)Timer0Init()函数完成定时器1的初始化。门控位不参与定时:GATE=0;定时:C/T=0;工作方式2,自动重装模式,M1M0=10。所以TMOD=0x20。200us 定时器溢出一次,8位自动重装模式,所以TH1=TL1=(256-200)=56。
(2)由于采用了自动重装模式,在中断服务函数中,不需要重新赋初值。本任务的定时目标是500ms ,程序中定时器的定时长度是200us ,所以需要连续定时。每进入一次中断服务函数,200us 计数变量(Cnt200us )加1,Cnt200us 计数到2500,整个计时的时间为200us ×2500次=500ms。
(3)500ms 到来后,程序中并没有像上一个计数例子一样,在中断服务函数中完成LED 的闪烁,而是把500ms 标志位(Flag500Ms)置1,在主程序中,检测到该标志位置1,则完成LED 的闪烁。
(4)计数变量Cnt200us 在定义的时候,uint 前面多了个关键字static ,其作用是声明变量Cnt200us 为局部静态变量。什么是局部静态变量呢?C 语言变量根据定义的位置不同分为全局变量、局部变量,
全局变量是在函数外部定义的变量,又称外部变量。全局变量一般定义在程序的开始处,可以为多个函数共同使用,其有效的作用范围是从它定义的位置开始整个程序文件结束为止。
局部变量是在一个函数内部内部定义的变量,该变量只能在定义它的那个函数范围内有效,不同的函数可以使用相同的局部变量名。由于它们的作用范围不同,不会互相干扰,函数的形式参数也属于局部变量。
局部变量根据存储空间分配的不同又分为自动变量和静态变量。在函数内部定义的变量,如果不做专门说明,则为自动变量。自动变量的特点是只在定义它们的时候才分配存储空间,
- 7 -
}
在定义他们的函数返回时系统回收变量所占用的存储空间,对这些变量存储空间的分配和回收是系统自动完成的,
静态变量在定义是前面多了个关键字static ,局部静态变量仍然保留局部变量的特性,只能在定义的函数内部使用,但与自动变量不同的是,再次调用定义它的函数时,它又可继续使用,而且保存了前次被调用后留下的值。 因此,当多次调用一个函数且要求在调用之间保留某些变量的值时,可考虑采用静态局部变量。
弄明白了什么是局部静态变量,再来看中断函数中计数变量Cnt200us 的使用就简单多了。首次调用中断函数时,计数变量Cnt200us 定义且赋初值0,紧跟着在函数中Cnt200us 的值加1,退出出中断服务函数后,Cnt200us 的值不释放,仍然为1。
3.5.3任务实施
本节的任务程序实现如下:
#incldue
#define uchar unsigned char #define uint unsigned int
//段码 uchar code Seg7Code[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90}; uchar code Seg7Posit[]={0xfe,0xfd,0xfb,0xf7,0xef,0xdf,0xbf,0x7f}; //位码 uchar DispBuffer[6]; //缓冲区 bit SystemFlag1Ms=1; //1ms标志位
//6位数码管显示子函数,在第二位和第四位上显示小数点 void Seg7Display() {
static uchar i=0;
P1=0xff; //消隐 P0=Seg7Code[disp_buff[i]]; //送段码
if((i==1)||(i==3)) P0&=~0x80; //在第二位和第四位数码管上加小数点 P1=Posit[i]; //送位选 if(++i>=6) i=0; }
//把时间送入缓冲区
void TimerToBuffer(uchar nhour,nminute,nsecond) {
DispBuffer[0]=nhour/10;
DispBuffer[1]=nhour%10; //小时的个位和十位的分解
- 8 -
DispBuffer[1]=nhour%10; //小时的个位和十位的分解 DispBuffer[2]=nsecond/10;
DispBuffer[3]=nsecond%10; //分的个位和十位的分解 DispBuffer[4]=nminute/10;
DispBuffer[5]=nminute%10; //秒的个位和十位的分解 }
//定时器0初始化 void Timer0Init() {
TMOD=0x02; //GATE=0,C/T=0,M1M0=02; TH0=56; //高8位RAM 赋值
TL0=56; //低8位RAM 赋值,200us 定时 ET0=EA=1; //开定时器中断和总中断
TR0=1; //开启定时器 }
//主函数
void main(void) {
uchar Hour=9; //小时
uchar Minute=23; //分
uchar Second=37; //秒
uint Cnt1Ms=0; //1ms计数器
Timer0Init(); //定时器0初始化 while(1) {
if(FlagSystem1Ms==1) //定时器产生的1ms 的时标信号到 {
SystemFlag1Ms=0; //时标信号清零
TimerToBuffer(); //显示数据送往缓冲区
Seg7Display(); //6位数码管动态显示
if(++Cnt1Ms>=1000) //1秒到,1ms*1000=1000ms {
Cnt2Ms=0; //2ms计数变量清零
if(++Second>=60) //秒加1,到60秒,则分加1 {
Second=0; //秒清零
if(++Minute>=60) //分家1,到60分,则小时加1
{ - 9 -
Second=0; //秒清零 {
Minute=0; //分清零
if(++Hour>=24) //小时加1,到24小时 Hour=0; //小时清零
}
} } } } }
//定时器0中断服务函数,提供1ms 的时标信号
void timer0_intr(void) interrupt 1 {
static uchar Cnt200us=0; //200us计数变量 if(++Cnt200us>=5) //0.2ms*5=1ms {
Cnt200us=0; //清空计数变量 SystemFlag1Ms=1; //1ms标志位置1 } }
程序解释:
(1)程序中第一次提出了时标信号这个概念,主程序中的任务是在时标信号的驱动下完成的。本程序的“时标信号”是标志位(Flag1ms ), 每隔1ms ,时标信号被置1,主程序中检测到时标信号置1后,再清零。1ms 的是通过定时器定时来完成,定时器200us 溢出一次,连续5次,置位1次时标信号。
(2)主程序中的任务有3个:缓冲区数据刷新(TimerToBuffer())、6位数码管的扫描(Seg7Display())和数字钟计时。缓冲区数据刷新和之前的函数一样,不需解释。数码管扫描的程序中和之前的程序相比,做了一些改动。在之前的Seg7Display()函数中,数码管的逐位扫描是通过延时1ms 来实现的,6位数码管延时需要6ms 的延时,在扫描数码管这段时间内,别的任务只能等待。扫描完数码管后,再执行别的任务,数码管的最后一位点亮的时间并非是1ms ,而是1ms+别的任务的执行时间,这将导致数码管最后一位和其余5位分配时间不一致,最后1位的亮度高于其它5位。
本程序中,利用定时器产生的1ms 时标信号,每间隔1ms 执行一次Seg7Display()函数, 执行一次Seg7Dislplay()只完成一位数码管的扫描,根据静态变量i 的值送入段码和位码,
- 10 -
变量i 加1,下一个1ms 的时标信号到来后,送入下一位数码管的段码和位码,依次循环。
(3)时间的计时也是通过1ms 的时标信号来完成的。1ms 时标信号每到来一次,计数变量Cnt2Ms 加1,计数500次,则为1秒,其余的和以前的程序一样。
总结:在本节之前,程序中任务的时间分配是通过延时函数来实现的,单个任务中有延时函数看不出问题,但当程序中的任务多了,任务之间会相互堵塞,运行效率较低。本节的例程给出了LED 闪烁、数码管扫描等任务采用定时器时标信号轮询的方法,没有延时语句,任务之间不会堵塞,提高了运行效率。
- 11 -