51单片机编程开发之定时器与定时器中断概述

控制/MCU

1878人已加入

描述

51单片机定时器概述

定时/计数器从电路上来讲是一个脉冲计数器,当计数脉冲来自于单片机内部机器周期时,我们习惯上称其为定时器,而当计数脉冲来自于单片机外部的输入信号时,则称其为计数器。8051系列单片机在片内集成了两个可编程的定时/计数器,分别称其为定时/计数器0(T0)和定时/计数器1(T1),二者都具有定时和计数的功能。

这两个定时计数器可以独立配置为定时器或计数器。当被配置为定时器时,将按照预先设置好的长度运行一段时间后产生一个溢出中断;当被配置为计数器时,如果单片机的外部中断引脚上检测到一个脉冲信号,该计数器加1,当达到预先设置好事件数目时,产生一个中断事件。

51单片机的定时计数器由内部寄存器和外部引脚组成,T0(P3.4)引脚和T1(P3.5)引脚用于接收外部的脉冲信号。

51单片机的52子系列还有一个和这两个计数器功能差别较大的16位定时计数器T2,T2的最常用功能是用作串行口的波特率发生器,这部分内容我们之后再进行讲解。

51单片机通过对相应寄存器的操作来实现对定时计数器的控制,这些寄存器包括工作方式控制寄存器TMOD(Timer MODe register)和控制寄存器TCON(Timer CONtrol register),此外T0和T1还分别拥有两个8位数据寄存器TH0、TL0和TH1、TL1。

波特率发生器

TMOD是定时计数器的工作方式控制寄存器,通过对该寄存器的操作可以改变T0和T1的工作方式。该寄存器的内部结构和说明如下图所示,该寄存器不支持位寻址,单片机复位后被清零。

波特率发生器

TCON是定时计数器控制寄存器,上一节讲解外部中断时中我们已经介绍过了,其内部结构如表下图所示,在51单片机复位后初始化值为所有位都清零。

波特率发生器

TH0、TL0/TH1、TL1分别是T0/T1的数据高位/低位寄存器,均为8位。当定时计数器收到一个驱动事件(定时、计数)后,对应的数据寄存器的内容加1,当数据寄存器的值到达最大时,将产生一个溢出中断,在单片机复位后所有寄存器的值都被初始化为0x00,这些寄存器都不能位寻址。

51单片机定时器工作原理

定时功能:定时功能是通过计数器的计数来实现的。计数脉冲来自单片机内部,每个机器周期产生1个计数脉冲,即每个机器周期使计数器加1。由于1个机器周期等于12个振荡脉冲周期,所以计数器的计数频率为振荡器频率的1/12。假如晶振的频率fosc =12MHz,则计数器的计数频率 fcont =fosc ×1/12 为1MHz,即每微秒计数器加1。这样,单片机的定时功能就是对单片机的机器周期数进行计数。由此可知,计数器的计数脉冲周期为:

T=1/fcont =1/(fosc ×1/12)=12/fosc

式中,fosc 为单片机振荡器的频率;fcont 为计数脉冲的频率,fcont =fosc /12。在实际应用中,可以根据计数值计算出定时时间,也可以反过来按定时时间的要求计算出计数器的初值。

单片机的定时器用于定时,其定时的时间由计数初值和选择的计数器的长度(如8位、13位或16位)来确定。

计数功能:计数功能就是对外部事件进行计数。外部事件的发生以输入脉冲表示,因此计数功能实质上就是对外部输入脉冲进行计数。STC89 系列单片机的T0(P3.4)、T1(P3.5)或T2(P1.0)信号引脚作为计数器的外部计数输入端,当外部输入脉冲信号产生由1至0的负跳变时,计数器的值加1。

在计数方式下,计数器在每个机器周期的S5P2期间,对外部脉冲输入进行1次采样。如果在第1个机器周期中采样到高电平“1”,而在第2个机器周期中采样到1个有效负跳变脉冲,即低电平“0”,则在第3个机器周期的S3P1期间计数器加1。由此可见,采样1次由“1”至“0”的负跳变计数脉冲需要花费2个机器周期,即24个振荡器周期,故计数器的最高计数频率为fcont =fosc ×1/24。例如,单片机的工作频率fosc 为12MHz,则最高的采样频率为0.5MHz。

对外部脉冲的占空比并没有什么限制,但外部计数脉冲的高电平和低电平保持时间均必须在1个机器周期以上,方可确保某一给定的电平在变化前至少采样1次。

51单片机定时器模式设置

51单片机T0和T1定时器都有4种模式,由TMOD寄存器中间的M1、M0这两位的设置来控制。寄存器配置工作模式如下图所示:

波特率发生器

接下来我们一起看看这些模式的设置与特点。

工作方式0:当M1、M0设定为“00”时,定时器工作于工作方式0,此时定时计数器的内部结构如图所示。

波特率发生器

在工作方式0下,定时器内部计数器计数值为13位,由TH0/TH1的8位和TL0/TL1的低5位组成;当TL0/TL1溢出时将向TH0/TH1进位,当TH0/TH1溢出后则产生相应的溢出中断。工作方式下的驱动事件来源则由GATE位、C/T#位来控制。

工作方式1:当M1、M0设定为“01”时,定时器工作于工作方式1,此时定时计数器的内部结构如图所示。

波特率发生器

和工作方式0相比,工作方式1的唯一区别在于此时的内部计数器宽度为16位,分别由TH0/TH1的8位和TL0/TL1的8位组成,其溢出方式和驱动事件的来源和工作方式相同。51系列单片机的定时计数器采用加1计数的方式,即当接收到一个驱动事件时计数器加1,当计数器溢出时则产生相应的中断请求,第一个驱动事件到来时刻和中断请求产生。

51单片机在接收到一个驱动事件之后计数器加1,当计数器溢出时则产生相应的中断请求。在定时的模式下,定时计数器的驱动事件为单片机的机器周期,也就是外部时钟频率的1/12,可以根据定时器的工作原理计算出工作方式0和工作方式1下的最长定时长度T为:

波特率发生器

通过对定时计数器的数据寄存器赋一个初始化值的方法可以让定时计数器得到0到最大定时长度中任意选择的定时长度,初始化值N的计算公式如下:

波特率发生器

注意:定时计数器的工作方式0和工作方式1,不具备自动重新装入初始化值的功能,所以如果要想循环得到确定的定时长度就必须在每次启动定时器之前重新初始化数据寄存器,通常是在中断服务程序里完成这样的工作。

工作方式2:当M1、M0设定为“10”时,定时器工作于工作方式2,此时定时计数器的内部结构如图所示。

波特率发生器

定时计数器的工作方式2和前两种工作方式有很大的不同,工作方式2下的8位计数器的初始化数值可以被自动重新装入。在工作方式2下,TL0/TL1为一个独立的8位计数器,而TH0/TH1用于存放时间常数,当T0/T1产生溢出中断时,TH0/TH1中的初始化数值被自动装入TL0/TL1中。这种方式可以大大减少程序的工作量,但是其定时长度也大大减少,应用较多的场合是较短的重复定时或用作串行口的波特率发生器。

工作方式3:当M1、M0设定为“11”时,定时器工作于工作方式3,此时定时计数器的内部结构如图所示。

波特率发生器

在这种工作方式下T0被拆分成了两个独立的8位计数器TH0和TL0,TL0使用T0本身的控制和中断资源,而TH0则占用了T1的TR1和TF1作为启动控制位和溢出标志。在这种情况下,T1将停止运行并且其数据寄存器将保持当前数值,所以设置T0为工作方式3也可以代替复位TR1来关闭T1定时计数器。

接下来

51单片机定时器中断

51系列单片机内部集成了两个定时器/计数器,分别提供了两个定时中断源:TF0和TF1。

TF0中断,定时器/计数器0中断请求,其中断请求号为1。

TF1中断,定时器/计数器1中断请求,其中断请求号为3。

51单片机的断控制寄存器IE中的EA位和ET0/ET1都被置“1”时,定时计数器0/1的中断被使能,在这种状态下,如果定时计数器0/1出现一个计数溢出事件,则会触发定时计数器中断事件。可以通过修改中断优先级寄存器IP中的PT0/PT1位来提高定时计数器的中断优先级,MCS-51单片机的定时计数器的中断处理函数的结构如下:

void 函数名(void) interrupt 中断向量号 (using 工作组寄存器)

{

//中断代码

}

51单片机定时器计数功能应用

现在我们设计一个简单的电路来实践一下定时器的使用方法。

波特率发生器

如图所示的电路中我们将T0对应的外部脉冲引脚连接一个按键K5,用来测试定时器的计数功能。

普通计数器:P3.4电平变化后计数器累加,如果将计数初值设置为0xff,则每按键一次后计数器都会溢出,标志位就会置位,程序中通过扫描计数器标志位状态来切换数字。

/*

*这是一个定时器计数应用程序

*目的是运用定时器计数功能进行捕获计数

*/

#include

#include

typedef unsigned char u8;

typedef unsigned int u16;

u8 data_L,data_H;

u8 num = 0;

u8 code num_codelist[10] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};

void delay(u8 ms);

void count_func(void);

void data_init(void);

void T0_init(void);

void display(void);

void main(void)

{

T0_init();



while(1)

{

	count_func();

	data_init();

	display();

}

}

void delay(u8 ms)

{

u8 i,j;



for(i=0; i

}

void data_init(void)

{

data_L = num%10;

data_H = num/10;

}

void display(void)

{

P2 = 0xfe;

P0 = num_codelist[data_H];

delay(1);



P2 = 0xfd;

P0 = num_codelist[data_L];

delay(1);

}

void count_func(void)

{

//查询标志位

if(1 == TF0)                                   

{

    TH0 = 0xff;                                

    TL0 = 0xff;

    if(20 == num)

    {

        num = 0;

    }

    else

    {

        num++;

    }

    TF0 = 0;

}

}

void T0_init(void)

{

//设置定时器0为模式2

TMOD = 0x05;  

  //设置计数初值(模式2具备自动重装)

TH0 = 0xff;

TL0 = 0xff;

  //中断使用设置,这里只启用定时器不使用中断功能

  ET0=0;

TR0 = 1;

EA = 0;

}

中断计数器:这里的操作原理与以上普通程序相似,只是启用了中断,就不用进行循环扫描操作了,定时器中断进行计数,捕获P3.4电平变化后计数器加1,当数据寄存器数值溢出后产生中断,在中断中处理程序功能即可。

/*

*这是一个定时器中断计数应用程序

*目的是运用定时器中断功能进行计数

*/

#include

#include

typedef unsigned char u8;

typedef unsigned int u16;

u8 data_L,data_H;

u8 num = 0;

u8 code num_codelist[10] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};

void delay(u8 ms);

//void count_func(void);

void data_init(void);

void T0_init(void);

void display(void);

void main(void)

{

T0_init();



while(1)

{

	data_init();

	display();

}

}

void delay(u8 ms)

{

u8 i,j;



for(i=0; i

}

void data_init(void)

{

data_L = num%10;

data_H = num/10;

}

void display(void)

{

P2 = 0xfe;

P0 = num_codelist[data_H];

delay(1);



P2 = 0xfd;

P0 = num_codelist[data_L];

delay(1);

}

void T0_init(void)

{

//设置定时器0为模式2

TMOD = 0x05;  

//设置计数初值

TH0 = 0xff;

TL0 = 0xff;

//中断使用设置,启用中断

ET0 = 1;

TR0 = 1;

EA = 1;

}

//void count_func(void)

//{

// if(1 == TF0) //查询是否溢出

// {

// TH0 = 0xff; //重新赋值

// TL0 = 0xff;

// if(99 == num)

// {

// num = 0;

// }

// else

// {

// num++;

// }

// TF0 = 0;

// }

//}

void count_func(void) interrupt 1

{

//重新赋值

TH0 = 0xff;                                

TL0 = 0xff;

// //中断中此位会硬件清零,这句可以不用写

// TF0 = 0;

// //这里相当于对按键进行消抖,实际使用时酌情使用

// delay(15);

//查询外部输入脉冲变化

if(0 == T0)                                   

{

    if(20 == num)

    {

        num = 0;

    }

    else

    {

        num++;

    }

}

}

这两个程序很大一部分都是相同的,不同之处在于计数处理部分,第一个程序没有使用中断,我们定义了一个void count_func(void),第二个程序中我们使用了定时器中断,中断函数就是void count_func(void) interrupt 1。程序的主要区别就在于这两个函数void count_func(void)是普通函数需要定义,说明,调用,而void count_func(void) interrupt 1是中断函数它只需要定义就行。另外两函数功能差异就是我们刚所讲到的,一个是在主函数循环中不断扫描寄存器标志位,一个是中断中查询引脚状态。

其他程序段都是比较简单易懂的吧,这里简单说明一下:void data_init(void)这个函数是对显示数据进行个位与十位处理的。void display(void)这个函数是数码管显示处理函数,前面讲数码管时是介绍过了,这里只是把它打包成一个函数了。void T0_init(void)这个函数是定时器T0的初始化函数,里面包括定时器的模式,计数初值,中断等设置内容。以上程序不理解的可以留言或参考资料分析一下。最后再来看一下电路仿真情况。

波特率发生器

两个程序实现的功能都是一样的,现在想想定时器使用计数功能时中断作用通过一些特殊处理(比如我们这里将计数初值设置为最大值0xff)是不是和前面讲的外部引脚P3.3和P3.4的中断就很相似了,按键按一下就会产生中断。所以如果某些程序需要多个外部中断而单片机没有那么多中断引脚时不妨可以使用这种方式来增加单片机功能。当然这都是题外话,等你真正做开发时很大概率是不会用这款单片机的,现在的单片机功能强大着呢!但这是一种开发者应该具有的思维,也不是说你想到了某个法子就能派上用场,但平时积累一些“奇巧淫技”是有必要的,万一哪天就用上了呢!或许你的一个软件优化就帮公司产品省去了一笔费用,如果是一个月产n千或n万,甚至更高产量的产品,那老板不给你加薪还给谁加薪呢。对于有这样思维的,像对待自己孩子一样对待工作的人,无论什么岗位,我认为如果遇到了都值得成为合伙人,可以直接给股份,共创未来。这就是你的个人,从某种意义上来说,工作能力是职场的信誉值,信誉是市场硬通货,信誉也是真正的财富密码。打工时拿到好的工资是因为老板认可你所做工作,产品找明星代言,当我们选择它时很大可能是因为信任那个明星。对于信誉值达到一定程度的产品,它本身就成了一种信誉,就像在股市你之所以买某只股票,很大的原因就是因为你研究过了这家公司后得出你的信任这家公司这个结论。以上为我个人分析,当然并不一定所有人(老板)都会这么想,但我认为若真正信任员工,那雇佣关系转换为具有规则契约的合作关系能取得更好成就,这是一种互信机制。这都是本人回顾曾经“沧海”的产生的真实理念,但往事“不堪回首”,故事就不说了,以后有机会再交流,5年或许10年后我还会再来看这段话,或许到时我也早已不是“光杆司令”了。

51单片机定时器定时功能应用

通过以上两个例程各位对定时器计数功能应该都能掌握了吧,想验证是否掌握最快的方法就是自己亲自敲一遍代码运行一下,看结果是否相符,如果自己敲代码后仿真运行或在实验板上没结果时记得对照程序好好检查一下,看是哪里出了问题。

接下来我们继续介绍定时器的定时功能。我们想来看一段代码,可以尝试一下在不看后面解析的情况下试试自己能否读出程序的结果。

/*

*这是一个定时器定时应用程序

*目的是运用定时器定时模式进行控制数码管和LED显示

*/

#include

#include

typedef unsigned char u8;

typedef unsigned int u16;

u8 data_L,data_H;

u8 T0_cnt = 0;

u8 T0_s = 0;

u8 T1_cnt = 0;

u8 crol = 0xfe;

u8 code num_codelist[10] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};

void delay(u8 ms);

void data_init(void);

void Timer_init(void);

void display(void);

void main(void)

{

Timer_init();

P1 = crol;



while(1)

{

	data_init();

	display();

}

}

void delay(u8 ms)

{

u8 i,j;



for(i=0; i

}

void data_init(void)

{

data_L = T0_s%10;

data_H = T0_s/10;

}

void display(void)

{

P2 = 0xfe;

P0 = num_codelist[data_H];

delay(1);



P2 = 0xfd;

P0 = num_codelist[data_L];

delay(1);

}

void Timer_init(void)

{

//使能中断总开关

EA = 1;          

// 使能定时器中断

ET0 = 1;    	

ET1 = 1;                          

// 设置工作方式1

TMOD = 0x11;                       

// 设置定时器0定时时间50ms

TH0 = (65536-50000)/256;          

TL0 = (65536-50000)%256;          

// 设置定时器1定时时间50ms

TH1 = (65536-50000)/256;          

TH1 = (65536-50000)%256;           

// 设置控制寄存器:启动定时器

TR0 = 1;                          

TR1 = 1;

}

void T0_func(void) interrupt 1

{

//重新赋值

TH0 = (65536-50000)/256;

TL0 = (65536-50000)%256;



//计时1s

T0_cnt++;

if(20 == T0_cnt)

{

	T0_cnt = 0;

	T0_s++;

	if(60 == T0_s)

	{

		T0_s = 0;

	}

}

}

void T1_func(void) interrupt 3

{

//重新赋值

TH1 = (65536-50000)/256;

TL1 = (65536-50000)%256;



//计时0.5s

T1_cnt++;

if(5 == T1_cnt)

{

	T1_cnt = 0;

	//灯移位

	crol = _crol_(crol,1);

	P1 = crol;

}

}

这个程序的功能是使用定时T0定时控制数码管每秒变化数字,利用定时器T1控制LED每秒移位点亮4个灯。

先看程序运行的结果。

波特率发生器

现在来分析一下程序,程序整体来说是非常基础的,很多都是出现过多次的程序段了,我们主要介绍一下几个新的函数。

void Timer_init(void)这个函数是定时器T0和T1的初始化函数,里面包含中断控制位配置,数据寄存器初始化赋值以及定时器开关设置,这里将定时器T0和T1都设置为模式1,即16位定时器功能,这种模式下定时器数据寄存器没有自动重装功能,所以在每次中断之后要进行赋初值操作,在定时器中断中有这段代码,计数通过前面内容中的公式得出,这里是设置每50ms一次中断。

计算初值这点肯定很多初学者不太理解,需要多看一下资料说明,原理其实很简单,51单片机的计算器都是向上计数的,即从0~设置的定时器数据寄存器最大值,这里是16位所以就是0xffff(十进制就是65535),当计数到0xffff时再记一次这个数就溢出了,就产生了溢出中断,这时在中断函数中我们将数据寄存器程序重新赋值它又重头开始进行计数,所以最大定时时间就是0xffff个机器周期,51单片机是一个机器周期为12个时钟周期,如果使用12M晶振则刚好一个机器周期对应为1us,如果我们只需定时1us数据寄存器中就赋值0xffff,即65536-1,,经过一个机器周期就会产生中断了,如果定时2us,则设置为65536-2,其他的数值以此类推。

void T0_func(void) interrupt 1这个函数是定时器0的中断函数,首先对定时器数据寄存器进行赋初值然后设置一个1秒的累加技术操作,使得变量T0_s在每一秒计时之后加一,加到60s时进行清零,之后再循环计数,数码管将显示当前秒数值。

void T1_func(void) interrupt 3这个函数是定时器T1中断函数,这里设置一个0.25s的时间进行LED点亮移位操作。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分