51-定时器与按键控制LED流水灯模式&定时器时钟
一、定时器
按键(以独立按键为例)控制LED流水灯模式:
在按键控制LED流水灯模式中,如果仅仅简单的把独立按键与LED流水灯拼接起来,则会出现一些问题:在LED流水灯的代码中会有长时间的Delay,此时按键检测就会很不灵敏:按下时不灵敏,需要一些时间才能被检测到,按下后再松手时,也不灵敏,也需要一些时间才能被检测到,本节课则通过使用定时器来让按键控制LED流水灯模式,就可以解决这个问题、
51单片机定时器内部工作原理:
计数单元有好多种连接方式,不是只能单纯的加一,还可以做一些其他的操作:
在上图中,整体组成了51单片机的定时器,其中TR0控制第三个开关的闭合从而控制定时器的启动和暂停、
此时,该定时器就可以看做是一个计数器,每来一个脉冲(计数脉冲)就会加1(以加1为例),此处的溢出就可以看作是中断源、
若GATE的值为0,则第三个开关的闭合取决于TR0,若GATE的值为1,则第三个开关的闭合取决于TR0和INTO杠(单片机的外部中断引脚);上图中,三角号加圆圈代表非门,若左边给1,则右边为0,若左边给0,则右边为1;月牙代表或门, 有1即得1,无1则为0;门框代表与门,有0即得0,没0则为1,这些知识在数电中有讲解、
除了定时器(整体)需要中断系统,串口和单片机的 IO 口也都需要中断系统,更高级的单片机中的很多设备都需要中断系统,中断系统会与单片机内部的设备打交道,也会与很多单片机外部的设备打交道;注意:此处若说定时器,则指的是定时器整体,若说定时器的中断系统,指的则是定时器整体中的定时器的中断系统这一部分、
在一定程度上而言,可以"同时"完成两项任务、
定时器(整体)离开中断系统也是可以工作的,只不过一般来说,定时器(整体)与中断系统是不分开的、
其中:不可位寻址的寄存器只能整体对寄存器赋值,不可对该寄存器中的每一位单独进行赋值;可位寻址的寄存器可对该寄存器中的每一位单独进行赋值,也可以整体对该寄存器赋值、
二、按键控制LED流水灯模式&定时器时钟
//main.c
#include <REGX52.H>
#include "Timer0.h"
#include "Key.h"
#include <INTRINS.H>
//全局变量、
unsigned char KeyNum,LEDMode; //char KeyNum,LEDMode;
void main()
{
P2=0xFE;
Timer0Init();
while(1)
{
KeyNum=Key();
if(KeyNum)
{
if(KeyNum==1)
{
LEDMode++;
if(LEDMode>=2)LEDMode=0;
}
}
}
//此时while(1)死循环中的内容不能放到中断子程序中的原因是:若将while(1)死循环中的内容
//放到中断子程序中的话,如果我们按住任意某个独立按键不松手,就相当于是在中断子程序中执行时间
//较长的任务,即使我们按下任意某个独立按键,紧接着松开手,而单片机的速度非常快,因此也相当于在
//中断子程序中执行时间较长的任务,所以在此处不能将while(1)死循环中的内容放到中断子程序中,否
//则会出现问题、
//由定时器0产生的所有中断(本质上是同一个中断)的优先级是一样的,在这些中断中,若前一个中
//断还未执行结束或还未执行,若下一个中断产生的话,此时该下一个中断就会进行排队,当该下一个中断
//的前面所有的中断都全部执行完毕后,会直接进入该下一个中断,不会推进主程序,并且,当该下一
//个中断产生时,若前一个中断还未执行结束或还未执行,此时不会先执行该下一个中断,这是因为,由定
//时器0生成的所有中断(本质上是同一个中断)的优先级是一样的,但如果下一个中断的优先级较高,则
//即使前一个中断还未执行结束或还未执行,当下一个优先级较高的中断产生时,此时就直接去执行这个
//优先级较高的中断,等这个优先级较高的中断执行完毕后,再返回到这里所谓的前面这个正在执行但还
//未执行完毕的中断,这个过程就叫做中断嵌套、
}
//中断服务程序(中断子程序)、
//中断服务程序(中断子程序)一般都放在main.c源文件中,但不能放在某一个函数体内部,可以放在所有
//函数体外部的任意位置处、
void Timer0_Routine(void) interrupt 1
{
static unsigned int T0Conut; //静态局部变量不初始化则默认为0、
//设置为静态局部变量的原因是让该变量的作用域只在该函数体内有效、
TL0 = 0x18 ; //设置定时器初值、
TH0 = 0xFC; //设置定时器初值、
T0Conut++;
if(T0Conut>=500) //0.5s - 500ms
{
T0Conut=0;
if(LEDMode==0)
{
P2=_crol_(P2,1); //#include <INTRINS.H>
}
if(LEDMode==1)
{
P2=_cror_(P2,1); //#include <INTRINS.H>
}
}
}
//其中:_crol_函数的作用是 循环 左移,_cror_函数的作用是 循环 右移、
//以二进制序列1111 1110为例:
//_crol_函数:将左移出去的最高位放到最低位上、
//1111 1110
//1111 1101
//1111 1011
//1111 0111
//1110 1111
//1101 1111
//1011 1111
//0111 1111
//1111 1110
//_cror_函数:将右移出去的最低位放到最高位上、
//1111 1110
//0111 1111
//1011 1111
//1101 1111
//1110 1111
//1111 0111
//1111 1011
//1111 1101
//1111 1110
//Delay.h
//保证头文件被某一个源文件多次包含时不会出现某些问题、
#ifndef __DELAY_H__
#define __DELAY_H__
//延时子函数的声明、
void Delay(unsigned int xms);
#endif
//Delay.c
//延时子函数的定义(可以理解成声明和定义)、
void Delay(unsigned int xms) //@12.000MHz
{
unsigned char i, j;
while(xms--)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}
}
//Key.h
//保证头文件被某一个源文件多次包含时不会出现某些问题、
#ifndef __KEY_H__
#define __KEY_H__
//获取按下并松开的独立按键对应的键码-子函数的声明、
unsigned char Key(); //char Key()
#endif
//Key.c
#include <REGX52.H>
#include "Delay.h"
//获取按下并松开的独立按键对应的键码-子函数的定义(可以理解成声明和定义)、
unsigned char Key() //char Key()
{
unsigned char KeyNumber=0; //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;
}
//Timer0.h
//保证头文件被某一个源文件多次包含时不会出现某些问题、
#ifndef __TIMER0_H__
#define __TIMER0_H__
void Timer0Init(void);//寄存器配置-子函数的声明、
#endif
//Timer0.c
#include <REGX52.H> //寄存器的声明都包含在了头文件:#include <REGX52.H>中、
//以定时器0为例,定时器0初始化、
//寄存器配置-子函数的定义(可以理解成声明和定义)、
void Timer0Init(void) //1ms@12.000MHz
{
//具体含义见STC89C52使用手册、
//生成的C代码针对于某些含有寄存器AUXR的单片机,
//而STC89C52单片机中不存在寄存器AUXR,但是定时器时钟已经是12Tmode模式了,所以可以删掉下面这条语句、
//AUXR &= 0x7F; //定时器时钟12T模式、
//定时器0对应的GATE(门控端)值设置为0、
TMOD &= 0xF0; //设置定时器模式、//寄存器TMOD对应的二进制序列中的低四位清零,高四位不变、
TMOD |= 0x01; //设置定时器模式、//寄存器TMOD对应的二进制序列中的最低位置为1,高四位不变、
//到达溢出时,最多记录65536us、
//离溢出差1000us,即差1ms、
//TH0=64536/256;
//TL0=64536%256;
TL0 = 0x18 ; //设置定时器初值、
TH0 = 0xFC; //设置定时器初值、
TF0 = 0; //清除TF0标志、
//若设置为1,则直接就会向CPU请求中断,因此设置为0,防止刚配置好TF0,就直接向CPU请求中断、
//此处不写这一条语句也是可以的(记住即可),但是最好要写上,因为清除TF0标志默认为0、
TR0 = 1; //定时器0开始计时、
//由于定时器0中的GATE为0,则该定时器是否能够正常工作则取决于TR0,我们要让该定时器正常
//工作所以赋值TR0为1、
//由于定时器0对应的GATE的值为0,因此不需要配置IE0和IT0,这两个是用来配置单片机的外部中断引脚INTO杠的、
//生成的C代码中缺少如下内容,加上即可:
ET0=1;
EA=1;
PT0=0; //可以不写,但是最好写上、
//默认PT0的值为0,则此处可以不写这一条语句,但最好还是要写上、
}
/*
//定时器中断函数模板、
//中断服务程序(中断子程序)、
//中断服务程序(中断子程序)一般都放在main.c源文件中,但不能放在某一个函数体内部,可以放在所有
//函数体外部的任意位置处、
void Timer0_Routine(void) interrupt 1
{
static unsigned int T0Conut; //静态局部变量不初始化则默认为0、
//设置为静态局部变量的原因是让该变量的作用域只在该函数体内有效、
T0Conut++;
if(T0Conut>=1000) //1s
{
T0Conut=0;
}
TL0 = 0x18 ; //设置定时器初值、
TH0 = 0xFC; //设置定时器初值、
}
*/
//main.c
#include <REGX52.H>
#include "Timer0.h"
#include "LCD1602.h"
//全局变量、
unsigned char Sec=56,Min=59,Hour=23;
void main()
{
LCD_Init();
Timer0Init();
//当执行完调用函数Timer0Init();后,经过1ms就会进入中断子程序,但是在进入中断子程序之前
//,还未在LCD1602液晶显示屏上面打印出Clock:,若把while(1)死循环中的内容放到中断子程序中
//的话,由于执行调用函数LCD_Num,则需要花费较长的时间,则会导致出现一些问题,而我们要保证中断
//子程序中不能执行时间较长的任务,只能执行一些时间较短的任务,否则就会出现问题;因此我们不能把
//while(1)死循环中的内容放到中断子程序中,否则就会出现问题、
//由于执行完调用函数LCD_ShowString()或调用函数LCD_ShowNum()需要花费较长的时间,所
//以在此期间,可能多次进入并退出中断子程序、
//上电显式静态字符串、
LCD_ShowString(1,1,"Clock:");
LCD_ShowString(2,1," : :");
while(1)
{
LCD_ShowNum(2,1,Hour,2);
LCD_ShowNum(2,4,Min,2);
LCD_ShowNum(2,7,Sec,2);
}
}
//中断服务程序(中断子程序)、
//中断服务程序(中断子程序)一般都放在main.c源文件中,但不能放在某一个函数体内部,可以放在所有
//函数体外部的任意位置处、
void Timer0_Routine(void) interrupt 1
{
static unsigned int T0Conut; //静态局部变量不初始化则默认为0、
//设置为静态局部变量的原因是让该变量的作用域只在该函数体内有效、
TL0 = 0x18 ; //设置定时器初值、
TH0 = 0xFC; //设置定时器初值、
//在keil5中写代码时,在某一个函数体中写代码时,必须将所有的局部变量的声明和定义(声明和
//定义不分开)放在函数体的最开头,这是因为,keil5中的gcc编译器使用的C语言标准是C99以下的标准
//,而C99以下的标准规定必须将所有的局部变量的声明和定义(声明和定义不分开写)放到函数体的最
//前面,而在C99及其更新的标准和C++中取消了这一限制;还有一点要注意:虽然keil5是在Windows系
//统下的,但是并不和Windows下的gcc版本一样,keil5中的gcc编译器使用的C语言标准是C99以下的
//标准,可以理解成一个特例、
//由于中断子程序很快就能执行完毕,因此将设置定时器的初值的两条语句放在该中断子程序中所
//有的局部变量的声明和定义(声明和定义不分开)以下,并且在if语句之外的任意位置处都是可以的,在
//这些范围内,这两条语句可以分开写,但最好不要分开;如果非要考虑执行该中断子程序需要花费时间
//的话,则最好将两条语句一块放到所有的局部变量的声明和定义(声明和定义不分开)的下面(紧靠着),
//但这个时间可以忽略不计,因此,将设置定时器的初值的两条语句一块放在该中断子程序中所有的局部
//变量的声明和定义(声明和定义不分开)以下,并且在if语句之外的任意位置处都是可以的,
T0Conut++;
if(T0Conut>=1000) //1s
{
T0Conut=0;
Sec++;
if(Sec>=60)
{
Sec=0;
Min++;
if(Min>=60)
{
Min=0;
Hour++;
if(Hour>=24)
{
Hour=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
//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(char Line,char Column, unsigned char Char)
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(char Line,char Column,unsigned char *String)
//若在Vs编译器下则不能写成unsigned char *String,只能写成:char *String,
//否则会报错:"初始化":无法从"const char[6]"转换为"unsigned char *"和"const char *"
//类型的值不能用于初始化"unsigned char *"类型的实体、
//至于在Linux下是否能够写成unsigned char *String,可以自己试验一下即可、
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(char Line,char Column,unsigned short Number,char Length)
//void LCD_ShowNum(char Line,char Column,unsigned int Number,char Length)
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(char Line,char Column,short Number,char Length)
//void LCD_ShowSignedNum(char Line,char Column,int Number,char Length)
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(char Line,char Column,unsigned int Number,char Length)
void LCD_ShowHexNum(unsigned char Line,unsigned char Column, 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(char Line,char Column,unsigned int Number,char Length)
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,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');
}
}
//Timer0.h
//保证头文件被某一个源文件多次包含时不会出现某些问题、
#ifndef __TIMER0_H__
#define __TIMER0_H__
void Timer0Init(void);//寄存器配置-子函数的声明、
#endif
//Timer.c
#include <REGX52.H> //寄存器的声明都包含在了头文件:#include <REGX52.H>中、
//以定时器0为例,定时器0初始化、
//寄存器配置-子函数的定义(可以理解成声明和定义)、
void Timer0Init(void) //1ms@12.000MHz
{
//具体含义见STC89C52使用手册、
//生成的C代码针对于某些含有寄存器AUXR的单片机,
//而STC89C52单片机中不存在寄存器AUXR,但是定时器时钟已经是12Tmode模式了,所以可以删掉下面这条语句、
//AUXR &= 0x7F; //定时器时钟12T模式、
//定时器0对应的GATE(门控端)值设置为0、
TMOD &= 0xF0; //设置定时器模式、//寄存器TMOD对应的二进制序列中的低四位清零,高四位不变、
TMOD |= 0x01; //设置定时器模式、//寄存器TMOD对应的二进制序列中的最低位置为1,高四位不变、
//到达溢出时,最多记录65536us、
//离溢出差1000us,即差1ms、
//TH0=64536/256;
//TL0=64536%256;
TL0 = 0x18 ; //设置定时器初值、
TH0 = 0xFC; //设置定时器初值、
TF0 = 0; //清除TF0标志、
//若设置为1,则直接就会向CPU请求中断,因此设置为0,防止刚配置好TF0,就直接向CPU请求中断、
//此处不写这一条语句也是可以的(记住即可),但是最好要写上、
TR0 = 1; //定时器0开始计时、
//由于定时器0中的GATE为0,则该定时器是否能够正常工作则取决于TR0,我们要让该定时器正常
//工作,所以赋值TR0为1、
//由于定时器0对应的GATE的值为0,因此不需要配置IE0和IT0,这两个是用来配置单片机的外部中断引脚INTO杠的、
//生成的C代码中缺少如下内容,加上即可:
ET0=1;
EA=1;
PT0=0; //可以不写,但是最好写上、
//默认PT0的值为0,则此处可以不写这一条语句,但最好还是要写上、
}
/*
//定时器中断函数模板、
//中断服务程序(中断子程序)、
//中断服务程序(中断子程序)一般都放在main.c源文件中,但不能放在某一个函数体内部,可以放在所有
//函数体外部的任意位置处、
void Timer0_Routine(void) interrupt 1
{
static unsigned int T0Conut; //静态局部变量不初始化则默认为0、
//设置为静态局部变量的原因是让该变量的作用域只在该函数体内有效、
T0Conut++;
if(T0Conut>=1000) //1s
{
T0Conut=0;
}
TL0 = 0x18 ; //设置定时器初值、
TH0 = 0xFC; //设置定时器初值、
}
*/
//main.c
#include <REGX52.H>
#include "Timer0.h"
#include "Key.h"
#include <INTRINS.H>
//全局变量、
unsigned char KeyNum,LEDMode; //char KeyNum,LEDMode;
void main()
{
P2=0xFE;
Timer0Init();
while(1)
{
KeyNum=Key();
if(KeyNum)
{
if(KeyNum==1)
{
LEDMode++;
if(LEDMode>=2)LEDMode=0;
}
}
}
//此时while(1)死循环中的内容不能放到中断子程序中的原因是:若将while(1)死循环中的内容
//放到中断子程序中的话,如果我们按住任意某个独立按键不松手,就相当于是在中断子程序中执行时间
//较长的任务,即使我们按下任意某个独立按键,紧接着松开手,而单片机的速度非常快,因此也相当于在
//中断子程序中执行时间较长的任务,所以在此处不能将while(1)死循环中的内容放到中断子程序中,否
//则会出现问题、
//由定时器0产生的所有中断(本质上是同一个中断)的优先级是一样的,在这些中断中,若前一个中
//断还未执行结束或还未执行,若下一个中断产生的话,此时该下一个中断就会进行排队,当该下一个中断
//的前面所有的中断都全部执行完毕后,会直接进入该下一个中断,不会推进主程序,并且,当该下一
//个中断产生时,若前一个中断还未执行结束或还未执行,此时不会先执行该下一个中断,这是因为,由定
//时器0生成的所有中断(本质上是同一个中断)的优先级是一样的,但如果下一个中断的优先级较高,则
//即使前一个中断还未执行结束或还未执行,当下一个优先级较高的中断产生时,此时就直接去执行这个
//优先级较高的中断,等这个优先级较高的中断执行完毕后,再返回到这里所谓的前面这个正在执行但还
//未执行完毕的中断,这个过程就叫做中断嵌套、
}
//中断服务程序(中断子程序)、
//中断服务程序(中断子程序)一般都放在main.c源文件中,但不能放在某一个函数体内部,可以放在所有
//函数体外部的任意位置处、
void Timer0_Routine(void) interrupt 1
{
static unsigned int T0Conut; //静态局部变量不初始化则默认为0、
//设置为静态局部变量的原因是让该变量的作用域只在该函数体内有效、
TL0 = 0x18 ; //设置定时器初值、
TH0 = 0xFC; //设置定时器初值、
T0Conut++;
if(T0Conut>=500) //0.5s - 500ms
{
T0Conut=0;
if(LEDMode==0)
{
P2=_crol_(P2,1); //#include <INTRINS.H>
}
if(LEDMode==1)
{
P2=_cror_(P2,1); //#include <INTRINS.H>
}
}
}
//其中:_crol_函数的作用是 循环 左移,_cror_函数的作用是 循环 右移、
//以二进制序列1111 1110为例:
//_crol_函数:将左移出去的最高位放到最低位上、
//1111 1110
//1111 1101
//1111 1011
//1111 0111
//1110 1111
//1101 1111
//1011 1111
//0111 1111
//1111 1110
//_cror_函数:将右移出去的最低位放到最高位上、
//1111 1110
//0111 1111
//1011 1111
//1101 1111
//1110 1111
//1111 0111
//1111 1011
//1111 1101
//1111 1110
//Delay.h
//保证头文件被某一个源文件多次包含时不会出现某些问题、
#ifndef __DELAY_H__
#define __DELAY_H__
//延时子函数的声明、
void Delay(unsigned int xms);
#endif
//Delay.c
//延时子函数的定义(可以理解成声明和定义)、
void Delay(unsigned int xms) //@12.000MHz
{
unsigned char i, j;
while(xms--)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}
}
//Key.h
//保证头文件被某一个源文件多次包含时不会出现某些问题、
#ifndef __KEY_H__
#define __KEY_H__
//获取按下并松开的独立按键对应的键码-子函数的声明、
unsigned char Key(); //char Key()
#endif
//Key.c
#include <REGX52.H>
#include "Delay.h"
//获取按下并松开的独立按键对应的键码-子函数的定义(可以理解成声明和定义)、
unsigned char Key() //char Key()
{
unsigned char KeyNumber=0; //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;
}
//Timer0.h
//保证头文件被某一个源文件多次包含时不会出现某些问题、
#ifndef __TIMER0_H__
#define __TIMER0_H__
void Timer0Init(void);//寄存器配置-子函数的声明、
#endif
//Timer0.c
#include <REGX52.H> //寄存器的声明都包含在了头文件:#include <REGX52.H>中、
//以定时器0为例,定时器0初始化、
//寄存器配置-子函数的定义(可以理解成声明和定义)、
void Timer0Init(void) //1ms@12.000MHz
{
//具体含义见STC89C52使用手册、
//生成的C代码针对于某些含有寄存器AUXR的单片机,
//而STC89C52单片机中不存在寄存器AUXR,但是定时器时钟已经是12Tmode模式了,所以可以删掉下面这条语句、
//AUXR &= 0x7F; //定时器时钟12T模式、
//定时器0对应的GATE(门控端)值设置为0、
TMOD &= 0xF0; //设置定时器模式、//寄存器TMOD对应的二进制序列中的低四位清零,高四位不变、
TMOD |= 0x01; //设置定时器模式、//寄存器TMOD对应的二进制序列中的最低位置为1,高四位不变、
//到达溢出时,最多记录65536us、
//离溢出差1000us,即差1ms、
//TH0=64536/256;
//TL0=64536%256;
TL0 = 0x18 ; //设置定时器初值、
TH0 = 0xFC; //设置定时器初值、
TF0 = 0; //清除TF0标志、
//若设置为1,则直接就会向CPU请求中断,因此设置为0,防止刚配置好TF0,就直接向CPU请求中断、
//此处不写这一条语句也是可以的(记住即可),但是最好要写上,因为清除TF0标志默认为0、
TR0 = 1; //定时器0开始计时、
//由于定时器0中的GATE为0,则该定时器是否能够正常工作则取决于TR0,我们要让该定时器正常
//工作所以赋值TR0为1、
//由于定时器0对应的GATE的值为0,因此不需要配置IE0和IT0,这两个是用来配置单片机的外部中断引脚INTO杠的、
//生成的C代码中缺少如下内容,加上即可:
ET0=1;
EA=1;
PT0=0; //可以不写,但是最好写上、
//默认PT0的值为0,则此处可以不写这一条语句,但最好还是要写上、
}
/*
//定时器中断函数模板、
//中断服务程序(中断子程序)、
//中断服务程序(中断子程序)一般都放在main.c源文件中,但不能放在某一个函数体内部,可以放在所有
//函数体外部的任意位置处、
void Timer0_Routine(void) interrupt 1
{
static unsigned int T0Conut; //静态局部变量不初始化则默认为0、
//设置为静态局部变量的原因是让该变量的作用域只在该函数体内有效、
T0Conut++;
if(T0Conut>=1000) //1s
{
T0Conut=0;
}
TL0 = 0x18 ; //设置定时器初值、
TH0 = 0xFC; //设置定时器初值、
}
*/
//main.c
#include <REGX52.H>
#include "Timer0.h"
#include "LCD1602.h"
//全局变量、
unsigned char Sec=56,Min=59,Hour=23;
void main()
{
LCD_Init();
Timer0Init();
//当执行完调用函数Timer0Init();后,经过1ms就会进入中断子程序,但是在进入中断子程序之前
//,还未在LCD1602液晶显示屏上面打印出Clock:,若把while(1)死循环中的内容放到中断子程序中
//的话,由于执行调用函数LCD_Num,则需要花费较长的时间,则会导致出现一些问题,而我们要保证中断
//子程序中不能执行时间较长的任务,只能执行一些时间较短的任务,否则就会出现问题;因此我们不能把
//while(1)死循环中的内容放到中断子程序中,否则就会出现问题、
//由于执行完调用函数LCD_ShowString()或调用函数LCD_ShowNum()需要花费较长的时间,所
//以在此期间,可能多次进入并退出中断子程序、
//上电显式静态字符串、
LCD_ShowString(1,1,"Clock:");
LCD_ShowString(2,1," : :");
while(1)
{
LCD_ShowNum(2,1,Hour,2);
LCD_ShowNum(2,4,Min,2);
LCD_ShowNum(2,7,Sec,2);
}
}
//中断服务程序(中断子程序)、
//中断服务程序(中断子程序)一般都放在main.c源文件中,但不能放在某一个函数体内部,可以放在所有
//函数体外部的任意位置处、
void Timer0_Routine(void) interrupt 1
{
static unsigned int T0Conut; //静态局部变量不初始化则默认为0、
//设置为静态局部变量的原因是让该变量的作用域只在该函数体内有效、
TL0 = 0x18 ; //设置定时器初值、
TH0 = 0xFC; //设置定时器初值、
//在keil5中写代码时,在某一个函数体中写代码时,必须将所有的局部变量的声明和定义(声明和
//定义不分开)放在函数体的最开头,这是因为,keil5中的gcc编译器使用的C语言标准是C99以下的标准
//,而C99以下的标准规定必须将所有的局部变量的声明和定义(声明和定义不分开写)放到函数体的最
//前面,而在C99及其更新的标准和C++中取消了这一限制;还有一点要注意:虽然keil5是在Windows系
//统下的,但是并不和Windows下的gcc版本一样,keil5中的gcc编译器使用的C语言标准是C99以下的
//标准,可以理解成一个特例、
//由于中断子程序很快就能执行完毕,因此将设置定时器的初值的两条语句放在该中断子程序中所
//有的局部变量的声明和定义(声明和定义不分开)以下,并且在if语句之外的任意位置处都是可以的,在
//这些范围内,这两条语句可以分开写,但最好不要分开;如果非要考虑执行该中断子程序需要花费时间
//的话,则最好将两条语句一块放到所有的局部变量的声明和定义(声明和定义不分开)的下面(紧靠着),
//但这个时间可以忽略不计,因此,将设置定时器的初值的两条语句一块放在该中断子程序中所有的局部
//变量的声明和定义(声明和定义不分开)以下,并且在if语句之外的任意位置处都是可以的,
T0Conut++;
if(T0Conut>=1000) //1s
{
T0Conut=0;
Sec++;
if(Sec>=60)
{
Sec=0;
Min++;
if(Min>=60)
{
Min=0;
Hour++;
if(Hour>=24)
{
Hour=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
//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(char Line,char Column, unsigned char Char)
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(char Line,char Column,unsigned char *String)
//若在Vs编译器下则不能写成unsigned char *String,只能写成:char *String,
//否则会报错:"初始化":无法从"const char[6]"转换为"unsigned char *"和"const char *"
//类型的值不能用于初始化"unsigned char *"类型的实体、
//至于在Linux下是否能够写成unsigned char *String,可以自己试验一下即可、
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(char Line,char Column,unsigned short Number,char Length)
//void LCD_ShowNum(char Line,char Column,unsigned int Number,char Length)
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(char Line,char Column,short Number,char Length)
//void LCD_ShowSignedNum(char Line,char Column,int Number,char Length)
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(char Line,char Column,unsigned int Number,char Length)
void LCD_ShowHexNum(unsigned char Line,unsigned char Column, 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(char Line,char Column,unsigned int Number,char Length)
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,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');
}
}
//Timer0.h
//保证头文件被某一个源文件多次包含时不会出现某些问题、
#ifndef __TIMER0_H__
#define __TIMER0_H__
void Timer0Init(void);//寄存器配置-子函数的声明、
#endif
//Timer.c
#include <REGX52.H> //寄存器的声明都包含在了头文件:#include <REGX52.H>中、
//以定时器0为例,定时器0初始化、
//寄存器配置-子函数的定义(可以理解成声明和定义)、
void Timer0Init(void) //1ms@12.000MHz
{
//具体含义见STC89C52使用手册、
//生成的C代码针对于某些含有寄存器AUXR的单片机,
//而STC89C52单片机中不存在寄存器AUXR,但是定时器时钟已经是12Tmode模式了,所以可以删掉下面这条语句、
//AUXR &= 0x7F; //定时器时钟12T模式、
//定时器0对应的GATE(门控端)值设置为0、
TMOD &= 0xF0; //设置定时器模式、//寄存器TMOD对应的二进制序列中的低四位清零,高四位不变、
TMOD |= 0x01; //设置定时器模式、//寄存器TMOD对应的二进制序列中的最低位置为1,高四位不变、
//到达溢出时,最多记录65536us、
//离溢出差1000us,即差1ms、
//TH0=64536/256;
//TL0=64536%256;
TL0 = 0x18 ; //设置定时器初值、
TH0 = 0xFC; //设置定时器初值、
TF0 = 0; //清除TF0标志、
//若设置为1,则直接就会向CPU请求中断,因此设置为0,防止刚配置好TF0,就直接向CPU请求中断、
//此处不写这一条语句也是可以的(记住即可),但是最好要写上、
TR0 = 1; //定时器0开始计时、
//由于定时器0中的GATE为0,则该定时器是否能够正常工作则取决于TR0,我们要让该定时器正常
//工作,所以赋值TR0为1、
//由于定时器0对应的GATE的值为0,因此不需要配置IE0和IT0,这两个是用来配置单片机的外部中断引脚INTO杠的、
//生成的C代码中缺少如下内容,加上即可:
ET0=1;
EA=1;
PT0=0; //可以不写,但是最好写上、
//默认PT0的值为0,则此处可以不写这一条语句,但最好还是要写上、
}
/*
//定时器中断函数模板、
//中断服务程序(中断子程序)、
//中断服务程序(中断子程序)一般都放在main.c源文件中,但不能放在某一个函数体内部,可以放在所有
//函数体外部的任意位置处、
void Timer0_Routine(void) interrupt 1
{
static unsigned int T0Conut; //静态局部变量不初始化则默认为0、
//设置为静态局部变量的原因是让该变量的作用域只在该函数体内有效、
T0Conut++;
if(T0Conut>=1000) //1s
{
T0Conut=0;
}
TL0 = 0x18 ; //设置定时器初值、
TH0 = 0xFC; //设置定时器初值、
}
*/
新版的晶振是11.0592Hz,我们之前选择的都是12Hz,在部分实验中,我们肉眼没有办法看出来结果是错的,但本质上,结果一定是错误的,而在另外一部分实验中,我们肉眼就能够看出来结果是错的,在后面我们选择11.0592Hz就可以了;新版指的是(STC89C52RC),旧版指的是(STC89C52),新版兼容旧版、
知识拓展:
此时,在LCD1602液晶显示器中打印出来的是23:59:56:,并且打印出来的所有内容均不会闪烁,但是如果写成如下所示:
此时,在LCD1602液晶显示器中打印出来的是23:59:56:,但是其中的23,59和56会一直在闪烁,原因是:当进入while循环内,将字符串“ : : :”打印出来,然后又将六个数字打印出来,并放在了6个空格中,当下一次进入while循环内时,此时六个数字所在的6个位置会被空格覆盖,如此循环,我们看起来,这六个数字就会不停地闪烁、
上图中所出现的问题的原因以及解决方法此处不再进行具体阐述,请参考博客:
//1、
https://blog.csdn.net/shenjin_s/article/details/108585518
//2、
https://blog.csdn.net/weixin_42880082/article/details/121330121