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 时,常常会遇到“背光亮但无字符显示”的问题,很多初学者误以为是程序错误,但实际上往往是以下两个硬件细节引起的:

  1. 供电电压问题
    虽然 PCF8574 和 LCD1602 都标称支持 3.3V 和 5V 工作电压,但在 3.3V 供电场景下,LCD 显示屏的对比度普遍较低,即使通过调节电位器已达到最大对比度,也可能难以识别字符,甚至造成“白屏”现象。建议在条件允许时优先使用 5V 供电,以确保显示清晰可靠。

  2. 对比度调节问题
    在程序尚未输出有效内容前,LCD1602 默认屏幕为全白底,这很容易误导开发者认为程序未运行成功。实际上,若未正确调节 V0 对比度引脚(通常接滑动变阻器中间抽头),即使字符已经输出,也会因显示过暗或过亮而无法看清。建议上电后优先检查对比度设置是否处于合适区间,以排除硬件视觉误判。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐