STM32实战:基于STM32F103的LCD1602液晶屏(并口/模拟时序)驱动

文章目录

一、前言

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配置:

  1. 打开STM32CubeMX,选择STM32F103C8T6芯片
  2. 配置系统时钟为72MHz
  3. 配置GPIO引脚(根据连接方式选择):

并口模式引脚配置:

复制代码
RS  -> PB0
RW  -> PB1
E   -> PB2
D0  -> PA0
D1  -> PA1
D2  -> PA2
D3  -> PA3
D4  -> PA4
D5  -> PA5
D6  -> PA6
D7  -> PA7
  1. 生成代码,打开工程

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 编译下载

  1. 在STM32CubeIDE中编译项目
  2. 连接ST-Link下载器
  3. 点击下载按钮烧录程序

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背光亮,但无字符显示。

原因分析:

  1. 对比度调节不当(VO引脚电压不合适)
  2. 初始化序列错误
  3. 引脚连接错误
  4. 时序不满足要求

解决方案:

方案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. 数据线连接不稳定
  2. 时序过快
  3. 指令发送错误

解决方案:

方案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:自定义字符显示为空白

现象:

自定义字符位置显示为空白或方块。

原因分析:

  1. CGRAM地址设置错误
  2. 字符数据格式错误
  3. 调用自定义字符时代码错误

解决方案:

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 学习资源

官方文档:

相关推荐
可乐鸡翅好好吃2 小时前
从四个 ble_evt_handler 看 Nordic BLE 架构:模块化解耦与优先级控制
单片机·嵌入式硬件
匿名了匿名了3 小时前
直流无刷与直流有刷电机
stm32·嵌入式硬件·mcu
水果里面有苹果3 小时前
26-MT41J64M16LA-187E 美光科技DDR3 SDRAM 1Gb
嵌入式硬件
三佛科技-187366133973 小时前
LPK8717省外围无需启动电阻,12W自供电PSR控制芯片恒压恒流方案
单片机·嵌入式硬件
陶瓷好烦4 小时前
智能编码助手:VSCode+Keil+Kilo Code打造自然语言编程环境
vscode·stm32·单片机
cmpxr_4 小时前
【单片机】51单片机的晶振选择
单片机·嵌入式硬件·51单片机
松小白song4 小时前
如何在定时器中断中实现PWM波形切换?
stm32·单片机·嵌入式硬件
asjodnobfy5 小时前
生产过程中的电容损坏分析
嵌入式硬件·硬件工程
be to FPGAer5 小时前
设计约束命令和SDC命令
单片机·嵌入式硬件