模块化编程
把各个模块的代码放在不同的c.文件里,再.h文件里提供外部可调用函数的声明,其他.c文件想使用其中的代码时,只需要#include "XXX.h"文件即可。使用模块化编程可极大的提高代码的可阅读性、可维护性、可移植性等。
个人理解:分开各个模块,使其作用专一,方便更改,或者复制到另一个项目,同时也简洁,更容易读懂代码。
格式:(以Delay函数举例)
- .h文件:可被外部调用的函数、变量的声明
//文件名一般大写
#ifndef _DELAY_H_//if not define如果没有定义,防止重复定义
#define _DELAY_H_
void Delay(unsigned char xms);//外部可调用的函数,注意:这里末尾要加分号
#endif
- .c文件:函数、变量的定义
//写Delay函数的定义
void Delay(unsigned int xms)
{
unsigned char i, j;
while(xms--)
{
i = 2;
j = 239;
do
{
while(--j);
}while(--i);
}
}
写好并添加进工程之后便可在main主函数中,添加头文件#include “Delay.h”(双引号表示自己写的),就可以使用Delay函数了。
注意:
- 任何自定义的变量、函数,在调用前必须有定义或者声明(同一个.c);
- 使用到的自动化函数的.c文件必须添加到工程参与编译;
- 使用到的.h为念必须要放到编译器可寻找的地方(工程文件夹根目录、安装目录、自定义)。
引申知识点
c预编译:c语言的预编译以#开头,作用是在真正的编译开始之前,对代码进行一些处理(预编译)
此外还有#ifndef,#if,#else,#elif,#undef等。
LCD调试工具
- 使用LCD1602液晶屏作为调试窗口、提供类似printf函数的功能,可实时观察单片机内部数据的变换情况,便于调试和演示。
这一节没什么好记的,因为用的都是他写好的函数,只要知道怎么用就好了。用法如下:
注意:LCD与数码管冲突。
矩阵键盘
介绍
- 在键盘中案件数量较多时,为了减少I/O口的占用,通常将按键排列成矩阵形式
- 采用逐行或逐列的“扫描”,就可以读出任何位置按键的状态(类似于数码管的扫描,只不过一个是输出,一个是输入,后面会谈到)
由图可知,P10至P13控制列,P14至P17控制行【可看作四排独立按键理解】
扫描的概念
对比
- 数码管扫描(输出扫描)
原理:显示第1位->显示第2位->显示第3位->…,快速循环,利用人眼视觉残留,实现所有数码管同时显示的效果。 - 矩阵键盘扫描(输入扫描)
原理:读取第1行(列)->读取第2行(列)->读取第3行(列)->…,快速循环这个过程,因为循环速度极快,最终实现所有按键同时检测的效果
以上两种扫描方式的共同特点:节省I/O口的数量
前提:因为矩阵键盘与步进电机(蜂鸣器)引脚冲突,为了避免蜂鸣器响,所以采取逐列扫描
难点:P1P2P3都是弱上拉工作模式,在上面的原理图中如果P1输出低电平,按键另一端连接VCC的话,检测时电流太大。因此另一端连接GND,检测P1口输入是否是低电平来判断按键是否被按下(个人理解)
根据上面的知识内容荣,就可以写出下面的矩阵键盘检测的
.h文件:
#ifndef __MATRIXKEY_H__
#define __MATRIXKEY_H__
unsigned char MatrixKey();
#endif
.c文件:
#include <REGX52.H>
#include "Delay.h"//定义中运用了Delay函数,因此需要加上头文件
/**
*@brief 检测矩阵键盘按键键码
*@param 无
*@retval KeyNumber 键码
*/
unsigned char MatrixKey(){
unsigned char KeyNumber = 0;
P1 = 0xFF;//全置1
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;//返回键码
}
【上面的写法简单且容易理解】
用LCD液晶屏显示矩阵键盘键码
#include <REGX52.H>
#include "MatrixKey.h"
#include "LCD1602.h"
#include "Delay.h"
void main(){
LCD_Init();
LCD_ShowString(1,1,"Hello");//第一行显示Hello
while(1){
unsigned char key = MatrixKey();//检测键码
if(key){ //用if语句,防止key立马清零后显示0
LCD_ShowNum(2,1,key,2);
}
}
}
并且,可以由此写出
矩阵键盘密码锁
#include <REGX52.H>
#include "MatrixKey.h"
#include "LCD1602.h"
#include "Delay.h"
unsigned char i = 0;//i表示位数
unsigned char a = 0;//a表示正确与否
unsigned char KeyNumber;
unsigned char password[4] = {1,3,4,1};//密码
void main(){
LCD_Init();
LCD_ShowString(1,1,"Password:");
LCD_ShowString(2,1," ");
while(1){
KeyNumber = MatrixKey();//检测键码
if(i < 4){
if(KeyNumber>0 && KeyNumber<=10){ //当键码为0~10时,因为只显示一位数字,因此10即是0
if(KeyNumber == password[i]&&a<4){ //对应密码正确的话,a增加1
a++;
}
LCD_ShowNum(2,i+1,KeyNumber,1);
i++; //输入数字后,位数加1后移
}
}
/*if(KeyNumber == 11 && i >= 0){
i--;
LCD_ShowString(2,i+1," ");
}*/
if(KeyNumber == 12){//键码为12,表示确认按键
if(i == 4){//输入了4位
if(a == 4){
LCD_ShowString(1,11,"Right!");//当四位数字都正确时,输出Right
Delay(1000);
break;
}
else LCD_ShowString(1,11,"Error!");//否则,输出Error
Delay(600);
LCD_ShowString(1,11," ");
LCD_ShowString(2,1," ");
}
else { //数字不是四位时
LCD_ShowString(1,11,"False!"); //输出错误False
Delay(600);
LCD_ShowString(1,11," "); //清屏
LCD_ShowString(2,1," ");
}
i = 0; //数据重置
a = 0;
}
}
}
定时器
介绍
- 51单片机的定时器属于单片机的内部资源,其电路的连接和运转均在单片机内部完成
- 作用:
(1)用于计时系统,可实现软件计时,或者使程序每隔以固定时间完成一项操作
(2)替代长时间的Delay,提高CPU的运行效率和处理速度
…
STC89C52定时器资源
- 定时器个数:3个(T0,T1,T2),T0和T1与传统的51单片机兼容,T2是此型号单片机增加的资源。
- 注意:定时器的资源和单片机的型号是关联在一起的,不同的型号可能会有不同的定时器个数和操作方式,但一般来说,T0和T1的操作方式是所有51单片机共有的。
定时器框图
- 定时器在单片机内部就像一个小闹钟,根据时钟的输出信号,每隔“一秒”,计数单元加一,当到达所设置的数字时,计数单元就会向中断系统发出中断申请,使程序跳转到中断函数中执行。
定时器的工作模式
模式0:13位定时器/计数器
模式1:16位定时器/计数器
模式2:8位自动重装模式
模式3:两个8位计数器
工作模式1框图:
时钟:
SYSclk:系统时钟,即晶振周期,本开发板上的晶振为11.0592MHz;
T0pin是外部引脚;
12T代表12分频,即12个周期一次输出;
C/T:如果是高电平1,那么就是计数器的功能,如果是低电平0,那就是计数器的功能。
计数单元:
TL0和TH0为一个16位的计数器,有两个字节,高字节为TH,低字节为TL;
当TL0和TH0最大时,就会向中断系统申请中断。
中断系统:
中断系统是为使CPU具有对外界紧急事件的实时处理能力而设置的。
如图所示过程:
当申请中断时,就会跳到中断程序,处理完后在返回主程序;
当加粗样式存在多个中断源时,CPU会根据优先级进行处理,首先响应高优先级的中断请求。
STC89C52中断资源:
- 中断源个数:8个(外部中断0、定时器0中断、外部中断1、定时器1中断、串口中断、定时器2中断、外部中断2、外部中断3)
- 中断优先级个数:4个
- 中断号
- 注意:中断的资源和单片机的型号是关联在一起的,不同的型号可能会有不同的中断资源,例如中断源个数不同、中断优先级个数不同等等
总的结构图:
寄存器
- 寄存器是连接软硬件的媒介
- 在单片机中寄存器就是一段特殊的RAM存储器,一方面,寄存器可以存储和读取数据,另一方面,每一个寄存器背后都连接了一根导线,控制着电路的连接方式
- 寄存器相当于一个复杂机器的“操作按钮”
定时器/计时器0和1的相关寄存器
因此,需要设置TCON(Timer Control)和TMOD寄存器来设置定时器
- TF1: 定时器/计数器T1溢出标志。T1被允许计数以后,从初值开始加1计数。当最高位产生溢出时由硬件置“1”TF1,向CPU请求中断,一直保持到CPU响应中断时,才由硬件清“0” TF1(TF1也可由程序查询清“0” )。
- TR1: 定时器T1的运行控制位。该位由软件置位和清零。当GATE(TMOD.7) =0,TR1=1时就TR1:允许T1开始计数,TR1=0时禁止T1计数。当GATE(TMOD.7) =1,TR1=1且INT1输入高电平时,才允许T1计数。
- TF0: 定时器/计数器TO溢出中断标志。T0被允许计数以后,从初值开始加1计数,当最高位产生溢出时,由硬件置“1”TFO,向CPU请求中断,一直保持CPU响应该中断时,才由硬件清“0”TFO ( TFO也可由程序查询清“0”)。[查询中断便是查询TF0是否为1]
- TR0:定时器T0的运行控制位。该位由软件置位和清零。当GATE(TMOD.3) =0,TR0=1时就允许T0开始计数,TR0=0时禁止T0计数。当GATE(TMOD.3) =1,TR1=0且INTO输入高电平时,才允许T0计数。
- IE1: 外部中断1请求源(INT1/P3.3) 标志。IE1=1,外部中断向CPU请求中断,当CPU响应该中断时由硬件清“0”IE1。
- IT1: 外部中断1触发方式控制位。IT1=0时,外部中断1为低电平触发方式,当INT1 (P3.3)输入低电平时,置位IE1 。采用低电平触发方式时,外部中断源(输入到INT1) 必须保持低电平有效,直到该中断被CPU 响应,同时在该中断服务程序执行完之前,外部中断源必须被清除(P3.3要变高),否则将产生另一次中断。当IT1=1时,则外部中断1(INT1)端口由“1”-“0”下降沿跳变,激活中断请求标志位IE1 ,向主机请求中断处理。
- IE0: 外部中断0请求源 (INTO/P3.2) 标志。IEO=1外部中断0向CPU请求中断,当CPU响应外部中断时,由硬件清“0”IEO(边沿触发方式)。
- IT0: 外部中断0触发方式控制位。ITO=0时,外部中断0为低电平触发方式,当ITO(P3.2)IT0:输入低电平时,置位IEO。采用低电平触发方式时,外部中断源(输入到INTO) 必须保持低电平有效,直到该中断被CPU响应,同时在该中断服务程序执行完之前,外部中断源必须被清除(P3.2要变高),否则将产生另一次中断。当ITO=1时,则外部中断0(INTO)端口由“1”-“0”下降沿跳变,激活中断请求标志位IE1 ,向主机请求中断处理。
【可位寻址:可以对寄存器中的每一位单独赋值】
2.
M1为0,M0为1时,即为16位定时器,我们要用的模式;
C/T:控制定时器还是计数器功能;
GATE:门控端,控制是否由外部控制启动定时器
中断寄存器
结合下图看
由图可以看,启动中断
IE中需要ET0置1,EA置1;
IP为设置中断优先级,可以不考虑,我们暂且置0,低优先级。
定时器的初始化:
#include <REGX52.H>
/**
*@brief 定时器0初始化
*@param 无
*@retval 无
*/
void Timer0Init(void)
{
//由于TMOD不可寻址,因此需要用与&,或|来对单独一位设置
TMOD &= 0xF0; //把TMOD的第四位清零而高四位不变
TMOD |= 0x01;//把TMOD的最低位置一
TL0 = 0x66;//低四位
TH0 = 0xFC;//高四位
TF0 = 0;//清零
TR0 = 1;
//中断寄存器配置
ET0 = 1;
EA = 1;
PT0 = 0;
}
一秒定时:
void Timer0_Routine() interrupt 1
{
static unsigned int count = 0;//静态变量
//重置
TL0 = 0x66;
TH0 = 0xFC;
count++;//每次中断加一
if(count >= 1000){
}
}
定时器也可由软件直接生成
由此,我们就可以写出
按键控制LED流水灯方向
#include <REGX52.H>
#include "Timer0.h"
#include "Delay.h"
unsigned char mode = 0;
void main(){
Timer0Init();
P2 = 0xFE;
while(1){//检测按键并改变模式
if(P3_1 == 0){
Delay(20);
while(P3_1 == 0);
Delay(20);
mode = 1;
}
if(P3_0 == 0){
Delay(20);
while(P3_0 == 0);
Delay(20);
mode = 0;
}
}
}
void Timer0_Routine() interrupt 1
{
static unsigned int count = 0;
TL0 = 0x66;
TH0 = 0xFC;
count++;
//流水灯代码
if(count >= 1000 && mode == 0){
P2 = (P2<<1) + 1;
if(P2 == 0xFF){
P2 = 0xFE;
}
count = 0;
}
if(count >= 1000 && mode == 1){
P2 = (P2>>1) + 0x80;
if(P2 == 0xFF){
P2 = 0x7F;
}
count = 0;
}
}
定时器计时
#include <REGX52.H>
#include "Timer0.h"
#include "LCD1602.h"
#include "Key.h"//独立按键检测模块
unsigned char hour=0,min=0,sec=0;//初始化
void main()
{
//初始化
LCD_Init();
Timer0Init();
LCD_ShowString(1,1,"Clock:");
while(1)
{
//显示时间
LCD_ShowNum(2,1,hour,2);
LCD_ShowNum(2,4,min,2);
LCD_ShowNum(2,7,sec,2);
switch(Key())//检测按键并选择时间加一
{
case 1:
hour++;
break;
case 2:
min++;
break;
case 3:
sec++;
break;
case 4://清零
hour = 0;
min = 0;
sec = 0;
break;
}
}
}
void Timer0_Routine() interrupt 1
{
static unsigned int count = 0;
TL0 = 0x66;
TH0 = 0xFC;
count++;
//时钟
if(count >= 1000){
count = 0;
sec++;
if(sec >= 60)
{
sec = 0;
min++;
if(min >= 60)
{
min = 0;
hour++;
if(hour >= 24)
{
hour = 0;
}
}
}
}
}
串口通信
介绍
- 串口是一种应用广泛的通讯接口,成本低、易使用,通信线路简单可用于实现两个设备之间的互相通信;
- 单片机的串口可以使单片机与单片机,单片机与电脑,单片机与各式各样的模块互相通信,极大地拓展了单片机的应用范围,增强了其硬件实力;
- 51单片机内部自带UART(Universal Asynchronous Receiver Transmitter,通用异步收发器),可实现单片机的串口通信。
硬件电路
TXD(Transmit Exchange Data)发送,RXD(Receive External Data)接收,因此需要两根通信线。
电平标准
电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种:
- TTL电平:+5V表示1,0V表示0(单片机)
- RS232电平:-3-15V表示1,+3+15V表示0(多用于电脑,稳定)
- RS485电平:两线压差+2+6V表示1,-2-6V表示0(差分信号)
【TTL电平和 RS232电平传输距离有限,而RS485电平传输距离可以很远,千米级别】
接口及引脚定义
常见通信接口比较
此外还有:CAN、USB等
相关术语:
单片机上的UART
-
STC89C52有1个UART
-
STC89C52的UART有四种工作模式:
模式0:同步移位寄存器
模式1:8位UART,波特率可变(常用)
模式2:9位UART,波特率固定
模式3:9位UART,波特率可变
串口参数及时序图
- 波特率:串口通信的速率(发送和接收各数据位的间隔时间)
- 检验位:用于数据验证(减压数据传输是否正确)
- 停止位:用于数据帧间隔
串口模式图
SBUF:串口数据缓存寄存器,物理上是两个独立的寄存器,但占用相同的地址。写操作时,写入的是发送寄存器(SBUF在等号左边),读操作时,读出的是接受寄存器(SBUF在等号右边)。
过程:
发送时,数据通过总线流入,通过定时器控制发送控制器来控制移位寄存器,将数据寄存到发送寄存器,同时触发中断,然后再通过总线发送出去,而速率通过定时器控制。接收类似。
串口相关寄存器
配置SCON和PCON,并打开中断IE就可以了。
SCON:
我们需要配置模式一:SM0为0,SM1为1;
不需要多机通信,因此SM2配为0;
REN:控制是否允许单片机接收数据,0为不允许;
TB8,RB8为9位数据,不是模式一的,因此不管,置0;
T1,R1标志位初始化给0;
PCON:
SMOD控制波特率是否加倍,不需要,因此给0;
SMOD0用于9位数据帧错误,模式一为8为数据,不需要,置0;
串口初始化
#include <REGX52.H>
void UART_Init(void) //4800bps@11.0592MHz
{
SCON = 0x40; //只发
PCON |= 0x80;
//设置定时器模式8位自动重装
TMOD &= 0x0F;
TMOD |= 0x20;
//设置定时初值
TL1 = 0xF4;
TH1 = 0xF4;
ET0 = 0; //禁止定时器中断
EA = 1;
TR1 = 1; //启动定时器1
}
也可通过软件配置串口
发送一个字节
void UART_SendByte(unsigned char Byte)
{
SBUF = Byte;
while(TI==0);//检测是否完成发送
TI=0;//重置复位
}
数据显示模式
- HEX模式/十六进制模式/二进制模式:以原始数据的形式显示
- 文本模式/字符模式:以原始数据编码后的形式显示(ASCII码)
例子:电脑通过串口控制LED
#include <REGX52.H>
#include "Delay.h"
#include "UART.h"
unsigned char x;
void main()
{
UART_Init(); //初始化
while(1){
UART_SendByte(x);
Delay(1000); //延时一秒发送数据x
}
}
void UART_Routine() interrupt 4 //串口中断为interrupt 4
{
if(RI == 1) //如果是接收
{
x = SBUF; //更新x的值
P2 = ~x; //改变LED
RI = 0; //复位
}
if(TI == 1) //如果是发送,x每秒加一
{
P2 = ~x++; //改变LED,并x加一
}
}
LED点阵屏
介绍
LED点阵屏由若干个独立的LED组成,LED以矩阵的形式排列,以灯珠亮灭来显示文字,图片,视频等。LED点阵屏广泛应用于各种公共场合。
- LED点阵屏分类
按颜色:单色、双色、全彩
按像素:8x8、16*16(大规模的LED点阵屏通常有很多个小点阵屏拼接而成)
显示原理
- LED显示屏的连接方式类似于数码管,只不过是数码管把每一列的像素以“8”字型排列而已;
- LED点阵屏与数码管一样,有共阴和共阳两种接法,不同的接法对应的电路结构不用;
- LED点阵屏需要进行逐行或逐列扫描,才能使LED同时显示
点阵屏上LED的引脚有可能是按最近引脚引出,因此可能是乱序排列
原理图
OE:输出使能,低电平有效【交线帽操作】
SRCLR:串行清零端
74HC595介绍
74HC595是串行输入并行输出的移位寄存器,可用3根线输入串行数据,8根线输出并行数据,多片级联后,可输出16位,24位,32位等,常用于I/O口扩展。
- SER输入串行数据;
- SERCLK:上升沿移位,即在上升沿(从低电平到高电平)时,会将SER的数据移进寄存器;
- RCLK:上升沿锁存,即在上升沿(从低电平到高电平)时,会将寄存器里的数据锁存,并将这并行数据一同输出。
开发板引脚对应关系
列由P0控制,行由74HC595控制。
补充知识点:C51的sfr,sbit
- sfr(special function register):特殊功能寄存器声明,例:sfr P0 = 0x80;
声明P0口寄存器,物理地址为0x80 - sbit(special bit):特殊位声明
例:sbit P0_1 = 0x81; 或 sbit P0_1 = P0^1;
声明P0寄存器的第1位 - 可位寻址/不可位寻址:在单片机系统中,操作任意寄存器或者某一位的数据时,必须给出其物理地址,又因为一个寄存器里有8位,所以位的数量是寄存器数量的8倍,单片机无法对所有位进行编码,故每8个寄存器中,只有一个是可以位寻址的。对不可位寻址的寄存器,若要只操作其中一位而不影响其他位时,可用“&=”、“|=”、“^=”的方法进行位操作
控制74HC595(代码)
详情请看注释
#include <REGX52.H>
sbit RCK = P3^5; //RCLK上升沿锁存
sbit SCK = P3^6; //SERCLK上升沿移位
sbit SER = P3^4; //串行数据
/**
*@brief 74HC595写入一个字节
*@param 要写入的字节
*@retval 无
*/
void _74HC595_WriteByte(unsigned char Byte)
{
unsigned char i;
for(i = 0; i < 8; i++) //循环8次,移入一个字节
{
SER = Byte&(0x80>>i); //SER先送高位数字,SER赋值的数非0即1
SCK = 1; //上升沿————移位
SCK = 0; //下降沿————清零重置
}
RCK = 1; //上升沿————锁存
RCK = 0; //下降沿————清零重置
}
控制点阵屏显示(代码)
由上面引脚图可知,P0控制列,74HC595控制行。
因此:
#define column P0
/**
*@brief 矩阵屏初始化
*@param 无
*@retval 无
*/
void MatrixLED_Init()
{
SCK = 0;
RCK = 0;
}
/**
*@brief 矩阵屏显示一列数据
*@param x列,y行
*@retval 无
*/
void MatrixLED_Show(unsigned char x, y) //x轴控制列,y轴控制行
{
_74HC595_WriteByte(y); //控制行,高位在上,低位在下,高电平1亮
column = ~(0x80>>x); //控制列,高位在左、低位在右,低电平0亮
Delay(1); //延时
column = 0xFF; //清零,消影
}
然后,我们就可以用上面的MatrixLED_Show函数显示图形了。
点阵屏显示动画
动画即是不断在动的画,因此将画连续显示,就成了动画。我们可以利用上面的点阵屏显示函数,写一个循环语句以达成该目的。
#include <REGX52.H>
#include "MatrixLED.h"
unsigned char code Hellocartoon[] = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0xFF,0x08,0x08,0x08,0x08,0xFF,0x00,0x00,0x0E,0x15,0x15,0x15,0x0D,0x00,0x00,0xFF,
0x01,0x02,0x00,0xFF,0x01,0x02,0x00,0x00,0x0E,0x11,0x11,0x0E,0x00,0x00,0xFD,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
}; //Hello动画数组存储数据,开头与末尾个八个0x00,用来循环时可无缝衔接
void main()
{
unsigned char i, offset = 0, cnt = 0;
MatrixLED_Init(); //初始化
while(1)
{
for(i=0;i<8;i++)
{
MatrixLED_Show(i,Hellocartoon[i+offset]); //显示一个画面
}
cnt++; //cnt控制一张画面显示多久时间
if(cnt == 10)
{
cnt = 0;
offset++; //cnt达到10时,画面切换成下一帧,因为只是要画面左移,所以offset加一即可
if(offset > 40)
{
offset = 0; //当offset加到40(最大数量48-8)时,重置
}
}
}
}
动画数组数据因为运行时是不用改变的,放在RAM占用空间,因此可以加一个code(如代码所示),存到闪存flash
取字模软件获取动画参数
示例:
DS1302
介绍
- DS1302是由美国DALLAS公司推出的具有涓细电流充电能力的低耗实时时钟芯片。他可以对年、月、日、周、时、分、秒进行计时,且具有闰年补偿等多种功能;
- RTC(Real Time Clock):实时时钟,是一种集成电路,通常被称为时钟芯片。
定时器时钟由于精度不高,占用CPU,因此通常不选择,而选择实时时钟芯片。
引脚定义和应用电路
注意:VCC2才是主电源
32.768kHz晶振精确,可以给出1Hz的稳定脉冲,称作石英钟
这里的CE,IO,SCLK与74HC595里的RCLK,SER,SERCLK是一样的,都是P3_5,P3_4,P3_6。因此实时时钟会与点阵屏冲突。
开发板上的原理图
可以看到,并没有备用电源,因此断电后始终无法继续走。
内部结构框图
CE使能,
寄存器定义
Seconds秒,Minutes分,Hour时,Date日期,Month月份,Day星期,Year年份
第一行的CH置1的话,时钟停止
倒数第二行中有一个WP,控制写保护
Address/Command Byte:命令字
由命令字表可以看到
第6位表示控制RAM还是时钟
第0位表示是否开启写保护(上头有横杠表示低电平有效)
命令字可以在RTC第一二列查到;
时序定义
- CE使能
- R/W为1时读出read,为0时写入write
- 在上升沿写入,在下降沿读出
- 读出数据是由时钟控制的
- 不管是读出还是写入,首先得明确位置,即应先输入命令字
DS1302读写代码
当电压为5V时,上升下降间隔时间最小为50ns,ns级别,单片机因此不用考虑
#include <REGX52.H>
sbit DS1302_SCLK = P3^6;
sbit DS1302_IO = P3^4;
sbit DS1302_CE = P3^5;
//写入
void DS1302_WriteByte(unsigned char Command, Date)
{
unsigned char i;
DS1302_CE = 1; //开启使能
for(i = 0; i < 8; i++)
{
DS1302_IO = Command&(0x01<<i); //命令字,0x01不断左移,逐个数字写入
DS1302_SCLK = 1; //上升沿移位
DS1302_SCLK = 0; //清零
}
for(i = 0; i < 8; i++)
{
DS1302_IO = Date&(0x01<<i); //要写入的数据也逐个写入
DS1302_SCLK = 1;
DS1302_SCLK = 0;
}
DS1302_CE = 0; //关闭使能
}
//读出
unsigned char DS1302_ReadByte(unsigned char Command)
{
unsigned char i, Date = 0x00;
Command |= 0x01; //读出的命令字最低位为1
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;
DS1302_SCLK = 0;
if(DS1302_IO){Date |= (0x01<<i);} 如果IO不为0,则存入1,以此办法读出数据
}
DS1302_IO = 0;
DS1302_CE = 0; //关闭使能
return Date;
}
补充知识:BCD码
- BCD码(Binary Coded Decimal),用4位二进制数来表示1位十进制数
- 例:0001 0011表示13,1000 0101表示85,0001 1010不合法
- 在十六进制中的体现:0x13表示13,0x85表示85,0x1A不合法
- BCD码转十进制:DEC=BCD/16*10+BCD%16;(2位BCD)
- 十进制转BCD码:BCD=DEC/10*16+DEC%10;(2位BCD)
时钟数据是以BCD码存储的
所以在显示时钟时需要将数据转化为十进制
显示时钟代码
因为上面的两个函数读出和写入时需要调用太多,太麻烦
因此,我们写一个DS1302_ReadTime函数直接读出时钟数据,一个DS1302_SetTime函数直接将时间写入时钟
#include <REGX52.H>
sbit DS1302_SCLK = P3^6;
sbit DS1302_IO = P3^4;
sbit DS1302_CE = P3^5;
//声明时间,方便后续书写
#define year 0x8C
#define day 0x8A
#define month 0x88
#define date 0x86
#define hour 0x84
#define minute 0x82
#define second 0x80
#define WP 0x8E
//初始时间
unsigned char Time[] = {23,12,13,12,0,0,3};
/**
*@brief 时钟初始化
*@param 无
*@retval 无
*/
void DS1302_Init(void)
{
DS1302_CE = 0;
DS1302_SCLK = 0;
}
/**
*@brief 写入对应时间
*@param Command命令字——对应时间地址(year,month...)
*@param Date数据——写入相应的时间数据
*@retval 无
*/
void DS1302_WriteByte(unsigned char Command, Date)
{
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 = Date&(0x01<<i);
DS1302_SCLK = 1;
DS1302_SCLK = 0;
}
DS1302_CE = 0;
}
/**
*@brief 读出对应时间
*@param Command命令字——对应时间地址(year,month...)
*@retval Date返回时间数据
*/
unsigned char DS1302_ReadByte(unsigned char Command)
{
unsigned char i, Date = 0x00;
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;
DS1302_SCLK = 0;
if(DS1302_IO){Date |= (0x01<<i);}
}
DS1302_IO = 0;
DS1302_CE = 0;
return Date;
}
/**
*@brief 将数组时间写入芯片
*@param 无
*@retval 无
*/
void DS1302_SetTime(void)
{
DS1302_WriteByte(WP,0x00); //关闭写保护
//年、月、日、时、分、秒、星期,依次写入数组里的时间
DS1302_WriteByte(year,Time[0]/10*16+Time[0]%10);
DS1302_WriteByte(month,Time[1]/10*16+Time[1]%10);
DS1302_WriteByte(date,Time[2]/10*16+Time[2]%10);
DS1302_WriteByte(hour,Time[3]/10*16+Time[3]%10);
DS1302_WriteByte(minute,Time[4]/10*16+Time[4]%10);
DS1302_WriteByte(second,Time[5]/10*16+Time[5]%10);
DS1302_WriteByte(day,Time[6]/10*16+Time[6]%10);
DS1302_WriteByte(WP,0x80); //打开写保护
}
/**
*@brief 将当前时间赋值给初始时间数组
*@param 无
*@retval 无
*/
void DS1302_ReadTime(void)
{
unsigned char temp; //暂存数据
依次将时钟时间赋给数组的时间
temp = DS1302_ReadByte(year);
Time[0] = temp/16*10+temp%16;
temp = DS1302_ReadByte(month);
Time[1] = temp/16*10+temp%16;
temp = DS1302_ReadByte(date);
Time[2] = temp/16*10+temp%16;
temp = DS1302_ReadByte(hour);
Time[3] = temp/16*10+temp%16;
temp = DS1302_ReadByte(minute);
Time[4] = temp/16*10+temp%16;
temp = DS1302_ReadByte(second);
Time[5] = temp/16*10+temp%16;
temp = DS1302_ReadByte(day);
Time[6] = temp/16*10+temp%16;
}
然后主函数
#include <REGX52.H>
#include "LCD1602.h"
#include "DS1302.h"
#include "Delay.h"
void main()
{
LCD_Init();
DS1302_Init();
LCD_ShowString(1,1," - - ");
LCD_ShowString(2,1," : : ");
DS1302_SetTime();
while(1)
{
DS1302_ReadTime(); //不断将时钟赋值给数组时间
//显示时间
LCD_ShowNum(1,1,Time[0],2);
LCD_ShowNum(1,4,Time[1],2);
LCD_ShowNum(1,7,Time[2],2);
LCD_ShowNum(2,1,Time[3],2);
LCD_ShowNum(2,4,Time[4],2);
LCD_ShowNum(2,7,Time[5],2);
LCD_ShowNum(2,13,Time[6],1);
}
}
进阶,可调时钟
#include <REGX52.H>
#include "LCD1602.h"
#include "DS1302.h"
#include "Delay.h"
#include "Key.h"
#include "Timer0.h"
void TimeShow(); //时间展示模式
void TimeSet(); //时间设置模式
//TimeSet里按键三四判断该月最大天数。输入月份、年份,输出天数
unsigned char JudgeDate(unsigned char month, year);
unsigned char mode,keynum,number,x;
void main()
{
//初始化
LCD_Init();
DS1302_Init();
Timer0Init();
//DS1302_SetTime();
LCD_ShowString(1,1," - - ");
LCD_ShowString(1,12,"Mode");
LCD_ShowString(2,1," : : ");
while(1)
{
keynum = Key(); //检测按键
LCD_ShowNum(1,16,mode,1); //显示模式
//按键一转换模式
if(keynum == 1)
{
if(mode == 0){mode = 1;}
else if(mode == 1)
{
mode = 0;
DS1302_SetTime();
}
}
//模式启动
if(mode == 0)
{
LCD_ShowString(2,10," "); //清除序号
TimeShow();
}
if(mode == 1){TimeSet();}
}
}
//时间显示函数
void TimeShow()
{
DS1302_ReadTime();
LCD_ShowNum(1,1,Time[0],2);
LCD_ShowNum(1,4,Time[1],2);
LCD_ShowNum(1,7,Time[2],2);
LCD_ShowNum(2,1,Time[3],2);
LCD_ShowNum(2,4,Time[4],2);
LCD_ShowNum(2,7,Time[5],2);
}
//调时间函数
void TimeSet()
{
//按键二切换年月日时分秒
if(keynum == 2)
{
//序号增加,越界清零
number++;
number%=6;
}
LCD_ShowNum(2,10,number+1,2); //显示年月日等序号顺序
//按键三增加
if(keynum == 3)
{
Time[number]++;
switch(number)
{
case 1: //月
if(Time[1]>12){Time[1]=1;}
break;
case 2: //日
if(Time[2]>JudgeDate(Time[1],Time[0]))
{Time[2]=1;}
break;
case 3: //时
if(Time[3]>23){Time[3]=0;}
break;
case 4: //分
if(Time[4]>59){Time[4]=0;}
break;
case 5: //秒
if(Time[5]>59){Time[5]=0;}
break;
}
}
//按键四减少
if(keynum == 4)
{
Time[number]--;
switch(number)
{
case 1: //月
if(Time[1]== 0){Time[1]=12;}
break;
case 2: //日
if(Time[2]== 0)
{Time[2]=JudgeDate(Time[1],Time[0]);}
break;
case 3: //时
if(Time[3]>23){Time[3]=23;}
break;
case 4: //分
if(Time[4]>59){Time[4]=59;}
break;
case 5: //秒
if(Time[5]>59){Time[5]=59;}
break;
}
}
//防止改变年月时日期超过最大天数
if(Time[2]>JudgeDate(Time[1],Time[0]))
{Time[2]=JudgeDate(Time[1],Time[0]);}
//展示更改时间
if(x == 0)
{
LCD_ShowNum(1,1,Time[0],2);
LCD_ShowNum(1,4,Time[1],2);
LCD_ShowNum(1,7,Time[2],2);
LCD_ShowNum(2,1,Time[3],2);
LCD_ShowNum(2,4,Time[4],2);
LCD_ShowNum(2,7,Time[5],2);
}
else //否则,对应位置清空,以达到闪烁效果
{
if(number < 3)
{
LCD_ShowString(1,number*3+1," ");
}
else LCD_ShowString(2,number*3-8," ");
}
}
//判断最大日期
unsigned char JudgeDate(unsigned char month, year)
{
unsigned char DateMax;
if(month==1||month==3||month==5||month==7
||month==8||month==10||month==12)
{DateMax = 31;} //31天月份
if(month==4||month==6||month==9||month==11)
{DateMax = 30;} //30天月份
if(month==2) //先判断2月,在判断闰年
{
if(year%4==0){DateMax=29;}
else DateMax=28;
}
return DateMax; //返回最大日期
}
//中断,计时,用来闪烁的周期——1s
void Timer0_Routine() interrupt 1
{
static unsigned int count = 0;
TL0 = 0x66;
TH0 = 0xFC;
count++;
if(count>=300)
{
count = 0;
x = ~x;
}
}
任务二代码(main函数):
#include <REGX52.H>
#include "LCD1602.h"
#include "DS1302.h"
#include "Delay.h"
#include "Key.h"
#include "Timer0.h"
#include "Nixie.h"
#include "MatrixKey.h"
#include "MatrixLED.h"
void mode0();
void mode1();
void mode2();
void mode3();
void clarm();
void showtime();//时分秒分开显示,后续模式二编写闪烁比较方便
void showhour();
void showminute();
void showsecond();
unsigned char keynum,key_last, mode, lastmode, x, y;
//按键keynum,模式mode,“-”闪烁判断x,时间数字闪烁判断y
unsigned char code clarm_p[] = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0xFB,0xFB,0x00,0x00,0x00,
0x00,0x00,0x00,0xFB,0xFB,0x00,0x00,0x00,//?????
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0xFB,0xFB,0x00,0x00,0x00,//?????
0x00,0x00,0x00,0xFB,0xFB,0x00,0x00,0x00,
0x00,0x00,0x00,0xFB,0xFB,0x00,0x00,0x00,
0x00,0x00,0x00,0x80,0x80,0x00,0x00,0x00,//????
0x00,0x00,0x00,0x80,0x80,0x40,0x20,0x00,
0x00,0x00,0x00,0x80,0x80,0x40,0x20,0x18,
0x00,0x00,0x00,0x80,0x80,0x42,0x24,0x18,
0x00,0x00,0x00,0x81,0x81,0x42,0x24,0x18,
0x00,0x04,0x02,0x81,0x81,0x42,0x24,0x18,
0x18,0x04,0x02,0x81,0x81,0x42,0x24,0x18,
0x18,0x24,0x42,0x81,0x81,0x42,0x24,0x18,
0x18,0x24,0x42,0x81,0x81,0x42,0x24,0x18,
0x18,0x24,0x42,0x81,0x81,0x42,0x24,0x18,//????
};
void main()
{
DS1302_Init();//初始化
Timer0Init();
MatrixLED_Init();
while(1)
{
keynum = MatrixKey();//读取按键
if(keynum == 4 && key_last == 0)mode = 1; //切换模式
if(keynum == 8 && key_last == 0)
{
DS1302_SetTime();//将数组时间写入芯片,更新模式一所调时间
mode = 2;
}
if(keynum == 9 && key_last == 0)mode = 3;
switch(mode)//启动模式
{
case 0:
mode0();//初始模式
break;
case 1:
mode1();//调节模式
break;
case 2:
mode2();//计时模式
break;
case 3:
mode3();//设置闹钟模式
DS1302_ReadTime();//不停更新数组时间,使得在调闹钟的时候,时钟依旧在走
MatrixLED_Init();
break;
case 4:
clarm();//闹钟
DS1302_ReadTime();//不停更新数组时间,使得在调闹钟的时候,时钟依旧在走
break;
}
key_last = keynum;
}
}
void mode0()
{
if(x == 1)
{
nixie(3,11);
nixie(6,11);
}
nixie(1,1);nixie(2,2);nixie(4,0);nixie(5,0);nixie(7,0);nixie(8,0);
}
unsigned char temp;
void mode1()
{
//加
if(keynum==1 && key_last == 0)//加小时,越界清零
{
Time[3]++;
if(Time[3]>23)
{
Time[3] = 0;
}
}
if(keynum==2 && key_last == 0)//加分钟,越界清零
{
Time[4]++;
if(Time[4]>59)
{
Time[4] = 0;
}
}
if(keynum==3 && key_last == 0)//加秒,越界清零
{
Time[5]++;
if(Time[5]>59)
{
Time[5] = 0;
}
}
//减
if(keynum==5 && key_last == 0)//减小时,越界清零
{
Time[3]--;
if(Time[3]>23)
{
Time[3] = 23;
}
}
if(keynum==6 && key_last == 0)//减分钟,越界清零
{
Time[4]--;
if(Time[4]>59)
{
Time[4] = 59;
}
}
if(keynum==7 && key_last == 0)//减秒,越界清零
{
Time[5]--;
if(Time[5]>59)
{
Time[5] = 59;
}
}
//显示横杠-
nixie(3,11);nixie(6,11);
//显示数字
if(keynum != 0)//暂存按键数据
{
temp = keynum;
}
//闪烁
if(y == 1)
{
showtime();
}
else
{
if(temp == 1||temp == 5)
{
showminute();
showsecond();
}
else if(temp == 2||temp == 6)
{
showhour();
showsecond();
}
else if(temp == 3||temp == 7)
{
showhour();
showminute();
}
else showtime();
}
}
void mode2()
{
DS1302_ReadTime();//更新数组时间
MatrixLED_Init();//因为加下来P0要置0,所以得先将点阵屏初始化
if(x == 1) //周期显示横杠-
{
nixie(3,11);
nixie(6,11);
}
showtime();
}
unsigned char clarm_hour,clarm_minute,clarm_second;//闹钟时分秒
void mode3()
{
//显示闹钟时间
nixie(3,11);nixie(6,11);//显示横杠-
nixie(1,clarm_hour/10);nixie(2,clarm_hour%10);//时
nixie(4,clarm_minute/10);nixie(5,clarm_minute%10);//分
nixie(7,clarm_second/10);nixie(8,clarm_second%10);//秒
//加
if(keynum==1 && key_last == 0)//加小时,越界清零
{
clarm_hour++;
if(clarm_hour>23)
{
clarm_hour = 0;
}
}
if(keynum==2 && key_last == 0)//加分钟,越界清零
{
clarm_minute++;
if(clarm_minute>59)
{
clarm_minute = 0;
}
}
if(keynum==3 && key_last == 0)//加秒,越界清零
{
clarm_second++;
if(clarm_second>59)
{
clarm_second = 0;
}
}
//减
if(keynum==5 && key_last == 0)//减小时,越界清零
{
clarm_hour--;
if(clarm_hour>23)
{
clarm_hour = 23;
}
}
if(keynum==6 && key_last == 0)//减分钟,越界清零
{
clarm_minute--;
if(clarm_minute>59)
{
clarm_minute = 59;
}
}
if(keynum==7 && key_last == 0)//减秒,越界清零
{
clarm_second--;
if(clarm_second>59)
{
clarm_second = 59;
}
}
}
unsigned char p_num, i, count3;//p_num图像帧数
void clarm() //闹钟
{
for(i = 0; i < 8; i++)
{
MatrixLED_Show(i,clarm_p[i+p_num]);//显示一帧
}
if(count3 > 200)
{
count3 = 0;
p_num += 8; //显示下一帧
if(p_num > 136)p_num = 48; //超出时重置
}
}
void showtime() //显示时间
{
showhour();
showminute();
showsecond();
}
void showhour()
{
nixie(1,Time[3]/10);nixie(2,Time[3]%10);//时
}
void showminute()
{
nixie(4,Time[4]/10);nixie(5,Time[4]%10);//分
}
void showsecond()
{
nixie(7,Time[5]/10);nixie(8,Time[5]%10);//秒
}
void Timer0_Routine() interrupt 1
{
static unsigned int count1 = 0, count2 = 0;
TL0 = 0x66;
TH0 = 0xFC;
count1++;
count2++;
if(count1 >= 500)
{
count1 = 0;
x = !x;
}
if(count2 >= 250)
{
count2 = 0;
y = !y;
}
if(Time[3]==clarm_hour&&
Time[4]==clarm_minute&&
Time[5]==clarm_second)//判断是否达到预定时间,达到就切换成闹钟
{
mode = 4;
}
count3++;
}