单片机接口与技术实验03:显示器与按键控制详解

目录

前言:

1、实验目的:

2、实验内容:

第一版:lcd1602显示器:

代码

电路图

第二版(提高版):

代码:

lcd1602.h

lcd1602.c

main.c

电路图:

电路图详解:

代码详解:

lcd1602.h

lcd1602.c

lcd1602命令字

重点:

        首先是初始化

        显示字符串

        设置光标位置

        设置屏幕闪烁和光标屏幕移动

        关于宏

main.c

总结


前言:

本次实验,第一版过于简单,所以简略带过,详解在第二版当中,弄懂了第二版,第一版完全就是小卡拉米。其实对于lcd操作的核心就是对lcd指令的封装,本文会很好的体现。

1、实验目的:

(1)学习lcd 1602的编程与使用;

(2)机械式复位开关button软件消抖的方法。

2、实验内容:

第一版:lcd1602显示器:

先显示开机画面,:在1602显示器上,分两行分别居中显示字符“  AHAU  CHINA  ” 和 "  I LOVE YOU  "。然后从右向左移动直至消失。其中1602的rs、rw、en引脚分别使用单片机的P2.0、P2.1、P2.2引脚。

要求:将两行字符存储在数组中。

将lcd1602的基本操作函数都写到一个头文件中,供主函数文件调用。

在主函数文件中,只处理与显示内容有关的业务。

代码

批注:第一版代码过于简单,所以没保存,但是提高版是在第一版的基础上扩展的,所以第二版的代码,可以向下兼容第一版的代码,核心是函数封装调用。

电路图

第二版(提高版):

两行逐个显示,两行字符全部出现后整体静态显示1秒左右,然后闪烁3次,最后从右向左移动直至消失。

 按键button与lcd1602:第一行居中显示:“a:?”,其中?为a的值,随按键变化。

其中无符号字节型变量a初值为0,其值实时显示在lcd 1602上。当复位开关S1(设置)按下时,a的值闪烁显示;S2(增加)每按一次,a增1;直至a增为 9,再按一次S2,a 归 0;S3(减少)按键每按一次,a 自减 1; 如此反复,并将 a 的值实时送至lcd 1602以闪烁的形式显示出来“a:?”,其中?为a的值,并闪烁显示。当按下S4(确认)时,a的值确定并不再闪烁。

其中S1、S2、S3、S4均为复位开关(button),即轻按接通、释放后自动复位断开。S1、S2、S3、S4与单片机的接口请遵照学习板电路的设置(见学习板电路原理图)。 

代码:

lcd1602.h
#ifndef LCD1602_H
#define LCD1602_H

#include <reg52.h>
#include "intrins.h"

/** LCD的所有操作都是基于指令的,关于LCD的功能函数的封装
  * 都是基于写入指令和写入数据两个操作实现的。
  * 即,下面的功能宏和函数都是对LCD指令的封装所以源文件不
  * 进行繁琐的注释,封装的功能在头文件都注释好了,详细可
  * 参见LCD1602的指令表。
  */

#define out P0		// 数据 to LCD
typedef unsigned int uint;
typedef unsigned char uchar;

sbit LCD_RS = P1^0; // 寄存器选择
sbit LCD_RW = P1^1; // 读/写
sbit LCD_EN = P1^2; // 使能

void Delay(uint t);	// 延迟函数

// 定义LCD1602基本操作函数
void LCD_CheckBusy(void);		// 检查忙标志位
void LCD_WriteCmd(uchar cmd);	// 写入命令
void LCD_WriteData(uchar dat);	// 写入数据
void LCD_Initial(void);			// 初始化LCD

void LCD_ShowString(uchar *s, uint t);			// 在LCD显示字符串
void LCD_SetCursor(uchar line, uchar column); 	// 设置光标位置
void LCD_ScreenFlicker(uchar num, uint t);		// 屏幕闪烁

// 光标/屏幕 左/右移 延迟
void LCD_CorS_Shift_LorR(uchar key, uchar disp, uint t); 

#define LCD_CSL(DISP) LCD_CorS_Shift_LorR(0, DISP, 0)			// 光标左移
#define LCD_CSR(DISP) LCD_CorS_Shift_LorR(1, DISP, 0)			// 光标右移
#define LCD_SSL(DISP, TIME) LCD_CorS_Shift_LorR(2, DISP, TIME)	// 屏幕左移
#define LCD_SSR(DISP, TIME) LCD_CorS_Shift_LorR(3, DISP, TIME)	// 屏幕右移

#define LCD_ShowMove_ON() LCD_WriteCmd(0x07)		// 打开屏幕随光标移动
#define LCD_ShowMove_OFF() LCD_WriteCmd(0x06)		// 关闭屏幕随光标移动
#define LCD_CursorFlicker_ON() LCD_WriteCmd(0x0f)	// 打开光标闪烁
#define LCD_CursorFlicker_OFF() LCD_WriteCmd(0x0c)	// 关闭光标闪烁

#define LCD_Clear() LCD_WriteCmd(0x01)	// 清屏

#endif
lcd1602.c
#include "lcd1602.h"

void Delay(uint t)
{
	uchar i = 240;
	for (; t > 0; t--)
	{
		while (--i);
		i = 249;
		while (--i);
		i = 250;
	}
}

void LCD_CheckBusy(void)
{
	uchar dt;
	do
	{
		dt = 0xff;
		LCD_EN = 0;
		LCD_RS = 0;
		LCD_RW = 1;
		LCD_EN = 1;
		dt = out;
	}
	while (dt & 0x80);
	LCD_EN = 0;
}

void LCD_WriteCmd(uchar cmd)
{
	LCD_CheckBusy();
	LCD_EN = 0;
	LCD_RS = 0;
	LCD_RW = 0;
	out = cmd;
	LCD_EN = 1;
	_nop_();
	LCD_EN = 0;
	Delay(1);
}	

void LCD_WriteData(uchar dat)
{
	LCD_CheckBusy();
	LCD_EN = 0;
	LCD_RS = 1;
	LCD_RW = 0;
	out = dat;
	LCD_EN = 1;
	_nop_();
	LCD_EN = 0;
	Delay(1);
}	

void LCD_Initial(void)
{
	LCD_WriteCmd(0x38); // 命令6
	LCD_WriteCmd(0x0c); // 命令4
	LCD_ShowMove_OFF(); // 命令3 : LCD_WriteCmd(0x06)
	LCD_Clear(); 		// 命令1      
	Delay(1);
}

void LCD_ShowString(uchar *s, uint t)
{
	while (*s > 0)
	{
		LCD_WriteData(*s++);
		Delay(t);
	}
}

void LCD_SetCursor(uchar line,uchar column)
{
	if (--column > 40 || --line > 2) return;
	LCD_WriteCmd((line ? 0x40 : 0) + 0x80 | column);
}

void LCD_ScreenFlicker(uchar num, uint t)
{
	t >>= 1;
	while (num--)
	{
		LCD_WriteCmd(0x08);
		Delay(t);
		LCD_WriteCmd(0x0c);
		Delay(t);
	}
}

void LCD_CorS_Shift_LorR(uchar key, uchar disp, uint t)
{
	if (key > 3) return;
	// 00:光标左移 01:光标右移 10:屏幕左移 11:屏幕右移
	do {
		LCD_WriteCmd(0x10 + (key << 2));
		Delay(t);
	} while (--disp);
}
main.c
#include "lcd1602.h"

void main(void)
{
	char number[2] = {48, 0};		// 让number可以顺利打印
	uchar key, sure = 0, enter = 0; // 一些标志
	LCD_Initial();					// 初始化

	// 开机界面
	LCD_SetCursor(1, 17);			// 设置光标在显示屏之外
	LCD_ShowString("AHAU CHINA", 10);
	LCD_SetCursor(2, 17);
	LCD_ShowMove_ON();				// 一边输出第二行子一边移动屏幕
	LCD_ShowString("I LOVE YOU", 100);
	LCD_ShowMove_OFF();				// 恢复状态(输出字符不移动屏幕)
	LCD_SSL(3, 100);				// 字体移动居中
	LCD_ScreenFlicker(3, 500);		// 闪烁三次,周期约1秒
	LCD_SSL(13, 100);				// 移动出屏幕
	Delay(100);
	LCD_Clear();
	Delay(100);

	// 初始化状态
	LCD_SetCursor(1, 7);
	LCD_ShowString("a:", 100);
	LCD_ShowString(number, 0);
	LCD_CSL(1);
	LCD_CursorFlicker_ON(); // 打开光标闪烁
	
	while (1)
	{
		key = P3 >> 4;
		switch (key)
		{
			case 0x0e: 				// s4
				Delay(10);			// 去抖
				if (!enter && key == P3 >> 4)
				{	// 锁定当前数字 关闭光标闪烁
					enter = 1;
					sure = 1;
					LCD_CursorFlicker_OFF();
				}
				break;
			case 0x0d: 				// s3
				Delay(10);
				if (!enter && !sure && key == P3 >> 4)
				{	// 数字0-9循环减1
					enter = 1;
					number[0] += number[0] == 48 ? 9 : -1;
					LCD_ShowString(number, 0);
					LCD_CSL(1);
				}
				break;
			case 0x0b: 				// s2
				Delay(10);
				if (!enter && !sure && key == P3 >> 4)
				{	// 数字0-9循环加1
					enter = 1;
					number[0] += number[0] == 57 ? -9 : 1;
					LCD_ShowString(number, 0);
					LCD_CSL(1);
				}
				break;
			case 0x07: 				// s1
				Delay(100);
				if (!enter && key == P3 >> 4)
				{	// 将a的值清零
					enter = 1;
					sure = 0;
					LCD_CursorFlicker_ON();
					number[0] = 48;
					LCD_ShowString(number, 0);
					LCD_CSL(1);
				}
				break;
			case 0x0F:
				// 解除按键锁定 每次按下将按键锁定,防止频繁触发按键效果
				enter = 0;
		}
	}
}

电路图:

电路图详解:

        左半部分主要是AT89C52单片机的时序电路、复位电路,固定套路没什么好说的。

        与第一版不同的是,为了与电路版相符合,将RS、RW、EN三个引脚从P2口切换到了P1口,负责控制lcd状态。

        为了让电路图清晰,将排阻和lcd的使能电路单独拿出来放在上方。P0口作为lcd的数据端口,右边添加了4个按键对应题目中的功能。

        注意单片机不宜供电负荷太大,所以与单片机连接的元器件都采用排阻外部供电的方式再与单片机连接。

代码详解:

        代码详解主要针代码实现思路,如何实现都是次要的,重要的是编程思维和思路。头文件可以快速了解使用方法,主要是方法的定义,mian文件主要是对头文件的调用,源文件就是通过包装指令写入实现功能。

lcd1602.h

        首先得包含单片机的寄存器头文件和汇编命令头文件。

        第一部分,将三个最重要的操作lcd的引脚用sbit封装好,加上一个常用的延迟函数。至于对uint和uchar使用宏定义还是typedef看个人习惯,我是感觉没啥差别。

        第二部分,是操作lcd最核心的三个函数,所有lcd的复杂操作,的相关的函数功能都是基于写入命令和写入数据函数的封装。

        第三部分,便是基于写入命令和写入数据,针对于常用或者接下来要用到的复杂操作进行封装。分别是:初始化lcd、显示字符串、设置光标位置、光标和屏幕的移动、屏幕闪烁(这个为了达成题目要求)。

        第四部分使用宏函数进一步进行封装放在源文件当中结合实现原理讲解更好。

lcd1602.c

        延迟函数大约是1ms,看自己习惯去写把,非工业级不要求精确,能用就行。

        查忙,写命令,写数据的函数都是固定的,明白原理就行。

lcd1602命令字

        

重点:

        我放出了lcd1602的指令表,接下来的所有操作都是基于这张表的。

        首先是初始化

        可以看到初始化就是写入四条命令
        1.命令6:8位数据接口,两行显示,5*7点阵。

        2.命令4:打开显示屏,无光标

        3.命令3:设置指针自增,屏幕不跟随

        4.命令1:清屏

        总结下来就是根据自己想要的效果打开屏幕,设置lcd的一些初始状态,因为我需要开机画面所以初始化不带光标,如果开机就是输入,那也可以初始化带光标。根据实际情况进行操作。

        显示字符串

        非常简单啊,就是像lcd中逐条写入数据

        设置光标位置

        设置光标位置是为了配合显示字符串使用的。lcd有80字节的数据存储器,地址为00H-27HH和40H-67H,配合指令就可以将光标移动到指令位置,写入数据。

        设置屏幕闪烁和光标屏幕移动

        现在在来看,屏幕闪烁就是命令4控制屏幕打开关闭加上延迟函数。

        而屏幕和光标的移动就是将命令5进行了封装,加上循环。

        关于宏

        有了前面的铺垫,再来讲为什么要用宏对上面的指令进行二次封装。首先要明确的是,对lcd的所有操作都是基于写入命令和写入数据来实现的。而所有复杂的功能也都是写入不同指令来完成的。那这样其实我们完全不需要其他函数了,一个写入命令一个查忙一个写入数据,仨函数就能完成所有的操作了。

        对此我想说的是,我们学汇编做什么呢,汇编有那么多指令,而机器语言只有0和1两个数组成,何必使用汇编助记符。

        所以那么多指令,加上其他元器件指令数不清,全部记下来效率太低了,将相同操作逻辑的指令抽象成函数,再将函数复杂的功能分离成简单的接口,这便是面向对象的思想。

        对指令5加循环的封装,第一个参数起作用的只有0123,但是0123不好记,而且可能会无传其他参数,将他们通过宏封装成4个固定的函数,可以解决这个问题,而通常光标移动不需要延迟,但是屏幕移动需要延迟,变将常用的封装。

        有人觉得将屏幕和光标分开成两个函数更好,但是题目的运行逻辑是相同的,分开反而会造成代码冗余,浪费存储空间。

        而对于一些调整lcd状态的命令,通过宏封装起来,增强可读性,也方便了自己的代码编写。

main.c

        关于main函数,其实就是对上面封装方法的调用,和一些编程的技巧。初始化没什么好说的,按照题目要求调整即可。

        需要注意的就是需要通过延迟函数进行消抖操作,防止抖动造成的按键按压,鼠标爱好者应该对此有所了解,追求极致的dpi和=,按键灵敏度就会造成消抖太短,或是有些鼠标直接没有消抖,就会出现一种情况,拿着鼠标往桌上敲一下,按键触发,敲一下,触发。

        第二点就是,我们可以通过一个标志变量来优化程序逻辑,比如通过sure来锁定加减按键,被锁定后,再次加减,无效。我通过enter来锁定,防止按下后一直频繁触发按键功能。

        也有其他思路,比如存储上一次按下的按键,当释放时按键生效,也可以通过enter关键字来实现按键之间的组合和冲突,实现复杂逻辑,或者按键按下一定时间后触发连击效果等等。

总结

        主要分享单片机编程的思路,针对lcd,其实就是对写入命令进行封装。二次封装的原因同为什么不用机器语言编程而使用汇编语言助记符时同样的,封装还可以减少代码体积,增强可读性等等。还分享了一些编程时候的技巧和编程思想。

物联沃分享整理
物联沃-IOTWORD物联网 » 单片机接口与技术实验03:显示器与按键控制详解

发表评论