本文最后更新于 2024-05-23,文章内容可能已经过时。

定时器/计数器阐述

AT89S51单片机有**两个16位**内部定时/计数器,记作T0、T1。

(AT89S52有3个定时/计数器,比AT89S51多了个T2。)

本质上,定时/计数器就是一个可以通过编程控制计数脉冲源、计数初值,并具有溢出标志和中断响应机制的**硬件加法计数器**。

定时/计数器在计数值**溢出时置位一个标志位TF**,可以供中断或者查询使用。

定时/计数器的计数脉冲源可以通过编程进行设置,从而起到**定时或者计数**的作用。

定时/计数器的结构

定时/计数器为16位硬件加法计数器,由高8位TH和低8位TL两个计数寄存器组成。

image-20210613164157169

定时/计数器具有可选的两个计数脉冲源:

1、外部输入脉冲

2、内部机器周期脉冲

image-20210613164305419

  • 定时器模式

加法计数器对**内部机器周期**进行计数。

机器周期**T**已知

通过设置计数值**N确定定时时间t**。

t=NxT

  • 计数器模式

加法计数器对**外部输入脉冲**进行计数,实现计数或频率测量功能。

外部计数脉冲由T0(P3.4)或T1(P3.5)引脚输入到计数器。计数器在检测到**外部输入脉冲下降沿时计数值加1,由于检测一个从1到0的下降沿需要2个机器周期,因此可以计数的外部脉冲最高频率为fosc/24**。

例:晶振频率12MHz时,最高外部计数频率500KHz。
12MHz / 24 = 500KHz

  • 定时/计数器具有两个设置寄存器:

TMOD**,确定工作方式和功能;
**TCON,控制T0、T1的启停,且包含溢出标志。

image-20210613165023779

定时/计数器有关的SFR

image-20210613165742980

定时/计数器0计数值寄存器(16位):

image-20210613165358451 共16位,分为高8位和低8位进行存储;
根据工作方式的不同可以使用其中的13位、8位、16位;
计数值范围根据使用位数而发生变化;
只能加法计数;
用户在编程时可以向TH和TL写入定时/计数器的计数初始值,在启动定时/计数器后,定时/计数器将从设置的初始值开始加法计数。
📌当计数到最大值时,产生溢出标志。

  • TMOD(T/C方式控制寄存器) SFR地址:0x89

image-20210613165853435

GATE:门控信号
1 -- T/C启动要求TR和INT同时为‘1’;
0 -- T/C启动仅受TR控制

C/T:计数器/定时器选择位
0 -- 定时器模式;内部时钟
1 -- 计数器模式;外部脉冲

M1和M0:T/C工作方式选择
可以选择工作方式0、1、2、3。

  • TCON(T/C控制寄存器) SFR地址:0x88

image-20210613170025906

TR0:T/C0启动控制位;
TF0:T/C0溢出标志位,用于中断响应;
TR1:T/C1启动控制位;
TF1:T/C1溢出标志位,用于中断响应;

TR:1--启动计数;0--停止计数;

TF:计数值溢出时由硬件置1;CPU转入中断响应程
序时由硬件自动清零,也可以软件清零。

定时/计数器的工作方式

8051单片机的定时/计数器具有**四种**工作方式,可以使用TMOD中的M1和M0进行选择:

M1M0方式功 能
00013位定时器/计数器
01116位定时器/计数器
102自动重载的8位定时器/计数器
113仅适用于T0,两个8位定时器/计数器

工作方式0(定时/计数器0和1均可)

方式0为**13位**计数,由TL0的低5位(高3位未用)和TH0的8位组成。TL0的低5位溢出时向TH0进位,TH0溢出时,置位TCON中的TF0标志,向CPU发出中断请求。

image-20210613170738280

工作方式1 (定时/计数器0和1均可)

方式1的计数位数是**16位**,由TL0作为低8位、TH0作为高8位,组成了16位加1计数器 。

image-20210613170840171

工作方式2 (定时/计数器0和1均可)

方式2为自动重装初值的8位计数方式。

image-20210613170932587

🎯**工作方式2特别适合于用作较精确的脉冲信号发生器。 **

定时器时间的设置(定时期初值的计算)

在内部定时方式下, T0、T1对内部机器周期计数,若fosc=6MHz,一个机器周期为12/fosc=2us,所以
✔ 方式0 13位定时器最大定时间隔=2^13×2us=16.384ms
✔ 方式1 16位定时器最大定时间隔=2^16×2us=131.072ms
✔ 方式2 8位定时器最大定时间隔 =2^8 ×2us=512us

例:若使T0工作在方式0,要求定时1ms,求计数初值。设计数初值为x,则有:
(2^13-x)×2us= 1000us
x = 2^13-500 = 0x1E0C
因此,TH,TL可置 8192-500
TH0 = 0xF0;TL0 = 0x0C;

image-20210613171201627 image-20210613171208244

工作方式3(仅适用于定时/计数器0)

方式3只适用于T/C0。当方式3时,TH0和TL0成为两个独立的计数器。 此时,TL0可用作定时/计数器,占用TR0和TF0;TH0只能用作定时器,占用TR1和TF1;T/C1仍可用于方式0、1、2,但不能使用中断。

image-20210613171418526

📡只有在T/C1被串口占用时,T/C0才使用方式3

定时/计数器的使用

定时器/计数器的初始化及其步骤

使用8051的T0、T1前,应对它进行编程初始化,主要是对TCON和TMOD编程;计算和装载计数初值(TH、TL)。

一般完成以下几个步骤:
○确定定时/计数器的工作方式(编程TMOD寄存器);
○计算计数初始值,并赋予TH、TL;
○如果在中断方式工作,需使能中断;
○启动定时器/计数器(编程TR0或TR1位)。

数码管显示实验

  • “实现数值0~65535的变化显示”

问题:如何使数值变化的速度减慢?(如每隔1秒数值加1)

一般方法(延时函数)

void main(void) 
{ 
    unsigned char i,j; 
    unsigned int  uiTemp = 0;
    while(1) 
    { 
        convert(uiTemp); 
        for(i=0;i<6;i++) 
        { 
            P2=LED_seg[i];   //送段码 
            P0=LED_bit[i];     //送位码 
            delay1ms(5);       //5ms延迟 
        } 
        j ++;
        if ( j == 33 )	//约1秒
        {          
            uiTemp++;
            j = 0;
        }
    } 
} 
  • 定时/计数器方法

实现数值0~65535的变化显示,每隔50ms数值加1。( 设晶振频率为12MHz)

#include < reg51.h>

unsigned int gluiTemp ;
void Timer0( ) interrupt 1 using 1
{
	TH0 = (65536 – 50000) / 256;
	TL0 = (65536 – 50000) % 256;
	gluiTemp ++ ;
}
void main(void) 
{ 
     unsigned char i,j; 
     gluiTemp = 0;
     
     TMOD = 0x01;
     TH0 = (65536 – 50000) / 256;
     TL0 = (65536 – 50000) % 256;
     ET0 = 1;
     EA = 1;
     TR0 = 1;
     while(1) 
     { 
          convert(gluiTemp); 
          for(i=0;i<6;i++) 
          { 
	P2=LED_seg[i];   //送段码 
	P0=LED_bit[i];     //送位码 
	delay1ms(5);       //5ms延迟 
	P0=0;
          } 
     } 
} 

实现数值0~65535的变化显示,每隔1s数值加1。( 设晶振频率为12MHz)

#include < reg51.h>
unsigned char glucCounter ;//定义一个全局变量,实现计数功能
unsigned int gluiTemp ;
void Timer0( ) interrupt 1 using 1
{
	TH0 = (65536 – 50000) / 256;
	TL0 = (65536 – 50000) % 256;
	glucCounter ++ ;
	if ( glucCounter == 20 )
	{
	gluiTemp ++ ;
	glucCounter = 0;
	}
}
void main(void) 
{ 
     unsigned char i,j; 
     gluiTemp = 0;
     glucCounter = 0;
     TMOD = 0x01;
     TH = (65536 – 50000) / 256;
     TL = (65536 – 50000) % 256;
     ET0 = 1;
     EA = 1;
     TR0 = 1;
     while(1) 
     { 
          convert(gluiTemp); 
          for(i=0;i<6;i++) 
          { 
	P2=LED_seg[i];   //送段码 
	P0=LED_bit[i];     //送位码 
	delay1ms(5);       //5ms延迟 
	P0=0;
          } 
     } 
} 

单片机输出(1)

例1:设晶振频率fosc=6MHz,要求在P2.0脚上输出周期为2ms的方波。

解:采用定时器T0的方式1进行编程

思路:采用定时间隔1ms,每次时间到P2.0取反并且启动下一次定时,从而实现2ms周期的方波。
定时所需计数次数n=1000us/2us=500

由于计数器递增计数,为得到500个计数之后的定时器溢出,必须给定时器置初值65536-500。

image-20210613172419400

中断方式

#include <reg51.h>
sbit P2_0=P2^0;
void T0_ISR() interrupt 1 using 1     //T0中断服务程序入口
{   
    TH0= (65536-500)/ 256;   //计数初值重载
    TL0=(65536-500)% 256; 
    P2_0=!P2_0;               //P2.0取反
}
void main(void)
{   
    TMOD=0x01;                 //T0工作在定时器方式l
    TH0=(65536-500)/256;     //计数初值
    TL0=(65536-500)%256; 
    ET0=1; EA=1;               //中断使能
    TR0=1;                     //启动T0
    while(1); 
}

但是这里精度还有待考究

中断方式

#include <reg51.h>
sbit P2_0=P2^0;
void main(void)
{   
    TMOD=0x01;                //T0工作在定时器方式l
    TH0= (65536-500)/256;   //计数初值
    TL0=(65536-500)%256; 
    TR0=1;                    //启动T0
    while(1)
    {
        TH0= (65536-500)/256;   //计数初值
        TL0=(65536-500)%256; 
  /*查询TF0是否为‘1’,如果为‘1’则说明溢出(定时时间到)*/
        while (!TF0);
        P2_0 = ! P2_0;
        TF0 = 0;
    }
}

单片机输出(2)

例2:设单片机fosc=6MHz,要求在P2.0引脚上输出周期为2ms,占空比为75%的矩形波。在例2基础上应作何修改?

image-20210613172908702

解:采用定时器T0的方式1进行编程

思路:采用定时间隔0.5ms,并设置一个全局变量i对定时器的中断次数进行计数,小于等于3时输出高电平,等于4时输出低电平,以此循环。

image-20210613173023643

#include "reg51.h"
sbit P1_0=P1^0;
unsigned char i;
void TIMER0_ISR (void) interrupt 1 
{   
    TH0=(65536-250)/256;
    TL0=(65536- 250)%256;
    i++;
    if(i = = 3)   { P1_0=0; }
    if(I = = 4)   { P1_0=1; i=0;  }    
}
void main (void)
{   
    TMOD=0x01;
    TH0=(65536- 250)/256;
    TL0=(65536- 250)%256;
    ET0=1; EA=1;
    TR0=1;
    i = 0;
    P1_0 = 1;
    while (1) { ; }               
}

小思考

如果周期2ms占空比要求是35%,可以采用采用定时间隔0.5ms吗?

image-20210613173311653

思路:高电平持续时间35%,低电平持续时间65%,应考虑取其公约数。一般取最大公约数。

所以可取时间间隔为0.1ms。

高电平持续7个定时间隔,低电平持续13个时间间隔。

单片机输出(3)

例3:设单片机fosc=6MHz,要求在P2.0引脚上输出周期为2ms,占空比为65%的矩形波。在例2基础上应作何修改?

#include "reg51.h"
sbit P1_0=P1^0;
unsigned char i;
void TIMER0_ISR (void) interrupt 1 
{   
    TH0=(65536-50)/256;
    TL0=(65536- 50)%256;
    i++;
    if(i = = 7)   { P1_0=0; }
    if(I = = 20)   { P1_0=1; i=0;  }    
}
void main (void)
{   
    TMOD=0x01;
    TH0=(65536- 50)/256;
    TL0=(65536- 50)%256;
    ET0=1; EA=1;
    TR0=1;
    i = 0;
    P1_0 = 1;
    while (1) { ; }               
}

单片机输出(4)

例4:设晶振频率fosc=6MHz,要求在P2.0脚上输出周期为2s的方波。

长定时的实现

思路:定时间隔为1秒,使用T0无法直接得到1秒的定时。因此,需要使用多次定时复合的方法来得到较长时间的定时。

方法一 使用两个定时/计数器实现1秒定时:

T0工作在定时方式1,定时100ms,从而控制P1.0输出周期为200ms的方波;将这个方波输入到T1,T1工作在计数方式2,计数5次后溢出,控制P2.0反向,从而实现周期为2秒的方波输出。

image-20210613173706504

#include <reg51.h>
sbit P1_0 = P1^0;
sbit P1_7 = P1^7;
void Timer0( ) interrupt 1 using 1 
{
	TH0 = (65536 – 50000)  /  256;
	TL0 = (65536 – 50000) % 256;
	P1_0 = ! P1_0;
}
void Timer1( ) interrupt 3 using 2 
{
	P1_7 = ! P1_7;
}
void main( )
{
	P1_7 = 0;
	P1_0 = 1;
    TMOD = 0x61;
    TH0 = (65536 – 50000)  /  256;
    TL0 = (65536 – 50000) % 256;
    TH1 = 256 – 5 ;
    TL1 = 256 – 5;
    IP = 0x08;
    EA = 1;
    ET0 = 1;
    ET1 = 1;
    TR0 = 1;
    TR1 = 1;
    while(1) { ; }
}

方法二 仅使用一个定时/计数器实现

使用方法一时需要使用两个定时器和两个IO引脚,资源消耗比较多。如何使用较少的资源实现同样的定时功能

#include < reg51.h>
sbit WAVE = P1^7;
unsigned char glucCounter;
void Timer0( ) interrupt 1 using 1
{
    TH0 = (65536 – 50000) / 256;//设定初值,100ms定时
    TL0 = (65536 – 50000) % 256;
    glucCounter ++ ;//定义一个全局变量,实现计数功能
    if ( glucCounter == 10 )
    {
        WAVE = ! WAVE;
        glucCounter = 0;
    }
}
void main( )
{
    WAVE = 0;
    TMOD = 0x01;
    TH0 = (65536 – 50000)  /  256;
    TL0 = (65536 – 50000) % 256;
    EA = 1;
    ET0 = 1;
    TR0 = 1;
    glucCounter = 0;
    while(1) { ; }
}

8051 定时计数器工程应用

设计要求

航标灯控制(工程设计)

2位拨码开关:灯质设定(频率与占空比)

image-20210613174114641

光敏电阻:实现光照采集(光照强,电阻小;光照弱,电阻大)

灯驱动:发光二极管采用12V供电,要实验单片机驱动电路设计,三极管驱动

image-20210613174204721

电路仿真图:

image-20210613174337712

软件程序

这里把相关函数先封装起来了,具体程序代码如下所示:

main.c

#include <reg51.h>
#include "timer.h"

/*******变量类型宏定义*******/
#define uchar unsigned char
#define uint unsigned int

/*******引用结构变量*******/
extern struct IALA IALA4[4];
extern struct Turn Counter50ms;

/*******IO口定义*******/
sbit D=P0^0; //灯输出控制,高电平有效
sbit OPCON=P1^2; //光照检测,高电平(亮)

/*******定时器0初始化*******/
void	Timer0Init()			
{
	TMOD=(TMOD&0xf0)|0x01;//定时器0 方式1
	TR0=1;								//开启定时器T0
	TH0=(65535-COUNT_50MS)/256;//设定定时器初始值(高8位)
	TL0=(65535-COUNT_50MS)%256;//设定定时器初始值(低8位)
	
	TF0=0;								//中断标志位清零
	ET0=1;								//允许定时器0中断
	EA=1;									//打开总中断
	PT0=0;								//中断优先级设置
}

/*******主函数*******/
void main(void)
{
	uchar key,pre_key=0,i;
	uint sum;
	Timer0Init();		//定时器0初始化	
	D=0;						//LED控制端初始化
	while(1)
	{
        key=key_scan(); //获取拨码开关对应状态
        if(key!=pre_key)//拨码开关状态改变
        {
            sum=0;				//变量初始化
            for(i=0;i<6;i++)
            {
                sum=sum+IALA4[key].gcd*IALA4[key].state[i];//结构体变量赋值且运算赋值给sum
                Counter50ms.state[i]=sum;			//确定定时时间
            }
        }
        pre_key=key;
	}
}
void T0_ISR(void) interrupt 1				//中断服务函数
{
    static uint i=0;								//定义静态变量i
    uchar j;
    TH0=(65535-COUNT_50MS)/256;			//重新赋初值
    TL0=(65535-COUNT_50MS)%256;
    if(OPCON==0)										//如果是黑夜,对应灯质状态
    {
        for(j=0; j<2; j++)						//循环闪烁
        {
            if(i==Counter50ms.state[2*j])
            {
                D=D_CLOSE;
            }
            if(i==Counter50ms.state[2*j+1])
            {
                D=D_OPEN;
            }
        }
            if(i==Counter50ms.state[4])		
            {
                D=D_CLOSE;
            }
            if(i>=Counter50ms.state[5])
            {
                D=D_OPEN;
                i=0;
            }
        i++;
    }
    else
    {
        //白天熄灯
        D=D_CLOSE;
        i=0;
    }
}

timer.c

#include <reg51.h>
#include "timer.h"

sbit D=P0^0; //灯输出控制,高电平有效
sbit OPCON=P1^2; //光照检测,高电平(亮)

//结构体全局变量定义,const:常数
const struct IALA IALA4[4]={
//NUM gcd ON1 OFF1 ON2 OFF2 ON3 OFF3
    {0,0,0,0,0,0,0,0},//全亮
    {1,10,1,3,0,0,0,0},
    {2,10,1,1,1,7,0,0},
    {3,10,1,2,1,2,1,5}
							};
							//结构体全局变量定义
struct Turn Counter50ms;		
							
unsigned char key_scan(void)		//拨码开关状态检测
{
	unsigned char key;
	key=0;
	if(P1&0x01)
	key=key|0x01;
	if(P1&0x02)
	key=key|0x02;
	return(key);
}

timer.h

#ifndef _TIMER_H_
#define _TIMER_H_
#define COUNT_50MS 500	     //50ms gcd:50ms的倍数
#define D_OPEN  1	     //开灯
#define D_CLOSE 0	     //关灯

//结构体类型声明1
struct IALA
{
	unsigned char num;     //拨码开关状态
	unsigned char gcd;     //各时间间隔最大公约数
	unsigned char state[6];//无符号字符型时间常数
};
//结构体类型声明2
struct Turn
{
	unsigned int state[6];	
};
unsigned char key_scan(void); //函数声明

#endif