STM32CubeMX + HAL 库:用PCF8574芯片实现 IIC 总线协议驱动 LCD1602 显示
本文介绍了基于STM32微控制器通过I²C总线驱动LCD1602显示模块的实现方案。传统并行接口方式需占用12个I/O引脚,而采用PCF8574扩展芯片可将接口简化为2线I²C通信(SCL/SDA),大幅节省引脚资源并提高系统稳定性。文章详细说明了硬件连接方案、STM32CubeMX配置步骤(包括I²C设置)、Keil工程构建方法,并提供了完整的驱动代码实现,包括初始化、指令/数据写入、光标控制、
1. 概述
1.1 实现目的
本实验旨在基于 STM32 微控制器,实践其硬件 I²C 通信接口的应用能力,通过接入 PCF8574 I/O 扩展芯片,实现对 LCD1602 显示模块的高效控制。通过 I²C 总线方式驱动 LCD1602,不仅简化了硬件连接,还显著提升了系统资源利用率与整体稳定性。
传统方案中,LCD1602 使用并口通信方式,需要占用多达 12 根 MCU I/O 引脚(8 位数据线 + 4 位控制线)。此类设计在资源受限的项目中极为不便,既浪费了宝贵的 MCU 引脚资源,又带来了复杂的布线和更高的连线错误风险。
为解决上述问题,本实验采用 PCF8574 芯片作为 LCD1602 与 STM32 之间的桥梁,将原始并行接口转化为 I²C 串行通信接口。LCD1602 通过PCF8574 芯片实现 I²C 控制,仅需两根信号线(SCL 和 SDA)即可完成全部操作。相比传统接法,该方案大幅降低了引脚资源占用,布线清晰简洁,具备更好的扩展性和工程可维护性。
通过本实验,学习者不仅可以掌握 LCD1602 的一种高效I²C 驱动方法,还能加深对 STM32 硬件 I²C 通信机制的理解,为后续项目开发和系统集成提供切实可行的参考方案。
1.2 PCF8574介绍
PCF8574 是基于 I²C(Inter-Integrated Circuit)总线通信协议。它能通过极少的引脚占用(仅需 SDA、SCL 两条信号线)为微控制器扩展出 8 路通用 I/O 端口,广泛应用于 LCD 显示控制、按键扫描、LED 控制、数据采集等场合。
| 地址格式 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
|---|---|---|---|---|---|---|---|---|
| 固定位(PCF8574) | 0 | 1 | 0 | 0 | A2 | A1 | A0 | 读/写控制位 |
| I²C 地址(7 位) | A2 | A1 | A0 | 十六进制 |
|---|---|---|---|---|
| 0B 0100 A2 A1 A0 x | 0 | 0 | 0 | 写地址 0x40 / 读地址0x41 |
| 0B 0100 A2 A1 A0 x | 1 | 1 | 1 | 写地址 0x4E / 读地址0x4F |
注意:本文采用的是最常见的T系列,即DIP/SOIC封装,其地址高四位如上表所示为0B 0100
如果PCF8574芯片是AT系列,即TSSOP封装的,其地址高四位是0B 0111,对应读写地址需要随之变更。
1.3 LCD1602介绍


1.4 转接板模块介绍
该转接板基于 PCF8574 芯片,采用最小系统封装设计,实现模块化、即插即用,便于快速集成与工程部署。模块默认将地址引脚 A0~A2 拉高,从而对应的 I²C 通信地址为 0x4E(写) 和 0x4F(读)。若与系统中其他 I²C 设备地址冲突,可通过焊接 0Ω 电阻或使用焊锡短接 A0~A2 的地址配置焊盘,灵活修改地址配置,满足多模块并行接入需求,具有良好的兼容性和可扩展性,适合嵌入式系统和工业级应用。

因为PCF8574 芯片只能扩展出8位I/O口,所以和LCD1602连接时,采用了4 位模式通信,需要通过两次 I2C 写入完成一次字节写入。

| PCF8574 引脚(Pn) | LCD1602 引脚功能 | LCD1602 引脚名 |
|---|---|---|
| P0 | 数据/指令 选择,1为数据,0为指令 | RS |
| P1 | 读/写 选择, 1为读,0为写 | RW |
| P2 | 使能,1为数据有效,下降沿执行命令 | CS / E |
| P3 | 背光控制,高电平开启 | LDE- A |
| P4 | 数据输入/输出 | DB4 |
| P5 | 数据输入/输出 | DB5 |
| P6 | 数据输入/输出 | DB6 |
| P7 | 数据输入/输出 | DB7 |
2. STM32CubeMX配置
打开软件后,点击ACCESS TO MCU SELECTOR 需要等待几分钟,进行相关资源下载和加载,耐心等待。如果建立过工程,可以去工程里拷贝一个ioc文件,这样就不用每次都新建配置了。

搜索对应的芯片型号,并在右侧点击对应芯片进入设置界面

2.1 SYS设置

2.2 RCC设置


2.3 IIC设置

2.4 project生成



3. keil MDK配置
3.1 debug设置
作者用的是J-Link调试下载器,如果用STLink选择对应选项即可

点击选项旁边的settings 进入下面的设置界面

3.2 新增驱动文件
在core目录下新建hardware目录,并在新建目录下新建两个文件 lcd1602.h 和 lcd1602.c,此步骤不一定非要在Keil上操作,在工程目录里操作一样适用,最终目的就是在对应目录下新建两个文件即可,注意文件后缀名要一致。很多读者因为计算机设置了隐藏后缀名,导致新建的文件后缀名并非.h和.c

3.3 构建工程目录
上一步新建了一个hardware文件夹,需要在工程中把这个目录添加上去,这样编译的时候才会识别此目录。
3.4 构建工程文件
同理,新建的驱动文件,也需要添加到工程中去,才会被编译软件识别并编译,添加.c文件即可

配置完后,关掉Keil软件,这时才会生成对应的配置文件,再通过VSCode对配置好的工程进行打开并编码。
4. VSCode编码
4.1 lcd1602.h头文件
#ifndef __LCD1602_H__
#define __LCD1602_H__
#include "stm32f1xx_hal.h" // 根据您的STM32系列修改
#include <string.h>
// I2C配置
#define PCF8574_I2C hi2c1 // 修改为您的I2C句柄
#define PCF8574_ADDR 0x27 // 常见地址0x27或0x3F,根据您的硬件调整
// PCF8574引脚定义(与您的对应关系一致)
#define RS_PIN 0x01 // P0 - 数据/指令选择
#define RW_PIN 0x02 // P1 - 读/写选择
#define EN_PIN 0x04 // P2 - 使能信号
#define BL_PIN 0x08 // P3 - 背光控制
// 数据引脚(DB4-DB7)
#define DB4_PIN 0x10 // P4
#define DB5_PIN 0x20 // P5
#define DB6_PIN 0x40 // P6
#define DB7_PIN 0x80 // P7
// 快速操作宏
#define LCD_BACKLIGHT_ON() HAL_I2C_Master_Transmit(&PCF8574_I2C, PCF8574_ADDR << 1, (uint8_t[]){BL_PIN}, 1, 10)
#define LCD_BACKLIGHT_OFF() HAL_I2C_Master_Transmit(&PCF8574_I2C, PCF8574_ADDR << 1, (uint8_t[]){0}, 1, 10)
// 函数声明
void LCD1602_Init(I2C_HandleTypeDef *hi2c);
void LCD1602_WriteCmd(uint8_t cmd);
void LCD1602_WriteData(uint8_t data);
void LCD1602_SetCursor(uint8_t row, uint8_t col);
void LCD1602_Print(char *str);
void LCD1602_Clear(void);
void LCD1602_Backlight(uint8_t state);
#endif
4.2 lcd1602.c
#include "lcd1602.h"
static I2C_HandleTypeDef *_hi2c; // 内部使用的I2C句柄
static uint8_t lcd_output_state = 0x00;
void LCD1602_Backlight(uint8_t state) {
if (state) {
lcd_output_state |= BL_PIN;
} else {
lcd_output_state &= ~BL_PIN;
}
HAL_I2C_Master_Transmit(_hi2c, PCF8574_ADDR << 1, &lcd_output_state, 1, 10);
}
// 内部函数:发送4位数据(高4位)
static void LCD1602_Send4Bit(uint8_t data, uint8_t rs_state) {
uint8_t output = lcd_output_state; // 加载当前背光状态(如BL_PIN)
// 设置RS引脚(0=命令,1=数据)
if (rs_state) output |= RS_PIN;
else output &= ~RS_PIN;
// 设置RW引脚为写
output &= ~RW_PIN;
// 清除数据位(P4~P7)
output &= ~(DB4_PIN | DB5_PIN | DB6_PIN | DB7_PIN);
// 设置数据位(DB4-DB7)
if (data & 0x01) output |= DB4_PIN;
if (data & 0x02) output |= DB5_PIN;
if (data & 0x04) output |= DB6_PIN;
if (data & 0x08) output |= DB7_PIN;
// 发送数据
HAL_I2C_Master_Transmit(_hi2c, PCF8574_ADDR << 1, &output, 1, 10);
// 产生使能脉冲
HAL_Delay(1);
output |= EN_PIN;
HAL_I2C_Master_Transmit(_hi2c, PCF8574_ADDR << 1, &output, 1, 10);
HAL_Delay(1);
output &= ~EN_PIN;
HAL_I2C_Master_Transmit(_hi2c, PCF8574_ADDR << 1, &output, 1, 10);
HAL_Delay(2); // 命令执行时间
}
// 内部函数:发送8位数据(分为两个4位)
static void LCD1602_Send8Bit(uint8_t data, uint8_t rs_state) {
uint8_t high_nibble = (data >> 4) & 0x0F;
uint8_t low_nibble = data & 0x0F;
// 发送高4位
LCD1602_Send4Bit(high_nibble, rs_state);
// 发送低4位
LCD1602_Send4Bit(low_nibble, rs_state);
}
void LCD1602_Init(I2C_HandleTypeDef *hi2c) {
_hi2c = hi2c;
HAL_Delay(50); // 等待 LCD 上电稳定
LCD1602_Send4Bit(0x03, 0); // 几次写 0x03 是 HD44780 初始化规范要求
HAL_Delay(5);
LCD1602_Send4Bit(0x03, 0);
HAL_Delay(5);
LCD1602_Send4Bit(0x03, 0);
HAL_Delay(5);
LCD1602_Send4Bit(0x02, 0); // 设置为 4 位模式
HAL_Delay(1);
LCD1602_WriteCmd(0x28); // Function set: 4-bit, 2 lines, 5x8 dots
LCD1602_WriteCmd(0x0C); // Display ON, Cursor OFF, Blink OFF
LCD1602_WriteCmd(0x06); // Entry mode: increase, no shift
LCD1602_WriteCmd(0x01); // Clear display
HAL_Delay(5);
}
// 写入命令
void LCD1602_WriteCmd(uint8_t cmd) {
LCD1602_Send8Bit(cmd, 0); // RS=0表示命令
}
// 写入数据
void LCD1602_WriteData(uint8_t data) {
LCD1602_Send8Bit(data, 1); // RS=1表示数据
}
// 设置光标位置
void LCD1602_SetCursor(uint8_t row, uint8_t col) {
uint8_t address;
if (row == 0)
address = 0x00 + col;
else
address = 0x40 + col;
// 设置DDRAM地址
LCD1602_WriteCmd(0x80 | address);
}
// 打印字符串
void LCD1602_Print(char *str) {
while (*str) {
LCD1602_WriteData(*str++);
}
}
// 清屏
void LCD1602_Clear(void) {
LCD1602_WriteCmd(0x01);
HAL_Delay(2); // 清屏需要较长时间
}
4.3. 主函数
/* USER CODE BEGIN Includes */
#include "lcd1602.h"
/* USER CODE END Includes */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_I2C1_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
LCD1602_Backlight(1); // 启用背光
LCD1602_Init(&hi2c1); // 初始化 LCD
LCD1602_SetCursor(0, 0);
LCD1602_Print("Hello");
LCD1602_SetCursor(1, 0);
LCD1602_Print("World");
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
LCD1602_Clear();
HAL_Delay(2000);
LCD1602_SetCursor(0, 0);
LCD1602_Print("Hello");
HAL_Delay(2000);
LCD1602_Backlight(1); // 启用背光
LCD1602_SetCursor(0, 0);
LCD1602_Print("world");
HAL_Delay(2000);
LCD1602_Backlight(0); // 启用背光
}
/* USER CODE END 3 */
}
5. 结果验证
在实际调试 LCD1602 时,常常会遇到“背光亮但无字符显示”的问题,很多初学者误以为是程序错误,但实际上往往是以下两个硬件细节引起的:
-
供电电压问题
虽然 PCF8574 和 LCD1602 都标称支持 3.3V 和 5V 工作电压,但在 3.3V 供电场景下,LCD 显示屏的对比度普遍较低,即使通过调节电位器已达到最大对比度,也可能难以识别字符,甚至造成“白屏”现象。建议在条件允许时优先使用 5V 供电,以确保显示清晰可靠。 -
对比度调节问题
在程序尚未输出有效内容前,LCD1602 默认屏幕为全白底,这很容易误导开发者认为程序未运行成功。实际上,若未正确调节 V0 对比度引脚(通常接滑动变阻器中间抽头),即使字符已经输出,也会因显示过暗或过亮而无法看清。建议上电后优先检查对比度设置是否处于合适区间,以排除硬件视觉误判。
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐



所有评论(0)