一、定时器
1. 定时器介绍
51单片机的定时器属于单片机的内部资源,其电路的连接和运转均在单片机内部完成。
定时器作用:
(1)用于计时系统,可实现软件计时,或者使程序每隔一固定时间完成一项操作
(2)替代长时间的Delay,提高CPU的运行效率和处理速度
……
定时器个数:3个(T0、T1、T2),T0和T1与传统的51单片机兼容,T2是此型号单片机增加的资源
注意:定时器的资源和单片机的型号是关联在一起的,不同的型号可能会有不同的定时器个数和操作方式,但一般来说,T0和T1的操作方式是所有51单片机所共有的
定时器在单片机内部就像一个小闹钟一样,根据时钟的输出信号,每隔一段时间,计数单元的数值就增加一,当计数单元数值增加到“设定的闹钟提醒时间”时,计数单元就会向中断系统发出中断申请,产生“响铃提醒”,使程序跳转到中断服务函数中执行。
2. 定时器工作原理
STC89C52的T0和T1均有四种工作模式:
模式0:13位定时器/计数器
模式1:16位定时器/计数器(常用)
模式2:8位自动重装模式
模式3:两个8位计数器
时钟给计数器 TL0 和 TH0 提供脉冲。每收到一个脉冲,TL0 和 TH0 就加 1 。当加到最大值(65535)时,计数器就会产生溢出,溢出之后,计数器回到 0 。计数器产生溢出后,会产生一个标志位 TF0 告诉中断系统,申请终端。
时钟有 2 个来源,一个是外部引脚T0 pin,一个是系统时钟SYSclk。
SYSclk:系统时钟,即晶振周期,本开发板上的晶振为12MHz
晶振是由压电陶瓷震动产生的固定频率。
C / T ‾ C/\overline{T} C/T 取 1 时是计数器(Counter),取 0 时是定时器(Timer)
3. 中断系统
中断系统是为使CPU具有对外界紧急事件的实时处理能力而设置的。
当中央处理器CPU正在处理某件事的时候外界发生了紧急事件请求,要求CPU暂停当前的工作,转而去处理这个紧急事件,处理完以后,再回到原来被中断的地方,继续原来的工作,这样的过程称为中断。实现这种功能的部件称为中断系统,请示CPU中断的请求源称为中断源。微型机的中断系统一般允许多个中断源,当几个中断源同时向CPU请求中断,要求为它服务的时候,这就存在CPU优先响应哪一个中断源请求的问题。通常根据中断源的轻重缓急排队,优先处理最紧急事件的中断请求源,即规定每一个中断源有一个优先级别。CPU总是先响应优先级别最高的中断请求。
当CPU正在处理一个中断源请求的时候(执行相应的中断服务程序),发生了另外一个优先级比它还高的中断源请求。如果CPU能够暂停对原来中断源的服务程序,转而去处理优先级更高的中断请求源,处理完以后,再回到原低级中断服务程序,这样的过程称为中断嵌套。这样的中断系统称为多级中断系统,没有中断嵌套功能的中断系统称为单级中断系统。
中断流程
STC89C52中断资源
中断源个数:8个(外部中断0、定时器0中断、外部中断1、定时器1中断、串口中断、定时器2中断、外部中断2、外部中断3)
中断优先级个数:4个
中断号:
定时器相关寄存器
二、按键控制LED流水灯模式
1. 初始化定时器0
(1)工作模式寄存器TMOD
想让定时器0以模式1(16位定时/计数)工作,得通过M1=0和M0=1选择工作模式,并通过 C / T ‾ C/\overline{T} C/T=0选择定时模式。然后让GATE=0,即TR0单独控制定时器工作。
所以TMOD应该等于0000 0001,16进制为0x01
(注意:可位寻址:可以单独赋值;不可位寻址:不可单独赋值,需要整体赋值。)
(2)控制寄存器TCON
TF0:是中断标志位,要置0
TR0:是运行控制位,要置1\
其他的位不管
(3)定时器TH0和TL0
定时器每隔1微秒+1,最大值为65535,总共定时时间为65535微妙。当设定为64535时,差值为1000,距离溢出还有1000微秒,即定时1毫秒。
所以TH0=64535/256
(取出高8位),TL0=64535%256
(取出低8位)
(4)中断器中的寄存器ET0、EA、PT0
要初始化中断允许控制寄存器、中断优先级控制寄存器。
为了接收到定时器的中断请求,需要ET0=1
和EA=1
若选择低优先级,则需要PT0=0
初始化代码如下:
void Timer0_Init()
{
TMOD = 0x01; // 0000 0001
TF0 = 0;
TR0 = 1;
TH0=64535/256;
TL0=64535%256;
ET0=1;
EA=1;
PT0=0;
}
2. 定时器0的中断函数
在中断号中可以看到定时器0的中断函数
在函数体内部编写中断后执行什么操作。
void Timer0_Rountine() interrupt 1
{
}
3. 中断后执行:D1每隔一秒闪烁
(1)完整代码
#include <REGX52.H>
void Timer0_Init()
{
TMOD = 0x01; // 0000 0001
TF0 = 0;
TR0 = 1;
TH0=64535/256;
TL0=64535%256;
ET0=1;
EA=1;
PT0=0;
}
void main()
{
Timer0_Init();
while(1)
{
}
}
unsigned int T0Count;
void Timer0_Rountine() interrupt 1
{
T0Count++;
TH0=64535/256; //重新赋初值,防止溢出后从0开始计数
TL0=64535%256;
if(T0Count>=1000) //每隔一秒执行一次
{
T0Count = 0;
P2_0 = ~P2_0;
}
}
编译后可以发现,主函数中并没有什么内容,但是D1却闪烁了。
(2)代码进化
TMOD = TMOD & 0xf0; //高四位不变,低四位清零
例:1010 0011 & 1111 0000 = 1010 0011(按位与)
TMOD = TMOD | 0X01; //高四位不变,最低位置1
例:1010 0000 | 0000 0001 = 1010 0001(按位或)
所以TMOD = 0x01; // 0000 0001
可以替换为:
TMOD &= 0xf0; //高四位不变,低四位清零
TMOD |= 0X01; //高四位不变,最低位置1
在STC-ISP中点击“定时器计算器”,系统频率选择12MHz,选择“定时器0”,定时长度设为“1毫秒”,定时器模式选择“16位”,定时器时钟选择“12T (FOSC/12)”。最后点击“生成C代码”。复制粘贴到Keil中。
89C52单片机没有16位自动重载模式,只有16位和8位自动重载。选择12T模式是因为下图:
若想选6T,则需在STC-ISP中给6T打勾。一般都用12T模式。
void Timer0Init(void) //1毫秒@12.000MHz
{
AUXR &= 0x7F; //定时器时钟12T模式
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //设置定时器模式
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
}
98C52没有AUXR寄存器,所以需要把AUXR &= 0x7F;
删掉。
我们发现,生成的代码没有中断系统的配置,需要我们手动加上。
void Timer0Init(void) //1毫秒@12.000MHz
{
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //设置定时器模式
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0=1;
EA=1;
PT0=0;
}
现在用计算器验证一下,刚刚我们自己设定的
TH0=64535/256; //重新赋初值,防止溢出后从0开始计数
TL0=64535%256;
和自动生成的代码是否一致
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
64535 ÷ 256 = 252…23
商数为252,余数为23
252的16进制是FC
23的16进制是17
发现TL0差一位,即差了一微秒65535-64535正好等于1000,差一位才溢出,但还没溢出。需要再+1
4. 将定时器模块化
新建Timer.c和Timer.h文件。
在Timer.c文件中写
#include <REGX52.H>
/**
* @brief 定时器0初始化,1毫秒@12.000MHz
* @param 无参数传入
* @retval 无返回值
*/
void Timer0Init(void) //1毫秒@12.000MHz
{
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //设置定时器模式
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0=1;
EA=1;
PT0=0;
}
/*
定时器中断函数模板
void Timer0_Rountine() interrupt 1
{
static unsigned int T0Count; //静态变量,退出函数后不丢失值
T0Count++;
TL0 = 0x18; //重新赋初值,防止溢出后从0开始计数
TH0 = 0xFC;
if(T0Count>=1000) //每隔一秒执行一次
{
T0Count = 0;
}
}
*/
在Timer.h文件中写
#ifndef __TIMER0__H__
#define __TIMER0__H__
void Timer0Init(void);
#endif
在main.c中写
#include <REGX52.H>
#include <Timer0.h>
void main()
{
Timer0Init();
while(1)
{
}
}
void Timer0_Rountine() interrupt 1
{
static unsigned int T0Count; //静态变量,退出函数后不丢失值
T0Count++;
TL0 = 0x18; //重新赋初值,防止溢出后从0开始计数
TH0 = 0xFC;
if(T0Count>=1000) //每隔一秒执行一次
{
T0Count = 0;
P2_0 = ~P2_0;
}
}
编译一下,发现D1一秒闪一下。
5. 按键控制流水灯
之前都是计时器的基础应用,现在进入正题。
新建Key.c和Key.h文件。去“6-1矩阵键盘”项目中把Delay.c和Delay.h文件复制到本项目。
然后把Delay.c和Delay.h文件添加到左侧边栏
在Delay.c里写获取独立按键的函数
#include <REGX52.H>
#include "Delay.h"
/**
* @brief 获取独立按键键码
* @param 无
* @retval 按下按键的键码,范围0~4,无按键按下时返回0
*/
unsigned char Key()
{
unsigned char KeyNumber=0;
if(P3_1==0){Delay(20);while(P3_1==0);Delay(20);KeyNumber=1;}
if(P3_0==0){Delay(20);while(P3_0==0);Delay(20);KeyNumber=2;}
if(P3_2==0){Delay(20);while(P3_2==0);Delay(20);KeyNumber=3;}
if(P3_3==0){Delay(20);while(P3_3==0);Delay(20);KeyNumber=4;}
return KeyNumber;
}
在Delay.h里先写上
#ifndef __KEY__H__
#define __KEY__H__
unsigned char Key();
#endif
然后,去main.c文件里加上#include "Key.h"
。
现在就可以调用刚刚在Delay.c里定义的函数Key()
了。
首先定义一个全局变量unsigned char KeyNum;
,然后再main()
函数里调用Key()
,即KeyNum = Key();
。
下面测试一下返回值是否正确(Key()函数定义是否正确)。
注意要把刚才写的和定时器有关的代码全都注释掉,单独测试Key()函数。
在main()函数里写
unsigned char KeyNum;
void main()
{
//Timer0Init();
while(1)
{
KeyNum = Key();
if(KeyNum)
{
if(KeyNum==1)P2_1=~P2_1;
if(KeyNum==2)P2_2=~P2_2;
if(KeyNum==3)P2_3=~P2_3;
if(KeyNum==4)P2_4=~P2_4;
}
}
}
把之前写的void Timer0_Rountine() interrupt 1
注释掉。
编译一下,可以看到按下K1后D2灯亮,再按一下D2灯灭;可以看到按下K2后D3灯亮,再按一下D3灯灭……
在main.c中写入#include "INTRINS.h"
,是为了调用循环左移_crol_
和循环右移_cror_
函数。
#include <REGX52.H>
#include "Timer0.h"
#include "Key.h"
#include "INTRINS.h"
unsigned char KeyNum, LEDMode;
void main()
{
P2=0xFE;
Timer0Init();
while(1)
{
KeyNum = Key();
if(KeyNum)
{
if(KeyNum==1)
{
LEDMode++;
if(LEDMode>=2)LEDMode=0;
}
}
}
}
void Timer0_Rountine() interrupt 1
{
static unsigned int T0Count; //静态变量,退出函数后不丢失值
T0Count++;
TL0 = 0x18; //重新赋初值,防止溢出后从0开始计数
TH0 = 0xFC;
if(T0Count>=1000) //每隔一秒执行一次
{
T0Count = 0;
if(LEDMode==0)P2=_crol_(P2, 1);
if(LEDMode==1)P2=_cror_(P2, 1);
}
}
编译一下,可以看到,按下K1后,LED模块开始循环向左移(D8->D1),按下K2后,LED模块开始循环向右移(D1->D8)。
三、定时器时钟
1. 复制粘贴之前的模块化文件
新建项目“7-2 定时器时钟”和main.c。
把“5-2 LCD1602调试工具”项目文件夹的Delay.c, Delay.h, LCD1602.c, LCD1602.h文件复制粘贴到“7-2 定时器时钟”项目路径中。
把“7-1 按键控制LED流水灯模式”路径中的Timer0.c和Timer0.h文件复制粘贴到“7-2 定时器时钟”项目路径中。
别忘了要在main.c中加上相应的头文件
#include <REGX52.H>
#include "LCD1602.h"
#include "Delay.h"
#include "Timer0.h"
调用LCD1602中的函数
void main()
{
LCD_Init();
LCD_ShowString(1, 1, "Clock: ");
while(1)
{
}
}
先编译一下,看看有没有错误。
可以看到没有错误。
2. 先做个秒钟
#include <REGX52.H>
#include "LCD1602.h"
#include "Delay.h"
#include "Timer0.h"
unsigned char Sec;
void main()
{
LCD_Init(); //LCD初始化
Timer0Init(); //定时器初始化
LCD_ShowString(1, 1, "Clock: ");
while(1)
{
LCD_ShowNum(2, 1, Sec, 2);
}
}
void Timer0_Rountine() interrupt 1
{
static unsigned int T0Count; //静态变量,退出函数后不丢失值
T0Count++;
TL0 = 0x18; //重新赋初值,防止溢出后从0开始计数
TH0 = 0xFC;
if(T0Count>=1000) //每隔一秒执行一次
{
T0Count = 0;
Sec++;
}
}
编译一下,发现没有问题。
3. 加入分钟和小时
#include <REGX52.H>
#include "LCD1602.h"
#include "Delay.h"
#include "Timer0.h"
unsigned char Sec=55, Min=59, Hour=23;
void main()
{
LCD_Init(); //LCD初始化
Timer0Init(); //定时器初始化
LCD_ShowString(1, 1, "Clock: ");
LCD_ShowString(2, 3, ":");
LCD_ShowString(2, 6, ":");
while(1)
{
LCD_ShowNum(2, 1, Hour, 2);
LCD_ShowNum(2, 4, Min, 2);
LCD_ShowNum(2, 7, Sec, 2);
}
}
void Timer0_Rountine() interrupt 1
{
static unsigned int T0Count; //静态变量,退出函数后不丢失值
T0Count++;
TL0 = 0x18; //重新赋初值,防止溢出后从0开始计数
TH0 = 0xFC;
if(T0Count>=1000) //每隔一秒执行一次
{
T0Count = 0;
Sec++;
if(Sec>=60)
{
Sec=0;
Min++;
if(Min>=60)
{
Min=0;
Hour++;
if(Hour>=24)
{
Hour=0;
}
}
}
}
}
编译一下,可以发现时分秒都有了。