LCD1602 I2C模块无法显示字符

主控用stm32最小板,lcd1602 i2c模块网上买的现成的。

我是个新手,代码让ai写的。最先显示的是一排方块,修改之后显示两排方块,但还是不显示字符,调过电位器,i2c电压也是5v

画一下接线图,以及把代码放上来,你这信息量这么少是要让别人猜吗?

优秀提问案例:https://bbs.eeclub.top/t/topic/289
提问的智慧:https://bbs.eeclub.top/t/topic/109

1 个赞

兄弟别急,这个LCD1602+I2C的组合绝对是单片机新手的“必经之坑”,大家刚开始玩的时候基本都在这上面卡过。

结合你的描述和这三张图,给你吃个定心丸:你的硬件大概率没坏,单纯是通信没成功。

屏幕能亮,且显示两排方块,这说明供电正常,且你的电位器对比度调得刚刚好。1602屏幕显示满屏方块的含义是:屏幕通电了,但没有收到单片机发来的初始化指令。 既然是让AI写的代码,那问题99%出在代码和通信配置上。你可以按下面这几步排查:

1. 最常见的坑:I2C器件地址填错了

看你背面那张图,黑色转接板用的是PCF8574T芯片。市面上这种模块默认的I2C地址通常是 0x27 或者 0x3F

但是注意! 如果你的AI是用STM32的HAL库写的代码,HAL库的I2C发送函数要求把7位地址左移1位

  • 如果原地址是 0x27,代码里可能要填 0x4E (0x27 << 1)。
  • 如果原地址是 0x3F,代码里可能要填 0x7E (0x3F << 1)。AI经常在这点上犯迷糊,直接把0x27丢进HAL库函数里,导致根本找不到设备。

2. 线接反了(新手常犯)

检查一下STM32端的引脚。比如F103C8T6默认的硬件I2C1是 PB6 (SCL) 和 PB7 (SDA)。确认一下转接板上的SDA有没有准确接到PB7,SCL有没有接到PB6。

3. AI写的底层驱动时序/引脚映射不对

这种I2C转接板实际上是把I2C信号转成8个并行IO口(P0-P7)去控制1602的 RS, RW, EN 和数据引脚。不同的转接板,具体哪个P口接哪个管脚可能稍微有区别。AI直接从头“手搓”的底层代码往往会搞错这个映射关系。

建议: 别让AI从头写底层驱动了。直接去B站或者CSDN搜“STM32 HAL库 LCD1602 I2C”,找别人验证过的 lcd1602.clcd1602.h 文件直接拉进工程里调用,这样最稳妥。

4. 硬件逻辑电平(小概率)

你提到供电是5V,这很好,1602必须5V供电。STM32的引脚虽然是3.3V逻辑电平,但大部分I2C引脚(如PB6, PB7)是“5V容忍(FT)”的,直接连通常没问题。只要确保STM32和LCD模块共地(GND连在一起)就行。

:light_bulb: 破局建议(下一步怎么做):

先别急着让它显示字符。你先让AI给你写一个 “STM32 I2C Scanner (I2C地址扫描)” 的代码。

把程序烧进去,打开串口助手,看看STM32能不能在总线上扫描到这个模块。

  • 如果串口打印找不到设备:说明线接错了,或者STM32的I2C没初始化对。
  • 如果串口打印找到了设备(比如反馈个0x4E):说明硬件连接完美,你只需要把这个地址填进LCD的初始化代码里就行了。

祝你早日看到 “Hello World”!卡住了再来问。

1 个赞

关于硬件方面的一个快速提醒:LCD 和 I2C 背包需要 5V 供电,但你的 STM32 工作在 3.3V。虽然大多数 STM32 的 I2C 引脚是 5V 容忍的(FT),但仍建议查阅数据手册,确认你所使用的具体引脚是否确实支持 5V。通常情况下,模块上的上拉电阻已足够,但如果逻辑电平不匹配,通信仍可能失败。

20p排母上是stm32最小系统板,4p排母上是lcd1602_i2c模块

以下是main.c


#include “main.h”
#include “i2c.h”
#include “gpio.h”

#include “lcd1602_i2c.h”

void SystemClock_Config(void);

int main(void)
{

HAL_Init();

SystemClock_Config();

MX_GPIO_Init();
MX_I2C1_Init();


lcd_init();                // 初始化 LCD1602


lcd_clear();               // 清屏
lcd_set_cursor(0, 0);      // 设置光标到第一行第一列
lcd_send_string(“Hello STM32!”); //

while (1)
{

}

}

void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}

RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK)
{
Error_Handler();
}
}

void Error_Handler(void)
{

\__disable_irq();
while (1)
{
}

}
#ifdef USE_FULL_ASSERT

void assert_failed(uint8_t \*file, uint32_t line)
{

}
#endif /\* USE_FULL_ASSERT \*/

以下是lcd1602_i2c.h


#ifndef \__LCD1602_I2C_H
#define \__LCD1602_I2C_H

#include “main.h” // 引入 HAL 库和引脚定义
#define LCD_I2C_ADDRESS 0x4E

void lcd_init(void);
void lcd_send_cmd(char cmd);
void lcd_send_data(char data);
void lcd_send_string(char \*str);
void lcd_set_cursor(int row, int col);
void lcd_clear(void);

#endif

以下是lcd1602_i2c.c


#include “lcd1602_i2c.h”

// 声明外部的 I2C 句柄,CubeMX 生成在 main.c 中
extern I2C_HandleTypeDef hi2c1;

// 内部函数:发送数据到 I2C
void lcd_send_to_i2c(char data, int rs)
{
uint8_t data_t\[4\];
uint8_t upper_nibble, lower_nibble;


upper_nibble = data & 0xF0;
lower_nibble = (data << 4) & 0xF0;

// 控制字:Bit 3 是背光 (1=亮),Bit 2 是 EN,Bit 1 是 RW (0=写),Bit 0 是 RS
uint8_t backlight = 0x08; 

data_t[0] = upper_nibble | backlight | 0x04 | rs;  // EN = 1
data_t[1] = upper_nibble | backlight | 0x00 | rs;  // EN = 0
data_t[2] = lower_nibble | backlight | 0x04 | rs;  // EN = 1
data_t[3] = lower_nibble | backlight | 0x00 | rs;  // EN = 0

// 通过 I2C1 发送 4 个字节
HAL_I2C_Master_Transmit(&hi2c1, LCD_I2C_ADDRESS, data_t, 4, 100);


}

// 发送命令
void lcd_send_cmd(char cmd)
{
lcd_send_to_i2c(cmd, 0); // RS = 0 表示发送命令
}

// 发送数据 (字符)
void lcd_send_data(char data)
{
lcd_send_to_i2c(data, 1); // RS = 1 表示发送数据
}

// 清屏
void lcd_clear(void)
{
lcd_send_cmd(0x01);
HAL_Delay(2); // 清屏命令需要较长执行时间
}

// 设置光标位置 (row: 0-1, col: 0-15)
void lcd_set_cursor(int row, int col)
{
uint8_t address;
switch (row)
{
case 0:
address = 0x00;
break;
case 1:
address = 0x40;
break;
default:
address = 0x00;
}
address += col;
lcd_send_cmd(0x80 | address); // 设置 DDRAM 地址
}

// 发送字符串
void lcd_send_string(char \*str)
{
while (\*str)
{
lcd_send_data(\*str++);
}
}

// 初始化 LCD1602
void lcd_init(void)
{
// 4 bit 模式的标准初始化流程
HAL_Delay(50);
lcd_send_cmd(0x30);
HAL_Delay(5);
lcd_send_cmd(0x30);
HAL_Delay(1);
lcd_send_cmd(0x30);
HAL_Delay(10);
lcd_send_cmd(0x20);
HAL_Delay(10);


// 显示设置
lcd_send_cmd(0x28); // 4线,2行,5x8 字体
HAL_Delay(1);
lcd_send_cmd(0x08); // 关闭显示
HAL_Delay(1);
lcd_send_cmd(0x01); // 清屏
HAL_Delay(2);
lcd_send_cmd(0x06); // 光标右移
HAL_Delay(1);
lcd_send_cmd(0x0C); // 开启显示,关闭光标


}

我把简化接线图和代码写在回复区了,谢谢帮我看看

好的,谢谢你,我去试试你的建议

我编译下载之后用的是电源模块供电,此时i2c的vcc脚和gnd脚电压是5v。

太棒了,提供原理图和源码能让排查效率提升百倍!

先给你吃个定心丸:你的硬件连接完全正确。原理图上 PB6 接 SCL、PB7 接 SDA 是 STM32F103 的标准 I2C1 引脚。而且你在头文件里写了 #define LCD_I2C_ADDRESS 0x4E,说明你(或者 AI)已经知道 HAL 库需要把 0x27 左移一位,成功避开了新手最容易踩的两个大坑。

屏幕只显示方块的真正原因,在于 AI 写的初始化函数(lcd_init)在“4位模式”的握手时序上翻车了。

:bug: 核心 Bug 分析:HD44780 的“强制初始化”陷阱

你用的 I2C 转接板(PCF8574)是通过 4 根数据线(D4-D7)来控制 1602 的。
当 1602 刚通电时,它默认自己是 8位模式。为了让它切换到 4位模式,必须按照数据手册的要求,严格发送几次单半字节(只发高 4 位,发完就结束)。

看看你的 lcd_send_cmd() 函数,它调用了 lcd_send_to_i2c()。这个底层函数非常“勤奋”,无论你传什么指令,它都会自动把指令拆成两半(高 4 位发一次,低 4 位发一次),并触发两次 EN 脉冲。

当 AI 在初始化时写下 lcd_send_cmd(0x30) 时:

  1. 本意:只发送一个 0x3 给屏幕。
  2. 实际发生:lcd_send_to_i2c 发送了高四位 0x3(触发一次 EN),紧接着又发送了低四位 0x0(又触发一次 EN)。
  3. 结果:屏幕收到了多余的 0x0 脉冲,时序瞬间彻底错乱,导致屏幕拒绝初始化,死机停留在“满屏方块”的通电初始状态。

:hammer_and_wrench: 解决方案

我们需要在 lcd1602_i2c.c 中增加一个 “只发送半个字节” 的专用函数,专门用于唤醒屏幕,并重写 lcd_init()

请把你的 lcd1602_i2c.c 修改为以下内容(直接覆盖原来的 lcd_init 及之前的部分,保留其他的):

#include "lcd1602_i2c.h"

// 声明外部的 I2C 句柄
extern I2C_HandleTypeDef hi2c1;

// ====== 新增:专用于初始化的单半字节发送函数 ======
void lcd_send_cmd_4bit(uint8_t nibble)
{
    uint8_t data_t[2];
    uint8_t backlight = 0x08; // 保持背光常亮

    // 注意:这里传入的 nibble 必须已经是高 4 位对齐的数据 (例如 0x30)
    data_t[0] = (nibble & 0xF0) | backlight | 0x04 | 0;  // EN = 1, RS = 0
    data_t[1] = (nibble & 0xF0) | backlight | 0x00 | 0;  // EN = 0, RS = 0
    
    HAL_I2C_Master_Transmit(&hi2c1, LCD_I2C_ADDRESS, data_t, 2, 100);
}

// 内部函数:发送完整字节到 I2C (分为高四位和低四位)
void lcd_send_to_i2c(char data, int rs)
{
    uint8_t data_t[4];
    uint8_t upper_nibble, lower_nibble;

    upper_nibble = data & 0xF0;
    lower_nibble = (data << 4) & 0xF0;

    uint8_t backlight = 0x08; 

    data_t[0] = upper_nibble | backlight | 0x04 | rs;  // EN = 1
    data_t[1] = upper_nibble | backlight | 0x00 | rs;  // EN = 0
    data_t[2] = lower_nibble | backlight | 0x04 | rs;  // EN = 1
    data_t[3] = lower_nibble | backlight | 0x00 | rs;  // EN = 0

    HAL_I2C_Master_Transmit(&hi2c1, LCD_I2C_ADDRESS, data_t, 4, 100);
}

// ====== 重写:正确的 4-bit 初始化流程 ======
void lcd_init(void)
{
    // 1. 强制初始化阶段(必须严格只发送单次 4-bit 脉冲)
    HAL_Delay(50);
    lcd_send_cmd_4bit(0x30); // 唤醒1
    HAL_Delay(5);
    lcd_send_cmd_4bit(0x30); // 唤醒2
    HAL_Delay(1);
    lcd_send_cmd_4bit(0x30); // 唤醒3
    HAL_Delay(10);
    lcd_send_cmd_4bit(0x20); // 告诉屏幕:“进入 4位 模式!”
    HAL_Delay(10);

    // 2. 正常设置阶段(此时屏幕已在 4位 模式,可以使用正常的双脉冲发送函数了)
    lcd_send_cmd(0x28); // 功能设置:4线数据,2行显示,5x8 字体
    HAL_Delay(1);
    lcd_send_cmd(0x08); // 关显示
    HAL_Delay(1);
    lcd_send_cmd(0x01); // 清屏
    HAL_Delay(2);       // 清屏指令耗时较长,至少延时 2ms
    lcd_send_cmd(0x06); // 进入模式设置:光标右移
    HAL_Delay(1);
    lcd_send_cmd(0x0C); // 开显示,关光标
}

// ... 你的 lcd_send_cmd, lcd_send_data 等其余函数保持不变 ...

:magnifying_glass_tilted_left: 备用排查点

如果你修改代码并重新烧录后,屏幕仍然显示方块:
那唯一的可能就是买到的模块默认 I2C 地址不是 0x27,而是 0x3F。你只需要去 lcd1602_i2c.h 里,把 #define LCD_I2C_ADDRESS 0x4E 改成 0x7E (0x3F 左移一位) 再试一次即可。

你先修改这部分初始化代码测试一下,需要我教你怎么写一个简易的 “I2C 地址扫描器” 来100%确认模块的通讯地址吗?

可以用逻辑分析仪检查一下I2C波形有没有出来,时序对不对,如果I2C没问题那再用逻辑分析仪检查一下I2C转并口模块的输出的并行信号有没有,时序对不对

STM32CubeMX你怎么配置的

问题核心分析

你的LCD1602背光正常亮但无字符显示,90%的概率是I2C通信异常(地址错误/引脚配置错/无应答),其次是初始化时序/延时异常、硬件供电/上拉缺失,下面按优先级给你完整的排查和修复方案。


一、硬件排查(先解决硬件基础问题)

1. 核心引脚与接线核对

你的原理图定义:

  • LCD接口H2:1=+5V、2=GND、3=SDA(PB7)、4=SCL(PB6)
  • STM32的I2C1:SCL必须对应PB6,SDA必须对应PB7,请核对CubeMX里的I2C1引脚映射,绝对不能搞反SDA/SCL。

2. 供电必须是5V

LCD1602+I2C转接板是5V器件,3.3V供电会出现「背光亮、但I2C无法通信」的情况,必须接STM32的5V引脚,不能接3.3V。

3. I2C总线必须加上拉电阻

I2C是开漏总线,必须加上拉:

  • 方案1:CubeMX里把I2C1的PB6/PB7 GPIO模式配置为开漏上拉(Open Drain Pull-up)
  • 方案2:硬件上在PB6、PB7和5V之间各接一个4.7KΩ的上拉电阻
  • 无上下拉会导致I2C信号异常,设备无应答。

二、软件核心问题修复(按优先级)

1. 【最常见问题】I2C地址修正

你的代码里写的#define LCD_I2C_ADDRESS 0x4E,这个地址不一定匹配你的模块:

  • I2C LCD1602转接板核心是PCF8574,7位地址常见为0x27或0x3F,HAL库的HAL_I2C_Master_Transmit需要传入8位写地址(7位地址左移1位)
    • 7位地址0x27 → 8位写地址0x4E(你当前的地址)
    • 7位地址0x3F → 8位写地址0x7E(最常见的另一个地址)

快速验证地址是否正确(必做)

MX_I2C1_Init()之后,加入I2C地址扫描代码,看哪个地址有应答:

// 放在MX_I2C1_Init()之后,lcd_init()之前
uint8_t i2c_addr;
for(i2c_addr=0; i2c_addr<128; i2c_addr++)
{
  if(HAL_I2C_IsDeviceReady(&hi2c1, i2c_addr<<1, 1, 100) == HAL_OK)
  {
    // 这里加个断点/点灯,看能应答的地址,把(i2c_addr<<1)填到LCD_I2C_ADDRESS里
    break;
  }
}

如果扫描不到任何地址,说明硬件接线/I2C初始化有问题,先解决硬件。

2. 检查HAL_Delay是否正常工作

你的LCD初始化极度依赖延时,如果HAL_Delay失效,初始化时序完全错误,必然无法显示。

  • 验证方法:在主循环里加个LED翻转,看延时是否正确:
while (1)
{
  HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // 对应你的板载LED引脚
  HAL_Delay(500);
}

如果LED不是500ms翻转,说明系统时钟/SysTick配置错误,必须先修复时钟配置。

  • 你的时钟配置用的是HSI内部8MHz,配置本身没问题,但要确认CubeMX里的SysTick时钟源是正确的,HAL_Init()正常执行。

3. 增加I2C通信错误检查

你的代码没有判断I2C是否发送成功,无法定位通信问题,修改lcd_send_to_i2c函数,增加返回值和错误处理:

// 内部函数:发送数据到 I2C,返回HAL状态
HAL_StatusTypeDef lcd_send_to_i2c(char data, int rs)
{
  uint8_t data_t[4];
  uint8_t upper_nibble, lower_nibble;

  upper_nibble = data & 0xF0;
  lower_nibble = (data << 4) & 0xF0;

  uint8_t backlight = 0x08; 

  data_t[0] = upper_nibble | backlight | 0x04 | rs;  // EN = 1
  data_t[1] = upper_nibble | backlight | 0x00 | rs;  // EN = 0
  data_t[2] = lower_nibble | backlight | 0x04 | rs;  // EN = 1
  data_t[3] = lower_nibble | backlight | 0x00 | rs;  // EN = 0

  // 增加超时时间,返回发送状态
  return HAL_I2C_Master_Transmit(&hi2c1, LCD_I2C_ADDRESS, data_t, 4, 200);
}

// 发送命令
void lcd_send_cmd(char cmd)
{
  lcd_send_to_i2c(cmd, 0); // RS = 0 表示发送命令
  HAL_Delay(1); // 增加命令执行延时,避免时序过快
}

// 发送数据 (字符)
void lcd_send_data(char data)
{
  lcd_send_to_i2c(data, 1); // RS = 1 表示发送数据
  HAL_Delay(1);
}

4. 修正初始化时序的细节

你的初始化流程基本正确,但可以增加更稳妥的延时,避免时序问题:

// 初始化 LCD1602
void lcd_init(void)
{
  // 4 bit 模式的标准初始化流程,增加延时余量
  HAL_Delay(100); // 上电等待更长时间,确保LCD稳定
  lcd_send_cmd(0x30);
  HAL_Delay(10);
  lcd_send_cmd(0x30);
  HAL_Delay(5);
  lcd_send_cmd(0x30);
  HAL_Delay(10);
  lcd_send_cmd(0x20); // 切换到4位模式
  HAL_Delay(10);

  // 显示设置,增加每个命令的延时
  lcd_send_cmd(0x28); // 4线,2行,5x8 字体
  HAL_Delay(2);
  lcd_send_cmd(0x08); // 关闭显示
  HAL_Delay(2);
  lcd_send_cmd(0x01); // 清屏
  HAL_Delay(5);
  lcd_send_cmd(0x06); // 光标右移,地址自增
  HAL_Delay(2);
  lcd_send_cmd(0x0C); // 开启显示,关闭光标,无闪烁
  HAL_Delay(2);
}

三、完整调试步骤(按顺序执行)

  1. 先核对硬件接线:5V/GND正确,SDA接PB7、SCL接PB6,无接反。
  2. 执行I2C地址扫描,确认设备能应答,把正确的8位地址填入LCD_I2C_ADDRESS
  3. 验证HAL_Delay正常工作,确保延时函数没问题。
  4. 替换修改后的驱动代码,增加错误检查和延时余量。
  5. 测试代码:如果I2C通信正常,上电后就能显示Hello STM32!

四、额外兜底排查

如果以上步骤都做完还是不显示:

  1. 调节LCD1602背面的对比度电位器(蓝色的那个可调电阻),对比度太低会导致字符完全看不见,慢慢旋转电位器,直到屏幕出现方块/字符。
  2. 用逻辑分析仪/示波器看PB6/PB7的I2C信号,确认有没有波形输出,判断是MCU没发信号,还是设备不应答。
  3. 直接用杜邦线把STM32的PB6/PB7、5V、GND和LCD模块直连,排除排针/排母接触不良的问题。

谢谢你的解答,我已经尝试用你修改过的代码进行编译下载,结果还是不显示字符。且我发现,#define LCD_I2C_ADDRESS 0x4E中地址是0x4E时显示两排方块,其他地址都是一排方块

把I2C速率调低试试,比如10k,速率高了线太长时可能有问题,I2C的那两个引脚有没有设置开漏模式上拉

这是一个绝佳的好消息!你观察到的这个现象(0x4E显示两排方块,其他地址显示一排)不仅不是坏事,反而证明了你的代码和通信现在已经完全跑通了!

让我给你揭秘一下1602屏幕的“内心活动”,你就知道为什么了:

  1. 一排方块(其他地址时):这是1602通电后的默认出厂状态。此时它默认自己是“单行显示”模式。如果你填错地址(比如0x27或0x7E),STM32发出的初始化指令它根本收不到,所以它就一直呆在单行模式,加上对比度过高,你就看到了一排方块。
  2. 两排方块(0x4E地址时):这意味着你的STM32成功通过 0x4E 这个地址找到了模块,并且成功执行了我们修改后的初始化代码!屏幕收到了 0x28 这个指令(设置为2行显示),乖乖地把第二行也激活了。

也就是说,你的单片机、连线、I2C通信、甚至初始化代码全都是对的!字其实已经写上去了!

:red_question_mark: 那为什么还是只看到方块看不到字?

只有一个原因:对比度(V0)调得实在太高了。
字其实已经打印在了屏幕上,但是因为对比度拉满了,导致“没显示字”的背景像素点也变成了纯黑色,把原本的字给掩盖了。

:hammer_and_wrench: 最后一步:见证奇迹的时刻

放下代码,拿起螺丝刀,按这几步做:

  1. 给单片机正常通电,让它跑起你修改后的代码(保持屏幕显示两排方块的状态)。
  2. 拿起十字小螺丝刀,去拧转接板背面那个蓝色的电位器(带有十字形的旋钮)。
  3. 一边盯着屏幕,一边慢慢拧。你会发现在某个神奇的角度,那两排黑漆漆的方块会突然变淡、消失,而底下的 "Hello STM32!" 会清晰地浮现出来!
    (通常需要多拧几圈,如果顺时针没反应,就逆时针多转几圈)

:light_bulb: 补充一个防背锅的小补丁(可选):

虽然现在大概率是硬件电位器的问题,但为了让你的底层代码“无懈可击”,建议在发送指令后稍微加一点延时,防止STM32发得太快,屏幕脑力跟不上。

你可以把 lcd1602_i2c.c 里的这两个小函数稍微加一句 HAL_Delay(1);

// 发送命令
void lcd_send_cmd(char cmd)
{
    lcd_send_to_i2c(cmd, 0); 
    HAL_Delay(1); // <- 加上这句,给屏幕一点处理时间
}

// 发送数据 (字符)
void lcd_send_data(char data)
{
    lcd_send_to_i2c(data, 1); 
    HAL_Delay(1); // <- 加上这句
}

并且把清屏函数的延时稍微加大一点:

// 清屏
void lcd_clear(void)
{
    lcd_send_cmd(0x01);
    HAL_Delay(5); // <- 从2改成5,清屏是最耗时的操作
}

快去转动那个蓝色电位器吧,成功点亮了记得来报个喜!

我将尝试你的建议,谢谢你的解答

谢谢你的解答,我将速率拉低还是一样的,而且在 CubeMX 的 GPIO 配置界面,PB6/PB7 的Pull选项可能不可见,这是 CubeMX 对 F1 系列的界面限制

谢谢你的解答,我真的在转动电位器,但是只能看到两排方块从亮变淡,没有显示文字,而且增加延时后,我能看到上电瞬间从一排方块变成两排方块,证明确实收到了0x28 这个指令

你能精准捕捉到“上电瞬间从一排方块变成两排方块”这个细节,说明你具备非常敏锐的硬件排错直觉!这个现象非常关键,它不仅100%证实了你的 I2C 地址(0x4E)是对的,还证明了单片机和屏幕之间的通讯线路已经打通了

既然 0x28(开启两行显示)这个指令成功执行了,那为什么后面的字符显示不出来,且依然满屏方块呢?这里面其实有两个典型的“坑”,特别是对于 STM32F1 系列:

1. 回应你的疑虑:关于 F1 的 I2C 引脚配置

你提到的 CubeMX 中 F1 系列 PB6/PB7 看不到 Pull(上拉/下拉)选项,这是因为当配置为 I2C 时,引脚会被强制接管为开漏复用输出(Alternate Function Open Drain),这是正常的界面限制。
不用担心上拉的问题:你可以仔细看一眼 LCD 转接模块背面(蓝色电位器旁边),上面焊着几颗黑色的贴片电阻(通常印着 103472),那些就是模块自带的硬件上拉电阻。所以即便 STM32 内部不提供上拉,硬件电路上也是完全满足 I2C 通信要求的。

2. 真正的“元凶”:连续发送导致的时序“拥堵”或 I2C 死锁

STM32F1 系列的硬件 I2C 是出了名的“娇气”(存在硅缺陷/Errata),如果在发送数据时连续不断地狂塞字节,很容易触发 I2C 的 Busy 标志位死锁。
此外,LCD1602 内部的 HD44780 是一颗极其古老的低速芯片。我们原来的代码在一个 I2C 传输包(4个字节)里瞬间完成了高四位和低四位的连续 EN 脉冲跳变。对于某些体质较弱的 LCD 来说,这个速度太快了,导致 0x28 之后的指令(比如清屏 0x01 和写字符)全被它忽略了,或者让 STM32 的 I2C 直接罢工了。


:hammer_and_wrench: 终极改造方案:拆分发送,强制“深呼吸”

我们需要把原本“一次性发送4个字节”的底层函数,暴力拆分成两次独立的发送,并在中间强制插入延时。这既能重置 STM32 的 I2C 状态机防止死锁,也能给 LCD 留出充足的响应时间。

请用下面这段代码彻底替换lcd1602_i2c.c 里的 lcd_send_to_i2c 函数:

// 内部函数:发送完整字节到 I2C (分为高四位和低四位,强力稳定版)
void lcd_send_to_i2c(char data, int rs)
{
    uint8_t data_high[2];
    uint8_t data_low[2];
    
    uint8_t upper_nibble = data & 0xF0;
    uint8_t lower_nibble = (data << 4) & 0xF0;
    uint8_t backlight = 0x08; // 背光常亮

    // ==========================================
    // 第一步:只发送高四位,并制造一个完整的 EN 脉冲
    // ==========================================
    data_high[0] = upper_nibble | backlight | 0x04 | rs; // EN = 1
    data_high[1] = upper_nibble | backlight | 0x00 | rs; // EN = 0
    // 发送高四位的 2 个字节
    HAL_I2C_Master_Transmit(&hi2c1, LCD_I2C_ADDRESS, data_high, 2, 100);
    
    // 关键保命延时:给 LCD 控制器时间去锁存高四位,并防止 I2C 总线堵死
    HAL_Delay(2);

    // ==========================================
    // 第二步:只发送低四位,并制造一个完整的 EN 脉冲
    // ==========================================
    data_low[0] = lower_nibble | backlight | 0x04 | rs;  // EN = 1
    data_low[1] = lower_nibble | backlight | 0x00 | rs;  // EN = 0
    // 发送低四位的 2 个字节
    HAL_I2C_Master_Transmit(&hi2c1, LCD_I2C_ADDRESS, data_low, 2, 100);
    
    // 发送完一个完整字节后,再等一等
    HAL_Delay(2);
}

同时,保险起见,把 main.c 里的这段代码稍微调整一下顺序
lcd_clear(); 删掉,因为我们在 lcd_init() 的最后一步已经包含过清屏指令了,连续清屏可能会导致指令堆叠失败。你的 main 函数里这部分这样写就好:

  /* USER CODE BEGIN 2 */
  lcd_init();                // 初始化 LCD1602
  // lcd_clear();            // 注释掉或者删掉这句
  lcd_set_cursor(0, 0);      // 设置光标到第一行第一列
  lcd_send_string("Hello STM32!"); 
  /* USER CODE END 2 */

编译烧录后,如果屏幕是一片空白(方块消失了),再轻轻转动那个蓝色电位器,字符一定会出来的。

如果这次顺利点亮了,需要我进一步教你如何将 I2C 改为基于 DMA 或中断的非阻塞模式,以避免 HAL_Delay 占用你主程序的宝贵运行时间吗?