☰
STM32驱动LCD1602实战:从点亮到二次开发
2026/10/26 10:43:47 网站建设 项目流程

简介:这份资源面向使用STM32单片机开发嵌入式显示功能的初学者与工程师,提供一套可直接集成的LCD1602字符液晶驱动库,解决从零编写底层时序与指令控制的麻烦。压缩包共2个文件,包含1个c源文件与1个h头文件,体积约1KB,源文件封装初始化、字符显示、清屏等核心函数,头文件则给出函数原型、控制指令与常量定义,便于在主程序中直接包含调用。资源已有1974人学习下载,说明其在STM32入门与课程设计中具有一定参考价值。库中将GPIO时序、使能信号与数据线操作等硬件细节统一封装,上层代码只需关注光标定位与文本输出,有助于提升代码可移植性与模块化程度,更换芯片或模块时改动较小。读者可据此快速在项目中点亮屏幕、显示状态与数据,并理解并行接口下RS、RW、E及数据线的基本控制逻辑。

1. 从一块只亮背光的屏说起

第一次把 LCD1602 接到 STM32 板子上,通电后屏幕只亮背光、一个字符都不出,是很多人都会遇到的场景。问题往往不在屏,而在时序和初始化顺序:LCD1602 用的是 HD44780 兼容控制器,上电后必须按固定节奏发指令,延时短一点、忙标志没等,屏幕就给你一片空白。这份资源就是围绕这个痛点整理的:一套可直接放进 STM32 工程的 LCD1602 基本驱动库,包含.c与.h文件,把引脚定义、初始化序列、写指令/写数据、光标控制、字符串显示这些底层动作封装成函数,主程序里几行调用就能点亮屏幕。它适合刚上手 STM32 GPIO 操作、想给项目加一块字符显示界面的开发者,也适合需要一份稳定底层驱动做二次封装的人。下面按「能干什么、怎么跑、怎么改」的顺序拆开讲。

2. 驱动库结构拆解:从引脚宏到显示函数的调用链

2.1 两个文件的分工与目录落位

这套库只有两个核心文件,但职责切得很清楚。.h文件负责对外暴露接口和硬件映射,.c文件负责具体时序实现。常见做法是把它们放到工程的Drivers/LCD1602/或User/目录下,然后在 IDE 的包含路径里加上对应目录,否则编译时会报找不到头文件。

一个典型的目录落位是这样的:

Project/ ├── Core/ │ ├── Inc/ │ └── Src/ │ └── main.c ├── Drivers/ │ └── LCD1602/ │ ├── lcd1602.c │ └── lcd1602.h └── ...

.h里通常先定义控制引脚,再声明函数原型。引脚用宏而不是写死,是为了换板子时只改一处。下面这段是这类库最常见的头文件骨架,引脚名按你实际接线替换即可:

#ifndef __LCD1602_H #define __LCD1602_H #include "main.h" /* 拿到 HAL 库的 GPIO 类型和 HAL_Delay */ /* 控制引脚:RS 决定写指令还是写数据,RW 决定读还是写,EN 是使能脉冲 */ #define LCD_RS_PORT GPIOB #define LCD_RS_PIN GPIO_PIN_0 #define LCD_RW_PORT GPIOB #define LCD_RW_PIN GPIO_PIN_1 #define LCD_EN_PORT GPIOB #define LCD_EN_PIN GPIO_PIN_2 /* 数据引脚:4 位模式下只用 D4~D7,8 位模式用满 D0~D7 */ #define LCD_D4_PORT GPIOB #define LCD_D4_PIN GPIO_PIN_4 #define LCD_D5_PORT GPIOB #define LCD_D5_PIN GPIO_PIN_5 #define LCD_D6_PORT GPIOB #define LCD_D6_PIN GPIO_PIN_6 #define LCD_D7_PORT GPIOB #define LCD_D7_PIN GPIO_PIN_7 void LCD_Init(void); void LCD_WriteCmd(uint8_t cmd); void LCD_WriteData(uint8_t data); void LCD_SetCursor(uint8_t row, uint8_t col); void LCD_ShowString(uint8_t row, uint8_t col, const char *str); void LCD_Clear(void); #endif

这里的参数含义要盯住两点:row取 0 或 1,对应两行;col取 0 到 15,对应 16 列。LCD_WriteCmd和LCD_WriteData的区别只在 RS 电平,前者 RS=0 表示指令,后者 RS=1 表示字符数据。把这两个函数分开,是后面所有显示函数的基础。

2.2 初始化序列:为什么必须按顺序发指令

LCD1602 上电后内部状态并不确定,HD44780 要求一套固定顺序的初始化。4 位模式比 8 位模式多一步「先按 8 位发三次,再切 4 位」的握手,这一步漏了,屏幕大概率不显示。下面这段是 4 位模式的典型初始化,逻辑逐行都有讲究:

void LCD_Init(void) { HAL_Delay(50); /* 上电后等待内部复位,短了会失败 */ /* 强制进入 8 位模式三次,这是 HD44780 的握手要求 */ LCD_WriteCmd(0x33); HAL_Delay(5); LCD_WriteCmd(0x32); /* 第三次后切到 4 位模式 */ HAL_Delay(5); LCD_WriteCmd(0x28); /* 4 位数据线,2 行显示,5x8 点阵 */ LCD_WriteCmd(0x0C); /* 开显示,关光标,不闪烁 */ LCD_WriteCmd(0x06); /* 写入后光标右移,画面不滚动 */ LCD_WriteCmd(0x01); /* 清屏 */ HAL_Delay(2); /* 清屏指令执行较慢,必须等 */ }

0x28是功能设置:DL=0 选 4 位,N=1 选两行,F=0 选 5x8 点阵。0x0C是显示开关:D=1 开显示,C=0 关光标,B=0 不闪烁。0x06是输入模式:I/D=1 光标右移,S=0 整屏不滚动。0x01清屏后内部执行时间约 1.5ms 以上,所以后面必须补延时。这几条指令的顺序不能随意调换,尤其是0x33/0x32握手,很多「只亮背光」的翻车都出在这里。

2.3 写时序:EN 脉冲与忙等待

写一个字节的核心是给 EN 一个高脉冲,让控制器在下降沿采样数据线。4 位模式下,一个字节分高 4 位、低 4 位两次发送。下面这段是写指令的实现,写数据只需把 RS 拉高:

static void LCD_WriteNibble(uint8_t nibble) { /* 把 4 位数据摆到 D4~D7 上 */ HAL_GPIO_WritePin(LCD_D4_PORT, LCD_D4_PIN, (nibble & 0x01) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(LCD_D5_PORT, LCD_D5_PIN, (nibble & 0x02) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(LCD_D6_PORT, LCD_D6_PIN, (nibble & 0x04) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(LCD_D7_PORT, LCD_D7_PIN, (nibble & 0x08) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(LCD_EN_PORT, LCD_EN_PIN, GPIO_PIN_SET); HAL_Delay(1); /* 保持高电平,给控制器采样时间 */ HAL_GPIO_WritePin(LCD_EN_PORT, LCD_EN_PIN, GPIO_PIN_RESET); HAL_Delay(1); } void LCD_WriteCmd(uint8_t cmd) { HAL_GPIO_WritePin(LCD_RS_PORT, LCD_RS_PIN, GPIO_PIN_RESET); /* RS=0 写指令 */ HAL_GPIO_WritePin(LCD_RW_PORT, LCD_RW_PIN, GPIO_PIN_RESET); /* RW=0 写 */ LCD_WriteNibble(cmd >> 4); /* 先发高 4 位 */ LCD_WriteNibble(cmd & 0x0F); /* 再发低 4 位 */ }

LCD_WriteNibble里用HAL_Delay(1)是最省事的做法,代价是速度慢。如果项目对刷新率有要求,可以改成读忙标志:把 RW 拉高、读 D7,直到 D7 变 0 再发下一个字节。忙等待能省掉固定延时,但要多接一根 RW 线并配置成输入,接线复杂度上升。新手先用固定延时跑通,再考虑优化,这是比较稳的路径。

2.4 显示函数:光标定位与字符串输出

有了写指令和写数据,显示字符串就是先定位再逐字符写。定位靠0x80 | 地址,第一行起始地址是0x00,第二行是0x40:

void LCD_SetCursor(uint8_t row, uint8_t col) { uint8_t addr = (row == 0) ? 0x00 : 0x40; LCD_WriteCmd(0x80 | (addr + col)); /* 设置 DDRAM 地址 */ } void LCD_ShowString(uint8_t row, uint8_t col, const char *str) { LCD_SetCursor(row, col); while (*str != '\0') { LCD_WriteData((uint8_t)*str++); /* RS=1 写字符 */ } }

LCD_WriteData与LCD_WriteCmd结构相同,只把 RS 拉高。LCD_ShowString里没有做越界保护,如果col加字符串长度超过 16,字符会写到下一行甚至回绕,这是后面避坑章节要展开的点。主程序里调用就三行:

LCD_Init(); LCD_ShowString(0, 0, "STM32 LCD1602"); LCD_ShowString(1, 0, "Hello World");

到这里,一个最小可运行链路就完整了:引脚宏 → 初始化 → 写时序 → 定位 → 显示。

3. 跑通第一屏:工程配置与接线落地

3.1 硬件接线与 GPIO 配置

4 位模式只需要 6 根控制/数据线加电源:RS、RW、EN、D4~D7,外加 VSS、VDD、V0(对比度)、BLA/BLK(背光)。V0 一般接一个 10k 电位器到地,调对比度,调不好同样会「有背光没字符」。GPIO 全部配成推挽输出、低速即可,LCD1602 的时序对速度不敏感,低速反而更稳。

在 CubeMX 里把对应引脚设为GPIO_Output,初始电平设为低,然后在main.c的MX_GPIO_Init()之后调用LCD_Init()。注意LCD_Init()里用了HAL_Delay,所以必须放在 HAL 初始化完成之后,放在while(1)之前。

3.2 编译与下载的检查顺序

跑不通时按这个顺序排查,比乱改代码有效:先确认工程包含路径里有lcd1602.h所在目录;再确认引脚宏和实际接线一一对应;然后确认HAL_Delay的时基已经启动(HAL_Init()里默认会配 SysTick);最后才怀疑初始化序列。下载后如果屏幕全黑,先调电位器看对比度,再量背光引脚电压。这套顺序能把大部分「不显示」问题挡在代码之外。

提示:不同开发板的 GPIO 端口编号不同,改引脚宏时端口和引脚号要成对改,只改一半是常见的手误。

4. 避坑与排查:五个让屏幕不亮的真实原因

4.1 只亮背光、无字符

现象是背光亮但屏幕空白。原因多数是对比度没调或初始化握手没发全。解决是先调 V0 电位器,若仍空白,检查LCD_Init里是否保留了0x33/0x32三次握手和每次之间的延时,缺一步控制器就停在 8 位模式,后续 4 位数据全被误读。

4.2 显示乱码或半个字符

现象是出现方块或错位字符。原因是 4 位模式下高低半字节顺序发反,或者 EN 脉冲太窄。解决是确认LCD_WriteNibble(cmd >> 4)先发高 4 位,并保证 EN 高电平保持时间不少于 1ms(用HAL_Delay(1)即可)。

4.3 第二行不显示

现象是第一行正常、第二行空白。原因是定位地址算错,第二行 DDRAM 起始是0x40而不是0x10。解决是检查LCD_SetCursor里row == 0 ? 0x00 : 0x40的分支,别把行地址和列地址混在一起加。

4.4 字符串写到一半就停

现象是长字符串只显示前几个字符。原因是col加字符串长度超过 16,字符回绕到不可见区域或覆盖指令区。解决是在LCD_ShowString里加长度截断,超过16 - col的部分不写,或者调用前自己保证不越界。

4.5 换板子后完全不工作

现象是同一份代码在原板子正常、新板子无反应。原因是引脚宏只改了端口没改引脚号,或者新板子的 RW 被硬件接地导致读忙不可用。解决是逐条核对宏定义与实际接线,若 RW 固定接地,就坚持用固定延时方案,不要改成读忙等待。

5. 二次开发:把这套库改成自己的显示层

这套库的定位是「基本库」,意味着它只保证能显示,不负责排版和业务。二次开发最值得做的一步,是在它上面加一层轻量显示封装,把「第几行第几列显示什么」变成带缓冲的接口,避免每次刷新都整屏重写导致闪烁。我的习惯是维护一个 2×16 的字符缓冲,业务层只改缓冲,定时器里统一刷差异:

static char lcd_buf[2][17]; /* 每行 16 字符 + 结束符 */ void LCD_BufferWrite(uint8_t row, uint8_t col, const char *str) { uint8_t i = 0; while (str[i] && (col + i) < 16) { lcd_buf[row][col + i] = str[i]; i++; } } void LCD_Flush(void) { for (uint8_t r = 0; r < 2; r++) { LCD_SetCursor(r, 0); for (uint8_t c = 0; c < 16; c++) { LCD_WriteData((uint8_t)lcd_buf[r][c]); } } }

lcd_buf用 17 字节是为了留结束符位置,LCD_BufferWrite里的(col + i) < 16就是前面说的越界保护。LCD_Flush整屏刷虽然简单,但 32 个字符每个都要两次半字节写加延时,刷新一次接近 100ms,放在主循环里会拖慢其他逻辑。更稳的做法是只刷变化的字符,或者把LCD_WriteNibble里的固定延时换成读忙等待,把单字节时间压到几十微秒。

改造时还有两个验证点值得固定下来:一是换引脚后先跑一个只显示固定字符串的自检函数,确认底层通路没问题再叠业务;二是把LCD_Init的返回值改成状态码,初始化后回读一次地址计数器,读不到预期值就点亮一个错误指示,比盯着白屏猜要快得多。从那以后我每次接新屏,都强制先跑一遍「清屏 → 写固定串 → 定位第二行」的三步自检,再动业务代码,省下的排查时间远比写自检多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询