【51单片机】6. 定时器、按键切换流水灯时钟Demo
1. 定时器
-
定义:51单片机定时器属于单片机内部资源,电路的连接和运转均在单片机内部完成
-
作用:
-
用作计时系统,实现软件计时,或使程序隔固定时间完成一项操作
-
替代长时间的Delay,提高CPU运行效率和处理速度
-
举例子,在写流水灯代码的时候,置状态→Delay→置状态→Delay→...
,假如想要边流水灯,边使用按键控制流水灯就会很困难,因为Delay的时候会占用所有的CPU,导致无法同时进行其他操作。
-
原理:
-
定时器在单片机内部像一个小闹钟,根据时钟的输出信号,每隔“一秒”,计数单元数值增加一
-
当计数单元数值增加到“设定的闹钟提醒时间”时,计数单元就会向中断系统发出中断申请,产生“响铃提醒”
-
从而程序跳转到中断服务函数中执行
-
2. 常用的定时模式(16位定时器/计数器)
-
Sysclk:系统时钟,即晶振周期,使用这个就是定时器
- Sysclk有
÷12
和÷6
,这个表示的是12分频或6分频。江科大视频里的单片机是12MHz
的,如果选择÷12
就得到输出的频率是1MHz
,一个周期就表示1μs
的时间,如果选择÷6
就得到2MHz
,则是2μs
- Sysclk有
-
T0 Pin:由单片机外部引脚提供时,单片机的定时器就是计数器
-
其中, C / T ‾ C/\overline{T} C/T表示选下(计数器Counter)还是选上(定时器Timer):
=0
表示选择上面的定时器,=1
表示选择下面的计数器 -
下面向右旋转90°的
△
表示非门,GATE
输入为0,经非门为1 -
最靠近非门的带有轻微弯曲的,还连接了 I N T 0 ‾ \overline{INT0} INT0的表示或门
-
靠近或门的直的、还连接了 T R 0 TR_0 TR0的表示与门
-
TL0
表示Time Low,TH0
表示Time High,就是低位和高位的区别,TL0和TH0连起来就是16位的计数器,每次+1+1,作为定时器使用,到达上限65536后触发中断
3. 中断系统
这是计算机组成原理里边比较基础的一个内容,这里仅粘贴一些概念:
-
中断系统是为使CPU具有对外界紧急事件的实时处理能力而设置的
-
CPU正处理某件事时外界发生了紧急事件请求,要求CPU暂停当前工作,转而去处理紧急事件,处理完后,再回到原来被中断的地方继续工作,则称为中断
-
微型机中断系统允许多个中断源,多个中断源同时向CPU请求中断,要求为它服务时,CPU优先响应哪一个中断源请求,通常根据中断源轻重缓急优先处理最紧急的中断请求源。即每隔中断源有一个优先级别,CPU总是优先响应优先级别最高的中断请求。
-
CPU在处理一个中断请求时,发生了另一个优先级更高的中断请求,CPU能够暂停对原来中断源的服务,转而去处理高优先级的中断请求源,处理完后再回到原低级中断服务程序,这样的过程称为中断嵌套,这样的中断系统称为多级中断系统。而没有中断嵌套功能的中断系统称为单级中断系统
4. 定时器、中断相关寄存器
单片机通过配置寄存器来控制内部线路的连接。
定时器/计数器与中断相关的寄存器如下所示:
只需要给这些寄存器赋对应的值,就可以控制电路进行工作。
以寄存器电路图为例:
这其中,根据单片机相关说明书:
-
EA:CPU总中断允许控制位,EA=1,CPU开放中断,EA=0,CPU屏蔽所有中断申请。EA的作用是使中断允许形成两级控制。即各中断源首先受EA控制;其次还受各中断源自己的中断允许控制位控制。
-
EX0/EX1,ET0/1:外部中断0/1中断允许位。为1时允许中断,为0禁止中断
-
PX0/1,PT0/1则是控制中断优先级的:为0低优先级,为1高优先级
5. 初始化定时器
配置可以参考STC89C52手册里面中断和定时器/计数器相关的内容。需要配置TCON和TMOD。
定时器相关位如下:
M1和M0共同决定工作在那种模式:
-
M1 M0 模式
-
0 0 13位定时器/计数器
-
0 1 16位定时器/计数器(常用)
-
1 0 8位自动重装载定时器
-
1 1 定时器0此时作为双8位定时器/计数器
C / T ‾ C/\overline{T} C/T给0就是定时器模式,给1就是计数器模式
GATE门控端给0就是TR_0
单独控制
TMOD = 0x01;
PS: 不可位寻址表示必须整体赋值,不能单个赋值;可位寻址就是可以整体赋值也可以单个赋值。
但上面这种配置会有一个问题,可能原本有其他程序在使用高4位的内容,这样一配置就会覆盖了高4位,所以换成如下方式:
TMOD &= 0xF0; // TMOD低四位清零,高四位不变
TMOD |= 0x01; // TMOD最低位置1,高位不变
TF0是定时器/计数器T0溢出中断标志。置1表示向CPU请求中断。
TF0 = 0;
TR0是定时器运行控制位,TR0=1时允许T0开始计数,TR0=0时禁止T0计数。
TR0 = 1;
IE0:IE0=1表示外部中断0向CPU请求中断,CPU响应外部中断时由硬件将IE0置为0。
IT0:外部中断0。
PS:IE0和IT0用于管GATE那一部分的,但是GATE已经置0了,此时不是从外面输入的无需理会。
6. 定时器进一步初始化
频率f
为12MHz,定时器时钟为12T,根据公式 T = 1 f T=\frac{1}{f} T=f1,有 12 T = 1 12 × 10 6 H z 12T=\frac{1}{12×10^6Hz} 12T=12×106Hz1,即1μs计时一次
定时器每次+1表示1μs,作为16位定时器,总共能计65536μs,也就是65ms。
想要能够计到1s,唯一的办法是每计1ms就触发中断来重置定时器,重复1000次后就是1s。
为了实现 “每计1ms就触发中断来重置定时器”,我们需要给TL0和TH0置不同的数:
也就是从64536开始计数,我们要给TH0和TL0一共赋上64536的值。通过除法和求余可以取出高8位值和低8位值:
TH0 = 64536 / 256;
TL0 = 64536 % 256;
以上定时器的位置,也可以通过stc-isp这个软件来快速初始化:
上图选的有点问题,应该选16位而不是16位自动重载
7. 中断部分初始化
实际上就是将4中的中断通路给打通,等待中断到来时,发起中断,观察4中的图,实际上需要处理的寄存器包括:
-
ET0
-
EA
-
PT0(IP,中断优先级)
ET0=1;
EA=1;
PT0=0;
8. 用定时器,让LED灯1s亮1次
#include <REGX52.H>void Timer0_Init(void) //1毫秒@11.0592MHz
{// 定时器初始化TMOD &= 0xF0; //设置定时器模式TMOD |= 0x01; //设置定时器模式TL0 = 0x18; //设置定时初值TH0 = 0xFC; //设置定时初值TF0 = 0; //清除TF0标志TR0 = 1; //定时器0开始计时// 中断初始化ET0=1;EA=1;PT0=0; // 优先级设置
}unsigned int timeCount = 0;
void Timer0_Rountine() interrupt 1 //定时器0中断函数,定时器计了1000次1μs为1ms
{TL0 = 0x18; //设置定时初值TH0 = 0xFC; //设置定时初值timeCount++; // 统计计数次数if (timeCount == 1000) // 产生1000次中断,即timeCount计了1000次为1s{timeCount = 0; // 重置计数P2_0 = ~P2_0; // 亮灯取反}
}void main()
{Timer0_Init();while (1){}
}
实际上我的板子是11.0592MHz,这样速度可能会比1s快,推理如下:
选择12MHz & 12T → 1μs计时一次,1ms = 1000μs,计1000次得1ms,while循环反复1000得1s
选择11.0592MHz & 12T → 0.922μs计时一次,1ms = 1000μs,计1000次得0.922ms,while循环反复1000得0.92s
整体的思路就是:
-
上电时初始化定时器及中断的寄存器(Timer0_Init()函数)
-
写中断函数,处理自己想要的流程
9. 利用定时器实现按钮控制LED流水灯向左/向右闪
主函数:
#include <REGX52.H>
#include <INTRINS.H>
#include "Timer0.h"
#include "Key.h"unsigned char keyNum,LEDMode = 0; // 表示获取的keyNum,以及当前流水灯所处状态void main()
{P2 = 0xFE; // 流水灯初始状态,第1个灯亮Timer0_Init(); // 上电,定时器初始化while (1){keyNum = key(); // 获取keyNumif (keyNum) // 如果keyNum不等于0,判断是否需要转变模式{if (keyNum == 1) // 如果按下按键{LEDMode++; // LED模式改变if (LEDMode == 2) LEDMode = 0; // 达到2时变回1}}}
}unsigned int timeCount = 0;
void Timer0_Rountine() interrupt 1 //定时器0中断函数,定时器计了1000次1μs为1ms
{TL0 = 0x18; //设置定时初值TH0 = 0xFC; //设置定时初值timeCount++; // 统计计数次数if (timeCount == 500) // 产生1000次中断,即timeCount计了1000次为1s{timeCount = 0; // 重置计数if (LEDMode == 0) P2 = _crol_(P2,1); // INTRINS.H封装的好用函数,左移1位,溢出会自动判断并回去else P2 = _cror_(P2,1); // INTRINS.H封装的好用函数,右移1位,溢出会自动判断并回去}
}
Timer0.c实际上就是封装了初始化定时器0的函数:
#include <REGX52.H>/*** @brief 定时器0初始化,1毫秒@11.0592MHz* @param 无* @retval 无*/
void Timer0_Init(void) //1毫秒@11.0592MHz
{// 定时器初始化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 //定时器0中断函数,定时器计了1000次1μs为1ms
{static unsigned int timeCount = 0;TL0 = 0x18; //设置定时初值TH0 = 0xFC; //设置定时初值timeCount++; // 统计计数次数if (timeCount == 1000) // 产生1000次中断,即timeCount计了1000次为1s{timeCount = 0; // 重置计数}
}
*/
Timer0.h:
#ifndef __TIMER0_H__
#define __TIMER0_H__void Timer0_Init(void);#endif
Key.c就是检测按下的按键键值:
#include <REGX52.H>
#include "Delay.h"/*** @brief 获取独立按键键码* @param 无* @retval */
unsigned char key()
{unsigned char currKey = 0;if (P3_1 == 0) {Delay(20); while(P3_1 == 0); Delay(20); currKey = 1;}if (P3_0 == 0) {Delay(20); while(P3_0 == 0); Delay(20); currKey = 2;}if (P3_2 == 0) {Delay(20); while(P3_2 == 0); Delay(20); currKey = 3;}if (P3_3 == 0) {Delay(20); while(P3_3 == 0); Delay(20); currKey = 4;}return currKey;
}
Key.h:
#ifndef __KEY_H__
#define __KEY_H__unsigned char key();#endif
Delay.c是很久之前写的延时:
#include <INTRINS.H>void Delay(unsigned int ms) //@11.0592MHz
{unsigned char i, j;while (ms){_nop_();i = 2;j = 199;do{while (--j);} while (--i);ms--;}
}
Delay.h:
#ifndef __DELAY_H__
#define __DELAY_H__void Delay(unsigned int ms);#endif
当然也可以自己试着写一下左右移1位的函数:
#include <REGX52.H>
#include <INTRINS.H>
#include "Timer0.h"
#include "Key.h"unsigned char keyNum,LEDMode = 0; // 表示获取的keyNum,以及当前流水灯所处状态
unsigned char bitMoveL(unsigned char P);
unsigned char bitMoveR(unsigned char P);void main()
{P2 = 0xFE; // 流水灯初始状态,第1个灯亮Timer0_Init(); // 上电,定时器初始化while (1){keyNum = key(); // 获取keyNumif (keyNum) // 如果keyNum不等于0,判断是否需要转变模式{if (keyNum == 1) // 如果按下按键{LEDMode++; // LED模式改变if (LEDMode == 2) LEDMode = 0; // 达到2时变回1}}}
}unsigned int timeCount = 0;
void Timer0_Rountine() interrupt 1 //定时器0中断函数,定时器计了1000次1μs为1ms
{TL0 = 0x18; //设置定时初值TH0 = 0xFC; //设置定时初值timeCount++; // 统计计数次数if (timeCount == 500) // 产生1000次中断,即timeCount计了1000次为1s{timeCount = 0; // 重置计数if (LEDMode == 0) P2 = bitMoveL(P2); // INTRINS.H封装的好用函数,左移1位,溢出会自动判断并回去else P2 = bitMoveR(P2); // INTRINS.H封装的好用函数,右移1位,溢出会自动判断并回去}
}unsigned char bitMoveL(unsigned char P)
{return P == 0x7F? 0xFE : ~((~P) << 1);
}unsigned char bitMoveR(unsigned char P)
{return P == 0xFE? 0x7F : ~((~P) >> 1);
}
注意:如果这里给的类型都是unsigned int的话,右移就会出现问题,因为unsigned int是16位的,右移可能会导致高8位部分移动过来出现一些不合理的情况
另附结果,注意:由于LED灯位置的设计,我们做左移操作时,板子上显示的是右移;我们做右移操作时,板子上显示的结果是左移。
按键切换流水灯模式
左移:
省去中间一部分…跳到D7
右移:
10. 定时器显示时钟
main.c
#include <REGX52.H>
#include "Timer0.h"
#include "Delay.h"
#include "LCD1602.h"unsigned char hours = 0;
unsigned char minutes = 0;
unsigned char seconds = 0;void main()
{//上电初始化LCD_Init();Timer0_Init();LCD_ShowString(1,1,"Clock:");LCD_ShowNum(2,1,hours,2);LCD_ShowString(2,3,":");LCD_ShowNum(2,4,minutes,2);LCD_ShowString(2,6,":");LCD_ShowNum(2,7,seconds,2);while(1){}
}void Timer0_Rountine() interrupt 1 //定时器0中断函数,定时器计了1000次1μs为1ms
{static unsigned int timeCount = 0;TL0 = 0x18; //设置定时初值TH0 = 0xFC; //设置定时初值timeCount++; // 统计计数次数if (timeCount == 1000) // 产生1000次中断,即timeCount计了1000次为1s{timeCount = 0; // 重置计数// 更新计时seconds++; // 每1s,秒针+1if (seconds == 60) // 当到60s时{minutes++; // 分针+1seconds = 0; // 重置秒针if (minutes == 60) // 当到60min时{hours++; // 时针+1minutes = 0; // 重置分针if (hours == 24) // 当到24h时{hours = 0; // 重置时针}LCD_ShowNum(2,1,hours,2); // 显示时针}LCD_ShowNum(2,4,minutes,2); // 显示分针}LCD_ShowNum(2,7,seconds,2); // 显示秒数}
}
Timer0.c
、Timer0.h
、Delay.c
、Delay.h
的代码和9中的一样
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]!='\0';i++){LCD_WriteData(String[i]);}
}/*** @brief 返回值=X的Y次方*/
int LCD_Pow(int X,int Y)
{unsigned char i;int Result=1;for(i=0;i<Y;i++){Result*=X;}return Result;
}/*** @brief 在LCD1602指定位置开始显示所给数字* @param Line 起始行位置,范围:1~2* @param Column 起始列位置,范围:1~16* @param Number 要显示的数字,范围:0~65535* @param Length 要显示数字的长度,范围:1~5* @retval 无*/
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{unsigned char i;LCD_SetCursor(Line,Column);for(i=Length;i>0;i--){LCD_WriteData(Number/LCD_Pow(10,i-1)%10+'0');}
}/*** @brief 在LCD1602指定位置开始以有符号十进制显示所给数字* @param Line 起始行位置,范围:1~2* @param Column 起始列位置,范围:1~16* @param Number 要显示的数字,范围:-32768~32767* @param Length 要显示数字的长度,范围:1~5* @retval 无*/
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length)
{unsigned char i;unsigned int Number1;LCD_SetCursor(Line,Column);if(Number>=0){LCD_WriteData('+');Number1=Number;}else{LCD_WriteData('-');Number1=-Number;}for(i=Length;i>0;i--){LCD_WriteData(Number1/LCD_Pow(10,i-1)%10+'0');}
}/*** @brief 在LCD1602指定位置开始以十六进制显示所给数字* @param Line 起始行位置,范围:1~2* @param Column 起始列位置,范围:1~16* @param Number 要显示的数字,范围:0~0xFFFF* @param Length 要显示数字的长度,范围:1~4* @retval 无*/
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{unsigned char i,SingleNumber;LCD_SetCursor(Line,Column);for(i=Length;i>0;i--){SingleNumber=Number/LCD_Pow(16,i-1)%16;if(SingleNumber<10){LCD_WriteData(SingleNumber+'0');}else{LCD_WriteData(SingleNumber-10+'A');}}
}/*** @brief 在LCD1602指定位置开始以二进制显示所给数字* @param Line 起始行位置,范围:1~2* @param Column 起始列位置,范围:1~16* @param Number 要显示的数字,范围:0~1111 1111 1111 1111* @param Length 要显示数字的长度,范围:1~16* @retval 无*/
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{unsigned char i;LCD_SetCursor(Line,Column);for(i=Length;i>0;i--){LCD_WriteData(Number/LCD_Pow(2,i-1)%2+'0');}
}
LCD1602.h:
#ifndef __LCD1602_H__
#define __LCD1602_H__//用户调用函数:
void LCD_Init();
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char);
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String);
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length);
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);#endif
效果如下:
定时器实现时钟
江科大的代码是将LCD_ShowNum的所有内容放在While循环里不断刷新:
#include <REGX52.H>
#include "Timer0.h"
#include "Delay.h"
#include "LCD1602.h"unsigned char hours = 23;
unsigned char minutes = 59;
unsigned char seconds = 55;void main()
{//上电初始化LCD_Init();Timer0_Init();LCD_ShowString(1,1,"Clock:");LCD_ShowNum(2,1,hours,2);LCD_ShowString(2,3,":");LCD_ShowNum(2,4,minutes,2);LCD_ShowString(2,6,":");LCD_ShowNum(2,7,seconds,2);while(1){LCD_ShowNum(2,1,hours,2); // 显示时针LCD_ShowNum(2,4,minutes,2); // 显示分针LCD_ShowNum(2,7,seconds,2); // 显示秒数}
}void Timer0_Rountine() interrupt 1 //定时器0中断函数,定时器计了1000次1μs为1ms
{static unsigned int timeCount = 0;TL0 = 0x18; //设置定时初值TH0 = 0xFC; //设置定时初值timeCount++; // 统计计数次数if (timeCount == 1000) // 产生1000次中断,即timeCount计了1000次为1s{timeCount = 0; // 重置计数// 更新计时seconds++; // 每1s,秒针+1if (seconds == 60) // 当到60s时{minutes++; // 分针+1seconds = 0; // 重置秒针if (minutes == 60) // 当到60min时{hours++; // 时针+1minutes = 0; // 重置分针if (hours == 24) // 当到24h时{hours = 0; // 重置时针}}}}
}
我是把赋值语句全写在中断里面了, 我个人感觉应该是差不多的。但是,问题是:不知道赋值语句会不会耗时长从而导致时钟计数不准:
-
不会的话,我觉得我的方法更好一些,必要时候才赋值,不用一直刷新
-
会的话,江科大的方法更好