51单片机内部外设:定时器和计数器

本文详细介绍了51单片机中的定时器工作原理,包括定时器的作用、工作模式、STC89C52的定时器资源及其配置步骤。重点讨论了定时器与中断系统的交互,以及定时器初始化、计数脉冲计算和中断处理程序的设置。同时,通过代码示例展示了如何利用定时器实现定时任务,如LED闪烁,并探讨了定时器在不同工作模式下的计时精度问题。最后,通过实验验证了定时器中断的执行顺序,确保代码的正确执行。
摘要由CSDN通过智能技术生成

定时器

51单片机的定时器属于单片机的内部资源,其电路的连接和运转均在单片机内部完成。

定时器作用:

1、用于计时系统,可实现软件计时,或者使程序每隔一固定时间完成一项操作;

2、替代长时间的for循环Delay,提高CPU的运行效率和处理速度;

……

工作框图:

计数器可以计算外部脉冲个数,定时器就是用计数的原始实现的。

STC89C52定时器资源:

定时器个数:3个(T0、T1、T2),T0和T1与传统的51单片机兼容,T2是此型号单片机增加的资源。

注意:定时器的资源和单片机的型号是关联在一起的,不同的型号可能会有不同的定时器个数和操作方式,但一般来说,T0和T1的操作方式是所有51单片机所共有的。
 

STC89C52的T0和T1均有四种工作模式:

模式0:13位定时器/计数器

模式1:16位定时器/计数器(常用)

模式2:8位自动重装模式

模式3:两个8位计数器

工作模式1框图:

SYSclk:系统时钟,即晶振周期,本开发板上的晶振为12MHz。

定时器和中断系统的特性决定它俩必须纠葛在一起:

一般开发步骤:

第1步:先设置好定时器的时钟源(如果需要的话)
第2步:初始化时钟相关寄存器
第3步:设置定时时间(计数个数)
第4步:设置中断处理程序
第5步:打开定时器
运行时:定时器计数结束后产生中断,然后执行中断isr

关于51单片机时钟的具体内容,自行查阅手册:

关于定时器和计数器,需要了解一下:

为什么计数脉冲来自系统时钟的就是定时方式呢?因为系统时钟比较稳定。而外部时钟有可能存在波动,如果用来做时钟会导致时间不准确。

一开始的错误想法另外,为什么内部12个时钟或者6个时钟才得到一个计数脉冲呢?为什么不像外部脉冲一样,来一个脉冲就加1呢?这是历史遗留下来的问题,早期的51单片机承受不了太高的频率,外部接了12M晶振,然后通过12分频得到1MHz主频,但计数是跟着外部晶振的频率走的。但是为什么6T的时候是6个时钟得到一个计数脉冲,我就有点想不太明白了。先记着吧,后面搞清楚了再来补充。

寄存器补充

TCON
8个位,但是有4个名字:TF、TR、IE、IT,每个名字的符号都有2个,后面分别带0和1,对应T0和T1。
TF:

timer flag,定时器(溢出)标志位,是只读(软件只是通过读取TF1来知道硬件的状态,而不用去写这一位来设置硬件的状态)的。timer定时时间到了后会做2件事情:第一个是把TF标志改为1,第二个是产生中断让CPU去中断处理;TF是硬件清零的(由1变0是自动的,不需要软件来干预。)有一些CPU的设计是需要软件去清零的,这时候用户的程序就一定要记得给标志位清零,不然就不能重复进入中断或者反复不停的重复进入中断。


TR:

就是timer run,就是定时器的启动计数的开关。当我们把整个定时器初始化好了之后,我们给TR位写1就可以开启计数了。TR位和GATE位有一定关联性。

IE:

也是个标志位,作用就是用来展示硬件的状态改变的。譬如IE1对应外部中断1(INT1),平时不发生INT1时IE1=0,当INT1发生中断时,硬件自动IE1=1,当CPU处理了INT1时硬件会自动给IE1=0(硬件自动清零)。


IT:

是用来设置外部中断的中断触发方式的。所谓中断的触发方式,就是指硬件在某种条件下才会被判定为要产生中断,所以其实就是中断产生的条件。中断触发方式一般就是:边沿触发和电平触发2种。边沿触发又分为:上升沿触发、下降沿触发、双边沿触发;电平触发方式分为:高电平触发、低电平触发2种。


TMOD
GATE:

GATE是TMOD寄存器中的,也有2个分别对应T0和T1。GATE位中文名叫门控位,工作方式是:当GATE=0时(相当于门是打开的,此时GATE位是可以忽略的),此时定时器开关就只受TR位影响。具体就是TR=1开启计数,TR=0结束计数。当timer处于定时器工作模式时GATE就要等于0;GATE一般是在timer处于计数器模式时用的。当timer用来计数时,很关键的就是什么条件下计数,什么条件下不计数。当GATE=0时计数条件只有TR1一个(TR1=1就计数,TR1=0就不计数),当GATE=1时是否计数不仅取决于TR1还取决于INT1引脚(P3.3),实际规则是:当TR1=1并且INT1引脚也为高电平时才会计数。

1000 = 0x3E8 = 高0x3  低0xE8   => TL0 = 0xE8   TH0 = 0x3
8888 = 0x22B8 = 高0x22 低0xB8  => TL0 = 0xB8   TH0 = 0x22

C/T位:

设置T0/T1工作在定时器模式还是计数器模式。1表示计数器,0表示定时器。

M1 + M0:

2个位一起来表示T0/T1处于哪种工作模式下,一般有4种:13位、16位、8位自动重载、双8位。

在寄存器中,通常有这几种类型的位,即标志位/状态位、控制位、数据位,有的是由硬件自动触发的,不用软件去操作,这些往往是标志位或者状态位,通常是只读的,只用读取来判断其状态;有的需要软件进行操作,比如硬件将某状态位置1,如果不用软件去置0,那么一直处于置1状态。

初步代码

按照我一开始的想法,写出了如下代码:

/**
  *@file    timerandcounter.c
  *@author  Timi
  *@date    2022.07.22
  */
#include <reg51.h>

#define AIM_COUNTER(time) (65536 - (time) * 1000 / 12)
        
//函数入口
void main(void)
{   
    TMOD = 0x01;     //设置定时器0工作在模式1下,16位定时器
    TL0 = (AIM_COUNTER(100) & 0xFF);    //写入低8位
    TH0 = AIM_COUNTER(100) >> 8;        //写入高8位
    TR0 = 1;    //使能定时器0
    ET0 = 1;    //使能定时器0对应的中断
    EA = 1;     //打开中断开关
    
    while(1);
}

/**
  *@brief   点亮LED
  *@param[in]
  *@param[out] 
  *@return
  */
void LightLed() interrupt 1 using 1
{
    P0 = 0xAA;
}

思路如下(只是一开始的想法,不一定对):

首先定义了一个宏函数:

AIM_COUNTER(time) (65536 - (time) * 1000 / 12)

宏函数中传入要定时的时长,单位ms;

定时的本质是计数,定时器就是对内部时钟脉冲进行计数,所以要先算出计数脉冲。51单片机在12T时,每12个脉冲周期计数一次;因为12T时,频率是1MHz,所以脉冲周期是1us,可得出一个计数周期是1us*12=12us,如果想要计数time毫秒,那么就需要time*1000/12个计数周期,也就是我们要计数的个数,又因为在51单片机中,要求传入的是补码,所以实际传入的是2^16 - time*1000/12,之后,将低8位传入TL0,高8位传入TH0,然后打开定时器0,并且打开相应的定时器中断。

当定时器的时间结束后,就会执行相应的中断程序,点亮对应的LED。

为什么要传入补码,而不是直接传入计数值。

因为51单片机定时器使用的是加法器,只有当加到65536个数之后才会溢出触发中断。所以,如果要计1000个数,就要从65536-1000个数开始数起,这样,才能保证数1000个数之后触发定时器中断。

这种情况好像可以实现功能,但是时间太短,也不知道到底定时是不是正确。

于是,我在想,51单片机最多能定2^16 * 12us = 786432us=786.432ms,不到1秒的时间,如果是这样,那定时器的意义在哪里呢?连秒级的时间都没法定时。

所以,肯定有某种方式可以设置任意时长的定时。

如果一次可以定时100ms,那么10次循环不就能定时1秒了?100次循环不就能定时10秒了?

于是,按照我的想法去改进,10秒倒计时后点亮LED:

/**
  *@file    timerandcounter.c
  *@author  Timi
  *@date    2022.07.22
  */
#include <reg51.h>

#define AIM_COUNTER(time) (65536 - (time) * 1000 / 12)

static int count = 0;
        
//函数入口
void main(void)
{   
    TMOD = 0x01;
    TL0 = (AIM_COUNTER(100) & 0xFF);
    TH0 = AIM_COUNTER(100) >> 8;
    TR0 = 1;
    ET0 = 1;
    EA = 1;
    
    while(1);
}

/**
  *@brief   点亮LED
  *@param[in]
  *@param[out] 
  *@return
  */
void LightLed() interrupt 1 using 1    //using 1不加也可以,加是为了使用寄存器1加快运行速度
{
    if (count++ < 99)    //再重装99次,共100次,
    {
        TL0 = (AIM_COUNTER(100) & 0xFF);
        TH0 = AIM_COUNTER(100) >> 8;
    }
    else
    {
        P0 = 0xAA;
    }   
}

这里基于的重要一点是,定时器只要打开,然后装入计数值后,就会进行倒计时,触发中断的条件就是倒计时时间到了,然后又会触发中断,只要没有跳出条件,那么就是一个对其中断程序的无限循环。调出的条件可以是不再装入计数值,或者直接把定时器关掉。

不知道对不对,待查阅资料后优化代码。

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~查阅了一些相关资料后得知。

上面计时的思路不对。怪不得我这里的程序本来是设定的10秒,但是经过测试只要7秒左右。源于对这句话的误解:

上面的解读是错误的。

不知道这句话怎么理解,每12个时钟得到一个计数脉冲是啥意思,不过就所查阅的资料,应该是12个周期才能得到1个计数脉冲,但是这个脉冲不是12us,就是一个脉冲的时间,1us。

所以,51单片机可定时的最大时长为:

65536 * 1us =65536us=65.536ms。

附:取1个数的高底8位的另一种思路是,x/256可得到高8位,x%256可得到低8位。

上面我用的那个方法好像是不对的,先不管了。

得到的正确代码如下所示:

/**
  *@file    timerandcounter.c
  *@author  Timi
  *@date    2022.07.23
  */
#include <reg51.h>

#define AIM_COUNTER(time) (65536 - (time) * 1000 / 1)

static int count = 0;
        
//函数入口
void main(void)
{   
    TMOD = 0x01;
    TL0 = (AIM_COUNTER(50) % 256);
    TH0 = (AIM_COUNTER(50) / 256);
    TR0 = 1;
    ET0 = 1;
    EA = 1;
    
    while(1);
}

/**
  *@brief   点亮LED
  *@param[in]
  *@param[out] 
  *@return
  */
void LightLed() interrupt 1 using 1
{
    if (count++ < 199)
    {
        TL0 = (AIM_COUNTER(50) % 256);
        TH0 = (AIM_COUNTER(50) / 256);
    }
    else
    {
        P0 = 0xAA;
    }   
}

经测试,定时时间为10s。

每隔1秒闪烁LED

之前做LED闪烁实验时,用的都是for循环来实现的Delay,但是Delay的时间是未知的,不精确的。如果想要让LED每1秒闪烁一次,就需要用到定时器。

代码如下:

/**
  *@file    timerandcounter.c
  *@author  Timi
  *@date    2022.07.23
  */
#include <reg51.h>

#define AIM_COUNTER(time) (65536 - (time) * 1000 / 1)

static int count = 0;
        
//函数入口
void main(void)
{   
    TMOD = 0x01;
    TL0 = (AIM_COUNTER(50) % 256);
    TH0 = (AIM_COUNTER(50) / 256);
    TR0 = 1;
    ET0 = 1;
    EA = 1;
    
    while(1);
}

/**
  *@brief   点亮LED
  *@param[in]
  *@param[out] 
  *@return
  */
void LightLed() interrupt 1 using 1
{
    TL0 = (AIM_COUNTER(50) % 256);
    TH0 = (AIM_COUNTER(50) / 256);
    
    if (count++ == 10)
    {
        P0 = ~P0;
        count = 0;
    }  
}

问题总结

关于定时器,有个疑惑。

在这段代码中,定时器定时了50ms就会触发再一次的中断,那么如果的执行代码不止50ms,那么还没等代码执行完就又触发了中断,怎么办?

void LightLed() interrupt 1 using 1
{
    TL0 = (AIM_COUNTER(50) % 256);
    TH0 = (AIM_COUNTER(50) / 256);
    
    if (count++ == 10)
    {
        P0 = ~P0;
        count = 0;
    }  
}

验证:

首先,不加延时,来闪烁LED

void LightLed() interrupt 1 using 1
{
    TL0 = (AIM_COUNTER(50) % 256);
    TH0 = (AIM_COUNTER(50) / 256);
    
    P0 = ~P0;
}

可以正常闪烁。

然后,加1s延时:

/**
  *@brief   点亮LED
  *@param[in]
  *@param[out] 
  *@return
  */
void LightLed() interrupt 1 using 1
{
    P0 = ~P0;
    TL0 = (AIM_COUNTER(50) % 256);
    TH0 = (AIM_COUNTER(50) / 256);
    Delay1s();
}

根据现象可知,Delay1s()被执行了。说明就算是倒计时时间到了,也会先把后面的代码程序执行完,然后再进入下一轮中断。

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值