文章目录
-
- 一、前言
-
- [1.1 技术背景](#1.1 技术背景)
- [1.2 应用场景](#1.2 应用场景)
- [1.3 本文目标](#1.3 本文目标)
- 二、环境准备
-
- [2.1 硬件准备](#2.1 硬件准备)
- [2.2 软件环境](#2.2 软件环境)
- [2.3 硬件连接图](#2.3 硬件连接图)
- 三、核心实现
-
- [3.1 LCD1602指令集](#3.1 LCD1602指令集)
- [3.2 并口驱动实现](#3.2 并口驱动实现)
- [3.3 4位并口模式(节省IO口)](#3.3 4位并口模式(节省IO口))
- [3.4 主程序示例](#3.4 主程序示例)
- 四、测试验证
-
- [4.1 编译下载](#4.1 编译下载)
- [4.2 预期结果](#4.2 预期结果)
- [4.3 调试技巧](#4.3 调试技巧)
- 五、故障排查与问题解决
-
- [5.1 显示问题](#5.1 显示问题)
- [5.2 时序问题](#5.2 时序问题)
- [5.3 自定义字符问题](#5.3 自定义字符问题)
- 六、进阶应用
-
- [6.1 滚动显示](#6.1 滚动显示)
- [6.2 数字显示格式化](#6.2 数字显示格式化)
- 七、总结
-
- [7.1 核心知识点回顾](#7.1 核心知识点回顾)
- [7.2 扩展学习方向](#7.2 扩展学习方向)
- [7.3 学习资源](#7.3 学习资源)
一、前言
1.1 技术背景
LCD1602(Liquid Crystal Display 1602)是一种广泛使用的字符型液晶显示模块,能够显示16列×2行共32个字符。因其价格低廉、接口简单、驱动程序成熟等特点,成为嵌入式开发中最常用的显示设备之一。
在STM32开发中,驱动LCD1602有两种主要方式:
- 并口模式:使用8位或4位数据线传输数据,速度快但占用IO口多
- 模拟时序模式:通过软件模拟LCD1602的时序信号,灵活性高
1.2 应用场景
- 工业控制面板显示
- 智能家居状态显示
- 电子仪器仪表
- 教学实验平台
1.3 本文目标
通过本文,你将学会:
- LCD1602的工作原理和引脚功能
- 并口和模拟时序两种驱动方式
- 完整的STM32 HAL库驱动代码
- 自定义字符显示方法
- 常见问题排查
技术栈:
- 开发板:STM32F103C8T6(最小系统板)
- 开发环境:STM32CubeIDE / Keil MDK
- 固件库:STM32 HAL库
- 显示模块:LCD1602(带I2C转接板或不带)
二、环境准备
2.1 硬件准备
| 器件 | 数量 | 说明 |
|---|---|---|
| STM32F103C8T6最小系统板 | 1 | 主控芯片 |
| LCD1602液晶模块 | 1 | 16×2字符显示 |
| 杜邦线 | 若干 | 连接线 |
| USB转TTL模块 | 1 | 程序下载/调试 |
| 面包板 | 1 | 电路搭建 |
LCD1602引脚说明:
| 引脚 | 名称 | 类型 | 功能说明 |
|---|---|---|---|
| 1 | VSS | 电源 | 接地(GND) |
| 2 | VDD | 电源 | 接+5V |
| 3 | VO | 电源 | 对比度调节(接电位器) |
| 4 | RS | 输入 | 寄存器选择(0=指令,1=数据) |
| 5 | RW | 输入 | 读写选择(0=写,1=读) |
| 6 | E | 输入 | 使能信号(下降沿锁存) |
| 7-14 | D0-D7 | 双向 | 8位并行数据线 |
| 15 | A | 电源 | 背光正极(+5V) |
| 16 | K | 电源 | 背光负极(GND) |
2.2 软件环境
STM32CubeMX配置:
- 打开STM32CubeMX,选择STM32F103C8T6芯片
- 配置系统时钟为72MHz
- 配置GPIO引脚(根据连接方式选择):
并口模式引脚配置:
RS -> PB0
RW -> PB1
E -> PB2
D0 -> PA0
D1 -> PA1
D2 -> PA2
D3 -> PA3
D4 -> PA4
D5 -> PA5
D6 -> PA6
D7 -> PA7
- 生成代码,打开工程
2.3 硬件连接图
STM32F103C8T6 LCD1602
5V ---------------- VDD (2)
GND ---------------- VSS (1)
GND ---------------- K (16)
5V ---------------- A (15)
PB0 ---------------- RS (4)
PB1 ---------------- RW (5)
PB2 ---------------- E (6)
PA0-PA7 ------------ D0-D7 (7-14)
电位器(10K):
1脚 -> 5V
2脚 -> VO (3)
3脚 -> GND
三、核心实现
3.1 LCD1602指令集
在编写驱动代码前,先了解LCD1602的核心指令:
| 指令 | 代码 | 功能 | 执行时间 |
|---|---|---|---|
| 清屏 | 0x01 | 清屏,光标归位 | 1.52ms |
| 归位 | 0x02 | 光标归位,显示归位 | 1.52ms |
| 输入模式 | 0x04-0x07 | 设置光标移动方向 | 37μs |
| 显示开关 | 0x08-0x0F | 显示/光标/闪烁控制 | 37μs |
| 光标设置 | 0x10-0x14 | 光标移动 | 37μs |
| 功能设置 | 0x20-0x3F | 数据位/行数/字体 | 37μs |
| 设置DDRAM | 0x80-0xCF | 设置显示地址 | 37μs |
常用指令组合:
0x38:8位数据接口,2行显示,5×8点阵0x0C:显示开,光标关,闪烁关0x06:写入数据后光标右移,显示不移动
3.2 并口驱动实现
📄 创建文件:
Inc/lcd1602.h
c
#ifndef __LCD1602_H
#define __LCD1602_H
#include "stm32f1xx_hal.h"
// 引脚定义
#define LCD_RS_GPIO GPIOB
#define LCD_RS_PIN GPIO_PIN_0
#define LCD_RW_GPIO GPIOB
#define LCD_RW_PIN GPIO_PIN_1
#define LCD_E_GPIO GPIOB
#define LCD_E_PIN GPIO_PIN_2
#define LCD_DATA_GPIO GPIOA
#define LCD_DATA_PORT GPIOA
// 指令定义
#define LCD_CLEAR 0x01
#define LCD_HOME 0x02
#define LCD_ENTRY_MODE 0x06
#define LCD_DISPLAY_ON 0x0C
#define LCD_DISPLAY_OFF 0x08
#define LCD_FUNCTION_8BIT 0x38
#define LCD_FUNCTION_4BIT 0x28
#define LCD_SET_DDRAM 0x80
// 函数声明
void LCD_Init(void);
void LCD_Clear(void);
void LCD_Home(void);
void LCD_SetCursor(uint8_t col, uint8_t row);
void LCD_WriteChar(char c);
void LCD_WriteString(char *str);
void LCD_WriteCommand(uint8_t cmd);
void LCD_WriteData(uint8_t data);
void LCD_CreateChar(uint8_t location, uint8_t *charmap);
#endif
📄 创建文件:
Src/lcd1602.c
c
#include "lcd1602.h"
#include "stm32f1xx_hal.h"
// 延时函数(微秒级)
static void LCD_Delay_us(uint32_t us)
{
// 简单延时,72MHz下约1us
for(volatile uint32_t i = 0; i < us * 8; i++);
}
// 延时函数(毫秒级)
static void LCD_Delay_ms(uint32_t ms)
{
HAL_Delay(ms);
}
// 写指令到LCD
void LCD_WriteCommand(uint8_t cmd)
{
// RS = 0 (指令)
HAL_GPIO_WritePin(LCD_RS_GPIO, LCD_RS_PIN, GPIO_PIN_RESET);
// RW = 0 (写)
HAL_GPIO_WritePin(LCD_RW_GPIO, LCD_RW_PIN, GPIO_PIN_RESET);
// 输出数据到PA0-PA7
uint16_t port_val = GPIOA->ODR & 0xFF00; // 保留高8位
port_val |= cmd; // 设置低8位为cmd
GPIOA->ODR = port_val;
// E产生下降沿(高->低)锁存数据
HAL_GPIO_WritePin(LCD_E_GPIO, LCD_E_PIN, GPIO_PIN_SET);
LCD_Delay_us(5);
HAL_GPIO_WritePin(LCD_E_GPIO, LCD_E_PIN, GPIO_PIN_RESET);
LCD_Delay_us(5);
}
// 写数据到LCD
void LCD_WriteData(uint8_t data)
{
// RS = 1 (数据)
HAL_GPIO_WritePin(LCD_RS_GPIO, LCD_RS_PIN, GPIO_PIN_SET);
// RW = 0 (写)
HAL_GPIO_WritePin(LCD_RW_GPIO, LCD_RW_PIN, GPIO_PIN_RESET);
// 输出数据到PA0-PA7
uint16_t port_val = GPIOA->ODR & 0xFF00;
port_val |= data;
GPIOA->ODR = port_val;
// E产生下降沿
HAL_GPIO_WritePin(LCD_E_GPIO, LCD_E_PIN, GPIO_PIN_SET);
LCD_Delay_us(5);
HAL_GPIO_WritePin(LCD_E_GPIO, LCD_E_PIN, GPIO_PIN_RESET);
LCD_Delay_us(5);
}
// LCD初始化
void LCD_Init(void)
{
// 等待LCD上电稳定
LCD_Delay_ms(20);
// 初始化序列
LCD_WriteCommand(0x38); // 8位数据接口,2行,5x8点阵
LCD_Delay_ms(5);
LCD_WriteCommand(0x38);
LCD_Delay_us(150);
LCD_WriteCommand(0x38);
LCD_Delay_us(150);
LCD_WriteCommand(0x08); // 显示关
LCD_Delay_us(50);
LCD_WriteCommand(0x01); // 清屏
LCD_Delay_ms(2);
LCD_WriteCommand(0x06); // 光标右移,显示不移动
LCD_Delay_us(50);
LCD_WriteCommand(0x0C); // 显示开,光标关,闪烁关
LCD_Delay_us(50);
}
// 清屏
void LCD_Clear(void)
{
LCD_WriteCommand(LCD_CLEAR);
LCD_Delay_ms(2);
}
// 光标归位
void LCD_Home(void)
{
LCD_WriteCommand(LCD_HOME);
LCD_Delay_ms(2);
}
// 设置光标位置
// col: 0-15, row: 0-1
void LCD_SetCursor(uint8_t col, uint8_t row)
{
uint8_t addr = col + (row == 0 ? 0x00 : 0x40);
LCD_WriteCommand(LCD_SET_DDRAM | addr);
LCD_Delay_us(50);
}
// 写一个字符
void LCD_WriteChar(char c)
{
LCD_WriteData((uint8_t)c);
LCD_Delay_us(50);
}
// 写字符串
void LCD_WriteString(char *str)
{
while(*str)
{
LCD_WriteChar(*str++);
}
}
// 创建自定义字符
// location: 0-7 (CGRAM位置)
// charmap: 5x8点阵数据(8字节)
void LCD_CreateChar(uint8_t location, uint8_t *charmap)
{
location &= 0x07; // 限制在0-7
LCD_WriteCommand(0x40 | (location << 3)); // 设置CGRAM地址
LCD_Delay_us(50);
for(uint8_t i = 0; i < 8; i++)
{
LCD_WriteData(charmap[i]);
LCD_Delay_us(50);
}
}
3.3 4位并口模式(节省IO口)
如果IO口紧张,可以使用4位模式,只需D4-D7四根数据线:
📄 创建文件:
Src/lcd1602_4bit.c
c
#include "lcd1602.h"
// 4位模式引脚定义(使用PA4-PA7)
#define LCD_DATA_MASK 0xF0
// 延时函数
static void LCD_Delay_us(uint32_t us)
{
for(volatile uint32_t i = 0; i < us * 8; i++);
}
// 发送4位数据
static void LCD_WriteNibble(uint8_t nibble, uint8_t rs)
{
// 设置RS
HAL_GPIO_WritePin(LCD_RS_GPIO, LCD_RS_PIN, rs ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(LCD_RW_GPIO, LCD_RW_PIN, GPIO_PIN_RESET);
// 输出高4位到PA4-PA7
uint16_t port_val = GPIOA->ODR & 0x0F; // 保留低4位
port_val |= (nibble << 4); // 设置高4位
GPIOA->ODR = port_val;
// E产生下降沿
HAL_GPIO_WritePin(LCD_E_GPIO, LCD_E_PIN, GPIO_PIN_SET);
LCD_Delay_us(5);
HAL_GPIO_WritePin(LCD_E_GPIO, LCD_E_PIN, GPIO_PIN_RESET);
LCD_Delay_us(5);
}
// 写指令(4位模式)
void LCD_WriteCommand(uint8_t cmd)
{
// 先发送高4位
LCD_WriteNibble(cmd >> 4, 0);
// 再发送低4位
LCD_WriteNibble(cmd & 0x0F, 0);
if(cmd == 0x01 || cmd == 0x02)
HAL_Delay(2);
else
LCD_Delay_us(50);
}
// 写数据(4位模式)
void LCD_WriteData(uint8_t data)
{
// 先发送高4位
LCD_WriteNibble(data >> 4, 1);
// 再发送低4位
LCD_WriteNibble(data & 0x0F, 1);
LCD_Delay_us(50);
}
// 4位模式初始化
void LCD_Init(void)
{
HAL_Delay(20);
// 初始化序列(4位模式)
LCD_WriteNibble(0x03, 0); // 设置为8位模式
HAL_Delay(5);
LCD_WriteNibble(0x03, 0);
LCD_Delay_us(150);
LCD_WriteNibble(0x03, 0);
LCD_Delay_us(150);
LCD_WriteNibble(0x02, 0); // 切换到4位模式
LCD_Delay_us(150);
LCD_WriteCommand(0x28); // 4位,2行,5x8点阵
LCD_WriteCommand(0x08); // 显示关
LCD_WriteCommand(0x01); // 清屏
LCD_WriteCommand(0x06); // 光标右移
LCD_WriteCommand(0x0C); // 显示开
}
3.4 主程序示例
📄 创建文件:
Src/main.c
c
#include "main.h"
#include "lcd1602.h"
// 自定义字符:心形
uint8_t heart_char[8] = {
0x00,
0x0A,
0x1F,
0x1F,
0x0E,
0x04,
0x00,
0x00
};
// 自定义字符:温度符号
uint8_t temp_char[8] = {
0x04,
0x0A,
0x0A,
0x0E,
0x0E,
0x1F,
0x1F,
0x0E
};
int main(void)
{
// HAL初始化
HAL_Init();
SystemClock_Config();
// 初始化GPIO
MX_GPIO_Init();
// 初始化LCD
LCD_Init();
// 创建自定义字符
LCD_CreateChar(0, heart_char);
LCD_CreateChar(1, temp_char);
// 显示欢迎信息
LCD_SetCursor(0, 0);
LCD_WriteString("Hello STM32!");
LCD_SetCursor(0, 1);
LCD_WriteString("LCD1602 Test");
HAL_Delay(2000);
while(1)
{
// 清屏并显示动态内容
LCD_Clear();
LCD_SetCursor(0, 0);
LCD_WriteString("Temp: 25");
LCD_WriteData(0x01); // 显示自定义温度符号
LCD_WriteString("C");
LCD_SetCursor(0, 1);
LCD_WriteString("STM32 ");
LCD_WriteData(0x00); // 显示心形
LCD_WriteString(" LCD1602");
HAL_Delay(1000);
}
}
// GPIO初始化
void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能GPIO时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
// 配置PA0-PA7为输出(数据线)
GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 |
GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置PB0-PB2为输出(控制线)
GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
四、测试验证
4.1 编译下载
- 在STM32CubeIDE中编译项目
- 连接ST-Link下载器
- 点击下载按钮烧录程序
4.2 预期结果
正常显示:
- 第一行显示:
Hello STM32! - 第二行显示:
LCD1602 Test - 2秒后切换到温度显示界面
自定义字符显示:
- 温度符号(℃)正确显示
- 心形符号正确显示
4.3 调试技巧
使用串口输出调试信息:
c
// 在main.c中添加
#include "stdio.h"
// 重定向printf到USART1
int _write(int file, char *ptr, int len)
{
HAL_UART_Transmit(&huart1, (uint8_t*)ptr, len, HAL_MAX_DELAY);
return len;
}
// 在LCD初始化后添加调试输出
printf("LCD Init Complete!\r\n");
五、故障排查与问题解决
5.1 显示问题
问题1:LCD无任何显示
现象:
LCD背光亮,但无字符显示。
原因分析:
- 对比度调节不当(VO引脚电压不合适)
- 初始化序列错误
- 引脚连接错误
- 时序不满足要求
解决方案:
方案1:调节对比度
使用10K电位器调节VO引脚电压(0-5V之间),
通常在0.5V-1V之间显示效果最佳。
方案2:检查初始化序列
c
// 确保初始化延时足够
HAL_Delay(20); // 上电后至少等待20ms
LCD_WriteCommand(0x38);
HAL_Delay(5); // 至少4.1ms
LCD_WriteCommand(0x38);
HAL_Delay(1); // 至少100us
LCD_WriteCommand(0x38);
方案3:检查引脚连接
c
// 添加引脚测试代码
HAL_GPIO_WritePin(LCD_E_GPIO, LCD_E_PIN, GPIO_PIN_SET);
HAL_Delay(500);
HAL_GPIO_WritePin(LCD_E_GPIO, LCD_E_PIN, GPIO_PIN_RESET);
HAL_Delay(500);
// 用万用表测量E引脚是否有高低电平变化
问题2:显示乱码
现象:
LCD显示随机字符或方块。
原因分析:
- 数据线连接不稳定
- 时序过快
- 指令发送错误
解决方案:
方案1:增加延时
c
// 在写指令和数据后增加延时
void LCD_WriteCommand(uint8_t cmd)
{
// ...原有代码...
HAL_Delay(2); // 增加延时
}
方案2:检查数据线
c
// 测试数据线
for(uint8_t i = 0; i < 8; i++)
{
GPIOA->ODR = (1 << i);
HAL_Delay(200);
}
// 观察LCD是否显示变化
问题3:光标位置错误
现象:
字符显示在错误的位置。
原因分析:
DDRAM地址计算错误。
解决方案:
c
// 正确的地址计算
void LCD_SetCursor(uint8_t col, uint8_t row)
{
// 第一行地址:0x00-0x27(实际显示0x00-0x0F)
// 第二行地址:0x40-0x67(实际显示0x40-0x4F)
uint8_t addr = col + (row == 0 ? 0x00 : 0x40);
LCD_WriteCommand(0x80 | addr); // 0x80是设置DDRAM地址指令
}
5.2 时序问题
问题4:数据写入不稳定
现象:
偶尔显示正确,偶尔错误。
原因分析:
使能信号E的脉冲宽度不足或建立时间不够。
解决方案:
c
// 确保足够的时序
void LCD_WriteByte(uint8_t data, uint8_t rs)
{
HAL_GPIO_WritePin(LCD_RS_GPIO, LCD_RS_PIN, rs);
// 数据建立时间 tAS >= 40ns
LCD_Delay_us(1);
// E高电平
HAL_GPIO_WritePin(LCD_E_GPIO, LCD_E_PIN, GPIO_PIN_SET);
// E周期时间 tCYC >= 1000ns
LCD_Delay_us(1);
// E低电平
HAL_GPIO_WritePin(LCD_E_GPIO, LCD_E_PIN, GPIO_PIN_RESET);
// 保持时间 tAH >= 10ns
LCD_Delay_us(1);
}
5.3 自定义字符问题
问题5:自定义字符显示为空白
现象:
自定义字符位置显示为空白或方块。
原因分析:
- CGRAM地址设置错误
- 字符数据格式错误
- 调用自定义字符时代码错误
解决方案:
c
// 正确的自定义字符创建
void LCD_CreateChar(uint8_t location, uint8_t *charmap)
{
location &= 0x07; // 确保位置在0-7
// 设置CGRAM地址
// 每个字符占8字节,地址 = 0x40 + location * 8
LCD_WriteCommand(0x40 | (location << 3));
// 写入8字节数据
for(uint8_t i = 0; i < 8; i++)
{
LCD_WriteData(charmap[i]);
}
}
// 正确的调用方式
LCD_CreateChar(0, heart_char); // 创建字符到位置0
LCD_WriteData(0x00); // 显示位置0的字符
六、进阶应用
6.1 滚动显示
c
// 左滚动显示长文本
void LCD_ScrollText(char *text, uint8_t row)
{
uint8_t len = strlen(text);
char buffer[17];
for(uint8_t i = 0; i <= len - 16; i++)
{
strncpy(buffer, text + i, 16);
buffer[16] = '\0';
LCD_SetCursor(0, row);
LCD_WriteString(buffer);
HAL_Delay(300);
}
}
6.2 数字显示格式化
c
// 显示整数(带前导零或空格)
void LCD_WriteInt(int num, uint8_t width)
{
char buffer[16];
sprintf(buffer, "%*d", width, num); // 右对齐
// 或 sprintf(buffer, "%-*d", width, num); // 左对齐
LCD_WriteString(buffer);
}
// 显示浮点数
void LCD_WriteFloat(float num, uint8_t decimal)
{
char buffer[16];
char format[8];
sprintf(format, "%%.%df", decimal);
sprintf(buffer, format, num);
LCD_WriteString(buffer);
}
七、总结
7.1 核心知识点回顾
- LCD1602工作原理:基于HD44780控制器,通过并行接口接收指令和数据
- 并口驱动:8位或4位数据传输,需要严格的时序控制
- 指令系统:掌握清屏、归位、显示控制、地址设置等核心指令
- 自定义字符:利用CGRAM可以创建8个5×8点阵的自定义字符
7.2 扩展学习方向
- I2C转接板:使用PCF8574芯片,通过I2C接口驱动LCD1602,仅需2根线
- 图形LCD:学习12864等图形液晶的驱动方法
- RTOS集成:将LCD驱动集成到FreeRTOS等实时操作系统中
7.3 学习资源
官方文档: