51单片机——LCD1602液晶显示(二)

目录

[4. 硬件设计](#4. 硬件设计)

[4.1 8位 lcd1602](#4.1 8位 lcd1602)

[4.2 VS 4位lcd1602](#4.2 VS 4位lcd1602)

[5. 软件设计](#5. 软件设计)

[5.1 lcd1602.h、lcd1602.c](#5.1 lcd1602.h、lcd1602.c)

[5.2 main.c](#5.2 main.c)

[5.3 public.c、public.h](#5.3 public.c、public.h)


4. 硬件设计
4.1 8位 lcd1602

lcd1602 液晶接口电路如下:

lcd1602 的8位数据口 DB0-DB7 与单片机的 P0.0-P0.7 管脚连接

lcd1602 的 RS、RW、E 脚与单片机的 P2.6、P2.5、P2.7 管脚连接

RJ1 是一个电位器,用来调节 lcd1602 对比度,即显示亮度

4.2 VS 4位lcd1602

LCD1602 接收的指令 / 数据都是 8 位(1 个字节),两种模式的传输方式完全不同

8 位 / 4 位模式的本质是LCD1602 数据总线的使用位数不同,最终显示效果完全一致

4 位模式是为了节省单片机 IO 口

① 硬件接线区别

维度 8 位模式 4 位模式
数据引脚使用 D0~D7 全部接单片机 IO 口 仅用 D4~D7 接单片机 IO 口(D0~D3 悬空 / 接地)
总 IO 口占用(含控制) 至少 10 个(RS、RW、E + D0~D7) 至少 6 个(RS、RW、E + D4~D7)
接线复杂度 高(要接 8 个数据引脚) 低(仅接 4 个数据引脚)

② 数据传输原理区别

LCD1602 接收的指令 / 数据都是 8 位(1 个字节),两种模式的传输方式完全不同:

  1. 8 位模式(一次传完 8 位)

逻辑:1 个字节(指令 / 数据)一次传输完成

过程(对应代码里 8 位模式的lcd1602_write_cmd):

① 把 8 位指令(如 0x38)直接放到 D0~D7 总线;

② 拉低 E→拉高 E(上升沿锁存)→拉低 E,一次时序完成传输;

举例:写指令 0x38(8 位模式初始化),直接把 00111000 放到 D0~D7,一次 E 时序就完成

  1. 4 位模式(分两次传 8 位)

逻辑:1 个字节拆成高 4 位、低 4 位,分两次传输(仅用 D4~D7);

过程(理论标准逻辑,对应你代码里 4 位模式的函数):

先传高 4 位:把指令的高 4 位(如 0x38 的高 4 位是 0011)放到 D4~D7;

② 一次 E 时序完成高 4 位传输;

再传低 4 位:把指令的低 4 位(如 0x38 的低 4 位是 1000)放到 D4~D7;

④ 第二次 E 时序完成低 4 位传输;

举例:写指令 0x28(4 位模式初始化),先传高 4 位 0010→D4~D7,再传低 4 位 1000→D4~D7,两次时序完成。

③ 优缺点对比

维度 8 位模式 4 位模式
传输速度 快(一次传输) 稍慢(分两次)
IO 口占用 多(费 IO) 少(省 IO,51 单片机首选)
代码复杂度 简单(逻辑少) 稍复杂(分两次传输)
显示效果 无差异 无差异
适用场景 单片机 IO 口充足、追求简洁代码 单片机 IO 口紧张(如同时接按键 / 传感器)
5. 软件设计
5.1 lcd1602.h、lcd1602.c

lcd1602.c

cpp 复制代码
#include "lcd1602.h"
/*******************************************************************************
* 函 数 名       : lcd1602_write_cmd
* 函数功能		 : LCD1602写命令
* 输    入       : cmd:指令
* 输    出    	 : 无
*******************************************************************************/
#if (LCD1602_4OR8_DATA_INTERFACE == 0)//8位LCD
void lcd1602_write_cmd(u8 cmd)
{
	LCD1602_RS = 0;//选择命令
	LCD1602_RW = 0;//选择写
	LCD1602_E  = 0;
	LCD1602_DATAPORT = cmd;//准备命令
	delay_ms(1);
	LCD1602_E = 1;//使能脚E先上升沿写入
	delay_ms(1);
	LCD1602_E = 0;//使能脚E后负跳变完成写入	
}
#else	//4位LCD
void lcd1602_write_cmd(u8 cmd)
{
	LCD1602_RS=0;//选择命令
	LCD1602_RW=0;//选择写
	LCD1602_E=0;
	LCD1602_DATAPORT=cmd;//准备命令
	delay_ms(1);
	LCD1602_E=1;//使能脚E先上升沿写入
	delay_ms(1);
	LCD1602_E=0;//使能脚E后负跳变完成写入
	
	LCD1602_DATAPORT = cmd<<4;//准备命令
	delay_ms(1);
	LCD1602_E=1;//使能脚E先上升沿写入
	delay_ms(1);
	LCD1602_E=0;//使能脚E后负跳变完成写入	
}
#endif

/*******************************************************************************
* 函 数 名       : lcd1602_write_data
* 函数功能		 : LCD1602写数据
* 输    入       : dat:数据
* 输    出    	 : 无
*******************************************************************************/
#if (LCD1602_4OR8_DATA_INTERFACE==0)//8位LCD
void lcd1602_write_data(u8 dat) 
{
	LCD1602_RS=1;//选择数据
	LCD1602_RW=0;//选择写
	LCD1602_E=0;
	LCD1602_DATAPORT=dat;//准备数据
	delay_ms(1);
	LCD1602_E=1;//使能脚E先上升沿写入
	delay_ms(1);
	LCD1602_E=0;//使能脚E后负跳变完成写入		
}
#else
void lcd1602_write_data(u8 dat) 
{
	LCD1602_RS=1;//选择数据
	LCD1602_RW=0;//选择写
	LCD1602_E=0;
	LCD1602_DATAPORT=dat;//准备数据
	delay_ms(1);
	LCD1602_E=1;//使能脚E先上升沿写入
	delay_ms(1);
	LCD1602_E=0;//使能脚E后负跳变完成写入
	
	LCD1602_DATAPORT=dat<<4;//准备数据
	delay_ms(1);
	LCD1602_E=1;//使能脚E先上升沿写入
	delay_ms(1);
	LCD1602_E=0;//使能脚E后负跳变完成写入		
}
#endif

/*******************************************************************************
* 函 数 名       : lcd1602_init
* 函数功能		 : LCD1602初始化
* 输    入       : 无
* 输    出    	 : 无
*******************************************************************************/
#if (LCD1602_4OR8_DATA_INTERFACE==0)//8位LCD
void lcd1602_init(void)
{
	lcd1602_write_cmd(0x38);//数据总线8位,显示2行,5*7点阵/字符
	lcd1602_write_cmd(0x0c);//显示功能开,无光标,光标不闪烁
	lcd1602_write_cmd(0x06);//写入新数据后光标右移,显示屏不移动
	lcd1602_write_cmd(0x01);//清屏	
}
#else
void lcd1602_init(void)
{
	lcd1602_write_cmd(0x28);//数据总线4位,显示2行,5*7点阵/字符
	lcd1602_write_cmd(0x0c);//显示功能开,无光标,光标闪烁
	lcd1602_write_cmd(0x06);//写入新数据后光标右移,显示屏不移动
	lcd1602_write_cmd(0x01);//清屏	
}
#endif

/*******************************************************************************
* 函 数 名       : lcd1602_clear
* 函数功能		 : LCD1602清屏
* 输    入       : 无
* 输    出    	 : 无
*******************************************************************************/
void lcd1602_clear(void)
{
	lcd1602_write_cmd(0x01);	
}

/*******************************************************************************
* 函 数 名       : lcd1602_show_string
* 函数功能		 : LCD1602显示字符
* 输    入       : x,y:显示坐标,x=0~15,y=0~1;
				   str:显示字符串
* 输    出    	 : 无
*******************************************************************************/
void lcd1602_show_string(u8 x,u8 y,u8 *str)
{
	u8 i=0;	//计数器,记录已经显示了多少个字符(初始值 0);

	if(y>1||x>15)return;//行列参数不对则强制退出

	if(y<1)	//第1行显示
	{	
		while(*str!='\0')//字符串是以'\0'结尾,只要前面有内容就显示
		{
			if(i<16-x)//如果字符长度超过第一行显示范围,则在第二行继续显示
			{
				lcd1602_write_cmd(0x80+i+x);//第一行显示地址设置	
			}
			else
			{
				lcd1602_write_cmd(0x40+0x80+i+x-16);//第二行显示地址设置	
			}
			lcd1602_write_data(*str);//显示内容:当前指针指向的字符
			str++;//指针递增,即指向字符串下一个字符
			i++;	
		}	
	}
	else	//第2行显示
	{
		while(*str!='\0')
		{
			if(i<16-x) //如果字符长度超过第二行显示范围,则在第一行继续显示
			{
				lcd1602_write_cmd(0x80+0x40+i+x);	
			}
			else
			{
				lcd1602_write_cmd(0x80+i+x-16);	
			}
			lcd1602_write_data(*str);
			str++;
			i++;	
		}	
	}				
}

lcd1602.h

cpp 复制代码
#ifndef _lcd1602_H
#define _lcd1602_H

#include "public.h"

//LCD1602数据口4位和8位定义,若为1,则为LCD1602四位数据口驱动,反之为8位
#define LCD1602_4OR8_DATA_INTERFACE	0	//默认使用8位数据口LCD1602

//管脚定义
sbit LCD1602_RS=P2^6;//数据命令选择
sbit LCD1602_RW=P2^5;//读写选择
sbit LCD1602_E=P2^7; //使能信号
#define LCD1602_DATAPORT P0	//宏定义LCD1602数据端口


//函数声明
void lcd1602_init(void);
void lcd1602_clear(void);
void lcd1602_show_string(u8 x,u8 y,u8 *str);

#endif

1. lcd1602_write_cmd:LCD1602 写命令函数

核心作用:向 LCD1602 写入控制指令(如初始化、地址设置、清屏等),是所有操作的底层基础

cpp 复制代码
#if (LCD1602_4OR8_DATA_INTERFACE == 0)//8位LCD
void lcd1602_write_cmd(u8 cmd)
{
	LCD1602_RS = 0;//选择命令
	LCD1602_RW = 0;//选择写
	LCD1602_E  = 0;
	LCD1602_DATAPORT = cmd;//准备命令
	delay_ms(1);
	LCD1602_E = 1;//使能脚E先上升沿写入
	delay_ms(1);
	LCD1602_E = 0;//使能脚E后负跳变完成写入	
}
#else	//4位LCD
void lcd1602_write_cmd(u8 cmd)
{
	LCD1602_RS=0;//选择命令
	LCD1602_RW=0;//选择写
	LCD1602_E=0;
	LCD1602_DATAPORT=cmd;//准备命令
	delay_ms(1);
	LCD1602_E=1;//使能脚E先上升沿写入
	delay_ms(1);
	LCD1602_E=0;//使能脚E后负跳变完成写入
	
	LCD1602_DATAPORT = cmd<<4;//准备命令
	delay_ms(1);
	LCD1602_E=1;//使能脚E先上升沿写入
	delay_ms(1);
	LCD1602_E=0;//使能脚E后负跳变完成写入	
}
#endif
  • 通用时序逻辑(8/4 位模式共用)

    • LCD1602_RS=0:告诉 LCD1602"当前传输的是命令,不是显示数据";
    • LCD1602_RW=0:告诉 LCD1602"当前是写操作,不是读操作";
    • LCD1602_E的电平跳变:先拉低 E 脚准备数据,上升沿让 LCD 锁存总线数据,下降沿完成写入(完全匹配 LCD1602 的核心时序要求);
    • delay_ms(1):提供足够的时序裕量,避免因单片机运行速度过快导致 LCD 识别不到数据

2. lcd1602_write_data:LCD1602 写数据函数

核心作用:向 LCD1602 写入 ASCII 字符数据,是字符显示的核心函数。

cpp 复制代码
#if (LCD1602_4OR8_DATA_INTERFACE==0)//8位LCD
void lcd1602_write_data(u8 dat) 
{
	LCD1602_RS=1;//选择数据
	LCD1602_RW=0;//选择写
	LCD1602_E=0;
	LCD1602_DATAPORT=dat;//准备数据
	delay_ms(1);
	LCD1602_E=1;//使能脚E先上升沿写入
	delay_ms(1);
	LCD1602_E=0;//使能脚E后负跳变完成写入		
}
#else
void lcd1602_write_data(u8 dat) 
{
	LCD1602_RS=1;//选择数据
	LCD1602_RW=0;//选择写
	LCD1602_E=0;
	LCD1602_DATAPORT=dat;//准备数据
	delay_ms(1);
	LCD1602_E=1;//使能脚E先上升沿写入
	delay_ms(1);
	LCD1602_E=0;//使能脚E后负跳变完成写入
	
	LCD1602_DATAPORT=dat<<4;//准备数据
	delay_ms(1);
	LCD1602_E=1;//使能脚E先上升沿写入
	delay_ms(1);
	LCD1602_E=0;//使能脚E后负跳变完成写入		
}
#endif

逻辑与写命令函数几乎一致,唯一区别是LCD1602_RS=1(告诉 LCD1602 "当前传输的是显示数据");

8 位模式:直接写入 8 位 ASCII 码,逻辑标准,字符显示无偏差;

4 位模式:同理,虽有数据偏移,但 LCD 仅识别高 4 位,而 ASCII 字符的核心编码(高 4 位)足以让 LCD 显示正确字符,因此实验中字符显示正常。

为什么代码里 4 位模式没有按 "标准拆高 / 低 4 位"(cmd>>4cmd&0x0F),而是直接写cmdcmd<<4,却依然能正常工作?

cpp 复制代码
// 第一步:直接写cmd
LCD1602_DATAPORT=cmd; 
// 第二步:写cmd<<4
LCD1602_DATAPORT=cmd<<4;

本质是LCD1602 4 位模式的硬件特性 + 接线匹配 + 时序裕量 共同抵消了代码的 "逻辑瑕疵"

① 硬件铁律

LCD1602 工作在 4 位模式时,只识别数据引脚 D4~D7(高 4 位),D0~D3(低 4 位)完全无效

② 分析代码

操作步骤 代码写入的值 单片机端口电平(8 位) LCD 实际识别的有效位(D4~D7) 对应标准操作的功能
第一步 cmd D7 D6 D5 D4 D3 D2 D1 D0(=cmd 的 8 位) D4~D7 = cmd 的 D4~D7(cmd 的 "中 4 位") (本应传 "高 4 位")
第二步 cmd<<4 cmd 的 D3~D0 0 0 0 0 D4~D7 = cmd 的 D0~D3(cmd 的 "低 4 位") (本应传 "低 4 位")

③ 时序裕量的 "兜底作用"

代码里每一步都加了delay_ms(1)------LCD1602 对 4 位模式的时序要求是 "每次传输后保持稳定≥40us",而 1ms=1000us,远超过要求。

哪怕数据传输的 "步骤顺序" 有小瑕疵,充足的延时也能让 LCD 有足够时间识别数据,不会因为时序过快导致识别错误

3. lcd1602_init:LCD1602 初始化函数

核心作用:向 LCD 写入初始化指令,配置显示模式,是 LCD 能正常工作的前提。

cpp 复制代码
#if (LCD1602_4OR8_DATA_INTERFACE==0)//8位LCD
void lcd1602_init(void)
{
	lcd1602_write_cmd(0x38);//数据总线8位,显示2行,5*7点阵/字符
	lcd1602_write_cmd(0x0c);//显示功能开,无光标,光标不闪烁
	lcd1602_write_cmd(0x06);//写入新数据后光标右移,显示屏不移动
	lcd1602_write_cmd(0x01);//清屏	
}
#else
void lcd1602_init(void)
{
	lcd1602_write_cmd(0x28);//数据总线4位,显示2行,5*7点阵/字符
	lcd1602_write_cmd(0x0c);//显示功能开,无光标,光标闪烁
	lcd1602_write_cmd(0x06);//写入新数据后光标右移,显示屏不移动
	lcd1602_write_cmd(0x01);//清屏	
}
#endif
  • 8 位模式初始化指令(完全标准):
    • 0x38:8 位数据总线、2 行显示、5×7 点阵(适配 LCD1602 的硬件特性);
    • 0x0c:开显示、无光标、光标不闪烁;
    • 0x06:光标随数据写入右移、屏幕不滚动(符合日常显示习惯);
    • 0x01:清屏(初始化必备,避免残留数据干扰)。
  • 4 位模式:仅将0x38替换为0x28(4 位数据总线),其余指令完全一致,初始化效果相同。

4. lcd1602_clear:LCD1602 清屏函数

核心作用 :封装清屏指令0x01,简化清屏操作。

cpp 复制代码
void lcd1602_clear(void)
{
	lcd1602_write_cmd(0x01);	
}

仅调用lcd1602_write_cmd(0x01),符合 LCD1602 的清屏时序,实验中清屏操作能立即生效。

5. lcd1602_show_string:LCD1602 字符串显示函数

核心作用:在指定坐标(x:0-15 列,y:0-1 行)显示字符串,是最贴近实验效果的函数,也是 "显示正确" 的关键。

cpp 复制代码
void lcd1602_show_string(u8 x,u8 y,u8 *str)
{
	u8 i=0;

	if(y>1||x>15)return;//行列参数不对则强制退出

	if(y<1)	//第1行显示
	{	
		while(*str!='\0')//字符串是以'\0'结尾,只要前面有内容就显示
		{
			if(i<16-x)//如果字符长度超过第一行显示范围,则在第二行继续显示
			{
				lcd1602_write_cmd(0x80+i+x);//第一行显示地址设置	
			}
			else
			{
				lcd1602_write_cmd(0x40+0x80+i+x-16);//第二行显示地址设置	
			}
			lcd1602_write_data(*str);//显示内容
			str++;//指针递增
			i++;	
		}	
	}
	else	//第2行显示
	{
		while(*str!='\0')
		{
			if(i<16-x) //如果字符长度超过第二行显示范围,则在第一行继续显示
			{
				lcd1602_write_cmd(0x80+0x40+i+x);	
			}
			else
			{
				lcd1602_write_cmd(0x80+i+x-16);	
			}
			lcd1602_write_data(*str);
			str++;
			i++;	
		}	
	}				
}
cpp 复制代码
while(*str!='\0')//字符串是以'\0'结尾,只要前面有内容就显示
		{
			if(i<16-x)//如果字符长度超过第一行显示范围,则在第二行继续显示
			{
				lcd1602_write_cmd(0x80+i+x);//第一行显示地址设置	
			}
			else
			{
				lcd1602_write_cmd(0x40+0x80+i+x-16);//第二行显示地址设置	
			}
			lcd1602_write_data(*str);//显示内容:当前指针指向的字符
			str++;//指针递增,即指向字符串下一个字符
			i++;	
		}	
  • 核心逻辑拆解:
  • 参数校验if(y>1||x>15)return------ 避免坐标越界导致的显示异常;
  • 地址映射
    • 第一行基地址:0x80,因此0x80+i+x是第一行第x+i列的地址;
    • 第二行基地址:0x80+0x40=0xC0,因此0x80+0x40+i+x是第二行第x+i列的地址;(地址计算完全匹配 LCD1602 的硬件地址映射规则)
    • 跨行显示 :当字符串长度超过当前行剩余位置(i≥16-x),自动切换到另一行显示,实验中能看到长字符串跨行显示,效果符合预期;④ 循环写入 :直到字符串结束符'\0',确保完整显示字符串。

举例:当 x = 15,y = 0,i = 0 时,显示字符串 AB(即在第一行第十五列显示 A,第二行第一列显示 B)

满足 i < 16 - x,在第一行显示,0x80+i+x = 0x80+0+15 = 0x8F,即第一行第15列硬件地址

之后,i = 1(I:记录已经显示了多少个字符)

不满足 i < 16 - x,在第二行显示,0x40+0x80+i+x-16 = 0x40+0x80+1+15-16 = 0xC0,即第二行第一列硬件地址

此外,LCD1602 的地址分为两种,很容易混淆:

类型 含义 格式 / 规则
DDRAM 绝对地址 LCD 内部存储显示字符的 RAM 地址(截图里标注的 00、40 等) 第一行 0-15 列 → 00H~0FH(0~15) 第二行 0-15 列 → 40H~4FH(64~79)
DDRAM 地址设置指令 发给 LCD 的 "定位显示位置" 的命令 (代码里的 0x80、0xC0 等) 指令格式为 1xxxxxxx(最高位 D7 必须为 1)即:指令 = 0x80 + DDRAM 绝对地址
cpp 复制代码
while(*str!='\0')
		{
			if(i<16-x) //如果字符长度超过第二行显示范围,则在第一行继续显示
			{
				lcd1602_write_cmd(0x80+0x40+i+x);	
			}
			else
			{
				lcd1602_write_cmd(0x80+i+x-16);	
			}
			lcd1602_write_data(*str);
			str++;
			i++;	
		}	

核心作用:从第二行第 x 列开始显示字符串,当第二行剩余位置不够时,自动切回第一行继续显示,直到字符串结束

5.2 main.c
cpp 复制代码
/**************************************************************************************	
实验现象:下载程序后,LCD1602上显示字符信息
注意事项:																				  
***************************************************************************************/
#include "public.h"
#include "lcd1602.h"


/*******************************************************************************
* 函 数 名       : main
* 函数功能		 : 主函数
* 输    入       : 无
* 输    出    	 : 无
*******************************************************************************/
void main()
{	
	lcd1602_init();//LCD1602初始化
	lcd1602_show_string(0,0,"Hello World!");//第一行显示
	lcd1602_show_string(0,1,"0123456789");//第二行显示
	while(1)
	{
		
	}	
}
cpp 复制代码
	while(1)
	{
		
	}	

核心作用 :进入死循环,让程序一直运行,保持 LCD 显示不变;

  1. 51 单片机的main函数如果执行完(没有死循环),程序会从头重新执行(导致 LCD 反复初始化、闪烁);
  2. 死循环内无代码,说明不需要动态更新显示,仅需保持当前字符稳定显示;
5.3 public.c、public.h

public.c

cpp 复制代码
#include "public.h"

/*******************************************************************************
* 函 数 名       : delay_10us
* 函数功能		 : 延时函数,ten_us=1时,大约延时10us
* 输    入       : ten_us
* 输    出    	 : 无
*******************************************************************************/
void delay_10us(u16 ten_us)
{
	while(ten_us--);	
}

/*******************************************************************************
* 函 数 名       : delay_ms
* 函数功能		 : ms延时函数,ms=1时,大约延时1ms
* 输    入       : ms:ms延时时间
* 输    出    	 : 无
*******************************************************************************/
void delay_ms(u16 ms)
{
	u16 i,j;
	for(i=ms;i>0;i--)
		for(j=110;j>0;j--);
}

public.h

cpp 复制代码
#ifndef _public_H
#define _public_H

#include "reg52.h"

typedef unsigned int u16;	//对系统默认数据类型进行重定义
typedef unsigned char u8;
typedef unsigned long u32;

void delay_10us(u16 ten_us);
void delay_ms(u16 ms);

#endif
相关推荐
竹叶萧萧2 小时前
stm32u575从bank2启动,擦写bank1失败
stm32·单片机·嵌入式硬件·双bank升级·双bank切换·bank2·dual-bank
悠哉悠哉愿意2 小时前
【嵌入式学习笔记】Key模块解析
笔记·单片机·嵌入式硬件·学习
huangjiazhi_2 小时前
arduino uno单片机+AM2032 DHT22 Sensor温湿度开发
单片机·嵌入式硬件
福尔摩斯张2 小时前
嵌入式硬件篇:常见单片机型号深度解析与技术选型指南
网络·数据库·stm32·单片机·网络协议·tcp/ip·mongodb
就是蠢啊2 小时前
51单片机——LCD12864液晶显示(二)
单片机·嵌入式硬件·51单片机
辰哥单片机设计2 小时前
STM32项目分享:智能家用垃圾桶
stm32·单片机·嵌入式硬件
v先v关v住v获v取3 小时前
红薯杀秧机设计cad10张+三维图+设计说明书
科技·单片机·51单片机
就是蠢啊3 小时前
51单片机——LCD12864液晶显示(一)
单片机·嵌入式硬件·51单片机
某林21213 小时前
基于SLAM Toolbox的移动机器人激光建图算法原理与工程实现
stm32·嵌入式硬件·算法·slam