文章目录
一.模块化编程和LCD调试使用
1.模块化编程
模块化编程就是将不同的代码模块放在不同的头文件中,在主函数文件中调用,这样就会使代码清晰很多。
拿动态数码管显示做案例,可以把Delay函数和显示数字做成两个模块;
使用<双括号>调用头文件是在文件库里面搜索调用,使用“双引号”调用头文件是在此源文件的目录里调用(基本都是自定义的);
建立Delay模块:
首先在目录中创建Delay.c文件,在文件中写入定义的Delay函数,如下:
void Delay(unsigned int xms) //@12.000MHz
{
unsigned char i, j;
while(xms)
{i = 2;
j = 239;
do
{
while (--j);
} while (--i);
xms--;
}
}
然后再创建Delay.h文件声明已经定义的Delay函数,其中需要用到预编译的知识,如下:
#ifndef __DELAY_H__
#define __DELAY_H__
void Delay(unsigned int xms);
#endif
在主函数中就可以直接调用Delay.h函数,注意:任何自定义的变量、函数在调用前都必须有定义或声明(同一个.c)。
所以在要想使用.c文件里的函数必须先建立相连的.h文件里面的函数声明,然后在主函数中使用前再次声明。
2.LCD1602调试工具
LCD调试工具已经由博主写好,我们只需要掌握如何操作这些函数即可。
函数如下:
LCD_Init();初始化
LCD_ShowChar(1,1,‘A’);显示一个字符
LCD_ShowString(1,3,“Hello”);显示字符串
LCD_ShowNum(1,9,123,3);显示十进制数字
LCD_ShowSignedNum(1,13,-66,2);显示有符号十进制数字
LCD_ShowHexNum(2,1,0xA8,2);显示十六进制数字
LCD_ShowBinNum(2,4,0xAA,8);显示二进制数字
让液晶屏从0开始每秒计数加一,代码如下:
先定义一个Result来计数,然后让LCD液晶屏显示Result递加的结果,这就可以用到显示数字函数LCD_ShowNum,第一个数字代表行,第二个数字代表列,第三表示要显示的数字(从要开始的第一行开始显示),最后一个数字表示要显示的位数(从显示数的最后一位开始算)
#include <REGX52.H>
#include "LCD1602.h"
#include "Delay.h"
int Result=0;
void main()
{
LCD_Init();
while(1)
{
Result++;
Delay(1000);
LCD_ShowNum(1,1,Result,3);
}
}
二.矩形键盘
1.矩形键盘的扫描
如下图,矩形键盘是4*4排列的,P17–P14四个端口是连接四行的,P13–P10是连接四列的,其扫描的原理是假如给第一行低电平,给其他行高电平(防止影响低电平检测),然后按下S2,那么S2这一行和这一列的P17和P12口就会联通,所以只需判断P12是否为低电平,就可以判断S2是否被按下。如此,依次分别给每一行低电平进行检测,就可以判断出来按下了哪几个键。你说妙不妙!
根据这个原理,我们可以构造函数来判断按键按下的模块,代码如下:
/**
* @brief 矩阵键盘读取按键键码
* @param 无
* @retval KeyNumer 按下键吗的键码值
如果按键按下不妨,程序会停留在此函数,松手的一瞬间,返回按键键码,没有按键时,返回0
*/
unsigned char MatrixKey()
{
unsigned char KeyNumber=0;//每执行一次就会清零
P1=0xFF;
P1_3=0;
if(P1_7==0){Delay(20);while(P1_7==0);Delay(20);KeyNumber=1;}
if(P1_6==0){Delay(20);while(P1_6==0);Delay(20);KeyNumber=5;}
if(P1_5==0){Delay(20);while(P1_5==0);Delay(20);KeyNumber=9;}
if(P1_4==0){Delay(20);while(P1_4==0);Delay(20);KeyNumber=13;}
P1=0xFF;
P1_2=0;
if(P1_7==0){Delay(20);while(P1_7==0);Delay(20);KeyNumber=2;}
if(P1_6==0){Delay(20);while(P1_6==0);Delay(20);KeyNumber=6;}
if(P1_5==0){Delay(20);while(P1_5==0);Delay(20);KeyNumber=10;}
if(P1_4==0){Delay(20);while(P1_4==0);Delay(20);KeyNumber=14;}
P1=0xFF;
P1_1=0;
if(P1_7==0){Delay(20);while(P1_7==0);Delay(20);KeyNumber=3;}
if(P1_6==0){Delay(20);while(P1_6==0);Delay(20);KeyNumber=7;}
if(P1_5==0){Delay(20);while(P1_5==0);Delay(20);KeyNumber=11;}
if(P1_4==0){Delay(20);while(P1_4==0);Delay(20);KeyNumber=15;}
P1=0xFF;
P1_0=0;
if(P1_7==0){Delay(20);while(P1_7==0);Delay(20);KeyNumber=4;}
if(P1_6==0){Delay(20);while(P1_6==0);Delay(20);KeyNumber=8;}
if(P1_5==0){Delay(20);while(P1_5==0);Delay(20);KeyNumber=12;}
if(P1_4==0){Delay(20);while(P1_4==0);Delay(20);KeyNumber=16;}
return KeyNumber;
}
这里是一列一列开始扫描的,和行扫描一个道理
然后在主函数就可以写一个判断按键按下的代码:
#include <REGX52.H>
#include "Delay.h"
#include "LCD1602.h"
#include "MatrixKey.h"
unsigned char KeyNum;
void main()
{
LCD_Init();
LCD_ShowString(1,1,"MatrixKey:");
while(1)
{
KeyNum=MatrixKey();
if(KeyNum)//每执行一次MatrixKey函数KeyNum就会清零
{
LCD_ShowNum(2,1,KeyNum,2);
}
}
}
注意这里必须加if(KeyNum)条件判断,因为每循环执行一次MatrixKey函数,KeyNum的值就会被清零,当有按键按下时KeyNum才会被赋值一个数,也就显示极短的一下,所以液晶屏上看着总是显示0。
2.矩形键盘密码锁
S1-S10表示1234567890,S11表示确认,S12表示取消重输,输错了显示2B,输对了显示NB,不多说,注释都在下面,懂得都懂,直接上代码:
#include <REGX52.H>
#include "Delay.h"
#include "LCD1602.h"
#include "MatrixKey.h"
unsigned char KeyNum;
unsigned int Password,cnt;
void main()
{
LCD_Init();
LCD_ShowString(1,1,"Password:");
while(1)
{
KeyNum=MatrixKey();
if(KeyNum)//每执行一次MatrixKey函数KeyNum就会清零
{
if(KeyNum<=10)//如果S1~S10按键按下,输入密码
{
if(cnt<4)//防止输多
{
Password*=10;
Password+=KeyNum%10;//获取一位密码
cnt++;
}
}
LCD_ShowNum(2,1,Password,4);
if(KeyNum==11)//按下S11键确认
{
if(Password==2004)//如果密码正确
{Password=0;//密码清零
cnt=0;//计数清零
LCD_ShowString(2,14,"NB");
}
else
{Password=0;//密码清零
cnt=0;//计数清零
LCD_ShowString(2,14,"2B");
}
}
if(KeyNum==12)
{
Password=0;//密码清零
cnt=0;//计数清零
LCD_ShowNum(2,1,Password,4);
}
}
}
}
三.控制按键控制LED流水灯模式&定时器
1.中断系统
STC89C5X 系列单片机提供了 8 个中断请求源,它们分别是:外部中断O(INTO)、外部中断 1(INT1)、外部中断 2(INT2)、外部中断 3(INT3)、定时器 0中断、定时器 1 中断、定时器 2 中断、串口(UART)中断。
一般来说都具有下面五个中断。定时器0/1中断T0/1,外部中断INT0/1,串行口中断(UART)包含接收中断TI和发送中断RI,当串行口接收完一帧串行数据时置位 RI 或当串行口发送完一帧串行数据时置位 TI,向 CPU 申请中断。
2.定时器
STC89C5X 单片机内有两个可编程的定时/计数器 T0、T1 和一个特殊功能定时器 T2。定时/计数器的实质是加 1 计数器(16 位),由高 8 位和低 8 位两个寄存器 THx 和 TLx 组成。它随着计数器的输入脉冲进行自加 1,也就是每来一个脉冲,计数器就自动加 1,当加到计数器为全 1 时(12MHZ记满65536),再输入一个脉冲就使计数器回零,且计数器的溢出使相应的中断标志位置 1,向 CPU 发出中断请求(定时/计数器中断允许时)。由溢出时计数器的值减去计数初值才是加 1 计数器的计数值。
TMOD:定时器/计数器模式控制;
TOCN:控制寄存器,作用是控制定时器的启、停,标志定时器溢出和中断情况。
配置定时器和中断系统,如下:
void Timer0Init(void)//配置定时器和中断系统 1毫秒@11.0592MHz
{
// TMOD=0X01;
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //设置定时器模式
TL0 = 0x66; //设置定时初值,低8位寄存器
TH0 = 0xFC; //设置定时初值,高8位寄存器
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
//后三行设置中断总开关并打开定时器0
ET0=1;
EA=1;
PT0=0;
}
频率为11.0592的1s定时器模板:
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count;
TL0 = 0x66; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count++;
if( T0Count>=1000)
{
T0Count=0;
}
}
控制LED流水灯,此处关键是_cro_函数使用,_crol_左移_cror_右移,它与<<不同,_cro_函数移到最高位后会从最低位开始移。
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_Routine() interrupt 1
{
static unsigned int T0Count;
TL0 = 0x66; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count++;
if( T0Count>=500)
{
T0Count=0;
if(LEDMode==0)
P2=_crol_(P2,1);
if(LEDMode==1)
P2=_cror_(P2,1);
}
}
设置一个定时器定时小时、分钟、秒,如下:
#include <REGX52.H>
#include "Delay.h"
#include "LCD1602.h"
#include "Timer0.h"
unsigned char Sec,Min,Hour;
void main()
{
LCD_Init();
Timer0Init();
LCD_ShowString(1,1,"Clock:");
while(1)
{
LCD_ShowNum(2,1,Hour,2);
LCD_ShowNum(2,3,Min,2);
LCD_ShowNum(2,5,Sec,2);
}
}
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count;
TL0 = 0x66; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count++;
if( T0Count>=1000)
{
T0Count=0;
Sec++;
if(Sec==60)
{
Min++;
Sec=0;
if(Min==60)
{
Hour++;
Min=0;
}
}
}
}
四.串口向电脑传输数据
1.串口初始化和串口发送一个字节数据
用串口发送一个字节数据很简单,只需要调用 UART_SendByte函数,将数据写入SBUF寄存器即可
void UART_Init()//4800bps@11.0592MHz
{
SCON=0x40;
PCON &= 0x7F;
TMOD &= 0x0F; //设置定时器模式
TMOD |= 0x20; //设置定时器模式
TL1 = 0xFA; //设定定时初值
TH1 = 0xFA; //设定定时器重装值
ET1 = 0; //禁止定时器1中断
TR1 = 1; //启动定时器1
}
void UART_SendByte(unsigned char Byte)
{
SBUF=Byte;//写入数据
while(TI==0);
TI=0;//软件复位
}
2.电脑通过串口发送数据
利用电脑通过串口发送数据需要先设置中断
void UART_Routine() interrupt 4//设置中断函数 interrupt是小尾巴
{
if(RI==1)//如果发送数据,则RI内置为1
{
P2=~SBUF;
UART_SendByte(SBUF);
RI=0;
}
}
波特率(Baud Rate)单位bps是用于衡量串口通信速度的单位,它表示每秒钟发送的比特数。如果一个串口的波特率为9600,就表示该串口在一秒钟内可以发送9600个比特的数据。
定时器1产生波特率, 串口一般使用定时器1,模式2,八位自动重装模式,来产生溢出率,从而产生波特率。而且在配置定时器相关的寄存器时不用配置定时器中断,只是使用定时器1来产生波特率的功能。
下图为计算不同定时器模式下的波特率
STC-ISP串口助手收发数据有两种模式,分别是HEX模式和文本模式。
文本模式显示的是可打印的字符,A b ? *之类
HEX模式则对应的其ascii码,以十六进制显示
五.DS1302时钟
DS1302 涓流充电计时芯片包含一个实时时钟/日历和 31 字节的静态 RAM.通过简单的串行
接口与微处理器通讯.这个实时时钟/日历提供年月日,时分秒信息.对于少于 31 天的月份月末
会自动调整,还有闰年校正.由于有一个 AM/PM 指示器,时钟可以工作在 12 小时制或者 24
小时制。
VCC1是涓流充电器:在主电源VCC2给时钟供电时,VCC1不供电并且VCC2给VCC1充电,一旦VCC2断电,VCC1给时钟持续稳定供电,这样时钟的计时工作不受总电源的控制(不同与定时器)所以保证了时钟的持续。
But,我手中的这个ST89C52中的这个时钟没有接VCC1电源(没有备用电源),所以断电就真啥也没了~
DS1302各引脚的名称和作用
x1,x2 接外部晶振,通过内部电路输出1HZ标准计时频率;
右下角:内部寄存器,内部时间存在这里;
1)完成时钟显示的基本流程:
在哪里 写入 什么
在哪里 读出 什么
2)CE:芯片使能 这里的CE相当于一个中介开关,CE高电平有效。
SCLK上升沿读入数据,下降沿输出数据
I/O引脚中R/W控制读或写,A0到A4告诉我们是哪一块地址,R/C控制RAM或时钟,最高位则默认为1。
3)单字节写:
首先把CE置高电平(开关打开),开始写,第二步命令字发两个字节,第一个:命令字;第二个字节数据
命令字:第一位先发R/(–W)设置到I/O口上(内部芯片就是这样设计的)
上升沿数据写入;依次循环,直到最高位被写入;
CE置高电平后,第一个写入的就是命令字(告诉它在哪里读,还是在哪里写)操作完成后CE置低电平
单字节读:
写入完成命令字后,下降沿把I/O口线释放掉,就不再操作I/O口了,就开始读出数据
每一个下降沿来一个数据,把数据一个一个读出来;注意单字节读D1到D7是不用操作I/O口的
4)BCD码(Binary Coded Decimal),用4位二进制数来表示1位十进制数
例:0001 0011表示13,1000 0101表示85,0001 1010不合法
在十六进制中的体现:0x13表示13,0x85表示85,0x1A不合法
BCD码转十进制:DEC=BCD/1610+BCD%16; (2位BCD)
十进制转BCD码:BCD=DEC/1016+DEC%10; (2位BCD)
DS1302.c时钟模块如下:
#include <REGX52.H>
sbit DS1302_SCLK=P3^6;
sbit DS1302_IO=P3^4;
sbit DS1302_CE=P3^5;
#define DS1302_SECOND 0x80
#define DS1302_MINUTE 0x82
#define DS1302_HOUR 0x84
#define DS1302_DATE 0x86
#define DS1302_MONTH 0x88
#define DS1302_DAY 0x8A
#define DS1302_YEAR 0x8C
#define DS1302_WP 0x8E
unsigned char DS1302_Time[]={23,11,16,12,59,55,6};
void DS1302_Init()
{
//上电时引脚都自动置1,所以要先初始化置0
DS1302_CE=0;
DS1302_SCLK=0;
}
/**
* @brief DS1302写一个字节
* @param Command 命令字/地址
* @param Data 要写入的数据,已经存在后八位寄存器里了
* @retval 无
*/
void DS1302_WriteByte(unsigned char Command,Data)
{
unsigned char i;
DS1302_CE=1;//打开开关,可以开始写了
for(i=0;i<8;i++)
{
DS1302_IO=Command&(0x01<<i);
DS1302_SCLK=1;
DS1302_SCLK=0;
}
for(i=0;i<8;i++)
{
DS1302_IO=Data&(0x01<<i);
DS1302_SCLK=1;
DS1302_SCLK=0;
}
DS1302_CE=0;
}
/**
* @brief DS1302读一个字节
* @param Command 命令字/地址,读完之后就知道我要显示哪种类型的数据(h?min?s?)
* @param 返回已经已经存入,后八位寄存器里的数据(这里由DS1302自己控制)
* @retval 无
*/
unsigned char DS1302_ReadByte(unsigned char Command)
{
unsigned char i,Data=0x00;
//Command|=0x01; //将指令转换为读指令
Command|=0x01;
DS1302_CE=1;
for(i=0;i<8;i++)
{
DS1302_IO=Command&(0x01<<i);
DS1302_SCLK=0;//读和写相反
DS1302_SCLK=1;
}
for(i=0;i<8;i++)
{
DS1302_SCLK=1;//过滤掉一个脉冲,先置1
DS1302_SCLK=0;
if(DS1302_IO){Data|=(0x01<<i);}//记录输出数据,每当输出1时,Data的这一位记录为1
}
DS1302_CE=0;
DS1302_IO=0;//必须置0,否则数据会出错
return Data;
}
void DS1302_SetTime(void)
{
DS1302_WriteByte(DS1302_WP,0x00);//关闭写保护
DS1302_WriteByte(DS1302_YEAR,DS1302_Time[0]/10*16+DS1302_Time[0]%10);
DS1302_WriteByte(DS1302_MONTH,DS1302_Time[1]/10*16+DS1302_Time[1]%10);
DS1302_WriteByte(DS1302_DATE,DS1302_Time[2]/10*16+DS1302_Time[2]%10);
DS1302_WriteByte(DS1302_HOUR,DS1302_Time[3]/10*16+DS1302_Time[3]%10);
DS1302_WriteByte(DS1302_MINUTE,DS1302_Time[4]/10*16+DS1302_Time[4]%10);
DS1302_WriteByte(DS1302_SECOND,DS1302_Time[5]/10*16+DS1302_Time[5]%10);
DS1302_WriteByte(DS1302_DAY,DS1302_Time[6]/10*16+DS1302_Time[6]%10);
DS1302_WriteByte(DS1302_WP,0x80);//打开写保护
}
void DS1302_ReadTime(void)
{
unsigned char Temp;
Temp=DS1302_ReadByte(DS1302_YEAR);
DS1302_Time[0]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_MONTH);
DS1302_Time[1]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_DATE);
DS1302_Time[2]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_HOUR);
DS1302_Time[3]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_MINUTE);
DS1302_Time[4]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_SECOND);
DS1302_Time[5]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_DAY);
DS1302_Time[6]=Temp/16*10+Temp%16;
}