概述
????write in front????
????大家好,我是謓泽,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流????
????2021年度博客之星物联网与嵌入式开发TOP5~2021博客之星Top100~阿里云专家& 阿里云星级博主~掘金⇿InfoQ创作者~周榜92 » 总榜1430????
????本文由 謓泽 原创 CSDN首发????如需转载还请通知⚠
????个人主页-謓泽的博客_CSDN博客 ????
????欢迎各位→点赞???? + 收藏⭐️ + 留言????
????系列专栏-【51单片机】系列_謓泽的博客-CSDN博客????
✉️我们并非登上我们所选择的舞台,演出并非我们所选择的剧本????
????本系列哔哩哔哩江科大51单片机的视频为主以及博主自己的总结梳理????
????『定时器₂』目录⇲
????write in front????
????配置寄存器
????⒈TMOD配置
????⒉TCON配置
????中断的配置
????void Timer0_Init(void)
????STC89C52中断资源
????流程图
????定时器点灯⇢间隔1s
????TMOD问题
????STC-ISP-自动配置
????作业⇢时钟
????配置寄存器
首先,让我们来先配置下定时器,如何配置定时器。定时器是要先把相关的符号先进行声明的,但由于#include <REGX52.H>当中已经帮我们声明好了,那么我们直接使用即可。
????⒈TMOD配置
〖内容⇢定时器/计数器工作模式寄存器TMOD〗
那么我们先创建一个子函数 void Timer0_Init() ⇥ 对定时器进行初始化。
那么首先是TMOD的一个配置,定时器1我们先不管(高四位定时器全部给上0),只管低四位的一个定时器0的一个配置。
在前面的定时器工作有介绍过,STC89C52 的 T0 和 T1 一共有四种工作模式:在这里我们选择最常用的工作模式1。
Ⅰ:工作模式①的话,我们只需要把 M1 = 0,M0 = 1 就是定时器模式①的配置了。
Ⅱ:C/T(取反):这个我们只需要给C/T(取反) = 0即可,SYSclk⇢系统时钟。
Ⅲ:GATE(门控端),GATE = 0,这里当中都是数字电路当中的一些基础逻辑门,TR0 = 1。
那么经过上述的TMOD配置,此时,TMOD = 0x01;// 0000 0001 (十六进制→二进制)
注→TMOD是不可位寻址:寄存器只能进行整体的进行赋值『十六进制0x』
????⒉TCON配置
〖内容⇢定时器/计数器控制寄存器TCON〗
TCON可位寻址,注→可位寻址可以为每一位寄存器单独赋值。
在这个我们主要针对 TF0 和 TR0,注:这里我们是单独赋值,用二进制(0/1)来。
Ⅰ: TF0中断溢出标志位,TF0 = 0 被允许计数以后,从处置开始计数+1,当最高位产生溢出的时候,由n硬件置"1"TF0 = 1的时候就会向CPU请求中断。
在程序当中我们先可以直接给TF0 = 0,因为一旦TF0 = 1的就会产生中断。
Ⅱ:TR0 就是计数系统会不会开启,TR0 = 1的时候就代表着开始计数。
那么我们就可以给 TR0 = 1 开始计数工作。
拓展→IE0和IT0就是用来控制外部中断引脚的,但是因为GATE门控端 = 0,那么我们就可以不用去配置这些东西。
Ⅲ:我们还需要配置 TL0 和 TH0 这里我们也需要给它写好一个值。上篇说过,这个计数脉冲就是在12MHZ情况下,每隔1us进行+1,等加到最大值的时候 65535us,它才进行产生一个中断。TL0是低字节,TH0是高字节。它们都是计数系统,这两个字节总共可以存储0~65535。每隔1us加1,总共时间是65535us,这就相当于是65ms计满之后就计数不下去了(好比一个沙漏⏳的沙子已经放满了)
64535us离计数器溢出差值1000us=1ms,每隔1ms的时候产生中断。每一次中断之后再次的计一次数,每计数1000us的时候再做其它的事情。
注:定时器的工作单位方式是以微妙(us)为单位的。
那么在这里我们可以先定义一个1ms的时间,所以我们刚开始的时候就可以给64535离计数器计满(溢出)差值的时候就为1000个数(1ms),注:我们寄存器是⑧个比特位为一位的,⑧位的只能存的下256。而这个65535它是指的是两个⑧位的拼接在了一起,一句话:要分开来。
TH0 = 64535/256;把高8位拿出来了,TL0=64535%256;低8位拿出来了。这个可能理解起来会比较难点,但是其实这样就很好理解了,用画图的方法来演示下吧↓
????中断的配置
那么定时器的配置已经配置好了,接下来就是中断的配置了。
在这里我们只需要配置T0的那一条路线即可。这个配置就比上述容易许多了。
Ⅰ:ET0 = 1
Ⅱ:EA = 1
Ⅲ:PT0 = 0 『默认:即为zero』
????void Timer0_Init(void)
那么上述的定时器初始化配置如下↓
void Timer0_Init(void) { TMOD = 0x01;//定时器工作模式 TF0 = 0; //允许计数 TR0 = 1; //开始计数 TH0 = 64536/256;//设置定时初值,高8位 TL0 = 64536%256;//设置定时初值,低8位 ET0=1; EA=1; PT0=0; }
那么以上void Timer0_Init() ⇥ 对定时器初始化的函数就已经好了。我们只需要在源文件.c的 main() 函数当中进行初始化,上电的时候它实际上已经是开始工作的了。哎,那现在有一个疑问:那么这样才能让它进行中断呢。
注:上述实际上是65536才到溢出,当然65535仅仅也知识多出了1微秒。
????STC89C52中断资源
interrupt0 表示的是外部中断。注:P3.2引脚可以使用。
interrupt1 表示(定时器中断0) 在这里我们使用的是定时器0的中断。
interrupt2 表示(外部中断1) 注:P3.3引脚可以使用。
interrupt3 表示(定时器中断1)
interrupt4 表示(串口中断)
我们此时还需要在main()函数后面再写一个子函数,这个子函数的作用就是代表"闹钟"响了之后跳转到这个子函数当中去执行最终的中断任务,⏰指的就是void Timer0_Init(void)
那么我们这个中断号的配置也就是上面用红色的框圈出来的那个了,注:interrupt是关键字。
#include <REGX52.H> void Timer0_Init(void) { TMOD = 0x01;//定时器工作模式 TF0 = 0; //允许计数 TR0 = 1; //开始计数 TH0 = 64536/256;//设置定时初值,高8位 TL0 = 64536%256;//设置定时初值,低8位 ET0=1; EA=1; PT0=0; } int main(void) { Timer0_Init(); while(1) { } } void Timer0_Routine(void) interrupt 1 { }
上述代码总的意思实际上就是→执行main()函数当中的定时器初始化Timer0_Init(); 定时器初始化好之后。原本这个程序是在while(1);执行的时候就会跳转到 void Timer0_Routine(void) interrupt 1 这个中断子函数当中去执行完这个子函数完的时候就会回到while(1);依次往复这个过程。
注⇢在中断任务当中需要重新赋值定时器初始值,如果你赋值它为2ms的话,那么也只是第一次为1ms,等到第二次的时候到中断任务就会编程2ms。所以我们通常会把中断任务的赋初始值和定时器任务赋初始值都是一样的。
但是如果我们在while(1);死循环+个让一脚点灯,它既可以执行while(1)循环里面的点灯的程序又可以执行中断任务,这个就是为什么使用定时器是可以在一定情况下同时完成⒉项任务
实验很简单我们可以点个灯试一下或者调试看下是不是如上述所说。
????流程图
主程序(main)函数 --> 其它子函数 --> 中断来了定时
器溢出,中断请求 --> 跳转到中断的一个函数里面执行中断里面的任务 --> 中断执行完之后再返回原来断点的地方就是原来从哪里停的现在从哪里回来 --> 然后再继续执行主函数。
从上面的理解当中我们可以看出,这个程序当中在一定的程度上,可以同时完成两项任务。比如说:执行主程序其它子程序也在执行。就像是LED的流水灯上,既可以执行流水灯的扫描,其次按键也可以进行控制流水灯。好处是:大大的提高了运行效率,有兴趣可以了解一下中断的概念实际上是非常多的。
????定时器点灯⇢间隔1s
程序代码如下所示↓
#include <REGX52.H> void Timer0_Init(void) { TMOD = 0x01;//定时器工作模式 TF0 = 0; //允许计数 TR0 = 1; //开始计数 TH0 = 64536/256;//设置定时初值,高8位 TL0 = 64536%256;//设置定时初值,低8位 ET0=1; EA=1; PT0=0; } int main(void) { Timer0_Init(); while(1) { } } //全局变量默认初始化为0 unsigned int g_Count; void Timer0_Rountine(void) interrupt 1 { TL0 = 64536%256; TH0 = 64536/256;//重新赋初值才能保证下一次还是1ms g_Count++; if(g_Count>=1000) { g_Count = 0; P2_0 = ~P2_0;//上电默认为高电平 } }
拓展:注如果g_Count不是用全局变量的话,而是局部变量的话,那么那个局部变量需要被static 进行修饰,其作用:延长生命周期,出了局部变量原来修饰变量的值不会被销毁,再次回到函数当中被修饰的变量依旧会保持原来的值的大小。⇢ 非常关键!!!
注:我们这里的 g_Count++ 是每次+1ms。所以当它不满足if()语句的判断条件的时候是还需要返回到while(1);语句当中去的。再次进入到中断执行中断任务。
????TMOD问题
当然上述代码当中还是存在着缺陷的,那就是TMOD,在上述当中我们TMOD是不可谓寻址就是不能单个赋值初始值必须⑧位才行。在这里我们使用一个定时器是当然没有任何问题的,因为我们的定时器1更本就没有开始工作不会出现问题。
出问题是在于:同时使用⒉个定时器的时候,定时器1和定时器0的话。高位会被刷新!
当然,实际上TOMD = 0x11;也是可以使用两个定时器的。但是它的操作上有很大的区别↓
TMOD = TMOD&0xF0;
TMOD = TMOD|0x01;这个????是可以进行统一的处理,而也建议大家这样养成一个好的习惯。
TMOD = 0x11;
这种????是单独进行处理。
所以,在使用TMOD的时候推荐用这种代码方式↓
TMOD = TMOD&0xF0;
TMOD = TMOD|0x01;
//同
TMOD&=0xF0;
TMOD|=0x01;
注:TMOD默认为低电平。
TMOD低四位清0,高四位保持不变。 0000 0000 & 1111 0000 = 0000 0000
任何数1&1还是等于1,任何数&0还是等于0的。
TMOD最低为置1,高四位保持不变。 0000 0000 | 0000 0001 = 0000 0001
任何数或上一个0,就是等于自身的那个数。
那么这个与或赋值法就可以操作其中的几个位而不会影响其它的位。
????STC-ISP-自动配置
在STC-ISP软件上可以直接配置定时器计算器的,如下图所示↓
AUTR:可以删除因为已经是12T模式了。当然这里还是少了中断的配置我们需要加上的。
Ⅰ:ET0 = 1
Ⅱ:EA = 1
Ⅲ:PT0 = 0 『默认:即为zero』
上述的TL1和TH1的配置都是可以的。
????作业⇢时钟
- 题目内容→定时器时钟。
- 功能如下↓
⒈显示时钟的时间,显示在LCD1602的屏幕上。
⒉分别定义全局变量 Hour小时、Min分钟、Sec秒的时间显示在LCD1602的屏幕上。
⒊分别用独立按键可以实现修改Hour小时Min分钟Sec秒,每按一次时钟或分钟或秒+1,也要实现-1的功能。还有按键可以提供定时器的计数功能的打开或者开启,也就是实现时钟开关的功能模式。
⒋当然也是可以在你的能力范围之内在增加一些你觉得有必要的功能哟(@^0^)
时钟作业示例代码如下↓
main.c
#include <REGX52.H> #include "Delay.h" #include "LCD1602.h" #include "Timer0.h" #include "MatrixKey.h" /** * @brief 独立按键控制数码管以及定时器的功能 * @param 无 * @retval 无 */ unsigned char Sec=0,Min=0,Hour=12; void Contorl() { e_KeyNum = MatrixKey(); if(e_KeyNum!=0) { switch(e_KeyNum) { case 1:Hour++;break; case 2:Hour--;break;//小时 case 3:Min++; break; case 4:Min--; break;//分钟 case 5:Sec++; break; case 6:Sec--; break;//秒 //控制TR0的启动(7)暂停(8) case 7:Timer0_Init();Delay(1);break; case 8:TR0=0;Delay(1);break; } if(e_KeyNum>=1 && e_KeyNum<=6) LCD_ShowString(1,14,"Yes"); else if(e_KeyNum==7 || e_KeyNum==8) LCD_ShowString(1,14,"Sus"); else LCD_ShowString(1,14,"Not"); } } int main(void) { LCD_Init(); LCD_ShowString(1,14,"xxx"); LCD_ShowString(1,1,"Clock:"); //上电显示静态字符串 LCD_ShowString(2,1," : :"); while(1) { Contorl(); LCD_ShowNum(2,1,Hour,2); //显示时分秒 LCD_ShowNum(2,4,Min,2); LCD_ShowNum(2,7,Sec,2); } } void Timer0_Routine() interrupt 1 { static unsigned int T0Count; TL0 = 0x18; //设置定时初值 TH0 = 0xFC; //设置定时初值 T0Count++; if(T0Count>=1000) //定时器分频,1s { T0Count=0; Sec++; //1秒到,Sec自增 if(Sec>=60) { Sec=0; //60秒到,Sec清0,Min自增 Min++; if(Min>=60) { Min=0; //60分钟到,Min清0,Hour自增 Hour++; if(Hour>=24) { Hour=0; //24小时到,Hour清0 } } } } }
Timer0.c
#include <REGX52.H> /** * @brief 定时器0初始化,1毫秒@12.000MHz * @param 无 * @retval 无 */ void Timer0_Init(void) { TMOD &= 0xF0; //设置定时器模式 TMOD |= 0x01; //设置定时器模式 TL0 = 0x18; //设置定时初值 TH0 = 0xFC; //设置定时初值 TF0 = 0; //清除TF0标志 TR0 = 1; //定时器0开始计时 ET0=1; EA=1; PT0=0; }
Timer0.h
#ifndef __TIMER0_H__ #define __TIMER0_H__ void Timer0_Init(void); #endif
LCD1602.c
#include <REGX52.H> //引脚配置: sbit LCD_RS=P2^6; sbit LCD_RW=P2^5; sbit LCD_EN=P2^7; #define LCD_DataPort P0 //函数定义: /** * @brief LCD1602延时函数,12MHz调用可延时1ms * @param 无 * @retval 无 */ void LCD_Delay() { unsigned char i, j; i = 2; j = 239; do { while (--j); } while (--i); } /** * @brief LCD1602写命令 * @param Command 要写入的命令 * @retval 无 */ void LCD_WriteCommand(unsigned char Command) { LCD_RS=0; LCD_RW=0; LCD_DataPort=Command; LCD_EN=1; LCD_Delay(); LCD_EN=0; LCD_Delay(); } /** * @brief LCD1602写数据 * @param Data 要写入的数据 * @retval 无 */ void LCD_WriteData(unsigned char Data) { LCD_RS=1; LCD_RW=0; LCD_DataPort=Data; LCD_EN=1; LCD_Delay(); LCD_EN=0; LCD_Delay(); } /** * @brief LCD1602设置光标位置 * @param Line 行位置,范围:1~2 * @param Column 列位置,范围:1~16 * @retval 无 */ void LCD_SetCursor(unsigned char Line,unsigned char Column) { if(Line==1) { LCD_WriteCommand(0x80|(Column-1)); } else if(Line==2) { LCD_WriteCommand(0x80|(Column-1+0x40)); } } /** * @brief LCD1602初始化函数 * @param 无 * @retval 无 */ void LCD_Init() { LCD_WriteCommand(0x38);//八位数据接口,两行显示,5*7点阵 LCD_WriteCommand(0x0c);//显示开,光标关,闪烁关 LCD_WriteCommand(0x06);//数据读写操作后,光标自动加一,画面不动 LCD_WriteCommand(0x01);//光标复位,清屏 } /** * @brief 在LCD1602指定位置上显示一个字符 * @param Line 行位置,范围:1~2 * @param Column 列位置,范围:1~16 * @param Char 要显示的字符 * @retval 无 */ void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char) { LCD_SetCursor(Line,Column); LCD_WriteData(Char); } /** * @brief 在LCD1602指定位置开始显示所给字符串 * @param Line 起始行位置,范围:1~2 * @param Column 起始列位置,范围:1~16 * @param String 要显示的字符串 * @retval 无 */ void LCD_ShowString(unsigned char Line,unsigned char Column,char *String) { unsigned char i; LCD_SetCursor(Line,Column); for(i=0;String[i]!='