b站江科大自化协51单片机入门教程笔记(2)
文章内容根据b站up主江科大自化协的51单片机入门课程编写,文章内容与代码均引用up主分享的内容以便于复习,仅供学习交流使用
文章目录
6-1 矩阵键盘
对数码管来说,在同一时间不能同时控制多位数码管显示不同数字,但可以利用扫描解决。
- 矩阵连接的越多,节省I/O口越明显。比如1080P的比例为1920*1080=2073600,显示屏需要2073600个像素点才能显示1080P的视频,且因为RGB通道,还需要乘3,共需6220800个LED。单独判断需要600多万个I/O口,但是如果连接成矩阵形式,只需要1920+1080=3000,再乘3为9000个,大幅减少了I/O口。
按行扫描:
如果是按行扫描,那么同一时间只有一行是0(P17-P14中只有一个为0),然后检测P13-P10,即可判断一行中哪个按键被按下。
但是不推荐逐行扫描,因为按行扫描P15会时高时低,而P15连接到步进电机,右边连接BZ,经过驱动器驱动会增加输出电流能力,连接到蜂鸣器上,这个开发板上BZ以一定频率高低变换时蜂鸣器会响。
按列扫描:
按列扫描时下面四个口(P10-P13)同时只有一个口给0,扫描上面四个口即可按列判断哪个开关按下。
由于本节会用到LCD1602和Delay模块,从5-2中将已经写好的复制到项目目录下:
针对上节无法打开LCD一系列函数定义的问题,可以先编译,然后全部保存,重新进入项目即可打开
对于每次都写.h文件,我们可以插入模板
MatrixKey.c文件代码如下
#include <REGX52.H>
#include "Delay.h"
/**
* @brief 矩阵键盘读取按键键码
* @param 无
* @retval KeyNumber 按下按键的键码值
如果按键按下不放,程序会停留在此函数,松手的一瞬间返回按键键码,没有按键按下时返回零
*/
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;
}
if(P1_7==0){Delay(20);while(P1_7==0);Delay(20);KeyNumber=1;}
- 在扫描第一列时,如果P1_7==0,那么此时是判断开关1的状态
- 由于是机械按键,加入延时函数消除抖动,然后判断是否松手;如果松手,继续消除抖动
- 返回值KeyNumber
这么做采用了模块化编程的思想,代码移植性强且在主函数中较为简洁,容易理解;本身机器将一个简单粗暴的思想用很快的速度执行很多次,是一种想法。
主函数:
#include <REGX52.H>
#include "Delay.h"
#include "LCD1602.h"
#include "MatrixKey.h"
unsigned char KeyNum;
int main()
{
LCD_Init();
LCD_ShowString(1,1,"MatrixKey:");
while(1)
{
KeyNum=MatrixKey();
if(KeyNum)
{
LCD_ShowNum(2,1,KeyNum,2);
}
}
}
如果删除了if,在开发板上怎么按都发现是0;其实显示过1,但很快到下一个循环,仔细看会发现LCD1602上的数字闪了一下。最后可以参考LCD1602的注释形式添加模板并为矩阵键盘读取键码添加注释
6-2 矩阵键盘密码锁
可以直接复制工程然后粘贴(也是常用的操作)
- 首先我们要定义按键功能:S1-S9定义为数字的1-9,S10定义为0,S11为确认键,S12为取消键,S13-S16按键不用
- 判断KeyNum<=10,然后实现密码左移,同时要加入一个计次变量,按下确认或者取消按键后密码计次清零。
#include <REGX52.H>
#include "Delay.h"
#include "LCD1602.h"
#include "MatrixKey.h"
unsigned char KeyNum;
unsigned int Password,Count; //Count全局变量默认初始化为0
int main()
{
LCD_Init();
LCD_ShowString(1,1,"Password:");
while(1)
{
KeyNum=MatrixKey();
if(KeyNum)
{
if(KeyNum<=10) // 如果S1~S10按键按下,输入密码
{
if(Count<4) //unsigned int类型0~65535,为了防止超过加一个变量计数
{
Password*=10; //密码左移一位
Password+=KeyNum%10; // 获取一位密码,对10取余即把10转化为0
Count++ ; // 计次+1
}
LCD_ShowNum(2,1,Password,4); //更新显示
}
if(KeyNum==11) //如果S11按键按下,确认
{
if(Password==2345) // 如果密码等于正确密码
{
LCD_ShowString(1,14,"OK "); // 显示OK,多空一格,要不会出现OKR
Password=0; //密码清零
Count=0; //计次清零
LCD_ShowNum(2,1,Password,4); //更新显示
}
else
{
LCD_ShowString(1,14,"ERR"); // 显示ERR
Password=0; //密码清零
Count=0; //计次清零
LCD_ShowNum(2,1,Password,4); //更新显示
}
}
if(KeyNum==12) //如果S12按键按下,取消
{
Password=0; //密码清零
Count=0; //计次清零
LCD_ShowNum(2,1,Password,4); //更新显示
}
}
}
}
7-1 定时器
加入独立按键和流水灯联动起来,如果二者简单拼接会出现一些问题:LED流水灯在移动的时候会有一个很长时间Delay,如果直接连在一起的话按键检测会很不灵敏,为了解决灵敏度问题研究本节内容。
前面几节讲的按键,数码管,LCD1602都是单片机IO口控制的外设,定时器是单片机内部完成的。
其他用途还可以进行任务切换,多任务同时执行
- 中间为计数系统(此处为16位) TL和TL两个一块最大只能存0-65535,溢出时会置一个标志位TF0,然后向中断系统申请中断
- 默认12T的模式会分频,输出为1MHz,那么连此时的线路每隔一微秒计数一次;C/T是一个选择开关,配置为1时为计数功能(count),给0为定时器(time);本节配置为实现定时器功能
- 时钟也可以由系统提供,也可以由外部引脚来提供,如下图中位置
意味着可以同时完成两项任务,主程序和中断程序
电路的连接依靠于定时器相关寄存器
- 单片机通过配置寄存器来控制内部线路的连接;开关拨到哪个位置就是靠寄存器控制的
7-2 按键控制LED流水灯 & 定时器时钟
- 可位寻址:可以对每一位赋值
- 不可位寻址:只能整体赋值
计数脉冲(12MHz情况下)每隔1us加1,加到最大值才产生中断,怎么让它一秒产生中断?赋初值
- 0-65535;每隔1us计数加一;总共定时时间65535us(即为65ms左右);可以用程序来实现
- 每隔1ms产生中断,每次中断以后再来计数,每1000次再做其他事情即可
- 赋初值64535,离计数器溢出差值1000,所以计时时间为1ms
验证一下是否有中断,是不是跳到这执行中断
#include <REGX52.H>
void Timer0_Init()
{
TMOD=0x01; // 0000 0001
TF0=0;
TR0=1;
TH0=64535/256; // 两个8位,即256*256,目的取高低位
TL0=64535%256;
ET0=1;
EA=1;
PT0=0;
}
int main()
{
Timer0_Init();
while(1)
{
}
}
void Timer0_Routine() interrupt 1 // 如果有中断,会点亮灯,验证一下
{
P2_0=1;
}
-
因为中断函数并没有前置声明,按理说是无法执行的。而且主函数循环中并没有调用该函数,当函数内代码被运行,就说明中断函数确实被运行了
-
中断之后TH0和TL0会溢出,溢出之后就变成0了,我们要做的就是让这个“沙漏”倒转回来,继续计时,每次中断以后重新赋初值
unsigned int T0Count;
void Timer0_Routine() interrupt 1
{
TH0=64535/256; // 两个8位,即256*256,目的取高低位
TL0=64535%256;
T0Count++;
if(T0Count>=1000)
{
T0Count=0;
P2_0=~P2_0;
}
}
执行之后会让D1按1s闪烁,但其实TMOD=0x01
这句代码有些缺陷:TMOD是不可位寻址,如果同时使用两个定时器,给定时器1配置好以后再配置定时器0会把定时器1状态给刷新。
- 利用“与或式赋值法”,只操作其中的某一位或者某些位,而不影响其他位。
void Timer0_Init()
{
// TMOD=0x01; // 0000 0001
TMOD=TMOD&0xF0; // 把TMOD的第低四位清零,高四位保持不变
TMOD=TMOD|0x01; // 把TMOD的最低位置一,高四位保持不变
TF0=0;
TR0=1;
TH0=64535/256; // 两个8位,即256*256,目的取高低位
TL0=64535%256;
ET0=1;
EA=1;
PT0=0;
}
也可以利用STC-ISP中的定时器计算器,但是需要加上ET0,EA,PT0的赋初值
void Timer0Init(void) //1毫秒@12.000MHz
{
AUXR &= 0x7F; //定时器时钟12T模式
TMOD &= 0xF0; //设置定时器模式
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0=1;
EA=1;
PT0=0;
}
TL0和TH0与上面我们自己算的相比会有一微秒的差别,可以自行计算:我们配置的64535%256是23,转化为16进制数为0x17,定时器计算器配置的是0x18,少了1;原因是65535并没有溢出,65536才溢出
接下来完成定时器的模块化,把1秒模板作为注释放到Timer0.c里,因为不太容易模块化,定时器和主程序耦合性比较大,
#include <REGX52.H>
void Timer0_Init(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_Routine() interrupt 1
{
static unsigned int T0Count;
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count++;
if(T0Count>=1000)
{
T0Count=0;
}
}
*/
中断函数一般放在主函数里
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count; // 静态局部变量只有本函数可以使用
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count++;
if(T0Count>=1000)
{
T0Count=0;
P2_0=~P2_0;
}
}
独立按键模块
Key.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;
}
Key.h文件
#ifndef __KEY_H__
#define __KEY_H__
unsigned char Key();
#endif
测试功能
#include <REGX52.H>
#include "Timer0.h"
#include "Key.h"
unsigned char KeyNum;
int 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_Routine() interrupt 1
//{
// static unsigned int T0Count;
// TL0 = 0x18; //设置定时初值
// TH0 = 0xFC; //设置定时初值
// T0Count++;
// if(T0Count>=1000)
// {
// T0Count=0;
// P2_0=~P2_0;
// }
//
//}
定时器模块
Timer0.c
#include <REGX52.H>
/**
* @brief 定时器0初始化,1毫秒@12.000MHz
* @param 无
* @retval 无
*/
void Timer0Init() //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_Routine() interrupt 1
{
static unsigned int T0Count;
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count++;
if(T0Count>=1000)
{
T0Count=0;
}
}
*/
Timer0.h
#ifndef __TIMER0_H__
#define __TIMER0_H__
void Timer0Init(void);
#endif
对于实现流水灯,我们可以添加函数库头文件#include <INTRINS.H>
// _crol_ 和 _cror_ 函数应用实现流水灯
unsigned char a = 0x01;
a= _crol_(a,1); //0x02,如果a是0x08,调用后会变成0x01,与位运算不一样
实现流水灯的代码:
#include <REGX52.H>
#include "Timer0.h"
#include "Key.h"
#include <INTRINS.H>
unsigned char KeyNum,LEDMode;
int 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 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count++;
if(T0Count>=500)
{
T0Count=0;
if(LEDMode==0)
P2=_crol_(P2,1);
if(LEDMode==1)
P2=_cror_(P2,1);
}
}
因为在中断函数中我们不执行过长的任务,把LCD_ShowNum()这个运行时间比较长的函数放在while循环里
#include <REGX52.H>
#include "Delay.h"
#include "LCD1602.h"
#include "Timer0.h"
unsigned char Sec=55,Min=59,Hour=23;
int main()
{
LCD_Init();
Timer0Init();
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);
}
}
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count;
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count++;
if(T0Count>=1000)
{
T0Count=0;
Sec++;
if(Sec>=60)
{
Sec=0;
Min++;
if(Min>=60)
{
Min=0;
Hour++;
if(Hour>=24)
{
Hour=0;
}
}
}
}
}
8-1 串口通信
本节介绍51单片机中的串口,本节以单片机和电脑作为串口通信的两个设备如何进行相互通信
- 本节实现的第一个代码即向电脑发送数据,通过stc-isp软件中的串口助手来接收,串口号必须一样;默认会保持一致
- TXD:transmit exchange data
-
TTL:Transistor-Transistor Logic 晶体管-晶体管逻辑(电路);(单片机就是用的TTL)
-
TTL电平和RS232电平只能传十多米,RS485可以传一千多米
- 24C02,是用来存储数据的,单片机的写入与读出就是靠 I 2 C I^2C I2C接口的
- DS1302的通信方式是SPI,但不是标准的SPI
- DS18B20温度传感器是通信方式就是1-Wire
- 如果操作的是端口的寄存器,用的就是IO口;如果操作的是串口的寄存器,就通过IO口发送数据
- 9位相比于8位多了一位,多出来的一位可以用于校验,验证前面的数据是否正确
- 双方约定都使用奇校验,发0000 0011 1(奇校验就是数一下数据位中有几个1,现在是2个,后面补一个1,保证9位数据中1的个数是奇数);接收到0000 0011 1,也发现1的个数是奇数,这样的数据就是正确的;但是如果接受到的是0000 0101 1,这样的错误是检测不出来的
- 串口靠定时器1的溢出率约定速率
8-2 串口向电脑发送数据 & 电脑通过串口控制LED
硬件了解以后软件就是配置寄存器
- IE,IPH,IP,不需要开启中断,与默认相同,不用配置、
- 配置定时器时只能用定时器1,
TMOD &= 0x0F;
为高四位清零;串口需要用8位自动重装模式- 之前讲的是16位,用两个8位表示一个大的计数器:0-65535;缺点是进入中断时需要赋初值,会占用一定时间,所以精度不是特别高
- 串口中需要更精准的,分成两个8位实现自动重装,可以使用工具完成初始化
void UART_Init() //4800bps@11.0592MHz
{
SCON=0x40;
PCON|= 0x80;
TMOD &= 0x0F; //设置定时器模式
TMOD |= 0x20; //设置定时器模式
TL1 = 0xF4; //设定定时初值
TH1 = 0xF4; //设定定时器重装值
ET1 = 0; //禁止定时器1中断
TR1 = 1; //启动定时器1
}
发送内部过程比较复杂,但是操作简单,只需要把数据写到SBUF即可
程序一:发送数据(十六进制形式)
#include <REGX52.H>
#include "Delay.h"
unsigned char Sec;
void UART_Init() //4800bps@11.0592MHz
{
SCON=0x40;
PCON|= 0x80;
TMOD &= 0x0F; //设置定时器模式
TMOD |= 0x20; //设置定时器模式
TL1 = 0xF4; //设定定时初值
TH1 = 0xF4; //设定定时器重装值
ET1 = 0; //禁止定时器1中断
TR1 = 1; //启动定时器1
}
void UART_SendByte(unsigned char Byte)
{
SBUF=Byte;
while(TI==0);
TI=0;
}
int main()
{
UART_Init();
while(1)
{
UART_SendByte(Sec);
Sec++;
Delay(1000);
}
}
串口模块
UART.c
#include <REGX52.H>
/**
* @brief 串口初始化,4800bps@11.0592MHz
* @param 无
* @retval 无
*/
void UART_Init()
{
SCON=0x40;
PCON|= 0x80;
TMOD &= 0x0F; //设置定时器模式
TMOD |= 0x20; //设置定时器模式
TL1 = 0xF4; //设定定时初值
TH1 = 0xF4; //设定定时器重装值
ET1 = 0; //禁止定时器1中断
TR1 = 1; //启动定时器1
}
/**
* @brief 串口发送一个字节数据
* @param Byte 要发送的一个字节数据
* @retval 无
*/
void UART_SendByte(unsigned char Byte)
{
SBUF=Byte;
while(TI==0);
TI=0;
}
UART.h
#ifndef __UART_H__
#define __UART_H__
void UART_Init();
void UART_SendByte(unsigned char Byte);
#endif
电脑通过串口控制LED,改造一下程序,收需要一个中断系统;因为不知道电脑什么时候发过来,也不能一直检测,所以我们利用中断,在电脑发过来的时候触发中断,在中断函数里面进行数据处理,把数据“拿”出来
重新配置一下UART.c:
#include <REGX52.H>
/**
* @brief 串口初始化,4800bps@11.0592MHz
* @param 无
* @retval 无
*/
void UART_Init()
{
SCON=0x50; //改为可以接收
PCON|= 0x80;
TMOD &= 0x0F; //设置定时器模式
TMOD |= 0x20; //设置定时器模式
TL1 = 0xF4; //设定定时初值
TH1 = 0xF4; //设定定时器重装值
ET1 = 0; //禁止定时器1中断
TR1 = 1; //启动定时器1
EA=1;
ES=1;
}
/**
* @brief 串口发送一个字节数据
* @param Byte 要发送的一个字节数据
* @retval 无
*/
void UART_SendByte(unsigned char Byte)
{
SBUF=Byte;
while(TI==0);
TI=0;
}
在主函数中写出中断函数,然后验证是否产生中断,因为写的中断函数没有主函数调用,也没有其他子函数调用,如果没有中断进来,这个函数就不会被执行
#include <REGX52.H>
#include "Delay.h"
#include "UART.h"
int main()
{
UART_Init();
while(1)
{
}
}
void UART_Routine() interrupt 4
{
P2=0x00; // 中断则会点亮LED
}
发送f0,灯亮了,触发了中断;但是不确定是发送中断还是接收中断
void UART_Routine() interrupt 4
{
if(RI==1) // 一旦进入中断就检测;如果是接受中断
{
P2=~SBUF;
RI=0;
}
}
-
流程是:如果电脑发送了数据,接收完成后会产生中断,如果是接收中断,把数据读出来放在P2口上并且把中断标志位清零
-
同时需要注意:一个函数不能既在主函数中出现,又在中断函数中出现,会破坏原来的函数;
主函数
#include <REGX52.H>
#include "Delay.h"
#include "UART.h"
int main()
{
UART_Init();
while(1)
{
}
}
void UART_Routine() interrupt 4
{
if(RI==1)
{
P2=~SBUF;
UART_SendByte(SBUF);
RI=0;
}
}
加入中断函数模板的UART.c
#include <REGX52.H>
/**
* @brief 串口初始化,4800bps@11.0592MHz
* @param 无
* @retval 无
*/
void UART_Init()
{
SCON=0x50; //改为可以接收
PCON|= 0x80;
TMOD &= 0x0F; //设置定时器模式
TMOD |= 0x20; //设置定时器模式
TL1 = 0xF4; //设定定时初值
TH1 = 0xF4; //设定定时器重装值
ET1 = 0; //禁止定时器1中断
TR1 = 1; //启动定时器1
EA=1;
ES=1;
}
/**
* @brief 串口发送一个字节数据
* @param Byte 要发送的一个字节数据
* @retval 无
*/
void UART_SendByte(unsigned char Byte)
{
SBUF=Byte;
while(TI==0);
TI=0;
}
/* 串口中断函数模板
void UART_Routine() interrupt 4
{
if(RI==1)
{
RI=0;
}
}
*/
==串口使用的过程:==先初始化,发送调用SendByte,接收以后判断进中断
波特率是怎么计算的呢?
用到T1溢出率计算,自动重装配置的是0xF3,对应十进制是243,每隔256溢出1次。256-243=13,说明在12MHz的晶振下每隔13us溢出一次,溢出频率是$ \frac{1}{13us}=0.07692Mhz$,设置的波特率倍数(SMOD=1),波特率就是0.07692MHz/16=0.00480769MHz=4807.69HZ,误差7.69/4800=0.001602
编码是看ASCII码表,也可以用单引号发字符
9-1 LED点阵屏
- 使用点阵屏时要把跳线帽JP595,JOE配置一下。GP595需要插上,GOE插到右边的两个
- 像素做成8的倍数相乘的原因是:一个字节有八位,为了充分利用这个因素,就设计成8的倍数,保证字节中的每一位对应到像素,减少浪费
- 数据分为串行和并行;如果是串行相当于是一个一个输出(类似于串口);如果是并行可以同时输出到8根线上
- 每一次上升沿把输入的数据向下移动,等满了以后RCLK来一个上升沿即可把8位数据一下传到右边,实现串行输入并行输出
- 如果QH’接到下一位的SER,就会向下一片中移动
总结一下如何用LED点阵屏显示:
- 首先要进行行选择和列选择;
- 列直接接在IO口上,操作的时候直接给P0赋值就可以;
- 行需要用74HC595
这里都是采用芯片进行驱动的,如果假设一个单片机只驱动点阵,能不能把行直接接在P1口上?
- 不行,单片机的IO口输出是弱上拉类型的,输出低电平能接收很大的电流,输出高电平电流比较小。
9-2 LED点阵屏显示图形 & 动画
- 点阵屏驱动有一个关键:移位寄存器(74HC595),先通过LED测试它的功能
- 单片机里有很多硬件电路,我们操控硬件电路都是通过操控寄存器实现的
为了避免重复定义,先进行特殊位声明
sbit RCK=P3^5; // RCLK
sbit SRCLK=P3^6; // SRCLK
sbit SER=P3^4; // SER
为了将参数的数据写入8个引脚,逻辑在子函数中实现:
-
首先把数据赋值给SER,而且高位在先;用与和或把最高位取出来
-
SER=Byte & 0x80;
这是一位,一般一位我们给1或者0;整个寄存器给0x00,0xff等,保证位对齐,这里没有位对齐;赋值满足非0即1;相当于最高位是1赋给SER为1,最高位是0赋给SER为0 -
上电以后默认都是1,先把SCK=0,然后再给1,让第一位进去,然后清零为下一次准备
unsigned char i; for(i=0;i<8;i++) { SER=Byte & (0x80>>i); SCK=1; SCK=0; // 这么做相当于把8位移进去了 }
void _74HC595_WriteByte(unsigned char Byte) { unsigned char i; for(i=0;i<8;i++) { SER=Byte & (0x80>>i); SCK=1; SCK=0; // 移进去8位 } RCK=1; // 上升沿所存,但必须在主函数中先清零再赋1 RCK=0; }
测试代码:
#include <REGX52.H> sbit RCK=P3^5; // RCLK sbit SCK=P3^6; // SRCLK sbit SER=P3^4; // SER void _74HC595_WriteByte(unsigned char Byte) { unsigned char i; for(i=0;i<8;i++) { SER=Byte & (0x80>>i); SCK=1; SCK=0; } RCK=1; RCK=0; } int main() { SCK=0; RCK=0; _74HC595_WriteByte(0xF0); while(1) { } }
完成了74HC595的初步使用
操控点阵屏可以参考数码管的代码,需要Delay模块
void Nixie(unsigned char Location,Number)
{
switch(Location)
{
case 1: P2_4=1; P2_3=1; P2_2=1; break;
case 2: P2_4=1; P2_3=1; P2_2=0; break;
case 3: P2_4=1; P2_3=0; P2_2=1; break;
case 4: P2_4=1; P2_3=0; P2_2=0; break;
case 5: P2_4=0; P2_3=1; P2_2=1; break;
case 6: P2_4=0; P2_3=1; P2_2=0; break;
case 7: P2_4=0; P2_3=0; P2_2=1; break;
case 8: P2_4=0; P2_3=0; P2_2=0; break;
}
P0=NixieTable[Number];
Delay(1);
P0=0x00;
}
数码管中我们函数的参数是位置和段码,在这里我们可以把每一列看作位置,每一行看作段码
#include <REGX52.H>
#include "Delay.h"
sbit RCK=P3^5; // RCLK
sbit SCK=P3^6; // SRCLK
sbit SER=P3^4; // SER
void _74HC595_WriteByte(unsigned char Byte)
{
unsigned char i;
for(i=0;i<8;i++)
{
SER=Byte & (0x80>>i);
SCK=1;
SCK=0;
}
RCK=1;
RCK=0;
}
void MatrixLED_ShowColumn(unsigned char Column,Data)
{
_74HC595_WriteByte(Data);
P0=~(0x80>>Column);
}
int main()
{
SCK=0;
RCK=0;
MatrixLED_ShowColumn(7,0xAA);
while(1)
{
}
}
- 完成测试,也需要和数码管一样进行消隐,按照写数据+选择列的循环时候,如果在写下一个过程中,会把上一列的数传过来,导致停留在上一位上。
还需要把MatrixLED_ShowColumn(7,0xAA)
放在while循环中,否则亮一下就会灭掉。
-
如果想要显示一个笑脸,我们只需要知道每一列的段码值即可,下为9-1显示笑脸的程序
#include <REGX52.H> #include "Delay.h" sbit RCK=P3^5; // RCLK sbit SCK=P3^6; // SRCLK sbit SER=P3^4; // SER #define MATRIX_LED_PORT P0 /** * @brief 74HC595写入一个字节 * @param 要写入的字节 * @retval 无 */ void _74HC595_WriteByte(unsigned char Byte) { unsigned char i; for(i=0;i<8;i++) { SER=Byte & (0x80>>i); SCK=1; SCK=0; } RCK=1; RCK=0; } /** * @brief LED点阵屏显示一列数据 * @param Column 要选择的列,范围:0~7,0在最左边 * @param Data 选择列显示的数据,高位在上,1为亮,0为灭 * @retval 无 */ void MatrixLED_ShowColumn(unsigned char Column,Data) { _74HC595_WriteByte(Data); MATRIX_LED_PORT=~(0x80>>Column); Delay(1); MATRIX_LED_PORT=0xFF; } int main() { SCK=0; RCK=0; while(1) { MatrixLED_ShowColumn(0,0x3C); MatrixLED_ShowColumn(1,0x42); MatrixLED_ShowColumn(2,0xA9); MatrixLED_ShowColumn(3,0x85); MatrixLED_ShowColumn(4,0x85); MatrixLED_ShowColumn(5,0xA9); MatrixLED_ShowColumn(6,0x42); MatrixLED_ShowColumn(7,0x3C); } }
LED点阵屏模块化
MatrixLED.c
#include <REGX52.H>
#include "Delay.h"
sbit RCK=P3^5; // RCLK
sbit SCK=P3^6; // SRCLK
sbit SER=P3^4; // SER
#define MATRIX_LED_PORT P0
/**
* @brief 74HC595写入一个字节
* @param 要写入的字节
* @retval 无
*/
void _74HC595_WriteByte(unsigned char Byte)
{
unsigned char i;
for(i=0;i<8;i++)
{
SER=Byte & (0x80>>i);
SCK=1;
SCK=0;
}
RCK=1;
RCK=0;
}
/**
* @brief 点阵屏初始化
* @param 无
* @retval 无
*/
void MatrixLED_Init()
{
SCK=0;
RCK=0;
}
/**
* @brief LED点阵屏显示一列数据
* @param Column 要选择的列,范围:0~7,0在最左边
* @param Data 选择列显示的数据,高位在上,1为亮,0为灭
* @retval 无
*/
void MatrixLED_ShowColumn(unsigned char Column,Data)
{
_74HC595_WriteByte(Data);
MATRIX_LED_PORT=~(0x80>>Column);
Delay(1);
MATRIX_LED_PORT=0xFF;
}
MatrixLED.h
#ifndef __MATRIX_LED_H__
#define __MATRIX_LED_H__
void MatrixLED_Init();
void MatrixLED_ShowColumn(unsigned char Column,Data);
#endif
- 做动画,Hello往左走,先存一个数组,这个数组是一长条的动画,然后不断偏移位置显示
在开发板资料里面有文字取模软件
先显示8列数据
#include <REGX52.H>
#include "Delay.h"
#include "MatrixLED.h"
unsigned char Animation[]={
0xFF,0x08,0x08,0x08,0xFF,0x00,0x0E,0x15,0x15,0x15,0x08,0x00,0x7E,0x01,0x02,0x00,
0x7E,0x01,0x02,0x00,0x0E,0x11,0x11,0x0E,0x00,0x7D,0x00,0x00,0x00,0x00,0x00,0x00,
};
int main()
{
void MatrixLED_Init();
while(1)
{
MatrixLED_ShowColumn(0,Animation[0]);
MatrixLED_ShowColumn(1,Animation[1]);
MatrixLED_ShowColumn(2,Animation[2]);
MatrixLED_ShowColumn(3,Animation[3]);
MatrixLED_ShowColumn(4,Animation[4]);
MatrixLED_ShowColumn(5,Animation[5]);
MatrixLED_ShowColumn(6,Animation[6]);
MatrixLED_ShowColumn(7,Animation[7]);
}
}
之后需要隔一段时间向后移动,定义一个偏移量Offset,隔一段时间偏移量进行增长就可以偏移
int main()
{
unsigned char i,Offset=1,Count=0;
void MatrixLED_Init();
while(1)
{
for(i=0;i<=8;i++)
{
MatrixLED_ShowColumn(i,Animation[i+Offset]);
}
Count++;
if(Count>10) // 不能直接调用Delay,因为扫描是不断进行的,这里直接计次,一帧扫描十次
{
Count=0;
Offset++; // 运行一圈以后会乱码,因为数组溢出了,也需要给Offset定时清零
}
}
}
为了让H完整移出去,首位补8位0x00
#include <REGX52.H>
#include "Delay.h"
#include "MatrixLED.h"
unsigned char Animation[]={
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0xFF,0x08,0x08,0x08,0xFF,0x00,0x0E,0x15,
0x15,0x15,0x08,0x00,0x7E,0x01,0x02,0x00,
0x7E,0x01,0x02,0x00,0x0E,0x11,0x11,0x0E,
0x00,0x7D,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
};
int main()
{
unsigned char i,Offset=3,Count=0;
void MatrixLED_Init();
while(1)
{
for(i=0;i<=8;i++)
{
MatrixLED_ShowColumn(i,Animation[i+Offset]);
}
Count++;
if(Count>10)
{
Count=0;
Offset++;
if(Offset>40)
{
Offset=0;
}
}
}
}
如果想实现逐帧动画,Offset可以+8
#include <REGX52.H>
#include "Delay.h"
#include "MatrixLED.h"
unsigned char Animation[]={
0x3C,0x42,0xA9,0x85,0x85,0xA9,0x42,0x3C,
0x3C,0x42,0xA1,0x85,0x85,0xA1,0x42,0x3C,
0x3C,0x42,0xA5,0x89,0x89,0xA5,0x42,0x3C,
};
int main()
{
unsigned char i,Offset=0,Count=0;
void MatrixLED_Init();
while(1)
{
for(i=0;i<=8;i++)
{
MatrixLED_ShowColumn(i,Animation[i+Offset]);
}
Count++;
if(Count>15)
{
Count=0;
Offset+=8;
if(Offset>16)
{
Offset=0;
}
}
}
}
==需要注意的是:==动画定义的数组可能会从出现很多的数据,这些数据都是放在RAM里的;另一种是程序存储器,后者空间会大一些,需要加个关键字code
unsigned char code Animation[]
但是在code里面不能更改了
10-1 DS1302实时时钟
单片机定时有几个缺点:
- 精度不高
- 占用单片机CPU
- 单片机定时器是时钟不能掉电继续运行
拿到芯片以后一定要看手册了解功能和用法
-
DIP封装是直插封装,SO是贴片封装。前者可以插在PCB板上,后者就是开发板上的封装
-
一般情况下有关实时时钟的晶振都是32.768KHz,原因是方便易用且精度较高,作用是给时钟芯片提供稳定的1Hz脉冲
内部是怎么运行的呢?
- 可以这么想:DS1302算是一个小型单片机,里面有一些寄存器,这些寄存器比较特殊。通过通信协议进行数据交互就可以进行寄存器的访问和读写
-
这些寄存器都有一个地址,每个地址下都有一个数据,数据以一个字节一个字节存储
-
命令字完成的任务是:在哪写入,在哪读出
- 单字节写入:
- 把CE置高电平
- 发命令字,移位寄存器先发最低位(时序规定),把命令字的最低位设置到IO口上
- 时钟给上升沿,会把命令字写入单片机;然后把要写入的数据再写入
- 单字节读出:
- 前半部分和写入一样
- RW给1,单片机收到命令会在下一个时钟下降沿把数据放在线上,把IO口释放掉,读出数据
10-2 DS1302时钟 & 可调时钟
时钟芯片需要LCD1602进行显示,先加入LCD1602模块,接着进行测试
#include <REGX52.H>
#include "LCD1602.h"
int main()
{
LCD_Init();
LCD_ShowString(1,1,"RTC");
while(1)
{
}
}
测试无误以后准备写DS1302模块,之前都是在main.c中写好测试完再进行模块化,这次直接建模块文件,DS1302怎么写要看芯片手册
- 要操作端口,就先把端口进行定义
sbit DS1302_SLCK=P3^6;
sbit DS1302_IO=P3^4;
sbit DS1302_CE=P3^5;
定义好之后再操作相应的引脚就操作名字即可
void DS1302_WriteByte(unsigned char Command,Data)
{
DS1302_CE=1;
DS1302_IO=Command & 0x01; // 相当于把第0位取出来
DS1302_SLCK=1; // 置1再置0需要考虑芯片性能,经过测试,此处不加延时也可以
DS1302_SLCK=0;
}
DS1302_IO=Command & 0x01; // 相当于把第0位取出来
DS1302_SLCK=1;
DS1302_SLCK=0;
DS1302_IO=Command & 0x02; // 0000 0010
DS1302_SLCK=1;
DS1302_SLCK=0;
之后也可以一直这么做,也可以通过for循环实现
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_SLCK=1;
DS1302_SLCK=0;
}
}
for循环结束,程序运行到了D0写入之前的时刻;后面和前面一样
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_SLCK=1;
DS1302_SLCK=0;
}
for(i=0;i<8;i++)
{
DS1302_IO=Data & (0x01<<i);
DS1302_SLCK=1;
DS1302_SLCK=0;
}
DS1302_CE=0;
}
接着,我们进行读取函数的编写
unsigned char DS1302_ReadByte(unsigned char Command)
{
DS1302_CE=1;
for(i=0;i<8;i++)
{
DS1302_IO=Command & (0x01<<i);
DS1302_SLCK=1;
DS1302_SLCK=0;
}
}
我们可以先给0,再给1,就可以实现;注意结束之后先给下降沿
unsigned char DS1302_ReadByte(unsigned char Command)
{
unsigned char i,Data=0x00; // Data是一个变量,是用来保存IO线上DS1302芯片发来的数据的
DS1302_CE=1;
for(i=0;i<8;i++)
{
DS1302_IO=Command & (0x01<<i);
DS1302_SLCK=0;
DS1302_SLCK=1;
}
DS1302_SLCK=1;
DS1302_SLCK=0;
if(DS1302_IO){Data|=0x01}; // 把第一位抄到Data里
}
- 输入读命令字的8个SCLK周期后,随后的8个SCLK周期的下降沿,一个数据字节被输出。注意第一个数据位的传送发生命令字被写完后的第一个下降沿。
unsigned char DS1302_ReadByte(unsigned char Command)
{
unsigned char i,Data=0x00;
DS1302_CE=1;
for(i=0;i<8;i++)
{
DS1302_IO=Command & (0x01<<i);
DS1302_SLCK=0;
DS1302_SLCK=1;
}
for(i=0;i<8;i++)
{
DS1302_SLCK=1; // 重复置1的目的是去掉一个下降沿,因为与写入相比读只有十五个脉冲
DS1302_SLCK=0;
if(DS1302_IO){Data|=(0x01<<i)};
}
DS1302_CE=0;
return Data;
}
DS1302模块
DS1302.c
#include <REGX52.H>
sbit DS1302_SLCK=P3^6;
sbit DS1302_IO=P3^4;
sbit DS1302_CE=P3^5;
void DS1302_Init(void)
{
DS1302_CE=0;
DS1302_SLCK=0;
}
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_SLCK=1;
DS1302_SLCK=0;
}
for(i=0;i<8;i++)
{
DS1302_IO=Data & (0x01<<i);
DS1302_SLCK=1;
DS1302_SLCK=0;
}
DS1302_CE=0;
}
unsigned char DS1302_ReadByte(unsigned char Command)
{
unsigned char i,Data=0x00;
DS1302_CE=1;
for(i=0;i<8;i++)
{
DS1302_IO=Command & (0x01<<i);
DS1302_SLCK=0;
DS1302_SLCK=1;
}
for(i=0;i<8;i++)
{
DS1302_SLCK=1;
DS1302_SLCK=0;
if(DS1302_IO) {Data|=(0x01<<i);}
}
DS1302_CE=0;
DS1302_IO=0;
return Data;
}
DS1302.h
#ifndef __DS1302_H__
#define __DS1302_H__
void DS1302_Init(void);
void DS1302_WriteByte(unsigned char Command,Data);
unsigned char DS1302_ReadByte(unsigned char Command);
#endif
显示会存在一个9之后跳到16的情况,这是因为内部是用BCD码进行存储
在主函数中我们调整一下,再进行测试:
#include <REGX52.H>
#include "LCD1602.h"
#include "DS1302.h"
#include "Delay.h"
unsigned char Second,Minute;
int main()
{
LCD_Init();
DS1302_Init();
LCD_ShowString(1,1,"RTC");
DS1302_WriteByte(0x80,0x55);
while(1)
{
Second=DS1302_ReadByte(0x81);
Minute=DS1302_ReadByte(0x83);
LCD_ShowNum(2,1,Second/16*10+Second%16,2);
LCD_ShowNum(2,3,Minute/16*10+Minute%16,2);
}
}
但是我们这样写年月日小时分钟秒都需要变量,进一步优化一下:
-
unsigned char DS1302_Time[]={19,11,16,12,59,55,6};
先将年月日时分秒星期几用数组存储 -
写两个函数是为了方便对同时数据进行读取
-
在设置时间之前还需要把写保护关闭掉
-
地址每次都查很麻烦,用#define配置一个表格
#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
我们知道,写入最低位是0,读出最低位为1,修改ReadByte函数,Command | =0x01;
,这样我们给地址的时候直接给写的地址,读的时候可以转化为读的地址,不需要重复定义太多地址了。
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);
}
DS1302_Time[0]= DS1302_ReadByte(DS1302_YEAR);
,读出来是BCD码,需要存成十进制,因为需要用两次,定义变量存一下
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;
}
把两个函数声明为外部可调用后,数组也需要声明为外部可调用,可以在前面加上extern,(变量声明外部必须加,数组函数可以不加,默认有一个)
完善后的DS1302模块
DS1302.c
#include <REGX52.H>
sbit DS1302_SLCK=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[]={19,11,16,12,59,55,6};
void DS1302_Init(void)
{
DS1302_CE=0;
DS1302_SLCK=0;
}
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_SLCK=1;
DS1302_SLCK=0;
}
for(i=0;i<8;i++)
{
DS1302_IO=Data & (0x01<<i);
DS1302_SLCK=1;
DS1302_SLCK=0;
}
DS1302_CE=0;
}
unsigned char DS1302_ReadByte(unsigned char Command)
{
unsigned char i,Data=0x00;
Command | =0x01;
DS1302_CE=1;
for(i=0;i<8;i++)
{
DS1302_IO=Command & (0x01<<i);
DS1302_SLCK=0;
DS1302_SLCK=1;
}
for(i=0;i<8;i++)
{
DS1302_SLCK=1;
DS1302_SLCK=0;
if(DS1302_IO) {Data|=(0x01<<i);}
}
DS1302_CE=0;
DS1302_IO=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;
}
DS1302.h
#ifndef __DS1302_H__
#define __DS1302_H__
extern unsigned char DS1302_Time[];
void DS1302_Init(void);
void DS1302_WriteByte(unsigned char Command,Data);
unsigned char DS1302_ReadByte(unsigned char Command);
void DS1302_SetTime(void);
void DS1302_ReadTime(void);
#endif
主函数
#include <REGX52.H>
#include "LCD1602.h"
#include "DS1302.h"
#include "Delay.h"
int main()
{
LCD_Init();
DS1302_Init();
LCD_ShowString(1,1," - - ");
LCD_ShowString(2,1," : : ");
DS1302_SetTime();
while(1)
{
DS1302_ReadTime();
LCD_ShowNum(1,1,DS1302_Time[0],2);
LCD_ShowNum(1,4,DS1302_Time[1],2);
LCD_ShowNum(1,7,DS1302_Time[2],2);
LCD_ShowNum(2,1,DS1302_Time[3],2);
LCD_ShowNum(2,4,DS1302_Time[4],2);
LCD_ShowNum(2,7,DS1302_Time[5],2);
}
}
接下来我们在此基础上加入按键设置,闪烁是靠定时器实现,所以把按键模块和定时器模块拿过来。
这个程序主要有两个部分:
- 时钟显示
- 时钟设置
定义两个函数,按键按下之后改变MODE,根据MODE值改变函数交替运行
void TimeSet(void)
{
if(KeyNum==2)
{
TimeSetSelect++;
TimeSetSelect%=6; // 相当于if(TimeSetSelect>5)TimeSetSelect=0;
}
}
接着设置按下按键3为加,按下按键4为减
void TimeSet(void)
{
if(KeyNum==2)
{
TimeSetSelect++;
TimeSetSelect%=6;
}
if(KeyNum==3)
{
DS1302_Time[TimeSetSelect]++;
}
if(KeyNum==4)
{
DS1302_Time[TimeSetSelect]--;
}
DS1302_ReadTime();
LCD_ShowNum(1,1,DS1302_Time[0],2);
LCD_ShowNum(1,4,DS1302_Time[1],2);
LCD_ShowNum(1,7,DS1302_Time[2],2);
LCD_ShowNum(2,1,DS1302_Time[3],2);
LCD_ShowNum(2,4,DS1302_Time[4],2);
LCD_ShowNum(2,7,DS1302_Time[5],2);
LCD_ShowNum(2,10,TimeSetSelect,2);
}
完成对++的判断之后,对–的判断需要注意:小于0越界,但0再–是255,需要改成有符号的
#include <REGX52.H>
#include "LCD1602.h"
#include "DS1302.h"
#include "Delay.h"
#include "Key.h"
#include "Timer0.h"
unsigned char KeyNum,MODE,TimeSetSelect;
void TimeShow(void)
{
DS1302_ReadTime();
LCD_ShowNum(1,1,DS1302_Time[0],2);
LCD_ShowNum(1,4,DS1302_Time[1],2);
LCD_ShowNum(1,7,DS1302_Time[2],2);
LCD_ShowNum(2,1,DS1302_Time[3],2);
LCD_ShowNum(2,4,DS1302_Time[4],2);
LCD_ShowNum(2,7,DS1302_Time[5],2);
}
void TimeSet(void)
{
if(KeyNum==2)
{
TimeSetSelect++;
TimeSetSelect%=6;
}
if(KeyNum==3)
{
DS1302_Time[TimeSetSelect]++;
if(DS1302_Time[0]>99){DS1302_Time[0]=0;}
if(DS1302_Time[1]>12){DS1302_Time[1]=1;}
if(DS1302_Time[1]==1 || DS1302_Time[1]==3 || DS1302_Time[1]==5 || DS1302_Time[1]==7 ||
DS1302_Time[1]==8 || DS1302_Time[1]==10|| DS1302_Time[1]==12 )
{
if(DS1302_Time[2]>31){DS1302_Time[2]=1;}
}
else if(DS1302_Time[1]==4 || DS1302_Time[1]==6 || DS1302_Time[1]==9 || DS1302_Time[1]==11)
{
if(DS1302_Time[2]>30){DS1302_Time[2]=1;}
}
else if(DS1302_Time[1]==2)
{
if(DS1302_Time[0]%4==0)
{
if(DS1302_Time[2]>29){DS1302_Time[2]=1;}
}
else
{
if(DS1302_Time[2]>28){DS1302_Time[2]=1;}
}
}
if(DS1302_Time[3]>23){DS1302_Time[3]=0;}
if(DS1302_Time[4]>59){DS1302_Time[4]=0;}
if(DS1302_Time[5]>59){DS1302_Time[5]=0;}
}
if(KeyNum==4)
{
DS1302_Time[TimeSetSelect]--;
if(DS1302_Time[0]<0){DS1302_Time[0]=99;}
if(DS1302_Time[1]<1){DS1302_Time[1]=12;}
if(DS1302_Time[1]==1 || DS1302_Time[1]==3 || DS1302_Time[1]==5 || DS1302_Time[1]==7 ||
DS1302_Time[1]==8 || DS1302_Time[1]==10|| DS1302_Time[1]==12 )
{
if(DS1302_Time[2]<1){DS1302_Time[2]=31;}
}
else if(DS1302_Time[1]==4 || DS1302_Time[1]==6 || DS1302_Time[1]==9 || DS1302_Time[1]==11)
{
if(DS1302_Time[2]<1){DS1302_Time[2]=30;}
}
else if(DS1302_Time[1]==2)
{
if(DS1302_Time[0]%4==0)
{
if(DS1302_Time[2]<1){DS1302_Time[2]=29;}
}
else
{
if(DS1302_Time[2]<1){DS1302_Time[2]=28;}
}
}
if(DS1302_Time[3]<0){DS1302_Time[3]=23;}
if(DS1302_Time[4]<0){DS1302_Time[4]=59;}
if(DS1302_Time[5]<0){DS1302_Time[5]=59;}
}
LCD_ShowNum(1,1,DS1302_Time[0],2);
LCD_ShowNum(1,4,DS1302_Time[1],2);
LCD_ShowNum(1,7,DS1302_Time[2],2);
LCD_ShowNum(2,1,DS1302_Time[3],2);
LCD_ShowNum(2,4,DS1302_Time[4],2);
LCD_ShowNum(2,7,DS1302_Time[5],2);
LCD_ShowNum(2,10,TimeSetSelect,2);
}
int main()
{
LCD_Init();
DS1302_Init();
LCD_ShowString(1,1," - - ");
LCD_ShowString(2,1," : : ");
DS1302_SetTime();
while(1)
{
KeyNum=Key();
if(KeyNum==1)
{
if(MODE==0){MODE=1;}
else if(MODE==1){MODE=0;}
}
switch(MODE)
{
case 0:TimeShow();break;
case 1:TimeSet();break;
}
}
}
以上程序基本实现了,但还有bug,比如12.31改为11月时31号没有改变,但11月没有31这天,需要在–部分加入大于的判断。
此外,为了能顺利把时间设置进去,需要把写保护关闭(DS1302.c中DS1302_SetTime最后一行注释掉)
完成以上工作后还需要把对应位闪烁,需要用定时器模块。一秒闪烁可以这么实现:定义一个变量1秒为周期1010翻转,对选择的位:1的话熄灭,0的话显示
- !是逻辑取反,即:把非0的数值变为0,0变为1;
- ~ 是按位取反,即在数值的二进制表示方式上,将0变为1,将1变为0;
最后的主函数
#include <REGX52.H>
#include "LCD1602.h"
#include "DS1302.h"
#include "Delay.h"
#include "Key.h"
#include "Timer0.h"
unsigned char KeyNum,MODE,TimeSetSelect,TimeSetFlashFlag;
void TimeShow(void)
{
DS1302_ReadTime();
LCD_ShowNum(1,1,DS1302_Time[0],2);
LCD_ShowNum(1,4,DS1302_Time[1],2);
LCD_ShowNum(1,7,DS1302_Time[2],2);
LCD_ShowNum(2,1,DS1302_Time[3],2);
LCD_ShowNum(2,4,DS1302_Time[4],2);
LCD_ShowNum(2,7,DS1302_Time[5],2);
}
void TimeSet(void)
{
if(KeyNum==2)
{
TimeSetSelect++;
TimeSetSelect%=6;
}
if(KeyNum==3)
{
DS1302_Time[TimeSetSelect]++;
if(DS1302_Time[0]>99){DS1302_Time[0]=0;}
if(DS1302_Time[1]>12){DS1302_Time[1]=1;}
if(DS1302_Time[1]==1 || DS1302_Time[1]==3 || DS1302_Time[1]==5 || DS1302_Time[1]==7 ||
DS1302_Time[1]==8 || DS1302_Time[1]==10|| DS1302_Time[1]==12 )
{
if(DS1302_Time[2]>31){DS1302_Time[2]=1;}
}
else if(DS1302_Time[1]==4 || DS1302_Time[1]==6 || DS1302_Time[1]==9 || DS1302_Time[1]==11)
{
if(DS1302_Time[2]>30){DS1302_Time[2]=1;}
}
else if(DS1302_Time[1]==2)
{
if(DS1302_Time[0]%4==0)
{
if(DS1302_Time[2]>29){DS1302_Time[2]=1;}
}
else
{
if(DS1302_Time[2]>28){DS1302_Time[2]=1;}
}
}
if(DS1302_Time[3]>23){DS1302_Time[3]=0;}
if(DS1302_Time[4]>59){DS1302_Time[4]=0;}
if(DS1302_Time[5]>59){DS1302_Time[5]=0;}
}
if(KeyNum==4)
{
DS1302_Time[TimeSetSelect]--;
if(DS1302_Time[0]<0){DS1302_Time[0]=99;}
if(DS1302_Time[1]<1){DS1302_Time[1]=12;}
if(DS1302_Time[1]==1 || DS1302_Time[1]==3 || DS1302_Time[1]==5 || DS1302_Time[1]==7 ||
DS1302_Time[1]==8 || DS1302_Time[1]==10|| DS1302_Time[1]==12 )
{
if(DS1302_Time[2]<1){DS1302_Time[2]=31;}
if(DS1302_Time[2]>31){DS1302_Time[2]=1;}
}
else if(DS1302_Time[1]==4 || DS1302_Time[1]==6 || DS1302_Time[1]==9 || DS1302_Time[1]==11)
{
if(DS1302_Time[2]<1){DS1302_Time[2]=30;}
if(DS1302_Time[2]>30){DS1302_Time[2]=1;}
}
else if(DS1302_Time[1]==2)
{
if(DS1302_Time[0]%4==0)
{
if(DS1302_Time[2]<1){DS1302_Time[2]=29;}
if(DS1302_Time[2]>29){DS1302_Time[2]=1;}
}
else
{
if(DS1302_Time[2]<1){DS1302_Time[2]=28;}
if(DS1302_Time[2]>28){DS1302_Time[2]=1;}
}
}
if(DS1302_Time[3]<0){DS1302_Time[3]=23;}
if(DS1302_Time[4]<0){DS1302_Time[4]=59;}
if(DS1302_Time[5]<0){DS1302_Time[5]=59;}
}
if(TimeSetSelect==0 && TimeSetFlashFlag==1){LCD_ShowString(1,1," ");}
else {LCD_ShowNum(1,1,DS1302_Time[0],2);}
if(TimeSetSelect==1 && TimeSetFlashFlag==1){LCD_ShowString(1,4," ");}
else {LCD_ShowNum(1,4,DS1302_Time[1],2);}
if(TimeSetSelect==2 && TimeSetFlashFlag==1){LCD_ShowString(1,7," ");}
else {LCD_ShowNum(1,7,DS1302_Time[2],2);}
if(TimeSetSelect==3 && TimeSetFlashFlag==1){LCD_ShowString(2,1," ");}
else {LCD_ShowNum(2,1,DS1302_Time[3],2);}
if(TimeSetSelect==4 && TimeSetFlashFlag==1){LCD_ShowString(2,4," ");}
else {LCD_ShowNum(2,4,DS1302_Time[4],2);}
if(TimeSetSelect==5 && TimeSetFlashFlag==1){LCD_ShowString(2,7," ");}
else {LCD_ShowNum(2,7,DS1302_Time[5],2);}
}
int main()
{
LCD_Init();
DS1302_Init();
Timer0Init();
LCD_ShowString(1,1," - - ");
LCD_ShowString(2,1," : : ");
DS1302_SetTime();
while(1)
{
KeyNum=Key();
if(KeyNum==1)
{
if(MODE==0){MODE=1;TimeSetSelect=0;}
else if(MODE==1){MODE=0;DS1302_SetTime();}
}
switch(MODE)
{
case 0:TimeShow();break;
case 1:TimeSet();break;
}
}
}
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count;
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count++;
if(T0Count>=500)
{
T0Count=0;
TimeSetFlashFlag=!TimeSetFlashFlag;
}
}
根据MODE的值选择不同的功能是一个比较重要的点
还有一点bug就是按下按键不松手时间会停住,可以利用定时器中断扫描按键,可以对上升沿下降沿单独捕获。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)