Autoleaders控制组——51单片机学习笔记(2)

51单片机学习(2)

1.模块化编程

1.1模块化编程的意义

在学习单片机的途中,随着我们的知识不断扩展,我们能写出的代码也越来越复杂,越来越长了,有时我们自己写出的很长的代码,出现了错误,需要调试,但奈何自己的代码实在是太长了,真的不好分析到底是哪里出错了。这是因为我们将太多的函数和代码放在一个main.c的文件里,导致代码过度堆积。如果我们能够条理清晰地去将不同作用的代码和函数放在不同的xxx.c文件里面,等我们需要用的时候直接调用它,不是会方便很多吗?
像上面说的这种方法被称为模块化编程,大致就是创建一些其他文件,将一些函数放在这个文件里面,并创建一个同名的可以被引入的头文件,在main.c里只需要引入头文件,就可以在主函数里使用,达到简化的目的。使用模块化编程可极大的提高代码的可阅读性、可维护性、可移植性等。

1.2模块化编程的操作

模块化编程的操作也很简单。我们打开keil 5后,像往常一样创建新的工程,在新的工程中创建新的main.c文件,再创建一个xxx.c,将平常要放到main.c的大把大把函数直接放进xxx.c里面,等这些都完成后,我们再点击创建一个新的文件,一个xxx.h文件,再把函数的原型声明放进来,像下面这样:

#ifndef __DELAY_H_ 
#define __DELAY_H_ 

void Delay(unsigned int h);//注意原型声明末尾有分号。
	
#endif

我们来解释以下,除了函数声明以外,还有很多其他的东西:

#ifndef  __XXX_H_  
 /*  其实这是if not define的缩写,
目的是判断之前有没有定义过__DELAY_H_,
避免重复定义,如果已经定义过了,
那就不执行以后的语句。  */

#define  __XXX_H_
/*用于定义一个头文件,定义完之后,
可以在其它文件里引入头文件,
然后可以直接使用头文件里面的函数。*/

#endif
// 一个结束语句的判断符,相当于一个  '}'

头文件格式是__XXX_H_,双下滑线+头文件名+下滑线+H+下滑线。
当定义完了头文件后,在main.c里面,我们只需要#include "XXX.H",就可以调用头文件里面的函数了。
在头文件里面,为了方便阅读,一般会给出较为详细的注释,以便于读懂函数。

2.LCD1602(液晶显示屏)

2.1液晶显示屏的介绍

LCD1602是一块两排的液晶显示屏,可以输出字母,数字或者一些符号,相比数码管,它可以显示的信息更多,但也使它更加难以解释。
这是一个原理简单,但驱动操作比较复杂的外设。
这是关于驱动液晶显示屏的代码。

#include <REGX52.H>

//引脚配置:
sbit LCD_RS=P2^6;
sbit LCD_RW=P2^5;
sbit LCD_EN=P2^7;
#define LCD_DataPort P0

//函数定义:
/**
  * @brief  LCD1602延时函数,可延时1ms    
  * @param  无
  * @retval 无
  */
void Delay1ms()		//@11.0592MHz
{
	unsigned char i, j;

	_nop_();
	_nop_();
	_nop_();
	i = 11;
	j = 190;
	do
	{
		while (--j);
	} while (--i);
}


/**
  * @brief  LCD1602写命令
  * @param  Command 要写入的命令
  * @retval 无
  */
void LCD_WriteCommand(unsigned char Command)
{
	LCD_RS=0;
	LCD_RW=0;
	LCD_DataPort=Command;
	LCD_EN=1;
	LCD_Delay();
	LCD_EN=0;
	LCD_Delay();
}

/**
  * @brief  LCD1602写数据
  * @param  Data 要写入的数据
  * @retval 无
  */
void LCD_WriteData(unsigned char Data)
{
	LCD_RS=1;
	LCD_RW=0;
	LCD_DataPort=Data;
	LCD_EN=1;
	LCD_Delay();
	LCD_EN=0;
	LCD_Delay();
}

/**
  * @brief  LCD1602设置光标位置
  * @param  Line 行位置,范围:1~2
  * @param  Column 列位置,范围:1~16
  * @retval 无
  */
void LCD_SetCursor(unsigned char Line,unsigned char Column)
{
	if(Line==1)
	{
		LCD_WriteCommand(0x80|(Column-1));
	}
	else if(Line==2)
	{
		LCD_WriteCommand(0x80|(Column-1+0x40));
	}
}

/**
  * @brief  LCD1602初始化函数
  * @param  无
  * @retval 无
  */
void LCD_Init()
{
	LCD_WriteCommand(0x38);//八位数据接口,两行显示,5*7点阵
	LCD_WriteCommand(0x0c);//显示开,光标关,闪烁关
	LCD_WriteCommand(0x06);//数据读写操作后,光标自动加一,画面不动
	LCD_WriteCommand(0x01);//光标复位,清屏
}

/**
  * @brief  在LCD1602指定位置上显示一个字符
  * @param  Line 行位置,范围:1~2
  * @param  Column 列位置,范围:1~16
  * @param  Char 要显示的字符
  * @retval 无
  */
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char)
{
	LCD_SetCursor(Line,Column);
	LCD_WriteData(Char);
}

/**
  * @brief  在LCD1602指定位置开始显示所给字符串
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  String 要显示的字符串
  * @retval 无
  */
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String)
{
	unsigned char i;
	LCD_SetCursor(Line,Column);
	for(i=0;String[i]!='\0';i++)
	{
		LCD_WriteData(String[i]);
	}
}

/**
  * @brief  返回值=X的Y次方
  */
int LCD_Pow(int X,int Y)
{
	unsigned char i;
	int Result=1;
	for(i=0;i<Y;i++)
	{
		Result*=X;
	}
	return Result;
}

/**
  * @brief  在LCD1602指定位置开始显示所给数字
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:0~65535
  * @param  Length 要显示数字的长度,范围:1~5
  * @retval 无
  */
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
	unsigned char i;
	LCD_SetCursor(Line,Column);
	for(i=Length;i>0;i--)
	{
		LCD_WriteData(Number/LCD_Pow(10,i-1)%10+'0');
	}
}

/**
  * @brief  在LCD1602指定位置开始以有符号十进制显示所给数字
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:-32768~32767
  * @param  Length 要显示数字的长度,范围:1~5
  * @retval 无
  */
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length)
{
	unsigned char i;
	unsigned int Number1;
	LCD_SetCursor(Line,Column);
	if(Number>=0)
	{
		LCD_WriteData('+');
		Number1=Number;
	}
	else
	{
		LCD_WriteData('-');
		Number1=-Number;
	}
	for(i=Length;i>0;i--)
	{
		LCD_WriteData(Number1/LCD_Pow(10,i-1)%10+'0');
	}
}

/**
  * @brief  在LCD1602指定位置开始以十六进制显示所给数字
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:0~0xFFFF
  * @param  Length 要显示数字的长度,范围:1~4
  * @retval 无
  */
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
	unsigned char i,SingleNumber;
	LCD_SetCursor(Line,Column);
	for(i=Length;i>0;i--)
	{
		SingleNumber=Number/LCD_Pow(16,i-1)%16;
		if(SingleNumber<10)
		{
			LCD_WriteData(SingleNumber+'0');
		}
		else
		{
			LCD_WriteData(SingleNumber-10+'A');
		}
	}
}

/**
  * @brief  在LCD1602指定位置开始以二进制显示所给数字
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:0~1111 1111 1111 1111
  * @param  Length 要显示数字的长度,范围:1~16
  * @retval 无
  */
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
	unsigned char i;
	LCD_SetCursor(Line,Column);
	for(i=Length;i>0;i--)
	{
		LCD_WriteData(Number/LCD_Pow(2,i-1)%2+'0');
	}
}

注意这段函数里面的那个延时函数,当你想使用的时候,看看你的单片机的晶振周期是多少,如果不是对应的晶振频率要改一改。
我们还有一个头文件,这里就不展示了,用我们之前学习的模块编程,创建一个头文件,再在main.c里面引入就行了。
各个函数的作用

LCD_Init();
LCD_ShowChar(X,X,‘X’);(要显示的行数,要显示的列数,’显示字符‘)
LCD_ShowString(X,X,“XXX”);(要显示的行数,要显示的列数,”显示的字符串“)
LCD_ShowNum(X,X,X,X);(行数,列数,十进制数字,显示所占用单元)
LCD_ShowSignedNum(X,X,X,X);(行数,列数,带符号的十进制数字,显示所占用单元)
LCD_ShowHexNum(X,X,0xXX,X);(行数,列数,显示十六进制数字,显示所占用单元)
LCD_ShowBinNum(X,X,0xXX,X); (行数,列数,显示二进制数字,显示所占用单元)
如果你要显示12,它应该占两个单元,但如果你给了三个单元来显示它,那最高位会补零。

利用这些函数,我们可以在液晶显示屏上显示东西。可以利用这块屏幕来一定程度上检查自己的代码。

3.矩阵键盘

矩阵按键

这是矩阵按键的原理图,回想之前学习的独立按键,我们当时是用的单片机的寄存器四位特殊位分别检测按钮是否按下,到了矩阵键盘这里,如果还按照那个方法,16个按键岂不是要16个引脚?这样很浪费资源,如果我们采用矩阵的连接模式,对于矩阵中的16个按钮,用一种类似与坐标的方式,用横纵坐标来确定检验一个按钮是否按下。其中P14–P17是行P10–P13是列,这样,16个按键只用了8个引脚就可以了,大大减少了引脚数。

3.1矩阵按键的使用

用两个引脚确定一个按键,那么就要分别判断两个来检测,可以先选中行,也可以先选中列。要扫描判读按键是否被按下,要做以下的这一步

unsigned char key=0;
P1=0xff;  //初始化
P1_3=0;//选中第一列
if(P1_7==0){delay20ms; while(P1_7==0); delay20ms; key=1;}//检测按键1是否按下
if(P1_6==0){delay20ms; while(P1_6==0); delay20ms; key=5;}//检测按键5是否按下
if(P1_5==0){delay20ms; while(P1_5==0); delay20ms; key=9;}//检测按键9是否按下
if(P1_4==0){delay20ms; while(P1_4==0); delay20ms; key=13;}//检测按键13是否按下
P1=0xff;
P1_2=0;//选中第二列
if(P1_7==0){delay20ms; while(P1_7==0); delay20ms; key=2;}//检测按键2是否按
if(P1_6==0){delay20ms; while(P1_6==0); delay20ms; key=6;}//检测按键6是否按下
if(P1_5==0){delay20ms; while(P1_5==0); delay20ms; key=10;}//检测按键10是否按下
if(P1_4==0){delay20ms; while(P1_4==0); delay20ms; key=14;}//检测按键14是否按下
P1=0xff;
P1_3=0;//选中第三列
if(P1_7==0){delay20ms; while(P1_7==0); delay20ms; key=3;}//检测按键3是否按下
if(P1_6==0){delay20ms; while(P1_6==0); delay20ms; key=7;}//检测按键7是否按下
if(P1_5==0){delay20ms; while(P1_5==0); delay20ms; key=11;}//检测按键11是否按下
if(P1_4==0){delay20ms; while(P1_4==0); delay20ms; key=14;}//检测按键15是否按下
P1=0xff;
P1_3=0;//选中第四列
if(P1_7==0){delay20ms; while(P1_7==0); delay20ms; key=4;}//检测按键4是否按下
if(P1_6==0){delay20ms; while(P1_6==0); delay20ms; key=8;}//检测按键8是否按下
if(P1_5==0){delay20ms; while(P1_5==0); delay20ms; key=12;}//检测按12是否按下
if(P1_4==0){delay20ms; while(P1_4==0); delay20ms; key=16;}//检测按键16是否按下

做以上这一步,当我们按下了某个按钮时,key值就是对应按下的按钮的序号,我们之后只需要做一些逻辑上的判断就可以实现按键的功能了。
当然,为了使用模块化编程,我们最好还是把这个长长的函数放进另一个文件,把它做成一个有返回值的函数,这样通过返回值就可以判断按下的是哪个按键了。

		if(key!=0)
		{
			switch(key)
			{
				case 1:
					
					break;
				case 2:
					
					break;
				case 3:
					
					break;
				case 4:
					
					break;	
				case 5:
					
					break;	
				case 6:
					
					break;	
				case 7:
					
					break;		
				case 8:
					
					break;		

比如我们可以用一串的switch case来做判断,这样就能利用矩阵按键了。

4.定时器

4.1定时器与延时器的区别

定时器与延时器的功能有点相近,但区别还是很明显的,定时器的功能就相当于一个闹钟,你定完闹钟还可以做其它的事情,闹钟一响,你就停下手中的事情,去做另外的事情,等那件事情结束,你再回到原先的事情上面来。
而延时器则不一样,延时就相当与等待,你等待的时候不能做任何的事情,只能干等着。所以定时器作用也比较广泛:
(1)用于计时系统,可实现软件计时,或者使程序每隔一固定时间完成一项操作
(2)替代长时间的Delay,提高CPU的运行效率和处理速度。

4.2定时器的原理与使用

定时器是一个寄存器,每隔固定周期就往里面加一,加到不能再加了,就停止了,”闹钟“就响了,我们会终断现在做的事情,去做”闹钟“响起以后该做的事情。我们可以更改寄存器里的初始值,让它定时一定时间。
我们的51单片机一般会有两个定时器(T1,T0),有的会有三个(T2,T1,T0),但其实原理都是一样的。
首先定时器有四个模式:
模式0:13位定时器/计数器
模式1:16位定时器/计数器(常用)
模式2:8位自动重装模式
模式3:两个8位计数器
我们一般会用的时模式1和模式2。
所以:
第一步:我们首先要配置定时器的模式,更改TMOD(这是一个寄存器,对它赋值就好了)
TMOD是一个八位的寄存器,他的八位二进制数分别表示的是:
GATE,C/T,M1,M0,GATE,C/T,M1,M0
左边高四位控制定时器1,右边低四位控制定时器0,两个虽然是分开的,但你必须要直接给TMOD整体赋值,如果你想要改变定时器0的状态,那可以这样做:

TMOD=TMOD&0xf0;//逻辑与,使高的四位数不变,低的四位清零
TMOD=TMOD|0x01;//逻辑或,使高的四位不变,低的四位变成了0001

这四个数分别是什么意思呢?
GATE,门系统,这个有点复杂,这里先不做解释,但我们现在一般让他是0就好。
C/T,这是定时器功能,C就是计数(高电平)/ T是计时(低电平)。
M1,M0一起控制计时器模式:
(M1,M0)
为(0,0)时是模式0 13位定时器/计数器
为(0,1)时是模式1 16位定时器/计数器(常用)
为(1,0)时是模式2 8位自动重装模式
为(1,1)时是模式3 两个8位计数器
当我们使用模式1的时候,使用的是16位定时器,这时定时器所能表达的最大数字就是65535,当这个定时器加到了65535的时候,再加一就会越界,就会产生一个信号,也就是中断信号,当接受到中断信号之后,如果设置允许,就会执行中断后的程序,等程序完成,再回来继续执行原来的程序。
那我们也可以控制定时器的初始值,来控制定时长短:
第二步:更改TCON
可以这么理解,TCON是由两个八位寄存器组成的十六位寄存器,高位的是TH0与TH1,低位的是TL0与TL1,后面的数字表示的是其所属的定时器。
假如我们现在需要用到的只是定时器0,就只需要给TH0和TL0赋值。
第三步
让:
TF0 / TF1=0(清楚计数标志)
TR0 / TR1=1(开始计时)
ET0 / ET1=1(单独的允许的中断)
EA=1(总的允许中断)
PT0=0/1或者PT1=0/1(中断的优先级1比0优先级高)(这里是两个优先级的单片机,如果不清楚你的单片机有没有更多优先级,最好看看你的单片机资料手册)
由于我们计时完成之后的闹钟是以中断信号的形式告诉我们的,当产生中断信号之后就会执行中断函数,我们可以通过设置中断函数来决定闹钟的形式:
比如我在中断函数里面让数码管显示88888888,那一但”闹钟“到了,我们的数码管就会全部亮起来,提醒我们时间到了,那么我们怎么写中断函数呢?

4.3中断函数

在51单片机里面一共有8种中断,分别是:
外部中断0、定时器0中断、外部中断1、定时器1中断、串口中断、定时器2中断、外部中断2、外部中断3。
这八个依次对应以下八个中断函数原型声明:

void Int0_Routine(void)			interrupt 0;外部中断0 
void Timer0_Routine(void)		interrupt 1;定时器0中断
void Int1_Routine(void)			interrupt 2;外部中断1
void Timer1_Routine(void)		interrupt 3;定时器1中断
void UART_Routine(void)			interrupt 4;串口中断
void Timer1_Routine(void)		interrupt 5;定时器2中断
void Int_Routine(void)			interrupt 6;外部中断2
void Int_Routine(void)			interrupt 7;外部中断3

函数名可以自己取,但要在后面跟上interrupt x;表示这是一个中断函数

我们只要在中断函数里面写好了中断后要做什么,就可以实现”闹钟“的作用了。
对于这里,我们如果使用的是由计时器0引发的中断,那就要对应的是interrupt 2。

4.4使用定时器

我们要用一个定时器,又想不那么麻烦,要怎么做呢?
很简单,我们打开stc/isp,这个软件的小功能很齐全,定时器计算器,就可以直接生成对应的定时代码了!
但你会发现,你甚至没有办法定时1秒,这是应为对于十六位的寄存器来说,它就算从0开始,间隔固定周期就加1,直到加满也没有满一秒,就像是一个小小的沙漏一样,想要计时更长,就要不断地反转沙漏:

void Timer0Init(void)		//定时10毫秒@11.0592MHz
{
	AUXR &= 0x7F;		//定时器时钟12T模式
	TMOD &= 0xF0;		//设置定时器模式
	TMOD |= 0x01;		//设置定时器模式
	TL0 = 0x00;		//设置定时初值
	TH0 = 0xDC;		//设置定时初值
	TF0 = 0;		//清除TF0标志
	TR0 = 1;		//定时器0开始计时
}
unsigned int sign;
void Timer0_Routine(void)		interrupt 1;//定时器0中断
{
	TL0 = 0x00;		//设置定时初值
	TH0 = 0xDC;		//设置定时初值	
	sign++;//这个变量就是用来反转沙漏的
	if(sign>=100)
	{
		//放点你想要的东西
	}
}

只需要在中断函数里面额外设置一个变量,每次计时完毕就让它加一,然后让它重新计时,就相当于将”小漏斗“反转了,这样进行累加,就可以定时1秒及以上了。

5.串口通信

5.1串口通信的作用

串口是一种应用十分广泛的通讯接口,成本低、容易使用、通信线路简单,串口通信可以连接两个或多个设备,使它们能互相传递信息,达到信息交流的目的。
单片机的串口可以使单片机与单片机、单片机与电脑、单片机与其他模块互相通信,极大的扩展了单片机的应用范围,增强了单片机系统的硬件实力。
51单片机内部自带UART(Universal Asynchronous Receiver Transmitter,通用异步收发器),可实现单片机的串口通信。UART是一种全双工、异步的点对点通信串口。

全双工:通信双方可以在同一时刻互相传输数据
半双工:通信双方可以互相传输数据,但必须分时复用一根数据线
单工:通信只能有一方发送到另一方,不能反向传输
异步:通信双方各自约定通信速率
同步:通信双方靠一根时钟线来约定通信速率

STC89C52的UART有四种工作模式:
模式0:同步移位寄存器
模式1:8位UART,波特率可变(常用)
模式2:9位UART,波特率固定
模式3:9位UART,波特率可变
由于UART是异步通信,所以需要收发双方约定一个相同的收发信息的频率,也就是波特率。

波特率:串口通信的速率(发送和接收各数据位的间隔时间)
检验位:用于数据验证
停止位:用于数据帧间隔

8位UART

9位UART
9位的UART相对于8位的UART只是少了一个检验位(就是RB8/TB8)检验位一般有两种检验方式,一种是通过改变第九位,让这串数据里面的1是偶数个,另一种是来让1是奇数个。一般需要两边使用同样的通信协议,在通信协议里写好,以哪一种为准,接受方可以检验自己收到数据的1的个数来一定程度上检测数据的准确性。当然,这种检测方式的准确性有限。

5.2波特率设置

想实现这个功能,还需要用到定时器来实现功能,这是因为波特率是由定时器来实现的。我们这里需要使用的是模式2,8位自动重装模式。这个模式时,因为只有八位了,所以它所寄存的最大的数也变小了,最大只有255了,但它不用手动重装(也就是手动初始化),可以减小它的误差。要想配置好串口,我们还要用到万能的stc/isp,里面有个叫作波特率计算器的,点进去,选好串口,晶振频率,波特率,就可以生成对应的代码。

5.3收发信息设置

我们已经设置好了波特率,接下来就等着收发信息了,收发信息要用到三个东西,分别是SBUF,TI和RI。这几个是干什么用的:
SBUF是读出和写入寄存器,在物理层面上,是两个独立的八位寄存器,一个只能写入,一个只能读出,但它们共用一个地址,所以既可以放在等号左边给它赋值,又可以放在等号右边读出它的值。在串口里面,我们是通过更改SBUF来发送信息,通过读取SBUF来接受信息的。
TI和RI分别是发送和读入的标志符,初始状态是0,当发送任务完成,它就会作为发送完成的标志,变成1,但它不能自己复位,所以我们需要自己复位。
RI与TI同理,区别是RI是读取完毕的标志。
以下就是发送与接受函数包含的部分内容

SUBF=0xff;
while(TI==0);
TI=0;               //发送标志复位

if(RI==0)
{
	xxx=SUBF;
	RI=0;           //接收标志复位
}

5.4利用串口接收和发送信息

我们前面搞清楚了接受和发送时要做的事情,下面让我们实际操作一下:
我们打开stc/isp,在波特率计算器里面配置好波特率,生成代码,之间放进我们的程序里,这段代码是用来初始化我们的串口和波特率的,在收发信息之前一定要初始化一下。

void UartInit(void)		//4800bps@11.0592MHz  初始化
{
	PCON |= 0x80;		//使能波特率倍速位SMOD
	SCON = 0x50;		//8位数据,可变波特率
	AUXR &= 0xBF;		//定时器1时钟为Fosc/12,即12T      //这个单片机可能没有,要删掉
	AUXR &= 0xFE;		//串口1选择定时器1为波特率发生器   //这个单片机可能没有,要删掉
	TMOD &= 0x0F;		//清除定时器1模式位
	TMOD |= 0x20;		//设定定时器1为8位自动重装方式
	TL1 = 0xF4;		//设定定时初值
	TH1 = 0xF4;		//设定定时器重装值
	ET1 = 0;		//禁止定时器1中断
	TR1 = 1;		//启动定时器1
}

下一步,写一个发送和一个接收信息的函数:
首先是发送的,我们在主函数里初始化以后,可以在主函数里面直接进行发送数据操作:

void main()
{
	UartInit(void);
	SUBF=0xff;
	while(TI==0);
	TI==0;
}

发送信息解决了,接下来就是接收信息了。但现在有个问题,我不可能一直等着电脑发东西给我是吧,如果我在主函数里面一直等着,我岂不是做不了其他的事情了?所以我们还要用到中断函数,在这里要选用串口中断,也就是void UART_Routine(void) interrupt 4; //串口中断
我们把我们接收信息的代码放进这个函数里面,一旦收到信息,我们就进入中断函数里,执行里面的东西:

UART_Routine(void)			interrupt 4;  //串口中断
{
	if(RI==0)
	{
		P2=SUBF;
		RI=0;
	}
}

6.LED点阵屏

6.1LED点阵屏与74HC595

LED点阵屏的原理其实和数码管矩阵按键都很像;对比LED数码管矩阵按键LED点阵屏的排列方式与矩阵按键那样的矩阵排列方式相同,又与数码管的显示方式类似。还记得之前利用74HC138译码器来选中数码管吗,和它类似,在点阵屏里面也要借助一个模块来选中LED,这个模块就是74HC595模块74HC138译码器可以用3根线来控制八根线的分别输出,也就是说,它可以在八个灯中选中一个,但没法做到同时选中八个。而74HC595模块,它可以同时选中八个数据,也就是可以同时控制八个灯的亮灭。
LED点阵屏的原理图

6.2.74HC595模块原理

来看看74HC595原理图:
74HC595模块原理图
仔细观察,我们可以看到,LED点阵屏的八列(k1~k8)是直接连在单片机的P0寄存器上的。
而它的八排(A1~A8)则直接连在了74HC595上面,要想显示点阵屏,我们首先要弄明白74HC595的工作原理是怎样的。74HC595是串行输入并行输出的移位寄存器,可用3根线输入串行数据,8根线输出并行数据,多片级联后,可输出16位、24位、32位等。说白了就是74HC595还是一个寄存器,但它是一个移位寄存器,它内部的数据可以一位一位地移动,它又是串行输入,就是一个一个字符输入,可以这样理解,好比一个狭窄的只够一个人的通道,我放进去一个人,然后我让这个人往里面挪一位,再进去一个人,再让他两个往里面挪一位,再·····
至于并行输出,就是可以将寄存器里的八个东西一次性输出给八根线,继续按我们刚刚的那个比喻,这个狭窄的通道其实是一个地铁等待口,地铁来了,大家就一起上车,一起输出。这就是74HC595大致的工作模式
我们看看原理图,首先看主体部分,在主体左侧,有主要的四根线,它们分别是:
OE(output enable,允许信号放出)·········在值为0(低电平)时允许信号放出;
P35—RCLK(输出控制)······················在值由0变为1(上升沿)时输出寄存器内数据;
P36—SRCLK(移位控制)····················在值由0变为1(上升沿)时寄存器内整体数据向高位移动;
P34—SER(串行数据)························准备输入寄存器内的串行数据。
至于VCC和GND就是正极和负极。
首先看OE,我们注意到,有个名叫J24的东西,上面有三根线OE、VCC和GND,这其实是一个外部设备,一个用跳线帽控制的设备。我们可以用一个跳线帽,选择让OE和哪个相连,如果接VCC就是高电平,接GND就是低电平,我们要想使用点阵屏和74HC595,就要让它与GND相连。
这一步完成,剩下的三个就要在程序里面配置了。
看看我们狭长的地铁等待口,我们先放一个人,移位,再放人,再移位,等人满了,就发车。
类比来,就是用SER输入,SRCLK移位,SER输入,SRCLK移位,等八位寄存器满了,就用RCLK来输出。
看起来好像有点复杂,其实就是控制P35,P36,P37的取值而已。
这里有个问题,我们SER是一位的,但我们不可能创建八个变量来分别赋值给SER做输入,那样很麻烦,也浪费资源,我们需要将一个八位二进制的数的任意一位取出来,想实现这个功能,我们可以利用一些逻辑运算:

DATA=0x88;//1000 1000
P37=(DATA&0x80);//1000 1000 & 1000 0000 取出第一位 

到这里你可能会有疑问,SER明明是一位的,(DATA&0x80)应该是个八位的二进制数,怎么能把八位的数赋值给一位的数呢?
其实这里有个小知识,如果把八位赋值给1位的二进制数,只要那个八位二进制数不是0,就会给一位二进制数赋值为1,如果八位二进制数是0,那就赋值为0。有了这个小知识,我们可以轻松写出这一步

6.3点亮LED点阵屏

想要点亮点阵屏,步骤就是选中灯和点亮灯。(检查下跳线帽的设置哦)
我们先设置一下74HC595

void _74HC595(unsigned char DATA)
{
	unsigned char i=0;
	for(i=0;i<8;i++)
	{
		P3_6=0;
		P3_4=DATA&(0x80>>i); //	取出第i位
		P3_6=1;
	}
	P3_5=1;   //输出控制
	P3_5=0;   //复位
}
void main()
{
	P3_5=0;//初始化
	void _74HC595(DATA);//放置一个数据
}

74HC595配置好了,也就是点阵屏的行配置好了,我们接下来要配置列,也就是P0寄存器:比如P0=0xff。得益于74HC595,我们可以每扫描一次就可以同时控制八盏灯,我们不需要将六十四个灯都扫描一遍,我们只需要扫描八次就可以遍历每盏灯了。
当然,配置行和列因该是配套的,我们要把它们放到一个函数里面,这样一调用这个函数,行和列都配置好了:

void _lightup(unsigned char P,DATA)//第一个表示行,第二个表示列,列上的显示需要计算,给1为亮。
{
	void _74HC595(DATA);
	P0=~(0x80>>P);
}	

当然别忘记要模块化编程哦!

6.4LED点阵屏动态显示

如何在LED点阵屏中动态显示呢,其实原理和我们动态显示数码管是一样的。我们先看看之前在数码管动态显示时说的:

很多时候我们需要再不同数码管上面显示不同的数字,这时就要用到动态数码管显示了。由于我们人的眼睛在观察高速运动的事物时会产生残影,也就是视觉暂留现象,最高分别能力是二十四分之一秒,而单片机速度很快,可以让单片机以较短的频率在不同数码管中分别显示数字,这样就可以达到在不同数码管中显示数字的目的了。
不过我们不能直接在上一个数码管中显示完就让下一个数码管显示,因为这会导致数码管显示篡位,我们也要给数码管显示“消抖”一下,就是加上一个短的延时函数。

在这里,也是一样的,显示完之后加上一个延时函数,防止它发生篡位。
这个好理解,那怎么写一个动态显示的函数呢?首先我们需要构建一个数组,把我们在每一列显示的内容数据按照顺序写进去,然后设置一个定时器,在中断函数里面,操作显示内容更改,最好是设一个变量,作为数组的下标,设置第二个变量,作为数组下标的偏移量(我们点阵屏有八列,我们一般需要同时亮起八列LED中的某些灯,每次遍历需要在数组里面取八个数据,用这个偏移量来实现这一步操作),我们数组里的数据要把每八个作一组,我们修改下标的时候,通过偏移量来实现一组的数据显示。这样,每经过固定的时间,数组下标就会改变,点阵屏的内容也会改变。

char a[]={0x7e,0x81,0xa9,0x85,0x85,0xa9,0x81,0x7e,
          0x7e,0x81,0xa9,0x85,0x85,0xa9,0x81,0x7e,
          0x7e,0x81,0xa9,0x85,0x85,0xa9,0x81,0x7e,
		  0x7e,0x81,0xa9,0x85,0x85,0xa9,0x81,0x7e,
		  0x7e,0x81,0xa9,0x85,0x85,0xa9,0x81,0x7e};

这是一个数组创建的示例,当然,数据你可以自己写,但实际实现这个数组功能的时候,注意防止数组下标超出范围,要加上一个逻辑判断哦。
如果你想产生一种类似滚动屏的效果,让显示的东西从一边流动播放到另一边,你就应该在中断函数里面每次让下标加一或减一:如果你想做逐帧动画,那就让它每次加八或减八,这比较好理解,如果下标每次只是加一,那上次显示的八列只是第一列消失了,的后面七列仍然在点阵屏上显示,只不过显示位置挪动了一下,在原本的第八列,显示了一行新的东西。如果下标每次加八,那每帧显示的东西都是不一样的。
如果要做逐帧动画,一般你的数据库就比较大,如果全部直接构建数组,存放在内存里,那单片机的内存很可能就不够用了,这时,你可以把你的数组定义在flash里。我们平时定义的变量其实都是存储在单片机的RAM寄存器里,RAM的内容可以实时读出和写入,但它的空间是有限的。而flash,也是单片机里的寄存器,你定义的变量如果是放在这里面,之后就不可以再对它进行修改了。当然,考虑到我们的逐帧动画数组数据一般情况下是不会再做修改的,所以可以直接放进flash。具体操作就是再定义的变量名前面放个code:

char code a[]={0x7e,0x81,0xa9,0x85,0x85,0xa9,0x81,0x7e};

7.DS1302时钟模块

7.1模块介绍

我们单片机有延时功能,有定时功能,但有没有一个模块可以让我们记录时间,查看时间呢?欸,是有的。
这就是DS1302时钟模块的作用了。DS1302时钟模块里面有一个晶振,它可以稳定而准确地传递信息,来计算时间推移,多亏了这个晶振,我们可以在单片机内可以较为精准地显示时间。
DS1302时钟
首先介绍一下DS1302时钟模块,以上是它的原理图,可以看到有两个VCC,这是因为正常的DS1302时钟芯片为了在主电源断电以后仍然能走时,一般会连接上备用电源,但很可惜,我们的单片机上面没有备用电源,所以这里其实只有一个VCC起作用。左侧有三根线,连接了单片机上面的P3寄存器的三位,我们其实就是控制着这三位来读取和写入时间的。想想我们之前学习的74HC595,类比一下这两个的原理图,你会发现,74HC595里的SRCLK同这里的SCLK好像诶!是的,其实DS1302时钟这里面也有一个移位寄存器,它原理其实与74HC595比较接近,但不完全一样。在74HC595中我们是控制两根线,一根SRCLK控制数据移位,一根SER放数据进来,在DS1302这里也是用两根线来控制移位寄存器。其中SCLK来控制数据写入读出,I/O来放数据。而CE在值为1(高电平)时允许时钟写入读出。

7.2命令制度–读入写出规则

首先让我们看看写入和读出的时候I/O处的数据内容,仔细看下面这张数据读出的流程图,除了I/O以外的几个东西,我们暂时先不去管(接下来会说明的),我们只看I/O那一条线:
I/O口说明

我们发现,在读出流程中,I/O中一共赋值了16次,前八个数据和后八个完全不一样,这里就要提到DS1302的命令制度了。我们首先想一个问题,我们的时间其实是挺大的一个数据,我们有秒、分钟、小时、星期、月份和年份,这么多的时间参数,我们不可能说一调用就全部显示出来给我们,一编写就要把这么多东西全部编写进去。在命令制度的帮助下,我们很好解决了这个问题,命令制度就是用来告诉单片机我们是读出还是写入,读出的是月份还是年份。也就是,这十六位数据中的前八位都是命令,而数据是在后面八位里面。命令的组成看下面这张表:

命令字

这张表是对命令的规定,最低位的二进制数用来决定我们到底是读出还是写入(给1是读出,给0是写入),第六位决定我们是对时钟操作还是对时钟里面的RAM操作(给1是操作RAM,给0是操作时钟)。其实我们不需要为了这条命令花太多时间,下面这张表列出了操作时钟时不同的地址用来干啥:

地址
表中最左边的那两列,就是进行对应操作时的命令。而右边八位,则是操作的数据内容。
举个例子,当我们想要读出秒的时候,我们的命令就是0x81,至于读出了啥,我们在I/O口需要一个一个取出来(这个之后说),总之命令制度就是这样的。

7.3时间数据DATA规则

DATA是由八位二进制数组成的,在表中,我们可以看见一些特殊位:
第一个:CH 用来暂停时间(为1时暂停)
第二个:12/24 小时表示制度(给1为12小时制,给0为24小时制)
第三个:WP 写入保护(给1时开启,不允许写入数据)
最后的地址0x91 0x90可以不用管。
DATA规则
DS1302里的时间数据不再是简单的二进制与十进制与十六进制之间的关系了,这里采用了一种编码方式叫BCD编码。BCD编码就是利用每四位的二进制数字来表示十进制数位,比如我们之前说明命令制度的那个图,右侧的八位二进制数就是时间数据,其中的低四位用来表示个位,高四位用来表示十位。这样方便是方便,但有个不好的地方,那就是会产生一些不合法的字符。虽然我们十进制每一位都有从0~9共十种数,但四位二进制数可以表示十六个数,那么就会空出那么几个数字不合法,我们如果直接把它单纯当成二进制,就会产生问题,比如用BCD编码的9与正常二进制的9是一样的,但如果直接给二进制的9加一,那它在BCD码的形式下就不合法了。这是我们需要注意的。
我们在编写代码的时候经常会需要把十进制转换为BCD码形式,或者把BCD码形式转换为十进制形式,这里有个小公式:

BCD码转十进制:DEC=BCD/16*10+BCD%16;(2位BCD)

十进制转BCD码:BCD=DEC/10*16+DEC%10;(2位BCD)
(此公式仅限于二位十进制数与BCD码之间转换)

这样我们就可以规划我们的时间数据了。

7.4写入/读出时间

要利用DS1302读出写入时间,我们还要了解下时序的工作原理。
以下是在读出和写入的时候的三根线CE、SCLK和I/O的赋值情况,也就是时序:

时序
我们可以看到:
CE在操作前由0变为1,操作完成后,复位为0。
SCLK的上升沿用来让单片机读取数据,下降沿用来让单片机输出。(上升沿就是从0到1的过程,下降沿就是由1变0过程)
I/O则不停将命令和DATA输入,而且先从低位开始,再到高位。(读出操作的时候不用操作I/O输入)
我们要使用DS1302,首先要关闭写入保护,也就是需要有一个初始化的函数,在初始化函数里面关闭写保护:

void init_clock()
{
	unsigned char i=0;
	P35=1;
	for(i=0;i<8;i++)
	{
		P34=0x8e&(0x01<<i);
		P36=1;
		P36=0;
	}
	for(i=0;i<8;i++)
	{
		P34=0;
		P36=1;
		P36=0;
	}
	P35=0;
}

我们再做一个写入的函数:

void write_clock(unsigned char Command,DATA)
{
	unsigned char i=0;
	P35=1;
	for(i=0;i<8;i++)
	{
		P34=Command&(0x01<<i);
		P36=1;
		P36=0;
	}
	for(i=0;i<8;i++)
	{
		P34=DAta&(0x01<<i);
		P36=1;
		P36=0;
	}
	P35=0;
}

做完了写入函数,接下来我们做读出函数,这里要注意一个问题,就是当命令的最后一位给完上升沿之后,如果直接给下降沿,那数据会直接到I/O。注意好这一点我们写这个函数就很轻松:

unsigned char read_clock(unsigned char Command)
{
	unsigned char i=0,DATA=0;
	P35=1;
	for(i=0;i<8;i++)
	{
		P36=0;
		P34=Command&(0x01<<i);
		P36=1;//由于读出操作在上升沿后紧接的下降沿就读出数据了,最后一步应该保持高电平
	}
		for(i=0;i<8;i++)
	{
		P36=1;
		if(P34==1){DATA= DATA | (0X01<<i);}
		P36=0;
	}
	P35=0;
	return DATA;
}

写出这三个函数,我们就可以在主函数里面直接使用了,但记得,使用函数的时候要初始化哦!

  • 55
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值