简介:STM32单片机与LCD1602字符液晶的驱动库资源,面向嵌入式开发者,适用于运行状态显示、数据输出、简单菜单等场景,解决在STM32工程中快速集成LCD1602的问题。LCD1602为双行16字符显示模块,常通过并行接口与MCU通信;驱动库将RS、RW、E及DB0-DB7等控制时序完整封装,开发者无需理解底层协议即可调用。压缩包共2个文件,包含lcd1602.c源文件与lcd1602.h头文件,总大小仅1KB。源文件提供初始化、字符显示、清屏等函数,可配置工作模式、显示位置等参数;头文件定义了函数原型及光标位置、开关显示、背光控制等指令常量,方便直接调用与二次开发。目前已有1973人学习下载。驱动库以简洁API屏蔽硬件细节,更换STM32型号或LCD1602模块时仅需调整少量配置即可复用,适合初学者快速入门,也可用于快速原型验证与教学实验。
1. 一个 LCD1602 基础库:把 STM32 屏幕点亮从两天缩到十分钟
调 LCD1602 最磨人的不是接线,而是初始化那串命令的顺序——错一条,屏幕就给你白屏或者满屏方块。有段时间我在 STM32 上调这块屏,网上找的例程要么绑死了某块开发板,要么揉在完整工程里拆不出来,折腾了两天,干脆自己抽了一份干净的基础库:一个 .c 一个 .h,把初始化、写命令、写数据、显示字符串、清屏、光标定位全部封装好。这份资源解决的事很具体:换引脚只改宏,换芯片型号只改头文件,主函数里三行代码就能显示一屏内容。适合刚开始摸 STM32 和 LCD1602 的学生,也适合手头有屏想快速点亮验证的开发者,以及想搞明白 4 位驱动时序原理的人。
2. 文件结构与技术选型:为什么这份库能独立抽走
2.1 包里的两个文件,各自干了什么
整个资源就两个核心文件。lcd1602.h 负责把所有和硬件相关的宏集中在一起,外加常用命令码和对外接口声明;lcd1602.c 负责真正驱动屏幕的全部逻辑,内部四个 static 函数加五个对外接口。如果你是从资源页拿到的完整压缩包,通常还会附带一个 demo_main.c 示例,演示最小调用顺序;就算只有两个核心文件,照着我第 4 章的步骤一样能跑起来。
这种「头文件管宏、源文件管逻辑」的拆法,最大的好处是边界清晰:看不懂驱动细节的人,只要打开 .h 看一眼引脚宏,就知道自己的板子要改哪里;想移植到别的芯片上的人,也只需要动 .h 里的 include 行。源码层面不藏黑匣子,每一行电平操作都摊在眼前。
2.2 为什么选 4 线模式而不是 8 线
LCD1602 支持 8 线和 4 线两种并行模式。8 线模式一次发 8 位数据,速度更快,但要占 8 个数据脚加上 RS、EN 一共 10 个 GPIO,很多小封装芯片根本分不出这么多脚。4 线模式只占 D4~D7 加 RS、EN 共 6 个脚,速度完全够用——显示静态文本和传感器数值本来就不是高频操作,4 位传输两个脉冲的耗时在微秒级,肉眼完全无感。
我一般做外设驱动都倾向 4 线模式,它省下的针脚可以留给按键、LED 或者通信接口。这套库采用的就是 4 线并行,这也是市面上最常见的直连接法,资料好查,出问题也好排查。
2.3 对比 I2C 转接板方案:这份库的边界在哪
市面上还有一种常见的 PCF8574 I2C 转接板,塞在屏幕背面,往外只引 SDA、SCL、VCC、GND 四根线。那种方案实际驱动对象已经变成 PCF8574 这颗芯片了,不再是直接操作 LCD1602 的引脚。这份库是给「GPIO 直连并行模式」用的,好处是中间没有任何芯片,时序完全透明,想学 LCD 底层协议的人读这份代码最合适。
如果你手里偏偏是 I2C 转接板,不用急着换资源——第 6 章我给了移植思路,初始化时序可以完全复用,只把底层字节发送改成写 PCF8574 就行。两种方案的差别就在数据通路,协议层的命令格式是通用的。
2.4 可移植性设计:为什么改引脚不会翻车
核心设计就一条:硬件差异用宏隔离。看 lcd1602.h 会发现,所有端口、引脚号全部收在文件顶部,驱动实现里一律用宏名,不出现任何硬编码数字。这样换引脚相当于改配置,而不是动逻辑,出错的概率大幅降低。
对比很多入门例程把引脚定义散落在一个 system_stm32 大文件里,或者直接在多个函数里写死 GPIOB、GPIO_PIN_x,这份库的做法明显更利于二次使用。我自己的习惯也是这样:凡是和外设引脚相关的配置,一律收进头文件顶部并加注释,驱动代码保持「不知道引脚是谁」的状态。
3. 核心模块拆解:一条数据从函数到屏幕引脚的完整通路
3.1 头文件:先看宏和接口
#ifndef __LCD1602_H #define __LCD1602_H #include "stm32f1xx_hal.h" /* 接线宏:这是唯一需要根据你的板子修改的地方 */ #define LCD_RS_PORT GPIOB #define LCD_RS_PIN GPIO_PIN_10 #define LCD_EN_PORT GPIOB #define LCD_EN_PIN GPIO_PIN_12 #define LCD_D4_PORT GPIOB #define LCD_D4_PIN GPIO_PIN_8 #define LCD_D5_PORT GPIOB #define LCD_D5_PIN GPIO_PIN_9 #define LCD_D6_PORT GPIOB #define LCD_D6_PIN GPIO_PIN_6 #define LCD_D7_PORT GPIOB #define LCD_D7_PIN GPIO_PIN_7 /* 常用命令码 */ #define LCD_CMD_CLEAR 0x01 #define LCD_CMD_HOME 0x02 #define LCD_CMD_ENTRY_MODE 0x06 #define LCD_CMD_DISP_ON 0x0C #define LCD_CMD_FUNC_SET 0x28 /* 对外接口 */ void LCD_Init(void); void LCD_Clear(void); void LCD_SetCursor(uint8_t col, uint8_t row); void LCD_WriteChar(char ch); void LCD_WriteString(char *str); #endif逻辑说明:include 的 stm32f1xx_hal.h 是芯片厂商 HAL 库对应 F1 系列的头文件,换 F4 或 L4 系列改成 stm32f4xx_hal.h、stm32l4xx_hal.h 即可。命令码 0x28 是「4 位模式 + 双行 + 5x8 点阵」的组合,0x0C 是「显示开、光标关、不闪烁」,这两个在初始化里最常用。
参数说明:LCD_RS_PIN 是寄存器选择脚,低电平时写命令、高电平时写数据;LCD_EN_PIN 是使能脚,它的下降沿负责锁存数据。D4~D7 对应屏幕背面的 11~14 脚,编号越小越容易被接反,接线的时侯一定要对清楚位置。
3.2 底层三件套:数据输出、使能脉冲、字节发送
static void LCD_SetData(uint8_t data) { HAL_GPIO_WritePin(LCD_D4_PORT, LCD_D4_PIN, (data & 0x01) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(LCD_D5_PORT, LCD_D5_PIN, (data & 0x02) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(LCD_D6_PORT, LCD_D6_PIN, (data & 0x04) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(LCD_D7_PORT, LCD_D7_PIN, (data & 0x08) ? GPIO_PIN_SET : GPIO_PIN_RESET); } static void LCD_PulseEnable(void) { HAL_GPIO_WritePin(LCD_EN_PORT, LCD_EN_PIN, GPIO_PIN_SET); LCD_DelayUs(1); HAL_GPIO_WritePin(LCD_EN_PORT, LCD_EN_PIN, GPIO_PIN_RESET); LCD_DelayUs(1); } static void LCD_SendByte(uint8_t data, uint8_t isData) { HAL_GPIO_WritePin(LCD_RS_PORT, LCD_RS_PIN, isData ? GPIO_PIN_SET : GPIO_PIN_RESET); LCD_SetData(data >> 4); /* 先送高 4 位 */ LCD_PulseEnable(); LCD_SetData(data); /* 再送低 4 位 */ LCD_PulseEnable(); }逻辑说明:LCD_SetData 把字节的 bit0~bit3 映射到 D4~D7 四个引脚上。4 位模式一次只能传半个字节,所以 LCD_SendByte 先右移四位送高半字节,再送低半字节,中间各夹一个使能脉冲。LCD_PulseEnable 拉高再拉低,屏幕在 EN 下降沿把四个数据脚的电平锁存进内部寄存器,这个脉冲是整个时序里最容易出问题的地方——太窄会锁存失败,常见做法是保持 1 微秒以上。
参数说明:isData 传 0 表示命令,传 1 表示要显示的字符数据。两个脉冲之间的间隔由 LCD_DelayUs 控制,如果换更高主频的芯片,这里的循环次数可能要加大,这是后面排查偶发乱码时重点怀疑的对象。
3.3 初始化时序:为什么命令顺序是死规矩
void LCD_Init(void) { HAL_Delay(50); /* 等待屏幕内部上电复位完成 */ /* 第一步:唤醒序列,必须逐条发送,顺序不能跳 */ LCD_SendByte(0x30, 0); /* 8 位模式命令,屏幕此时默认在 8 位模式 */ LCD_DelayUs(5000); LCD_SendByte(0x30, 0); /* 重复两次,保证稳定进入工作状态 */ LCD_DelayUs(150); LCD_SendByte(0x30, 0); LCD_DelayUs(150); LCD_SendByte(0x20, 0); /* 切到 4 位模式,关键一步 */ LCD_DelayUs(150); /* 第二步:功能设置、显示设置、输入模式、清屏 */ LCD_WriteCommand(0x28); /* 4 位 / 双行 / 5x8 点阵 */ LCD_WriteCommand(0x0C); /* 显示开,光标关,不闪烁 */ LCD_WriteCommand(0x06); /* 写入后光标右移,屏幕不整体移动 */ LCD_WriteCommand(0x01); /* 清屏,光标回原点 */ HAL_Delay(2); /* 清屏命令耗时最长,需额外等待 */ }逻辑说明:LCD1602 上电后默认工作在 8 位模式,所以初始化第一步必须先用 0x30 连续唤醒三次,再用 0x20 切到 4 位模式。如果跳过这个序列直接发 0x28,屏幕会因为你发的每个字节都少了一半,立刻满屏乱码。0x28 和前面手动发的「0x20 切换 + 0x08 功能设置」从结果上看是等价的,但芯片手册明确要求用单条 0x28 完成最终设置,这里按手册来。
参数说明:开头的 HAL_Delay(50) 是等屏幕内部复位完成,上电后立刻初始化大概率白屏,这个延时是我见过被删掉次数最多的「保命代码」。0x0C 的三位分别是显示开关、光标开关、闪烁开关,想看到下划线光标改成 0x0E,想看到闪烁块改成 0x0F。
3.4 对外 API:光标定位与字符串输出
void LCD_SetCursor(uint8_t col, uint8_t row) { uint8_t addr = (row == 0) ? (0x80 + col) : (0x80 + 0x40 + col); LCD_WriteCommand(addr); } void LCD_WriteChar(char ch) { LCD_SendByte((uint8_t)ch, 1); } void LCD_WriteString(char *str) { while (*str) { LCD_SendByte((uint8_t)*str, 1); str++; } } void LCD_Clear(void) { LCD_WriteCommand(LCD_CMD_CLEAR); HAL_Delay(2); }逻辑说明:LCD1602 内部的 DDRAM 分两行,第一行地址从 0x00 开始,第二行从 0x40 开始。命令字节的最高位 0x80 表示「设置 DDRAM 地址」,所以 row 为 1 时要在 0x40 基础上加列偏移。网上不少库只处理了第一行地址,导致第二行显示位置错乱,这个函数专门做了 row 分支,是「写对位置」的关键。
参数说明:col 范围 0~15,row 范围 0~1。LCD_WriteString 逐字符调用发送,不做长度检查,调用侧传入字符串最长不要超过 32 字符;如果显示的数据长度会变化,我一般会在调用前自己截断,或者直接用第 6 章的格式化接口。
4. 工程接入:从 CubeMX 配置到 main.c 调用的完整步骤
4.1 硬件接线:六根线加一颗电位器
接线表如下,六根信号线全部直连 STM32 的 GPIO,RW 脚直接接地,电源部分多一颗 10K 电位器。
| LCD1602 引脚 | 接法 |
|---|---|
| VSS (1) | GND |
| VDD (2) | 5V(部分屏 3.3V 也能亮,但字迹偏淡) |
| V0 (3) | 10K 电位器中点,电位器两端接 5V 和 GND |
| RS (4) | STM32 的 PB10 |
| RW (5) | 直接接 GND,只写不读 |
| E (6) | STM32 的 PB12 |
| D4~D7 (11~14) | STM32 的 PB8 / PB9 / PB6 / PB7 |
| A / K (15/16) | 背光正负,串一个 100Ω 电阻接 5V 和 GND |
RW 接 GND 是最重要的简化——库不读忙碌标志,所以不需要控制 RW 电平,省下一个 GPIO。V0 那颗电位器不装的话,屏幕大概率「蓝屏无字」,这不是代码问题,是对比度没调到位。背光串电阻是为了限流,直接怼 5V 容易让 LED 过热老化。
4.2 CubeMX 配置 GPIO
CubeMX 是芯片厂商提供的图形化初始化工具,引脚配置按以下几步:
- 在芯片图上把 PB6、PB7、PB8、PB9、PB10、PB12 这六个脚点击设置为 GPIO_Output。
- 每个引脚的初始电平都设 Low,避免上电瞬间 LCD 误判一次无效数据。
- 输出速度选 Low,LCD 是慢速外设,High Speed 反而可能引入信号振铃。
- 推挽输出、不上拉不下拉,默认即可。
- 选好 HAL 库生成工程。
我一般会顺手把 User Label 改一下,比如在 CubeMX 里给 PB10 起名叫 LCD_RS,这样生成的 main.c 里引脚定义可读性高不少,后面排查接线问题也方便。生成后 GPIO 初始化代码不用手写,CubeMX 会把它放在 MX_GPIO_Init 函数里。
4.3 文件拷贝与引脚宏修改
拿到这份库之后的操作流程:
- 把 lcd1602.c 和 lcd1602.h 分别拷进工程的 Src 和 Inc 目录。
- 在 IDE 里把 .c 文件 Add 进工程,.h 文件确认头文件搜索路径包含 Inc 目录。
- 打开 lcd1602.h,按你自己的接线修改每个 PORT 和 PIN 宏。
- 确认 include 的 HAL 头文件名和你的芯片系列一致,F4 就改成 stm32f4xx_hal.h。
提示:宏里 PORT 和 PIN 是成对修改的。只改 PIN 不改 PORT 是换引脚后最常见的翻车原因——PB8 和 PA8 引脚号一样但端口不同,宏必须完整替换,不然函数操作的是另一个端口的同名引脚。
4.4 最小调用示例
#include "lcd1602.h" int main(void) { HAL_Init(); SystemClock_Config(); /* CubeMX 生成,配置系统时钟 */ MX_GPIO_Init(); /* CubeMX 生成,初始化 GPIO */ LCD_Init(); LCD_WriteString("Hello, LCD1602"); LCD_SetCursor(0, 1); LCD_WriteString("STM32 4-bit mode"); while (1) { HAL_Delay(1000); } }逻辑说明:初始化顺序是「先系统后外设」。HAL_Init 和 MX_GPIO_Init 必须在 LCD_Init 之前执行,否则引脚电平根本没有准备好,LCD 在初始化瞬间收到的是随机电平,后续再补救只能重新上电。第一行写满后,SetCursor 把光标移到第二行开头再写,两句字符串拼出两行完整显示。
参数说明:LCD_Init 内部自带 50ms 上电延时,所以主循环里不需要额外等待。LCD_SetCursor(0, 1) 表示 0 列、第 1 行,如果你发现第二行是从某个中间位置开始显示的,回头检查 3.4 节的 0x40 偏移逻辑,别怀疑屏幕坏了。
5. LCD1602 常见问题排查:点亮不算成功,显示正常才算
5.1 现象:上电后蓝屏或白屏,一个字都没有
原因:八成不是代码问题。最常见的是对比度电位器没调到位,V0 电压不对会让字符完全看不见;其次是供电不足,3.3V 供电时部分屏幕字迹会淡到看不清。解决:先调电位器,让屏幕上出现一排淡淡的方块或黑影;调不出来再量 VDD 和 V0 电压;最后排查 RS、EN 是否悬空。记住一条:有背光没字,优先怀疑硬件和对比度,不要急着改代码。
5.2 现象:有字但全是实心方块
原因:初始化时序错了,最常见的是唤醒序列里丢了 0x20 切换命令,屏幕还停留在 8 位模式,后续所有数据都按 4 位发送,高低半字节完全错位。另一个可能是 D4~D7 四根数据线接反了顺序,比如 D4 接到了屏幕的 D5 脚。解决:对照 3.3 节的初始化和 4.1 节接线表逐条检查。血泪经验:四根数据线最好用不同颜色的杜邦线区分,顺序错了用肉眼很难查出来。
5.3 现象:第一行正常,第二行字符位置错乱
原因:SetCursor 对第二行的地址计算错误。LCD1602 第二行首地址是 0x40,命令字节高位置 0x80 后变成 0xC0;如果库函数里只做了 0x80+col,第二行就会从第一行地址的尾部继续写,出现「第一行后面接着写了一半」的奇怪效果。解决:检查 SetCursor 的 row==1 分支是否加了 0x40 偏移。也可以直接发一条 0xC0 命令验证第二行能否正确定位。
5.4 现象:偶尔乱码,重新上电又正常
原因:使能脉冲宽度不够,或者初始化时上电延时不足。芯片跑在 72MHz 时,1 微秒的延时如果实现得不好,实际可能只有几百纳秒,恰好卡在芯片时序要求的临界点上。解决:把 LCD_DelayUs 的循环次数加大两倍;初始化前的 HAL_Delay(50) 一定保留。另外如果工程里开了中断或 RTOS,不要在中断回调里写屏幕,LCD 的字节时序被打断一两次就会花屏。这类「偶尔乱」的问题往往不是逻辑错误,而是时序余量不足,把延时拉大通常就好了。
5.5 现象:改了引脚宏之后完全不工作
原因:宏的 PORT 和 PIN 只改了一半;或者新引脚被其他外设复用了,比如某个定时器通道、串口 TX;还有可能是 CubeMX 里根本没把这个脚配置成输出模式。解决:先回 CubeMX 确认新引脚的 GPIO 配置和初始电平,再看芯片数据手册确认引脚没有第二功能冲突,最后逐对核对宏定义。换引脚这件事,九成问题出在「配置没同步」而不是「库写错了」。
6. 二次开发:从「能显示」到「好用」的三个改造方向
6.1 自定义字符:用 CGRAM 画进度条
LCD1602 内部有 64 字节 CGRAM,最多自定义 8 个 5x8 点阵字符,每个字符占 8 字节,每字节低 5 位代表一行的点阵:
void LCD_DefineChar(uint8_t code, uint8_t pattern[8]) { LCD_WriteCommand(0x40 + code * 8); /* 指向 CGRAM 首地址 */ for (uint8_t i = 0; i < 8; i++) { LCD_SendByte(pattern[i], 1); } LCD_SetCursor(0, 0); /* 写完回到 DDRAM */ }定义完用 LCD_WriteChar(0x00~0x07) 就能显示自定义图形。注意写完 CGRAM 后 DDRAM 地址已经被偏移,必须重新定位光标,这是最常见的坑。
6.2 移植到 I2C 转接板
如果手里是 PCF8574 转接板,把 LCD_SendByte 里的 GPIO 操作整体替换成向 PCF8574 写一个字节即可。典型位映射是 P7~P4 对应 D7~D4,P3 控制背光,P2 是 EN,P1 是 RW(直接写 0),P0 是 RS。初始化时序完全复用,改动量不超过二十行。
6.3 更顺手的格式化输出接口
我习惯在库外层再加一个格式化接口,配合 C 标准库的 vsnprintf 实现:
void LCD_Printf(const char *fmt, ...) { char buf[17]; /* 一屏最多 16 字符 + 结束符 */ va_list args; va_start(args, fmt); vsnprintf(buf, sizeof(buf), fmt, args); va_end(args); LCD_WriteString(buf); }用起来就是LCD_Printf("T:%d.%d C", temp / 10, temp % 10);,传感器数据拼接再也不用 sprintf 加手动拷贝了。buf 限制 17 字节是为了防止格式化超长把显存写穿,一屏写不满要记得用空格覆盖旧内容。
从那以后,我每次调 LCD 屏都强制走一遍「上电延时、唤醒序列、对比度电位器」三关,再回头查代码。顺序反了或者跳过一步,屏幕就会用白屏和方块教我做人。这个习惯帮我省掉了大量冤枉时间,希望帮到你。
本文还有配套的精品资源,点击获取