目录
[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 个字节),两种模式的传输方式完全不同:
- 8 位模式(一次传完 8 位)
逻辑:1 个字节(指令 / 数据)一次传输完成;
过程(对应代码里 8 位模式的lcd1602_write_cmd):
① 把 8 位指令(如 0x38)直接放到 D0~D7 总线;
② 拉低 E→拉高 E(上升沿锁存)→拉低 E,一次时序完成传输;
举例:写指令 0x38(8 位模式初始化),直接把 00111000 放到 D0~D7,一次 E 时序就完成
- 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>>4和cmd&0x0F),而是直接写cmd和cmd<<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 显示不变;
- 51 单片机的
main函数如果执行完(没有死循环),程序会从头重新执行(导致 LCD 反复初始化、闪烁); - 死循环内无代码,说明不需要动态更新显示,仅需保持当前字符稳定显示;
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