定时器
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 = 0x22C/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()被执行了。说明就算是倒计时时间到了,也会先把后面的代码程序执行完,然后再进入下一轮中断。