6-1 矩阵键盘

image-20220809231200626

image-20220809231330278

对数码管来说,在同一时间不能同时控制多位数码管显示不同数字,但可以利用扫描解决。

  • 矩阵连接的越多,节省I/O口越明显。比如1080P的比例为1920*1080=2073600,显示屏需要2073600个像素点才能显示1080P的视频,且因为RGB通道,还需要乘3,共需6220800个LED。单独判断需要600多万个I/O口,但是如果连接成矩阵形式,只需要1920+1080=3000,再乘3为9000个,大幅减少了I/O口。

按行扫描:

image-20220809232831560

如果是按行扫描,那么同一时间只有一行是0(P17-P14中只有一个为0),然后检测P13-P10,即可判断一行中哪个按键被按下。

但是不推荐逐行扫描,因为按行扫描P15会时高时低,而P15连接到步进电机,右边连接BZ,经过驱动器驱动会增加输出电流能力,连接到蜂鸣器上,这个开发板上BZ以一定频率高低变换时蜂鸣器会响。

按列扫描:

image-20220809233511912

按列扫描时下面四个口(P10-P13)同时只有一个口给0,扫描上面四个口即可按列判断哪个开关按下。

由于本节会用到LCD1602和Delay模块,从5-2中将已经写好的复制到项目目录下:

image-20220810103231047

针对上节无法打开LCD一系列函数定义的问题,可以先编译,然后全部保存,重新进入项目即可打开

image-20220810103701785

对于每次都写.h文件,我们可以插入模板

image-20220810105331332

image-20220810104924345

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全局变量默认初始化为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,如果直接连在一起的话按键检测会很不灵敏,为了解决灵敏度问题研究本节内容。

image-20220811114600892

前面几节讲的按键,数码管,LCD1602都是单片机IO口控制的外设,定时器是单片机内部完成的。

其他用途还可以进行任务切换,多任务同时执行

image-20220811115250831

image-20220811120216811

image-20220811154917681

  • 中间为计数系统(此处为16位) TL和TL两个一块最大只能存0-65535,溢出时会置一个标志位TF0,然后向中断系统申请中断

image-20220811155359318

  • 默认12T的模式会分频,输出为1MHz,那么连此时的线路每隔一微秒计数一次;C/T是一个选择开关,配置为1时为计数功能(count),给0为定时器(time);本节配置为实现定时器功能
  • 时钟也可以由系统提供,也可以由外部引脚来提供,如下图中位置

image-20220811155555507

image-20220811162247941

image-20220811162225627

意味着可以同时完成两项任务,主程序和中断程序

image-20220811165834977

image-20220811165944293

电路的连接依靠于定时器相关寄存器

image-20220811170320344

  • 单片机通过配置寄存器来控制内部线路的连接;开关拨到哪个位置就是靠寄存器控制的

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单片机中的串口,本节以单片机和电脑作为串口通信的两个设备如何进行相互通信

image-20220813094645065

  • 本节实现的第一个代码即向电脑发送数据,通过stc-isp软件中的串口助手来接收,串口号必须一样;默认会保持一致

image-20220813095051506

image-20220813101255256

  • TXD:transmit exchange data

image-20220813101726887

  • TTL:Transistor-Transistor Logic 晶体管-晶体管逻辑(电路);(单片机就是用的TTL)

  • TTL电平和RS232电平只能传十多米,RS485可以传一千多米

image-20220813161424849

  • 24C02,是用来存储数据的,单片机的写入与读出就是靠 I 2 C I^2C I2C接口的
  • DS1302的通信方式是SPI,但不是标准的SPI
  • DS18B20温度传感器是通信方式就是1-Wire

image-20220813162541534

image-20220813163927256

  • 如果操作的是端口的寄存器,用的就是IO口;如果操作的是串口的寄存器,就通过IO口发送数据

image-20220813164436941

  • 9位相比于8位多了一位,多出来的一位可以用于校验,验证前面的数据是否正确
  • 双方约定都使用奇校验,发0000 0011 1(奇校验就是数一下数据位中有几个1,现在是2个,后面补一个1,保证9位数据中1的个数是奇数);接收到0000 0011 1,也发现1的个数是奇数,这样的数据就是正确的;但是如果接受到的是0000 0101 1,这样的错误是检测不出来的

image-20220813170228932

  • 串口靠定时器1的溢出率约定速率

image-20220813171216243

image-20220813171352719

8-2 串口向电脑发送数据 & 电脑通过串口控制LED

硬件了解以后软件就是配置寄存器

  • IE,IPH,IP,不需要开启中断,与默认相同,不用配置、
  • 配置定时器时只能用定时器1,TMOD &= 0x0F;为高四位清零;串口需要用8位自动重装模式
    1. 之前讲的是16位,用两个8位表示一个大的计数器:0-65535;缺点是进入中断时需要赋初值,会占用一定时间,所以精度不是特别高
    2. 串口中需要更精准的,分成两个8位实现自动重装,可以使用工具完成初始化

image-20220813224645964

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
}

image-20220813232513640

发送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

image-20220814000534673

编码是看ASCII码表,也可以用单引号发字符

9-1 LED点阵屏

image-20220814100816774

  • 使用点阵屏时要把跳线帽JP595,JOE配置一下。GP595需要插上,GOE插到右边的两个
  • 像素做成8的倍数相乘的原因是:一个字节有八位,为了充分利用这个因素,就设计成8的倍数,保证字节中的每一位对应到像素,减少浪费

image-20220814101931749

image-20220814103620808

  • 数据分为串行和并行;如果是串行相当于是一个一个输出(类似于串口);如果是并行可以同时输出到8根线上
  • 每一次上升沿把输入的数据向下移动,等满了以后RCLK来一个上升沿即可把8位数据一下传到右边,实现串行输入并行输出
  • 如果QH’接到下一位的SER,就会向下一片中移动

image-20220814103635021

总结一下如何用LED点阵屏显示:

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

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

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

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

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

image-20220815093505921

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

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

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往左走,先存一个数组,这个数组是一长条的动画,然后不断偏移位置显示

在开发板资料里面有文字取模软件

image-20220815222209196

image-20220815222430760

image-20220815222725871

先显示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实时时钟

image-20220814163053101

单片机定时有几个缺点:

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

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

image-20220814164106862

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

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

内部是怎么运行的呢?

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

image-20220814165452454

image-20220814171736382

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

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

image-20220814173407735

  • 单字节写入:
    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 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;
	}
}

image-20220816115614458

我们可以先给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码进行存储

image-20220816171046911

在主函数中我们调整一下,再进行测试:

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

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

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

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

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐