2.1 51单片机内部定时/计数器应用
51单片机内部集成了两个16位定时/计数器T0和T1,可以用于定时或者计数操作,某些型号的51单片机还有第三个和这两个定时/计数器略有区别的定时/计数器T2。
2.1.1 内部定时/计数器T0/T1基础
1.相关寄存器
51单片机通过对相关寄存器的操作来实现对定时/计数器T0和T1的控制,这些寄存器包括工作方式寄存器TMOD、控制寄存器TCON,T0数据寄存器TH0和TL0,T1数据寄存器TH1和TL1。
TMOD是定时/计数器的工作方式寄存器,其地址为0x89,TMOD的内部结构如表2.1所示,不支持位寻址,在51单片机复位后初始化值为所有位都被清零。
表2.1 定时/计数器的工作方式寄存器TMOD
TCON是定时/计数器的控制寄存器,其地址为0x88,TCON的内部结构如表2.2所示,在单片机复位后初始化值为所有位都被清零。
表2.2 定时/计数器的运行控制寄存器TCON
数据寄存器TH0、TL0、TH1、TL1用于存放相关的计数值,当定时/计数器收到一个驱动事件(定时、计数)后,数据寄存器的内容加1,当数据寄存器的值到达最大的时候,将产生一个溢出中断,在51单片机复位后所有寄存器的值都被初始化为0x00,这些寄存器都不能位寻址。
2.工作方式
51单片机的定时/计数器T0和T1有四种工作方式,由TMOD寄存器中间的M1、M0这两位来决定。
工作方式0和工作方式1:当“M1M0=00”时,T0/T1工作于工作方式0,其内部计数器由TH0/TH1的8位和TL0/TL1的低5位组成的13位计数器,当TL0/TL1溢出时将向TH0/TH1进位,当TH0/TH1溢出后则产生相应的溢出中断,由GATE位、C/T#位来决定定时器的驱动事件来源。当“M1M0=01”时,T0/T1工作于工作方式1,其内部计数器为TH0/TH1和TL0/TL1组成的16位计数器,其溢出方式和驱动事件的来源和工作方式0相同。51单片机在接收到一个驱动事件之后计数器加1,当计数器溢出时则产生相应的中断请求。在定时的模式下,定时/计数器的驱动事件为单片机的机器周期,也就是外部时钟频率的1/12,可以根据定时器的工作原理计算出工作方式0和工作方式1下的最长定时长度T为
通过对定时/计数器的数据寄存器赋一个初始化值的方法可以让定时/计数器得到0到最大定时长度中任意选择的定时长度,初始化值N的计算公式如下:
注:定时/计数器的工作方式0和工作方式1,不具备自动重新装入初始化值的功能,所以如果要想循环得到确定的定时长度就必须在每次启动定时器之前重新初始化数据寄存器,通常是在中断服务程序里完成这样的工作。
工作方式2:当“M1M0=10”时,T0/T1工作于工作方式2。定时/计数器的工作方式2和前两种工作方式有很大的不同,工作方式2下的8位计数器的初始化数值可以被自动重新装入。在工作方式2下TL0/TL1为一个独立的8位计数器,而TH0/TH1用于存放时间常数,当T0/T1产生溢出中断时,TH0/TH1中的初始化数值被自动地装入TL0/TL1中。这种方式可以大大地减少程序的工作量,但是其定时长度也大大地减少,应用较多的场合是较短的重复定时或用做串行口的波特率发生器。
工作方式3:当“M1M0=11”时,T0工作于工作方式3。在这种工作方式下T0被拆分成了两个独立的8位计数器TH0和TL0,TL0使用T0本身的控制和中断资源,而TH0则占用了T1的TR1和TF1作为启动控制位和溢出标志。在这种情况下,T1将停止运行并且其数据寄存器将保持其当前数值,所以,设置T0为工作方式3也可以代替复位TR1来关闭T1定时/计数器。
3.中断处理
当51单片机的中断控制寄存器IE中的EA位和ET0/ET1都被置“1”的时候,定时/计数器T0/T1的中断被使能,在这种状态下,如果定时/计数器T0/T1出现一个计数溢出事件,则会触发定时/计数器中断事件。由第2章可知,可以通过修改中断优先级寄存器IP中的PT0/PT1位来提高定时/计数器的中断优先级。定时/计数器T0/T1的中断处理函数的结构如下:
void 函数名(void) interrupt 1 using 寄存器编号 //这是定时/计数器0的;如果是定时/计数器则把中断标号修改为3即可 { 中断函数代码; }
2.1.2 使用T0精确定时
1.功能描述
T0的精确定时常应用于51单片机的应用系统对“片时间”比较敏感的场合,需要在一段时间之后进行一项操作或者在某个时间间隔之内反复进行一项操作。
2.设计思路和操作步骤
使用T0精确定时,可以让T0在需要定时的时间长度到来的时候触发一个中断事件,在这个中断事件中进行相应的操作,该思路的具体操作步骤如下:
(1)根据51单片机的工作频率和需要定时的长度选择T0的工作方式。
(2)设置T0的TH和TL数据寄存器的初始化值。
(3)启动T0并且开启T0中断。
(4)在T0的中断服务子程序中进行需要的操作,并且根据T0的工作方式来决定是否需要重新装入TH和TL数据寄存器的初始化值。
3.应用实例——T0控制I/O引脚输出方波信号
【例2.1】initT0是T0的初始化函数,使用T0进行100μs的定时,在main函数中调用该函数,然后进入主循环等待中断事件,在中断服务子函数Timer0Interrupt中将P1.0引脚翻转。
#include <AT89X52.h> //初始化T0的函数,51单片机工作频率为11.0592MHz void initT0(void) { TMOD = 0x01; //工作方式1 TH0 = 0xFF; TL0 = 0xA4; //装入初始化值,100μs EA = 1; ET0 = 1; //开中断 TR0 = 1; //启动T0 } //T0中断服务子函数 void Timer0Interrupt(void) interrupt 1 { TH0 = 0xFF; //再次装入初始化值 TL0 = 0xA4; P1_0 = ~P1_0; //P1.0电平翻转 } main() { initT0(); //初始化T0 while(1) //主循环,等待中断 { } }
如例2.1所示,51单片机会在P1.0引脚上输出一个方波信号,参考图2.1,可以看到该方波的宽度为100μs。
图2.1 51单片机的引脚输出波形
2.1.3 T1精确定时
使用T1进行精确定时的方法和T0完全相同,设计思路和操作步骤也完全相同,例2.2是使用T1在工作方式2下进行100μs定时的应用代码。
【例2.2】和例2.1类似,本应用也是使用T1精确定时驱动P1.0输出方波信号,所不同的是T1使用工作方式2,其不需要手动重新装入定时器初始化值。另外,使用了一个位变量flg来作为中断事件的标志位,在中断服务子程序中仅仅修改这个标志位,然后在主程序中去判断这个标志位,最后做相应的操作,这样的好处是能大大减少中断服务子程序的执行时间,从而减少由于中断子程序执行带来的定时误差。
#include <AT89X52.h> //初始化T1的函数,51单片机工作频率为11.0592MHz bit flg = 0; //定义标志位 void InitT1(void) { TMOD = 0x20; //T1,工作方式2 TH1 = 0xA4; //装入初始化值,100ms TL1 = 0xA4; EA = 1; ET1 = 1; //开启中断 TR1 = 1; //启动T1 } //T1的中断服务子函数 void Timer1Interrupt(void) interrupt 3 { flg = 1; //将标志位置位 } void main(void) { InitT1(); //初始化T1 while(1) { while(flg==0); //等待标志位置位 flg = 0; //清除标志位 P1_0 = ~P1_0; //将P1.0翻转 } }
程序的波形输出可以参考图2.1。
2.1.4 使用T0/T1计数
1.功能描述
T0/T1的作用除了作为定时器,还可以作为计数器使用,对外加在对应引脚(T0/T1)上的外部脉冲信号进行计数。
2.设计思路和操作步骤
使用T0/T1进行计数时,当外部引脚上检查到一个脉冲信号之后让计数器加1,可以使T0/T1在计数溢出之后产生一个中断事件,然后在中断事件中进行检查在此次溢出之前进行了多少次计数,则可以得到对应的计数值,其具体操作步骤如下:
(1)设置T0/T1工作方式为计数。
(2)设置T0/T1对应的数据寄存器初始化值。
(3)启动T0/T1并且开启对应的中断。
(4)在中断服务子程序中根据初始化值来计算此次中断过程中T0/T1完成了多少次计数,重装T0/T1的数据寄存器初始化值。
3.应用实例——使用T0外部脉冲计数
【例2.3】T0工作于计数方式,外部脉冲加在51单片机的T0(P3.5)引脚上,当检查到一个脉冲之后T0加1,由于T0的数据寄存器TH0和TL0的初始化值为0xFFF1,所以,当检测到0xFFFF~0xFFF1个计数脉冲之后溢出,触发T0计数器中断,在中断服务子程序中对标志位flg进行置位,然后在主循环中检查该标志位并且计算计数值。
#include <AT89X52.h> bit flg = 0; //标志位 unsigned int counter = 0; //计数值 //T0初始化函数 void InitT0(void) { TMOD = 0x05; //T0计数工作方式,工作方式1 TH0 = 0xFF; TL0 = 0xF1; //T0数据寄存器的初始化值 EA = 1; ET0 = 1; //开启中断 TR0 = 1; //启动T0 } //T0中断服务子函数 void Timer0Interrupt(void) interrupt 1 { TH0 = 0xFF; //重装初始化值 TL0 = 0xF1; flg = 1; //标志位置位 } void main(void) { InitT0(); while(1) { while(flg == 0); //等待标志位被置位 flg = 0; //清除标志位 counter = 65536 - ((0xFF * TH0) + TL0); //计算计数值 } }
注:如果将T0/T1的数据寄存器的初始值设置为0xFFFF,则可以在每次脉冲信号到来的时候都触发定时器中断,也可以用于计数。
2.1.5 使用T0和T1产生PWM波形
1.功能描述
PWM波形输入常用于驱动电机、蜂鸣器等器件,其输出波形由两个参数决定,一个是频率,也就是多长时间产生一个高脉冲电平;另外一个是高脉冲电平的宽度,也就是每次高脉冲电平的持续时间。
2.设计思路和操作步骤
为了产生一个PWM波形,需要控制51单片机的对应引脚输出高低电平,通常使用T0和T1其中一个来控制该引脚输出电平的频率,而另一个来控制该引脚输出的电平宽度,其具体操作步骤如下(假设T1控制频率T0控制宽度):
(1)将对应引脚输出电平置低。
(2)设置T0和T1的工作方式。
(3)根据PWM波形的需求计算出T0和T1的预置值。
(4)启动T0和T1,开启中断,同时将该引脚输出电平置高。
(5)当T0中断到来的时候将该引脚输出电平置低。
(6)当T1中断到来的时候再次将该引脚的输出电平拉高,同时开启T0,跳转到步骤(5),循环执行。
3.应用实例——T0和T1控制I/O引脚输出PWM波形
【例2.4】使用T1来进行10ms的定时控制PWM的频率(100Hz),在T1的定时器中断中启动T0控制PWM输出2ms的高电平,51单片机对应的PWM波形输出引脚为P1.0。
#include <AT89X52.h> //T0和T1的初始化函数 void initTimer(void) { TMOD = 0x11; //设置工作方式 EA = 1; ET0 = 1; //开启T0和T1的中断 ET1 = 1; TH1 = 0xDC; //10μs TL1 = 0x00; //T1的预置值 TH0 = 0xF8; //2μs TL0 = 0xCD; //T0的预置值 TR0 = 1; TR1 = 1; //启动两个定时器 } //TL0中断服务子函数,使用通用工作寄存器组1 void Timer0(void) interrupt 1 using 1 { P1_0 = 0; //P1_0引脚为低电平 TR0 = 0; //停止定时器TL0 TH0 = 0xF8; //2μs TL0 = 0xCD; //T0的预置值 } //TH0中断服务子函数,占用定时计数器1的中断向量,使用通用工作寄存器组2 void Timer1(void) interrupt 3 using 2 { P1_0 = 1; //P1_0引脚为高电平 TR0 = 1; //启动定时器TL0 TH1 = 0xDC; //10μs TL1 = 0x00; //T0的预置值 } main() { P1_0 = 0; //初始化为低电平 initTimer(); //初始化T0和T1 while(1) { } }
例2.4输出的PWM波形如图2.2所示。
图2.2 PWM波形输出
2.1.6 使用T0/T1来测量脉冲宽度
1.功能描述
由于T0/T1的控制寄存器中有一个门控信号位,当该位被置“1”的时候,只有当51单片机的外部中断引脚上为高电平时T0/T1计数,所以,可以利用这个特点来测量一个外加到单片机外部中断引脚上的外部电平的宽度。
2.设计思路和操作步骤
由于定时器此时受到外部中断引脚和TR0/TR1位的双重控制,所以,可以在外部电平为低电平的时候启动定时器,然后当引脚上电平变为高电平之后定时器开始计数,同时监视引脚电平变化,当电平再次变为低电平的时候停止计数,此时的数据寄存器内容则为电平宽度对应的计数值,具体的操作步骤如下:
(1)设置GATE位和定时器的工作方式。
(2)启动定时器。
(3)监视外部中断引脚上的电平变化,当电平变为低的时候停止定时器。
(4)读取定时器对应的数据寄存器的内容。
(5)根据对应的工作频率计算出脉冲宽度。
3.应用实例——使用T0测量外部脉冲宽度
【例2.5】在initT0的T0初始化函数中,设置了GATE位,然后在外部中断0引脚(P3.2)变低的时候将T0关闭,读取TH0和TL0的值,再计算获得脉冲的宽度。
#include <AT89X52.h> long time=0; //计数器宽度 //T0初始化函数 void initT0(void) { TMOD = 0x09; //GATE位设置 TH0 = 0x00; TL0 = 0x00; } main() { initT0(); //初始化T0 while(1) { while(P3_2 == 0); //等待电平变高 TR0 = 1; //启动T0 while(P3_2== 1); //等待电平变高 TR0 = 0; //停止定时器 time = (256 * TH0 + TL0)* 11.0592; //计算脉冲宽度时间,单位为ms,51单片机工作频率11.0592MHz } }
2.1.7 使用T0/T1来扩展外部中断
1.功能描述
当51单片机的外部中断0和外部中断1都被占用的时候,可以使用T0/T1的外部引脚来模拟一个“外部中断”。
2.设计思路和操作步骤
和使用T0/T1来计数的方法类似,如果把T0/T1设置为计数模式下的自动重装工作方式(工作方式2),同时把自动装入的预置值设置为0xFF,将需要检测的信号接到定时器的外部引脚(P3.4和P3.5)上,当该信号输入给出一个负脉冲的时候,计数器加1,触发51单片机的计数器溢出中断。
用定时计数器来扩展外部中断的应用有一定使用限制,首先这个信号必须是边沿触发的;其次,在单片机检测到负脉冲信号和中断响应之间有一个指令周期的延时,因为只有当定时计数器加1之后才会产生一个溢出中断;第三,当这个定时计数器用做外部中断的时候,就不能用于定时/计数功能,其具体的操作步骤如下:
(1)将对应的电平信号连接到51单片机的T0/T1引脚上。
(2)设置T0/T1为计数器工作方式,工作方式2。
(3)设置T0/T1的TH0/TH1数据寄存器预置值为0xFF。
(4)启动T0/T1并且开启中断。
3.应用实例——使用T0和T1扩展外部中断
【例2.6】51单片机使用T0和T1扩展了两个外部中断,此时Timer0和Timer1定时器中断服务子函数被当做外部中断服务子函数来使用。
#include <AT89X52.h> void initTimer(void) { TMOD = 0x66; //定时计数器工作方式2 TH0 = 0xFF; TL0 = 0xFF; TH1 = 0xFF; TL1 = 0xFF; //都设置初始化值为0xFF EA = 1; ET0 = 1; ET1 = 1; //打开相应中断 TR0 = 1; TR1 = 1; //启动定时计数器0和1 } void Timer0(void) interrupt 1 using 1 //定时计数器0中断处理函数 { //外部中断事件处理 } void Timer1(void) interrupt 3 using 2 //定时计数器1中断处理函数 { //外部中断事件处理 } main() { initTimer(); //初始化T0和T1 while(1) { } }
2.1.8 内部定时/计数器T2基础
在某些51单片机中还有一个16位的定时/计数器T2,其有捕获、重装和波特率发生器三种工作方式。
1.相关寄存器
51单片机同样通过对相关寄存器的操作来实现对定时/计数器T2的控制,T2相关的寄存器用于控制寄存器T2CON,状态寄存器T2MOD,两个8位的数据寄存器TH2、TL2及重装/捕获高/低位寄存器RCAP2H/ RCAP2L。
T2CON是T2的控制寄存器,其寄存器地址为0xC8,内部功能如表2.3所示,51单片机复位后该寄存器被清零,该寄存器支持位寻址。
表2.3 T2CON寄存器
T2MOD是T2的工作状态寄存器,其寄存器地址为0xC9,内部功能如表2.4所示,在51单片机复位后该寄存器被清零,该寄存器不支持位寻址。
表2.4 T2MOD寄存器
T2的数据寄存器TH2(寄存器地址0xCD)、TL2(寄存器地址0xCC)分别是T2的数据高位/低位寄存器,每当接收到一个驱动事件后,对应的数据寄存器加1,该寄存器不能够位寻址,在51单片机复位后被清零。
RCAP2H(寄存器地址0xCB)和RCAP2L(0xCA)是T2的重装/捕获高/低位寄存器,该寄存器不能够位寻址,在51单片机复位后被清零。
2.工作方式
T2有自动重装、捕获、波特率发生器三种工作方式,可以通过对RCLK、TLCK、CP/R2#和TR2位的设置来控制T2工作于不同的工作方式下,如表2.5所示。
表2.5 T2的工作方式设置
在T2的捕获工作方式下可以通过设置EXEN2位来获得两种不同的工作方式,如果EXEN2=0,T2是一个16位的定时器/计数器,当T2溢出后将置位TF2并且请求T2中断。如果EXEN2=1,T2在定时/计数过程中,在T2EX引脚上检测到一个负跳变,则将TH2和TL2的当前值保存到RCAP2H和RCAP2L中,同时置位EXF2位并且请求T2中断,利用T2的这种工作方式可以方便地测量一个信号的脉冲宽度。
在T2的自动重装工作方式下也可以通过设置EXEN2位来获得两种不同的工作方式,如果EXEN2=0,T2是一个16位的定时/计数器,当其溢出时,不仅置位TF2,产生T2中断,还将把RCAP2H和RCAP2L中的值装入到TH2和TL2中,这个数值可以事先通过程序设定;当EXEN2=1时,在T2定时/计数过程中,如果在T2外部引脚(P1.0)上检测到一个负跳变,则置位EXF2标志位,同时也把RCAP2H和RCAP2L中的数据装入TH2和TL2中。
在自动重装工作方式下的定时/计数器T2可以配置为加1或者减1方式,当T2MOD寄存器中的DCEN位被置位之后,T2的增长方式受到T2EX外部引脚上电平的控制。当T2EX引脚上加上高电平时,T2为加1计数器;反之则为减1计数器。当T2为加1计数器的时候,T2在计数到FFFF时候溢出,置位TF2位,产生T2中断并且把RCAP2H和RCAP2L中的值装入TH2和TL2。当T2为减1计数器时,T2在TH2和TL2中的数值和RCAP2H和RCAP2L中的数值相等的时候溢出,同样进行相应的操作,唯一不同的是在TH2和TL2中装入FFFF,而不是RCAP2H和RCAP2L中的值。不管是在什么计数方式下,在溢出后,T2会置位EXF2标志位,但是不会引起EXF2中断。
注:在自动重装模式下,T2的溢出会置位EXF2,但是不会引发中断,所以,可以把EXF2当成计数器的第17位来使用。
T2也可以工作于方波产生器模式,在该模式下可以控制T2外部引脚输出一定频率的方波。方波的频率由单片机的工作频率和预先装入的RCAP2H和RCAP2L的数值决定,其计算公式如下:
3.中断处理
定时/计数器T2拥有独立的中断系统,中断向量的入口地址为0x2BH,在Cx51的中断号为5,其优先级别低于串行口。拥有T2的单片机IE寄存器中第6位ET2为T2的中断使能位,在EA被置位的前提下置位该位打开T2中断,复位该位关闭T2中断。T2工作在捕获方式时且EXEN2被置位时,定时器/计数器的溢出和外部引脚T2EX上的一个负跳变都可以引起T2中断,51单片机可以响应T2的中断,但是51单片机自身并不能够判断这个中断是由哪一个事件引起的,这需要用户在中断服务程序中通过对TF2位和EXF2位的检测予以判断。定时/计数器T2的中断处理函数的结构如下:
void 函数名(void) interrupt 5 using 寄存器编号 { 中断函数代码; }
2.1.9 使用T2输出方波
1.功能描述
T2作为一个独立的16位定时计数器,可以在独立的其对应的51单片机引脚T2上输出一个频率可控的方波信号。
2.设计思路和操作步骤
使用T2输出方波和使用T0/T1的进行定时的方法类似,只是需要将T2(P1.0)引脚设置为信号输出引脚,其操作步骤如下:
(1)置位T2MOD控制寄存器中的T2OE位以打开对应的输出引脚。
(2)清除T2CON中的C/T2#位选择T2工作于定时状态。
(3)置位TCLK和RCLK位以自动重装T2的初始化值。
(4)根据51单片机的工作频率和需要定时的时间长度计算出RCAP2H和RCAP2L的初始化值。
(5)根据需求设置TH2、TL2的初始化值,这个值可以和RCAP2H、RCAP2L相同,也可以不同,其只影响T2的第一次定时。
(6)置位TR2启动T2。
3.应用实例——T2控制I/O引脚输出方波
【例2.7】由于T2有自动重装功能,支持把RCAP2H和RCAP2L的数据自动装入TH2和TL2中,所以不需要手动装入初始化值。
#include <AT89X52.h> //T2的初始化函数 void initT2(void) { T2MOD = 0x02; //设置T2OE,该寄存器不能够位寻址 C_T2 = 0; //清除C/T2#位 TCLK = 1; RCLK = 1; //选择工作方式 RCAP2H = 0x91; RCAP2L = 0xF1; TH2 = 0x91; TL2 = 0xF1; //设置初始化值 TR2 = 1; //启动T2 } main() { initT2(); //初始化T2 while(1) { } }
2.1.10 使用T2进行精确定时
1.功能描述
和T0/T1一样,T2也可以作为定时器使用,用于产生精确定时。
2.设计思路和操作步骤
使用T2进行精确定时的设计思路和T0/T1类似,需要注意的是,T2的初始常数不需要在中断中手动重新装入,但是必须手动清除T2的中断标志TF2,其详细操作步骤如下:
(1)清除T2CON中的C/T2#位选择T2工作于定时状态。
(2)置位TCLK和RCLK位以自动重装T2的初始化值。
(3)根据51单片机的工作频率和需要定时的时间长度计算出RCAP2H和RCAP2L的初始化值。
(4)根据需求设置TH2、TL2的初始化值,这个值可以和RCAP2H、RCAP2L相同,也可以不同,其只影响T2的第一次定时。
(5)置位TR2启动T2。
(6)在T2的中断服务子程序中清除TF2标志位并且做相应的操作。
3.应用实例——T2定时控制I/O引脚输出秒方波
【例2.8】应用代码首先使用initT2对T2进行初始化,在timer2_int的中断服务子程序中将TF2标志位清除,并且将counter计数器加1,当counter到达10的时候则为定时1s完成,将P1.7引脚翻转输出波形。
#include "AT89X52.h" void initT2(void) { RCAP2H=0X3C; //定时50ms常数 RCAP2L=0xB0; TH2=RCAP2H; //定时器2赋初值 TL2=RCAP2L; ET2=1; //开外定时器2中断 EA=1; //开总中断 TR2=1; //启动定时器2 } void timer2_int(void) interrupt 5 { static unsigned char counter; TF2=0; //注意 T2的溢出标志位必须软件清零,件不能清零,这与T0和 T1不同!! counter++; if(counter==10) //定时50ms×20=1000ms即1s { P1_7 = ~P1_7; counter=0; } } //初始化主函数 void main(void) { initT2(); //初始化T2 while(1) { } }
图2.3所示是使用T2进行精确定时的波形输出。
图2.3 T2的定时波形输出