文章目录

  • 6-1 矩阵键盘
  • 6-2 矩阵键盘密码锁
  • 7-1 定时器
  • 7-2 按键控制LED流水灯 & 定时器时钟
    • 独立按键模块
    • 定时器模块
  • 8-1 串口通信
  • 8-2 串口向电脑发送数据 & 电脑通过串口控制LED
    • 串口模块
  • 9-1 LED点阵屏
  • 9-2 LED点阵屏显示图形 & 动画
    • LED点阵屏模块化
  • 10-1 DS1302实时时钟
  • 10-2 DS1302时钟 & 可调时钟
    • DS1302模块
    • 完善后的DS1302模块

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;}

  1. 在扫描第一列时,如果P1_7==0,那么此时是判断开关1的状态
  2. 由于是机械按键,加入延时函数消除抖动,然后判断是否松手;如果松手,继续消除抖动
  3. 返回值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 矩阵键盘密码锁

可以直接复制工程然后粘贴(也是常用的操作)

  1. 首先我们要定义按键功能:S1-S9定义为数字的1-9,S10定义为0,S11为确认键,S12为取消键,S13-S16按键不用
  2. 判断KeyNum<=10,然后实现密码左移,同时要加入一个计次变量,按下确认或者取消按键后密码计次清零。
#include <REGX52.H>
#include "Delay.h"
#include "LCD1602.h"
#include "MatrixKey.h"unsigned char KeyNum;
unsigned int Password,Count;    //Count全局变量默认初始化为0int 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转化为0Count++ ;    // 计次+1              }LCD_ShowNum(2,1,Password,4);  //更新显示}if(KeyNum==11)  //如果S11按键按下,确认{if(Password==2345)   // 如果密码等于正确密码 {LCD_ShowString(1,14,"OK ");    // 显示OK,多空一格,要不会出现OKRPassword=0;                   //密码清零Count=0;                     //计次清零LCD_ShowNum(2,1,Password,4);  //更新显示}else{LCD_ShowString(1,14,"ERR");   // 显示ERRPassword=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 0001TF0=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 0001TMOD=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,是用来存储数据的,单片机的写入与读出就是靠I2CI^2CI2C接口的
  • 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位自动重装模式
    1. 之前讲的是16位,用两个8位表示一个大的计数器:0-65535;缺点是进入中断时需要赋初值,会占用一定时间,所以精度不是特别高
    2. 串口中需要更精准的,分成两个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;     //启动定时器1EA=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;     //启动定时器1EA=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点阵屏显示:

  1. 首先要进行行选择和列选择;
  2. 列直接接在IO口上,操作的时候直接给P0赋值就可以;
  3. 行需要用74HC595

这里都是采用芯片进行驱动的,如果假设一个单片机只驱动点阵,能不能把行直接接在P1口上?

  • 不行,单片机的IO口输出是弱上拉类型的,输出低电平能接收很大的电流,输出高电平电流比较小。

9-2 LED点阵屏显示图形 & 动画

  • 点阵屏驱动有一个关键:移位寄存器(74HC595),先通过LED测试它的功能

  • 单片机里有很多硬件电路,我们操控硬件电路都是通过操控寄存器实现的

为了避免重复定义,先进行特殊位声明

sbit RCK=P3^5;       // RCLK
sbit SRCLK=P3^6;     // SRCLK
sbit SER=P3^4;       // SER

为了将参数的数据写入8个引脚,逻辑在子函数中实现:

  1. 首先把数据赋值给SER,而且高位在先;用与和或把最高位取出来

  2. SER=Byte & 0x80; 这是一位,一般一位我们给1或者0;整个寄存器给0x00,0xff等,保证位对齐,这里没有位对齐;赋值满足非0即1;相当于最高位是1赋给SER为1,最高位是0赋给SER为0

  3. 上电以后默认都是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实时时钟

单片机定时有几个缺点:

  1. 精度不高
  2. 占用单片机CPU
  3. 单片机定时器是时钟不能掉电继续运行

拿到芯片以后一定要看手册了解功能和用法

  • DIP封装是直插封装,SO是贴片封装。前者可以插在PCB板上,后者就是开发板上的封装

  • 一般情况下有关实时时钟的晶振都是32.768KHz,原因是方便易用且精度较高,作用是给时钟芯片提供稳定的1Hz脉冲

内部是怎么运行的呢?

  • 可以这么想:DS1302算是一个小型单片机,里面有一些寄存器,这些寄存器比较特殊。通过通信协议进行数据交互就可以进行寄存器的访问和读写

  • 这些寄存器都有一个地址,每个地址下都有一个数据,数据以一个字节一个字节存储

  • 命令字完成的任务是:在哪写入,在哪读出

  • 单字节写入:

    1. 把CE置高电平
    2. 发命令字,移位寄存器先发最低位(时序规定),把命令字的最低位设置到IO口上
    3. 时钟给上升沿,会把命令字写入单片机;然后把要写入的数据再写入
  • 单字节读出:
    1. 前半部分和写入一样
    2. 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 0010DS1302_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); }
}

但是我们这样写年月日小时分钟秒都需要变量,进一步优化一下:

  1. unsigned char DS1302_Time[]={19,11,16,12,59,55,6};先将年月日时分秒星期几用数组存储

  2. 写两个函数是为了方便对同时数据进行读取

  3. 在设置时间之前还需要把写保护关闭掉

  4. 地址每次都查很麻烦,用#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        0x8Eunsigned 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);  }
}

接下来我们在此基础上加入按键设置,闪烁是靠定时器实现,所以把按键模块和定时器模块拿过来。

这个程序主要有两个部分:

  1. 时钟显示
  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就是按下按键不松手时间会停住,可以利用定时器中断扫描按键,可以对上升沿下降沿单独捕获。

b站江科大自化协51单片机入门教程笔记(2)相关推荐

  1. b站江科大自化协51单片机入门教程笔记(1)

    文章目录 1 综述 2-1 点亮一个LED 2-2 LED闪烁 2-3 LED流水灯 2-4 LED流水灯PLus 3-1 独立按键控制LED亮灭 3-2 独立按键控制LED状态 3-3 独立按键控制 ...

  2. 一份标准的STM32工程模板都需要哪些文件?(B站江科大自化协)

    大家好,我是烟火.目前BMS软件工程师在职,利用自由时间,输出一些基础知识合集,一方面巩固,另一方面写博客作为成长记录. 人间清醒: 明明有能力可以变成更优秀的人 遇见更好的人 过更喜欢的生活 如果因 ...

  3. 01、【江科大自化协stm32F103c8t6】笔记之【入门32单片机及GPIO初始化参数配置】

    目录 一.前言 二.必要资料 1.C语言类型 2.片上资源/外设 3.引脚定义 4.系统结构 三.GPIO初始化 1.首先使用RCC开启GPIO的时钟 2.其次使用GPIO_Init函数初始化GPIO ...

  4. 51单片机入门学习笔记(上)

    笔记整理自B站UP主江科大自化协教程<51单片机入门教程-2020版 程序全程纯手打 从零开始入门> 所用单片机为普中51 STC89C52单片机2022新款,文中所提到的调试现象及结果也 ...

  5. 51单片机入门教程学习笔记

    基于江科大自化协B站教学视频<51单片机入门教程-2020版 程序全程纯手打 从零开始入门> 一.单片机介绍 单片机,英文Micro Controller Unit,简称MCU 内部集成了 ...

  6. 51单片机入门教程(6)——外部中断

    51单片机入门教程(6)--外部中断 一.外部中断 1.1 中断 1.2 外部中断 二.中断优先级 一.外部中断 1.1 中断 关于中断的概念在上一篇博客中已经提到了.(传送门:51单片机入门教程(5 ...

  7. 51单片机入门教程(5)——定时器中断

    51单片机入门教程(5)--定时器中断 一.中断的概念 二.定时器中断 2.1 软件延时的不足 2.2 中断寄存器 2.2.1 中断允许控制寄存器 IE 2.2.2 定时器工作方式寄存器 TMOD 2 ...

  8. 51单片机入门教程(1)——点亮一个LED灯

    51单片机入门教程(1)--点亮一个LED灯 一.什么是单片机 单片机(Microcontrollers)是一种集成电路芯片,是采用超大规模集成电路技术把具有数据处理能力的中央处理器CPU.随机存储器 ...

  9. 51单片机入门教程(3)——数码管显示

    51单片机入门教程(3)--数码管显示 一.LED数码管简介 二.数码管静态显示 三.数码管动态显示 特别鸣谢:HFUTer-朱聪强 一.LED数码管简介 LED数码管(LED Segment Dis ...

最新文章

  1. c语言加法减法乘法,一元多项式的加法减法乘法c语言描述线性表应用
  2. 本人真实经历:面试了20家大厂之后,发现这样介绍项目经验,显得项目很牛逼!...
  3. python fine函数_python find()函数
  4. signature=02d2eb69b4d24e2f9bb2956f66089339,Signature Balancing
  5. Java Swing Mysql实现图书管理系统源码附带高清视频指导运行教程
  6. java零碎要点---用java实现生成二维码,与解析代码实现
  7. NP、NP-完全、NP-难问题
  8. OpenCV 画圆circle、画椭圆ellipse
  9. 对“xxx”类型的已垃圾回收委托进行了回调。这可能会导致应用程序崩溃、损坏和数据丢失。向非托管代码传递委托时,托管应用程序必须让这些委托保持活动状态,直到确信不会再次调用它们。...
  10. Spark机器学习数据流水线
  11. linux展示文件最尾部内容 最新,LINUX tailf命令-显示文件的末尾若干行内容
  12. OneNote网页版链接用桌面应用打开报错的解决
  13. 谈谈 跨境劳工的噩梦深坑。
  14. horovod 安装及使用
  15. 有计划,坚决执行——写给大三犹豫的女生
  16. nest-mysql:RBAC权限管理
  17. zsh介绍:2: CentOS下使用zsh
  18. 【luogu1468】[Violet]蒲公英--求区间众数
  19. 弘辽科技:百分之99卖家不知道的秘密
  20. 概率论发展史上的几个重要悖论

热门文章

  1. 电源适配器适用GB8898-2001和GB4943-2001的差异
  2. xampp集成环境里查看php版本
  3. 培训php包装自己的简历,用一流的简历包装自己
  4. 什么地图制作软件好用,简单的地图绘制软件
  5. mysql服务攻击检测_3款SQL INJECTION攻击检测工具_MySQL
  6. js 将金额转换成大写汉字
  7. nedc和epa续航里程什么意思_NEDC、WLTP和EPA续航里程哪种最真实?
  8. 用C语言实现控制台播放音乐的功能
  9. J2cache两级缓存原理
  10. 【自动化测试】每天自动执行pytest自动化测试脚本,并生成allure报告