初学51单片机之 LCD1602液晶相关

     前面的博文已经介绍了流水灯、数码管、LED点阵这三种LED设备,这次介绍下LCD显示设备-1602液晶,最近OLED也比较火,最新的steamdeck都换OLED屏了。推荐一个科普视频【硬核科普】全网最简洁易懂的OLED与LCD屏幕工作原理与优劣科普_哔哩哔哩_bilibili

  言归正传,在详细说明LCD1602之前先看下相关图片:

划红线部分是LCD1602的外接引脚,我们在EDA软件中的原理图可以是如下

这16个引脚是和原理图上一一对应的,可以看到LCD1602引脚处,第一个引脚边上标有“1”,第16个引脚标有“16”.然后描述一下LCD1602各个引脚的基本功能。

我们区分一下这样引脚的基本功能:

  •   供电部分   1:VSS  2:GND这两个引脚主要是LCD1602的供电引脚,它的工作电压在4.5V-5.5V,在工作的时间不需要接额外的限流电阻,直接接电源即可。
  •    硬件控制部分  3:VL(LCD Operation Voltage test pin):这个引脚是用来调整显示的黑点和不显示黑点之间的对比度的,调整好对比度,就可以让显示的更加清晰。在调试过程中我们可以采用点位器来确定一个合适的值,然后大规模生产的时候采用固定的电阻。市面上1602这个下拉电阻大概是1~1.5KΩ合适,本案使用的是18Ω。      这就是1602液晶屏VCC,GND 引脚接上电,但是没有写入控制液晶程序的效果。可以看到它和之前提到的点阵非常相似,红线框起来的部分能够显示1个字符。可以看到它其实是由5*8个微小的LED组成的。通过控制这些微小区域的亮灭来显示需要的字符。
  • 软件控制部分 4:RS(data and control register select input)数据/命令选择端(H/L)这个引脚的功能是:通过把该引脚置1或者置0,来确定D0~D7这个数据端口接收到的是数据还是指令。置1D0~D7接收到的是数据,置0则D0~D7接收到的是命令。
  • 软件控制部分 5:R/W 读/写选择端 (H/L)通过把该引脚置1或者置0,来确定是读液晶内部的数据或者状态,还是写给液晶命令或者数据。
  • 软件控制部分 6:E 使能信号,液晶的读写命令和数据都是通过它给信号功能才能开始。
  • 数据引脚  7~14引脚 通过该8个引脚来读写数据和命令
  • 背光显示引脚 15:BLA 背光源正极 16:背光源负极 同样无需限流电阻直接接5V电源即可。

把引脚区分了一下,那LCD1602的功能模块就比较明显了。那么在使用这个外设的步骤那就是

  1. 4个供电端口接上相应的5V电源。
  2. 偏压信号接1个合适的下拉电阻或者电位器。
  3. 把软件控制引脚以及数据传输引脚与单片机相连,本案的连接方式见前文的原理图与下图。

可以看到软件控制引脚是和单片机的P1.0 P1.1 p1.5端口,(这里要说明下P1.0是ADDR0 ,p1.1是ADDR1),数据引脚是P0端口。51单片机P0口是开漏输出的,如果要作为双向IO口,则必须外接上拉电阻,本案接的上拉电阻是4.7K。如此LCD1602电路已经连接完毕,然后还需要什么操作才能是液晶显示显示我们需要的字符呢。

而且在此之前还存在一些问题:

  • 1:前面51系列博文用的开发板上的点阵,LED小灯,数码管模块都是通过控制P0相应端口的电平且是使端口置低电平来实现功能的,它们之间是通过38译码器切换模块使能的,那么液晶的数据端口是直接和P0的端口相连,那么存在一个问题,在液晶通电且E端未使能的情况下,液晶的端口会影响单片机P0端口的电平信息吗?
  • 答:这就涉及到准双向IO口的输入输出功能的实现了,在E端是低电平的时候,DB0与DB7输出都是高电平,笔者前面的博文也介绍过IO口,真要详细讲要花很大的篇幅,推荐一篇文章,单片机小白学步(20) IO口原理,笔者总结一下准双向Io口的基本功能。
  • 1:你只有先向IO口先写入“1”,该IO端口的电压才能随着外源电压变化,如果现在IO口的电压是“1”那外源电平信号就是高电平,如果现在IO口的电压是“0”那外源电平信号就是低电平。在程序的写法是 P0 = 0xFF; Sta = P0;这种操作下Sta的值就是P0接收到的外源电平信号。
  • 2:输入输出两边只要有一端的电平信号是低电平,那么两端的信号都会被拉成低电平。从结果上讲,它们是满足“与”逻辑的。因此在E端是低电平的时候,DB0-DB7端口输出高电平不会影响P0端口的电平信息,用另一种的说法就是不会占用P0总线。
  • 3:从上述得知若要想液晶不影响P0的输入输出,那么液晶在E端未使能的情况下都得是高电平的,即DB0~DB7在未接收到E端信号前都是高电平。同样在修改液晶状态的时候其它模块都需要暂时关闭。
  • 4:51单片机IO口在默认的状态下都是高电平。
  • 5:1602LCD液晶E端在接上5V电源后的默认状态是高电平,因此正常使用液晶需要先把电平拉低。拉低它有两种方式1:直接在程序中修改,以本案例即 P1^5= 0;即函数开头就得加上该句。第二种方式:接下拉电阻把P1^5端口的电压拉低,即E端的电平也被拉低。笔者的开发板采用的是第二种方式:如图,笔者的开发板是通过跳线帽把P1.5端口和液晶E端相连并且接了15K下拉电阻。因此p1.5的端口被拉成低电平。关于下拉电阻推荐一个视频:上拉电阻有什么用?上拉电阻的起源--洋桃电子大百科P021_哔哩哔哩_bilibili
  • 6:那么必然出现一个问题?你都接下拉电阻了,那E端怎么使能高电平信号?正常我们接下拉电阻的端口如果还需要端口持续输出高电平,一般是需要它再接一个上拉电阻到VCC,其实就相当于VCC电源被两个电阻分压。它们之间还需要一个“开关器件”通断,来使能高低电平。
  • 7:笔者的开发板是直接接了下拉电阻,未接其它器件的。那它如何使能呢?那就涉及液晶的使能了方式以及IO相关了。对此笔者在学习群里得到的答案是如下:

        液晶使能引脚默认需要保持为低电平,所以用了下拉电阻,那它为什么又能输出高电平呢?
是因为51的IO结构的特殊性,51的IO在你像其写入1的时候,它会瞬间把电平拉高,这个时候它相当于是强推挽输出的,但这个就是一瞬间的事,然后它就恢复成开漏的形式,这个时候在下拉电阻的作用下,经过一段时间会把电平重新拉成低电平,这个过程相对较长,大概有个十几到几十us吧,电阻值越小,下拉的强度就越大,电平下降的就越快。
      但在液晶的代码里,并不是等电阻把电平拉下来,而是很短的时间内就会向IO写0而瞬间把电平重新置低。这就是即便用了下拉电阻也还是能在IO上输出一个高脉冲的原因,而用下拉电阻是为了默认情况下、或稳态的时候让IO保持低电平。(这段说的是程序设置高脉冲部分,后面看程序

至此:关于液晶工作的初期任务完成,下文开始正常的工作演示。

第1个问题:液晶能显示几个字符?看下图:

一共可显示两排字符,每排16共32个。

它使能步骤:手册给出是如下图

本案写的程序5.1-5.6这边都不涉及的,5.8-5.12这5条指令是初始化过程(主要控制液晶显示以及光标的显示移动方向),5.7这条指令是“忙”信号检测,5.8-5.12每一条指令的使能之前都需要执行5.7这条指令即“忙”判断。

第二个问题:怎么进行“忙”判断?所谓“忙”判断是指液晶是否在干其他的一些事情,如果它没有再干其它事情在E端使能的状态下,它的DB7端口会显示相应的电平。即如果在“忙”置1,空闲置0.“忙”的状态下它不能接收新的指令或者数据,只有空闲的时候才可以。

看手册上的表格:

第三个问题:我们知道液晶的DB0-DB7端口是可以接受信息也可以输出信息的,那么是通过何种方式来控制呢?液晶又是如何知道要接受数据还是发送数据呢?这就需要液晶的软件控制端口以及使能端口E配合确定液晶的各种工作模式。看下手册资料:

提供一个记忆小贴士:RS端口是数据/指令选择端口,数据在前指令在后,在前的是1在后的是0,即数据相关都置1,指令相关都置0。同理RW选择端,“读”在前“写”在后,那么与“读”相关的指令都置1,于“写”的指令都置0.同样在进行这些指令操作的时候都需要“忙”判断。

我们在使用过程中常用的指令有:

  • 1:读状态主要是进行“忙”状态的读取
  • 2:写指令,除了初始化液晶控制的时候需要写指令。其它时候用到指令的功能就是在写液晶的起始地址。
  • 3:读数据,一般不用。数据是我们单片机向液晶写入的,一般用不到这个功能。
  • 4:写数据,就是写字符。输入我们需要的信息,那么信息从那个位置开始写呢?前文说到,液晶是有两排共32个字符位置,每个位置都有相应的地址。这就需要写指令功能配合确定你字符的起始位置在哪。内部字符显示地址

基于此液晶可以开始初始化过程了:

      即向P0口写入0x38,即P0= 0x38;

光标表显示这里我们常用的是 D = 1; C = 0; B =  0;那么写入的就是P0 =0000 1100 = 0x0C ;

地址指针移动方式常用的是 N = 1;S = 0;那么写入的就是P0 =0000 0110= 0x06;地址指针加1事实上液晶默认的就是这种模式。这里很多人用错了,后文笔者会展开说一下。

地址指针移动方式另一种方式 N = 0 S= 0;那么写入的就是P0 = 0000 0100 =0x04;地址指针减1

看下之前笔者发的图:即液晶内部真实地址是(地址码+0x80),比如第1行的首地址是:0+0x80 = 0x80 第一行最后的地址是0x0F+0x80 = 0x8F; 第二行的首地址是 0x40+0x80 = 0xC0如此。字符指针就是这些,其他地址不需要关注。

这个数据或者指针清零。

除了时序外基本上都已经介绍完了看下程序:

# include<reg52.h>

# define LCD1602_DB P0
sbit LCD1602_RS = P1^0;
sbit LCD1602_RW = P1^1;
sbit LCD1602_E = P1^5;

void InitLcd1602();
void LcdShowStr(unsigned char x ,unsigned char y, unsigned char* str);

void main()
{
   unsigned char str[] = "Firewood 2024";

	 InitLcd1602();
	 LcdShowStr(2,0,str);
	 LcdShowStr(0,1,"I love You BeBy");
	 while(1);

}
/*等待液晶准备好 */
void LcdWaitReady()
{
   unsigned char sta;
	
	LCD1602_DB = 0xFF;
	LCD1602_RS = 0;
	LCD1602_RW = 1;
	do{
	   LCD1602_E = 1;
		 sta = LCD1602_DB; //读取状态字
		 LCD1602_E = 0;
	}while(sta & 0x80);//bit7等于1表示液晶正忙,重复检测直到等于0为止

}
/*向LCD1602液晶写入一字节命令,cmd为待写入的命令值 */
void LcdWriteCmd(unsigned char cmd)
{
  LcdWaitReady();
	LCD1602_RS = 0;
	LCD1602_RW = 0;
	LCD1602_DB = cmd;
	LCD1602_E = 1;    //高脉冲
	LCD1602_E = 0;    //高脉冲

	
}
/*向LCD1602液晶写入一字节数据,dat为待写入数据   */
void LcdWriteDat(unsigned char dat)
{
  LcdWaitReady();
	LCD1602_RS = 1;
	LCD1602_RW = 0;
	LCD1602_DB = dat;
	LCD1602_E = 1;
	LCD1602_E = 0;
}
/* 设置显示RAM起始地址,亦即光标位置,(x,y)为对应屏幕上的字符坐标*/
void LcdSetCursor(unsigned char x,unsigned char y)
{
  unsigned char addr;
	if(y == 0)
		addr = 0x00+x;
	else
		addr = 0x40+x;
	LcdWriteCmd(addr|0x80);//0x80 = 1000 0000

}
/*在液晶上显示字符串,(x,y)为对应屏幕上的起始坐标,str为字符串指针 */
void LcdShowStr(unsigned char x,unsigned char y,unsigned char* str)
{
  LcdSetCursor(x,y); //设置其实地址
	while(*str != '\0')
	 {
	   LcdWriteDat(*str++);
	 } 

}

/*初始化1602液晶  */
void InitLcd1602()
{
	
 LcdWriteCmd(0x38);//0x38 = 0011 1000 16*2显示,5*7点阵,8位数据接口

 LcdWriteCmd(0x08);//显示关闭
 LcdWriteCmd(0x01);//清屏

 LcdWriteCmd(0x06);//0x04 = 0000 0100 文字不动,地址自动加1
 LcdWriteCmd(0x0C);//显示器开 ,光标关闭
 		 
}

看下结果图片:

在此我要搞个事情:CSDN上关于1602液晶初始化的过程,我翻看一下即使是有很多赞以及收藏的文章,它们的初始化过程都是不完全正确的,它们的初始语句是4句式的,笔者的教材也是4句式的。看下我的截图这是一个有200多收藏的初始化过程,但可惜是不正确的。如果你把LCD_WriteCmd(0x06),这里改成0x04,它的结果依然和0x06的一样的,0x06的功能是地址自动加1,0x04的功能是地址自动减1.也就是这种4句模式是无法改变液晶的工作方式 的,甚至你删除该句液晶也是可以正常工作的。如果你手上有1602液晶你大可一试.也就是这种写法其实是工作在液晶的默认模式下。若想正确的切换模式需要采用笔者给的代码5句式,其实这就是之前文档给出步骤,

文档给出的步骤是在切换功能前需要关闭液晶,没有这句指令你的光标功能就不能正确使能。看下我的步骤

/*初始化1602液晶  */
void InitLcd1602()
{
	
 LcdWriteCmd(0x38);//0x38 = 0011 1000 16*2显示,5*7点阵,8位数据接口

 LcdWriteCmd(0x08);//显示关闭
 LcdWriteCmd(0x01);//清屏

 LcdWriteCmd(0x06);//0x04 = 0000 0100 文字不动,地址自动加1
 LcdWriteCmd(0x0C);//显示器开 ,光标关闭
 		 
}

为了实验这个功能,笔者把字符的显示的起点改成了液晶的中点即

void main()
{
   unsigned char str[] = "Firewood 2024";

	 InitLcd1602();
	 LcdShowStr(7,0,str);
	 LcdShowStr(7,1,"I love You BeBy");
	 while(1);

}

在0x06下它显示的是:在0x04下它显示的是

可以看到它正确使能了光标功能,如果是4句模式无论你怎么改,它都是0x06模式下的结果,即默认的结果。

2:程序本身来说结构是清晰的,这个“或”运算的结果其实就是加法,主要是它的地址是有限制的最高位肯定是0,因此这个或运算是不会有进位发生的,因此就相当于加法运算了。

       这个部分就相当于“忙”状态判断,而且用了do,while()函数,并且E端(LCD1602_E)在读状态前置1,读状态后又被置0。这里LCD1602_E马上拉低是为了释放P0总线,不要一直占用。这个不同于高脉冲.当然如果你这里不写CD1602_E = 0这句,那么在高脉冲那里就要加上这句即:

这部分就是计算液晶内部真实地址的函数,该坐标系如图,有些1602的博文把y轴写在小括号的第1位,本案y轴是写在小括号第2位的。

其他程序结构应该是清晰易懂的,不再赘述。

再分享一个显示移动的程序:

main.c:

# include<reg52.h>

bit flag500ms = 0;
unsigned char T0RH = 0;
unsigned char T0RL = 0;
//待显示的第1行字符串
unsigned char code str1[] = "Kingst Studio";
//待显示的第二行字符串,需保持与第一行字符串等长,较短的可用空格补齐
//unsigned char code str2[] = "Let's move..."怎么把分号忘了
	unsigned char code str2[] = "Let's move...";
	
void ConfigTimer0(unsigned int ms);

extern void InitLcd1602();
extern void LcdShowStr(unsigned char x,unsigned char y,unsigned char* str,unsigned char len);

void main()
{
  unsigned char i;
	unsigned char index = 0;
	unsigned char pdata bufMove1[16+sizeof(str1)+16];
	unsigned char pdata bufMove2[16+sizeof(str2)+16];

	
	EA = 1;
	ConfigTimer0(10);//配置T0定时10ms
	InitLcd1602();//初始化液晶
	//缓冲区开头一段填充为空格
	for(i = 0; i < 16;i++)
	{
	  bufMove1[i] = ' ';
		bufMove2[2] = ' ';
	}
	//待显示字符串复制到缓冲区中间位置
	for(i = 0; i<(sizeof(str1)-1);i++)
	{
	  bufMove1[16+i] = str1[i]; //perfect下标数是数组元素个数减1
		bufMove2[16+i] = str2[i]; 
		
	}
	//缓冲区结尾一段也填充为空格
	for(i =(16+sizeof(str1)-1);i<sizeof(bufMove1);i++)
	{
	   bufMove1[1] = ' ';
		 bufMove2[2] = ' ';
	}
	
	while(1)
	{
	   if(flag500ms)
     {
		   flag500ms = 0;
			 //从缓冲区抽出需显示的一段字符显示到液晶上
			 LcdShowStr(0,0,bufMove1+index,16);
			 LcdShowStr(0,1,bufMove2+index,16);
			 //移动索引递增,实现左移
			 index++;
			 if(index >= (16+sizeof(str1)-1))
			 {
			   //其实位置达到字符串尾部后即返回从头开始
				 index = 0;
			 
			 }
			 
		 
		 }	
	}

}

/*配置并启动T0,ms为T0定时时间 */
void ConfigTimer0(unsigned int ms)
{
  unsigned long tmp;
	
	tmp = 11059200/12;
	tmp = (tmp*ms)/1000;
	tmp = 65536 - tmp;
	tmp = tmp +12;
	T0RH = (unsigned char)(tmp>>8);
	T0RL = (unsigned char)tmp;
	TMOD &= 0xF0;
	TMOD |= 0x01;
	TH0 = T0RH;
	TL0 = T0RL;
	ET0 = 1;
	TR0 = 1;
	
}
/* T0中断服务函数,定时500ms */
void InterruptTimer0() interrupt 1
{
  static unsigned char tmr500ms = 0;
	TH0 = T0RH;
	TL0 = T0RL;
	tmr500ms++;
	if(tmr500ms >= 50)
	{
	  tmr500ms= 0;
		flag500ms = 1;
	}
}

1602LCD.c

#include<reg52.h>

#define LCD1602_DB P0

sbit LCD1602_RS = P1^0;
sbit LCD1602_RW = P1^1;
sbit LCD1602_E = P1^5;

/*等待液晶准备好,“忙”判断   */
void LcdWaitReady()
{
   unsigned char sta;
	
	LCD1602_DB = 0xFF;
	LCD1602_RS = 0;
	LCD1602_RW = 1;
	do{
	   LCD1602_E = 1;
		 sta = LCD1602_DB; //read the status of bit 7 postion
		 LCD1602_E = 0;
	
	  } while(sta & 0x80);// bit 7 equal 1,indicating that LCD is busy.Repeat the detection until it equal 0.
}
/*向LCD1602液晶写入一字节命令,cmd为待写入命令值  */
void LcdWriteCmd(unsigned char cmd)
{
   LcdWaitReady();
	 LCD1602_RS = 0;
	 LCD1602_RW = 0;
	 LCD1602_DB = cmd;
	 //High Pulse operation ,Default state is low level
	 LCD1602_E = 1;
	 LCD1602_E = 0;

}
/*向LCD1602液晶写入一字节数据,dat为待写入数据值  */
void LcdWriteDat(unsigned char dat)
{
  LcdWaitReady();
	LCD1602_RS = 1;
	LCD1602_RW = 0;
	LCD1602_DB = dat;
	//High Pulse operation ,Default state is low level
	LCD1602_E = 1;
	LCD1602_E = 0;
}
/*设置显示RAM的起始地址,亦即光标位置,(x,y) 为对于屏幕上的字符坐标   */
void LcdSetCursor(unsigned char x, unsigned char y)
{
  unsigned char addr;
	
	 if(y == 0)           
		 addr = 0x00 + x;  //The first line adress starts from 0x00;
	 else
	   addr = 0x40 + x;  //The second line adress starts from 0x40;
	LcdWriteCmd(addr|0x80);//this operation is actually adding 0x80 to the addr.
	   	 
}
/*在液晶上显示字符串,(x,y)为对应屏幕上的起始坐标,str为字符指针,len为需要显示的字符长度 */
void LcdShowStr(unsigned char x,unsigned char y, unsigned char *str,unsigned char len)
{
  LcdSetCursor(x,y);    //Set the starting position of the cursor
	while(len--)
	{
	  LcdWriteDat(*str++);// Continuously write len character data
	}

}
/*初始化1602液晶 */
void InitLcd1602()
{
  LcdWriteCmd(0x38);//0x38 = 0011 1000 16*2显示,5*7点阵,8位数据接口

 LcdWriteCmd(0x08);//显示关闭
 LcdWriteCmd(0x01);//清屏

 LcdWriteCmd(0x06);//0x04 = 0000 0100 文字不动,地址自动加1
 LcdWriteCmd(0x0C);//显示器开 ,光标关闭
	

}

结果视频:

液晶左移_哔哩哔哩_bilibili

     之前就多次提到笔者使用的是金沙滩工作室的51单片机板子与教材。可以直接在B站搜索相关的视频。这个程序在烧录过程中多次出现显示一些乱码不知什么原因,如果你用这个程序也出现乱码不用惊慌,重新烧录下其他液晶程序的hex文件,再烧录回这个程序就显示正常,现在也不是非常清楚原因,从现象看是数据传输这里出现问题,不知是否硬件出现问题,主要是笔者这个板子其实是很多年前买的。

  • 7
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值