前面的博文已经介绍了流水灯、数码管、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的功能模块就比较明显了。那么在使用这个外设的步骤那就是
- 4个供电端口接上相应的5V电源。
- 偏压信号接1个合适的下拉电阻或者电位器。
- 把软件控制引脚以及数据传输引脚与单片机相连,本案的连接方式见前文的原理图与下图。
可以看到软件控制引脚是和单片机的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);//显示器开 ,光标关闭
}
结果视频:
之前就多次提到笔者使用的是金沙滩工作室的51单片机板子与教材。可以直接在B站搜索相关的视频。这个程序在烧录过程中多次出现显示一些乱码不知什么原因,如果你用这个程序也出现乱码不用惊慌,重新烧录下其他液晶程序的hex文件,再烧录回这个程序就显示正常,现在也不是非常清楚原因,从现象看是数据传输这里出现问题,不知是否硬件出现问题,主要是笔者这个板子其实是很多年前买的。