定时器/计数器阐述

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进行选择:

M1 M0 方式 功 能
0 0 0 13位定时器/计数器
0 1 1 16位定时器/计数器
1 0 2 自动重载的8位定时器/计数器
1 1 3 仅适用于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 = (6553650000) / 256;
TL0 = (6553650000) % 256;
gluiTemp ++ ;
}
void main(void)
{
unsigned char i,j;
gluiTemp = 0;

TMOD = 0x01;
TH0 = (6553650000) / 256;
TL0 = (6553650000) % 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 = (6553650000) / 256;
TL0 = (6553650000) % 256;
glucCounter ++ ;
if ( glucCounter == 20 )
{
gluiTemp ++ ;
glucCounter = 0;
}
}
void main(void)
{
unsigned char i,j;
gluiTemp = 0;
glucCounter = 0;
TMOD = 0x01;
TH = (6553650000) / 256;
TL = (6553650000) % 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= (65536500)/ 256; //计数初值重载
TL0=(65536500)% 256;
P2_0=!P2_0; //P2.0取反
}
void main(void)
{
TMOD=0x01; //T0工作在定时器方式l
TH0=(65536500)/256; //计数初值
TL0=(65536500)%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= (65536500)/256; //计数初值
TL0=(65536500)%256;
TR0=1; //启动T0
while(1)
{
TH0= (65536500)/256; //计数初值
TL0=(65536500)%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 = (6553650000) / 256;
TL0 = (6553650000) % 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 = (6553650000) / 256;
TL0 = (6553650000) % 256;
TH1 = 2565 ;
TL1 = 2565;
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 = (6553650000) / 256;//设定初值,100ms定时
TL0 = (6553650000) % 256;
glucCounter ++ ;//定义一个全局变量,实现计数功能
if ( glucCounter == 10 )
{
WAVE = ! WAVE;
glucCounter = 0;
}
}
void main( )
{
WAVE = 0;
TMOD = 0x01;
TH0 = (6553650000) / 256;
TL0 = (6553650000) % 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