El módulo LCD1602 con I2C no muestra caracteres

Placa mínima STM32 para control principal, módulo LCD1602 con I2C comprado listo por Internet.

Soy principiante, el código lo escribió una IA. Al principio mostraba una fila de cuadros, después de modificarlo muestra dos filas de cuadros, pero aún así no muestra caracteres. He ajustado el potenciómetro y la tensión en el bus I2C también es de 5 V.

Dibuja un diagrama de conexiones y sube el código, ¿con tan poca información esperas que los demás adivinen?

Ejemplo de una buena pregunta: https://bbs.eeclub.top/t/topic/289
La sabiduría de hacer preguntas: https://bbs.eeclub.top/t/topic/109

1 me gusta

Hermano, no te preocupes, esta combinación de LCD1602 + I2C es una “piedra en el camino obligatoria” para todos los principiantes en microcontroladores. Casi todos hemos pasado por este problema al empezar.

Con base en tu descripción y las tres imágenes, te doy una buena noticia: es muy probable que tu hardware no esté dañado, simplemente la comunicación no se ha establecido correctamente.

El hecho de que la pantalla se encienda y muestre dos filas de cuadros negros indica que la alimentación eléctrica es correcta y que el potenciómetro de contraste está bien ajustado. Que aparezcan bloques completos en la pantalla 1602 significa lo siguiente: la pantalla está encendida, pero aún no ha recibido la instrucción de inicialización del microcontrolador. Dado que el código fue escrito por una IA, el problema en un 99 % de los casos estará en el código o en la configuración de comunicación. Puedes seguir estos pasos para depurarlo:

1. El error más común: dirección I2C mal configurada

En la foto de la parte trasera se ve que la placa adaptadora usa un chip PCF8574T. En el mercado, la dirección I2C predeterminada de estos módulos suele ser 0x27 o 0x3F.

¡Pero atención! Si tu código fue escrito con la biblioteca HAL para STM32, debes saber que la función de transmisión I2C en HAL requiere que la dirección de 7 bits se desplace un lugar hacia la izquierda (shift left).

  • Si la dirección original es 0x27, en el código deberías poner 0x4E (porque 0x27 << 1 = 0x4E).
  • Si la dirección original es 0x3F, en el código deberías poner 0x7E (porque 0x3F << 1 = 0x7E).

Las inteligencias artificiales suelen equivocarse aquí, colocando directamente 0x27 en la función HAL, lo cual hace imposible detectar el dispositivo.

2. Conexión de cables invertida (error común en principiantes)

Revisa los pines del lado del STM32. Por ejemplo, en el F103C8T6, el puerto I2C1 por defecto utiliza PB6 (SCL) y PB7 (SDA). Asegúrate de que el SDA del módulo esté conectado correctamente a PB7 y el SCL a PB6.

3. Código generado por IA con errores en la secuencia o asignación de pines

Esta placa adaptadora convierte en realidad las señales I2C en 8 salidas paralelas (P0-P7) para controlar las señales RS, RW, EN y los pines de datos del LCD1602. Dependiendo del modelo del adaptador, la asignación entre cada pin P y las señales del LCD puede variar ligeramente. Un código de bajo nivel escrito completamente desde cero por una IA suele cometer errores en esta asignación.

Recomendación: No pidas a la IA que escriba el controlador desde cero. Mejor ve a Bilibili o CSDN y busca “STM32 HAL LCD1602 I2C”, descarga archivos lcd1602.c y lcd1602.h ya verificados por otros usuarios y añádelos directamente a tu proyecto. Así será mucho más fiable.

4. Niveles lógicos de hardware (poca probabilidad, pero posible)

Mencionas que usas alimentación de 5 V, lo cual es perfecto, ya que el LCD1602 necesita 5 V. Aunque los pines del STM32 trabajan con niveles lógicos de 3,3 V, muchos pines I2C (como PB6 y PB7) son “tolerantes a 5 V (FT)”, por lo que conectarlos directamente generalmente no causa problemas. Solo asegúrate de que el STM32 y el módulo LCD compartan masa común (GND conectado).

:light_bulb: Sugerencia clave (qué hacer ahora):

No intentes aún mostrar caracteres. Primero pide a la IA que te genere un código simple de “Escáner I2C para STM32” (I2C Scanner).

Grábalo en el microcontrolador y abre un asistente serial. Observa si el STM32 logra detectar el módulo en el bus I2C.

  • Si el monitor serial indica que no encuentra ningún dispositivo: hay un error en las conexiones o la inicialización I2C del STM32 es incorrecta.
  • Si el monitor serial muestra un dispositivo encontrado (por ejemplo, responde con 0x4E): ¡entonces el hardware está bien conectado! Solo necesitas usar esa dirección en tu código de inicialización del LCD.

¡Deseándote ver pronto un "Hello World" en pantalla! Si te atasca de nuevo, vuelve a preguntar.

1 me gusta

Solo un aviso rápido sobre el hardware: la pantalla LCD y el módulo I2C funcionan a 5 V, pero tu STM32 opera a 3,3 V. Aunque la mayoría de los pines I2C en el STM32 son tolerantes a 5 V (FT), conviene consultar la hoja de datos para asegurarte de que los pines específicos que estás utilizando sean realmente tolerantes a 5 V. Por lo general, las resistencias de pull-up en el módulo son suficientes, pero la comunicación podría fallar si los niveles lógicos no son compatibles.

El conector hembra de 20 pines es una placa mínima del sistema STM32, y el conector hembra de 4 pines es un módulo LCD1602_I2C.

A continuación se muestra el archivo 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();                // Inicializa el LCD1602
    lcd_clear();               // Limpia la pantalla
    lcd_set_cursor(0, 0);      // Posiciona el cursor en la primera fila, primera columna
    lcd_send_string("Hello STM32!"); // Muestra texto

    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 */

A continuación se muestra el archivo lcd1602_i2c.h:

#ifndef __LCD1602_I2C_H
#define __LCD1602_I2C_H

#include "main.h" // Incluye la biblioteca HAL y definiciones de pines
#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

A continuación se muestra el archivo lcd1602_i2c.c:

#include "lcd1602_i2c.h"

// Declara el manejador externo I2C, generado por CubeMX en main.c
extern I2C_HandleTypeDef hi2c1;

// Función interna: envía datos por 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;

    // Byte de control: Bit 3 es retroiluminación (1=encendido), Bit 2 es EN, Bit 1 es RW (0=escritura), Bit 0 es 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

    // Transmite 4 bytes mediante I2C1
    HAL_I2C_Master_Transmit(&hi2c1, LCD_I2C_ADDRESS, data_t, 4, 100);
}

// Envía un comando
void lcd_send_cmd(char cmd)
{
    lcd_send_to_i2c(cmd, 0); // RS = 0 indica que se envía un comando
}

// Envía datos (carácter)
void lcd_send_data(char data)
{
    lcd_send_to_i2c(data, 1); // RS = 1 indica que se envían datos
}

// Limpia la pantalla
void lcd_clear(void)
{
    lcd_send_cmd(0x01);
    HAL_Delay(2); // El comando de limpieza requiere tiempo adicional para ejecutarse
}

// Establece la posición del cursor (fila: 0-1, columna: 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); // Establece la dirección DDRAM
}

// Envía una cadena de caracteres
void lcd_send_string(char *str)
{
    while (*str)
    {
        lcd_send_data(*str++);
    }
}

// Inicializa el LCD1602
void lcd_init(void)
{
    // Secuencia estándar de inicialización en modo 4 bits
    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);

    // Configuración de visualización
    lcd_send_cmd(0x28); // 4 líneas, 2 filas, fuente 5x8
    HAL_Delay(1);
    lcd_send_cmd(0x08); // Apaga la visualización
    HAL_Delay(1);
    lcd_send_cmd(0x01); // Limpia la pantalla
    HAL_Delay(2);
    lcd_send_cmd(0x06); // Desplaza el cursor hacia la derecha
    HAL_Delay(1);
    lcd_send_cmd(0x0C); // Activa la visualización, desactiva el cursor
}

He escrito el diagrama de conexiones simplificado y el código en la sección de respuestas, gracias por ayudarme a revisarlo.

Vale, gracias. Voy a probar tu sugerencia.

Después de compilar y descargar, utilicé un módulo de alimentación, con el cual el voltaje en los pines VCC y GND del I2C es de 5 V.

¡Genial! Proporcionar el esquema y el código fuente aumenta cien veces la eficiencia del diagnóstico.

Primero te doy tranquilidad: tu conexión de hardware es completamente correcta. En el esquema, PB6 conectado a SCL y PB7 a SDA son los pines estándar de I2C1 en el STM32F103. Además, en tu archivo de cabecera escribiste #define LCD_I2C_ADDRESS 0x4E, lo que indica que tú (o la IA) ya sabéis que la HAL requiere desplazar 0x27 un bit a la izquierda, evitando así con éxito las dos trampas más comunes para principiantes.

La verdadera causa de que la pantalla solo muestre cuadros es un fallo en la función de inicialización (lcd_init) escrita por la IA, específicamente en la secuencia de comunicación del modo “4 bits”.

:bug: Análisis del error principal: La trampa de la “inicialización forzada” del HD44780

El módulo adaptador I2C (PCF8574) que estás usando controla el 1602 mediante 4 líneas de datos (D4-D7).
Cuando el 1602 se enciende, por defecto está en modo de 8 bits. Para cambiarlo al modo de 4 bits, según el manual del componente, debes enviar varias veces un solo nibble (mitad de byte) (solo los 4 bits superiores, y terminar inmediatamente después).

Observa tu función lcd_send_cmd(), que llama a lcd_send_to_i2c(). Esta función de bajo nivel es demasiado “diligente”: independientemente del comando que le envíes, siempre divide automáticamente el comando en dos partes (los 4 bits altos primero, luego los 4 bits bajos), generando dos pulsos EN.

Cuando la IA escribió lcd_send_cmd(0x30) durante la inicialización:

  1. Intención original: enviar solo 0x3 a la pantalla.
  2. Lo que realmente ocurre: lcd_send_to_i2c envía primero los 4 bits altos 0x3 (activa EN una vez), y luego envía los 4 bits bajos 0x0 (activa EN nuevamente).
  3. Resultado: la pantalla recibe un pulso adicional 0x0, arruinando completamente la temporización. Como consecuencia, la pantalla rechaza la inicialización y queda bloqueada mostrando “cuadros llenos”, estado inicial tras la alimentación.

:hammer_and_wrench: Solución

Necesitamos añadir en lcd1602_i2c.c una función especializada en enviar solo medio byte, exclusivamente para despertar la pantalla, y reescribir lcd_init().

Por favor, modifica tu archivo lcd1602_i2c.c como sigue (reemplaza desde el principio hasta lcd_init, manteniendo el resto sin cambios):

#include "lcd1602_i2c.h"

// Declarar el manejador externo de I2C
extern I2C_HandleTypeDef hi2c1;

// ====== Nueva función: envío de un solo nibble, solo para inicialización ======
void lcd_send_cmd_4bit(uint8_t nibble)
{
    uint8_t data_t[2];
    uint8_t backlight = 0x08; // Mantener retroiluminación encendida

    // Nota: el valor 'nibble' debe tener ya los 4 bits altos alineados (por ejemplo, 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);
}

// Función interna: enviar un byte completo a I2C (dividido en dos nibbles)
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);
}

// ====== Reescritura: flujo correcto de inicialización en modo 4 bits ======
void lcd_init(void)
{
    // 1. Fase de inicialización forzada (debe enviar solo un pulso de 4 bits)
    HAL_Delay(50);
    lcd_send_cmd_4bit(0x30); // Despertar 1
    HAL_Delay(5);
    lcd_send_cmd_4bit(0x30); // Despertar 2
    HAL_Delay(1);
    lcd_send_cmd_4bit(0x30); // Despertar 3
    HAL_Delay(10);
    lcd_send_cmd_4bit(0x20); // Indicar a la pantalla: ¡entrar en modo 4 bits!
    HAL_Delay(10);

    // 2. Fase de configuración normal (ahora la pantalla ya está en modo 4 bits, usar funciones normales)
    lcd_send_cmd(0x28); // Configuración: datos de 4 bits, 2 líneas, fuente 5x8
    HAL_Delay(1);
    lcd_send_cmd(0x08); // Apagar pantalla
    HAL_Delay(1);
    lcd_send_cmd(0x01); // Borrar pantalla
    HAL_Delay(2);       // Esta instrucción tarda más, mínimo 2ms
    lcd_send_cmd(0x06); // Configurar modo: cursor avanza a la derecha
    HAL_Delay(1);
    lcd_send_cmd(0x0C); // Encender pantalla, apagar cursor
}

// ... el resto de funciones como lcd_send_cmd, lcd_send_data, etc., permanecen iguales ...

:magnifying_glass_tilted_left: Puntos alternativos de diagnóstico

Si después de modificar y volver a grabar el código, la pantalla aún muestra cuadros,
la única posibilidad restante es que la dirección I2C predeterminada de tu módulo no sea 0x27, sino 0x3F.
En ese caso, simplemente abre lcd1602_i2c.h y cambia #define LCD_I2C_ADDRESS 0x4E por 0x7E (que es 0x3F desplazado un bit a la izquierda), y prueba nuevamente.

Prueba primero esta modificación en el código de inicialización. ¿Quieres que te enseñe cómo escribir un sencillo “escáner de direcciones I2C” para confirmar al 100% la dirección de comunicación de tu módulo?

Puedes usar un analizador lógico para verificar si la forma de onda I2C se genera correctamente y si la temporización es adecuada. Si el I2C está bien, entonces usa el analizador lógico para comprobar si hay señales paralelas en la salida del módulo convertidor de I2C a paralelo y verifica que su temporización también sea correcta.

¿Cómo configuraste STM32CubeMX?

Análisis del problema principal

Tu LCD1602 tiene la retroiluminación encendida pero no muestra caracteres. Con una probabilidad del 90%, el problema es una comunicación I2C anómala (dirección incorrecta, pines mal configurados o sin respuesta), seguido por problemas en la secuencia de inicialización/demoras o falta de alimentación/hardware pull-up. A continuación te ofrezco un plan completo de diagnóstico y solución, ordenado por prioridad.


Uno: Diagnóstico de hardware (resuelve primero los problemas básicos de hardware)

1. Verificación crítica de pines y conexiones

Según tu esquema:

  • Conector H2 del LCD: 1=+5V, 2=GND, 3=SDA(PB7), 4=SCL(PB6)
  • I2C1 del STM32: SCL debe corresponder a PB6, SDA debe corresponder a PB7. Verifica en CubeMX que los pines de I2C1 estén correctamente asignados. ¡Es fundamental no intercambiar SDA y SCL!

2. La alimentación debe ser de 5V

El módulo LCD1602 con adaptador I2C es un dispositivo de 5V. Si se alimenta con 3.3V, puede ocurrir que “la retroiluminación encienda pero no haya comunicación I2C”. Debes conectarlo al pin de 5V del STM32, no al de 3.3V.

3. El bus I2C necesita resistencias pull-up

I2C es un bus de drenaje abierto (open-drain) y requiere resistencias pull-up:

  • Opción 1: En CubeMX, configura los pines PB6 y PB7 del I2C1 como GPIO modo Open Drain con Pull-up
  • Opción 2: Agrega resistencias de pull-up de 4.7kΩ entre PB6/PB7 y 5V en el circuito físico
  • Sin estas resistencias, la señal I2C será inestable y el dispositivo no responderá.

Dos: Solución de problemas clave en software (por orden de prioridad)

1. 【Problema más común】Corrección de la dirección I2C

En tu código defines #define LCD_I2C_ADDRESS 0x4E, pero esta dirección puede no coincidir con tu módulo:

  • El chip base del adaptador I2C para LCD1602 es el PCF8574. Las direcciones de 7 bits más comunes son 0x27 o 0x3F. La función HAL_I2C_Master_Transmit de HAL requiere la dirección de escritura de 8 bits (desplazando la de 7 bits un bit a la izquierda):
    • Dirección 7 bits 0x27 → Dirección escritura 8 bits 0x4E (tu valor actual)
    • Dirección 7 bits 0x3F → Dirección escritura 8 bits 0x7E (otra muy común)

Verificación rápida de la dirección correcta (obligatorio)

Después de MX_I2C1_Init(), añade código para escanear direcciones I2C y ver cuál responde:

// Insertar después de MX_I2C1_Init(), antes de 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)
  {
    // Puedes poner un punto de interrupción o encender un LED para ver qué dirección responde.
    // Usa (i2c_addr << 1) como valor para LCD_I2C_ADDRESS
    break;
  }
}

Si no detecta ninguna dirección, hay un problema de conexión física o de inicialización I2C. Resuélvelo antes.

2. Verifica que HAL_Delay funcione correctamente

La inicialización del LCD depende mucho de las demoras. Si HAL_Delay no funciona, la secuencia fallará y no habrá visualización.

  • Prueba: Agrega en el bucle principal un parpadeo de LED para verificar el tiempo:
while (1)
{
  HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // Reemplaza con el pin de tu LED
  HAL_Delay(500);
}

Si el LED no cambia cada 500ms, hay un error en el reloj del sistema o en SysTick. Debes corregir primero la configuración del reloj.

  • Usas HSI interno de 8MHz, lo cual es válido, pero asegúrate en CubeMX que la fuente de SysTick sea correcta y que HAL_Init() se ejecute bien.

3. Añadir verificación de errores en la comunicación I2C

Tu código no verifica si la transmisión I2C fue exitosa, por lo que no puedes diagnosticar fallos. Modifica la función lcd_send_to_i2c para incluir retorno de estado y manejo de errores:

// Función interna: enviar datos por I2C, devuelve estado 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

  // Aumenta timeout y devuelve estado
  return HAL_I2C_Master_Transmit(&hi2c1, LCD_I2C_ADDRESS, data_t, 4, 200);
}

// Enviar comando
void lcd_send_cmd(char cmd)
{
  lcd_send_to_i2c(cmd, 0); // RS = 0 indica comando
  HAL_Delay(1); // Añade demora para evitar tiempos demasiado rápidos
}

// Enviar dato (carácter)
void lcd_send_data(char data)
{
  lcd_send_to_i2c(data, 1); // RS = 1 indica dato
  HAL_Delay(1);
}

4. Corregir detalles en la secuencia de inicialización

Tu flujo de inicialización es correcto, pero puedes añadir márgenes de seguridad en las demoras:

// Inicializar LCD1602
void lcd_init(void)
{
  // Flujo estándar para modo 4 bits, con más margen de demora
  HAL_Delay(100); // Espera más tras encendido, para estabilización
  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); // Cambiar a modo 4 bits
  HAL_Delay(10);

  // Configuración de pantalla, con demora entre comandos
  lcd_send_cmd(0x28); // 4 líneas, 2 filas, fuente 5x8
  HAL_Delay(2);
  lcd_send_cmd(0x08); // Apagar pantalla
  HAL_Delay(2);
  lcd_send_cmd(0x01); // Borrar pantalla
  HAL_Delay(5);
  lcd_send_cmd(0x06); // Cursor derecha, autoincremento dirección
  HAL_Delay(2);
  lcd_send_cmd(0x0C); // Encender pantalla, sin cursor ni parpadeo
  HAL_Delay(2);
}

Tres: Pasos completos de depuración (ejecuta en orden)

  1. Verifica las conexiones físicas: 5V/GND correctos, SDA conectado a PB7, SCL a PB6, sin intercambios.
  2. Ejecuta el escaneo de direcciones I2C, confirma que el dispositivo responda, y usa la dirección correcta de 8 bits en LCD_I2C_ADDRESS.
  3. Verifica que HAL_Delay funcione correctamente.
  4. Usa el código modificado con verificaciones de error y más demoras.
  5. Prueba: si la comunicación I2C es correcta, al encender debería mostrarse Hello STM32!.

Cuatro: Diagnóstico adicional (si aún no funciona)

  1. Ajusta el potenciómetro de contraste en la parte trasera del LCD1602 (el resistor azul ajustable). Un contraste muy bajo hace que los caracteres sean invisibles. Gira lentamente hasta ver bloques o caracteres.
  2. Usa un analizador lógico u osciloscopio para observar las señales I2C en PB6/PB7. Determina si el MCU envía señales o si el dispositivo no responde.
  3. Conecta directamente con cables Dupont los pines PB6, PB7, 5V y GND del STM32 al módulo LCD, para descartar mal contacto en conectores.

Gracias por tu respuesta. Ya he intentado compilar y descargar con el código que modificaste, pero aún así no se muestran los caracteres. Además, he notado que cuando la dirección en #define LCD_I2C_ADDRESS 0x4E es 0x4E, se muestran dos filas de cuadros; para cualquier otra dirección, solo aparece una fila de cuadros.

Prueba reduciendo la velocidad de I2C, por ejemplo a 10k; cuando la velocidad es alta y los cables son demasiado largos puede haber problemas. ¿Los dos pines de I2C están configurados en modo de drenador abierto con resistencias de pull-up?

¡Esta es una excelente noticia! El fenómeno que observaste (que 0x4E muestre dos filas de cuadros negros mientras otras direcciones muestran solo una fila) no es un problema en absoluto, ¡por el contrario, demuestra que tu código y la comunicación ya funcionan perfectamente!

Permíteme revelarte lo que está pasando “dentro” de la pantalla 1602 para que entiendas por qué:

  1. Una fila de cuadros (cuando usas otras direcciones): Esto es el estado predeterminado de fábrica de la pantalla 1602 al encenderse. Por defecto, asume que está en modo “una sola línea”. Si introduces una dirección incorrecta (por ejemplo, 0x27 o 0x7E), el STM32 envía instrucciones de inicialización que la pantalla no puede recibir, así que permanece en modo de una línea. Sumado a un contraste demasiado alto, ves una fila completa de cuadros negros.
  2. Dos filas de cuadros (con la dirección 0x4E): ¡Esto significa que tu STM32 ha logrado encontrar correctamente el módulo mediante la dirección 0x4E, y además ha ejecutado con éxito tu código de inicialización modificado! La pantalla recibió la instrucción 0x28 (configurada para mostrar 2 líneas) y obedientemente activó también la segunda fila.

Es decir, ¡tu microcontrolador, conexiones, comunicación I2C e incluso tu código de inicialización son completamente correctos! ¡El texto en realidad ya ha sido escrito en la pantalla!

:red_question_mark: Entonces, ¿por qué solo veo cuadros y no el texto?

Solo hay una razón: el contraste (V0) está ajustado demasiado alto.
El texto ya está impreso en la pantalla, pero como el contraste está al máximo, los píxeles del fondo (que normalmente no se verían) también se vuelven completamente negros, ocultando las letras.

:hammer_and_wrench: El último paso: el momento de la magia

Deja el código por ahora y toma un destornillador. Sigue estos pasos:

  1. Enciende normalmente el microcontrolador para que ejecute tu código modificado (manteniendo la pantalla mostrando dos filas de cuadros).
  2. Toma un pequeño destornillador de cruz y gira el potenciómetro azul en la parte trasera de la placa adaptadora (el botón con forma de cruz).
  3. Mientras miras fijamente la pantalla, gira lentamente. En cierto ángulo mágico, verás que las dos filas de cuadros negros de repente se atenúan o desaparecen, ¡y el mensaje \"Hello STM32!\" aparecerá claramente!

(Normalmente necesitas girar varias vueltas; si no pasa nada en sentido horario, inténtalo en sentido antihorario varias veces)


:light_bulb: Un pequeño parche opcional para prevenir problemas futuros:

Aunque muy probablemente el problema sea del potenciómetro físico, para hacer tu código aún más robusto, te recomiendo agregar pequeños retardos después de enviar comandos, evitando que el STM32 envíe datos demasiado rápido para que la pantalla pueda procesarlos.

Puedes modificar ligeramente estas dos funciones en el archivo lcd1602_i2c.c, añadiendo HAL_Delay(1);:

// Enviar comando
void lcd_send_cmd(char cmd)
{
    lcd_send_to_i2c(cmd, 0); 
    HAL_Delay(1); // <- Añade esta línea, da tiempo a la pantalla para procesar
}

// Enviar dato (carácter)
void lcd_send_data(char data)
{
    lcd_send_to_i2c(data, 1); 
    HAL_Delay(1); // <- Añade esta línea
}

Y aumenta un poco el retardo en la función de limpieza de pantalla:

// Limpiar pantalla
void lcd_clear(void)
{
    lcd_send_cmd(0x01);
    HAL_Delay(5); // <- Cambia de 2 a 5, ya que limpiar la pantalla es la operación más lenta
}

¡Ve ahora y gira ese potenciómetro azul! Cuando lo consigas, ¡no olvides venir a celebrarlo!

Voy a intentar con tu sugerencia, gracias por tu respuesta.

Gracias por tu respuesta, he reducido la velocidad y sigue siendo lo mismo. Además, en la interfaz de configuración GPIO de CubeMX, la opción Pull para PB6/PB7 podría estar invisible, debido a una limitación de la interfaz de CubeMX para la serie F1.

Gracias por tu respuesta. Estoy girando realmente el potenciómetro, pero solo veo cómo dos filas de cuadros pasan de brillar a atenuarse, sin que aparezca ningún texto. Además, tras aumentar el retardo, puedo observar en el momento de la alimentación cómo cambia de una fila de cuadros a dos filas, lo que demuestra que efectivamente se recibió el comando 0x28.

¡Puedes captar con precisión el detalle de “al encenderse, pasa de una fila de cuadros a dos filas de cuadros”, lo que demuestra que tienes un instinto muy agudo para depurar problemas de hardware. Este fenómeno es extremadamente importante: no solo confirma al 100 % que tu dirección I2C (0x4E) es correcta, sino que también prueba que la comunicación entre el microcontrolador y la pantalla ya está establecida.

Dado que la instrucción 0x28 (activar visualización en dos líneas) se ejecutó correctamente, ¿por qué los caracteres posteriores no aparecen y aún sigue mostrando cuadros por toda la pantalla? En realidad hay dos “trampas” clásicas aquí, especialmente comunes en la serie STM32F1:

1. Respondiendo a tus dudas: sobre la configuración de pines I2C en F1

Has mencionado que en CubeMX, en la serie F1, no ves opciones de resistencias Pull-up/down en los pines PB6/PB7. Esto se debe a que cuando se configuran como I2C, los pines pasan automáticamente a modo de salida de drenador abierto con función alternativa (Alternate Function Open Drain). Esta limitación en la interfaz es normal.
No debes preocuparte por las resistencias pull-up: observa cuidadosamente la parte trasera del módulo convertidor LCD (junto al potenciómetro azul). Verás varias resistencias SMD negras soldadas (normalmente marcadas como 103 o 472). Esas son resistencias pull-up físicas integradas en el módulo. Por tanto, aunque el STM32 no active pull-ups internos, el circuito externo cumple perfectamente los requisitos para la comunicación I2C.

2. El verdadero culpable: congestión de temporización o bloqueo I2C por envío continuo

El periférico I2C hardware de la serie STM32F1 es notoriamente “delicado” (tiene defectos de silicio conocidos / erratas). Si envías bytes sin parar durante la transmisión, es fácil activar un estado de bloqueo permanente debido a la señal Busy del I2C.
Además, el chip HD44780 dentro del LCD1602 es un componente extremadamente antiguo y lento. En nuestro código original, dentro de un único paquete I2C (4 bytes), generábamos instantáneamente pulsos EN consecutivos para los nibbles alto y bajo. Para ciertos LCD más sensibles, esta velocidad es demasiado elevada, haciendo que ignoren todas las instrucciones posteriores a 0x28 (como limpiar pantalla 0x01 o escribir caracteres), o incluso provocando que el I2C del STM32 se bloquee completamente.


:hammer_and_wrench: Solución definitiva: dividir los envíos e insertar “respiraciones profundas”

Debemos dividir violentamente la función original que enviaba “4 bytes de golpe” en dos transmisiones independientes, insertando retrasos obligatorios entre medias. Esto reiniciará la máquina de estados del I2C del STM32 evitando bloqueos, y dará al LCD tiempo suficiente para responder.

Por favor, reemplaza completamente la función lcd_send_to_i2c en tu archivo lcd1602_i2c.c con este código:

// Función interna: enviar byte completo por I2C (dividido en nibble alto y bajo, versión robusta)
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; // Retroiluminación siempre encendida

    // ==========================================
    // Paso 1: enviar solo el nibble alto, generando un pulso EN completo
    // ==========================================
    data_high[0] = upper_nibble | backlight | 0x04 | rs; // EN = 1
    data_high[1] = upper_nibble | backlight | 0x00 | rs; // EN = 0
    // Enviar los 2 bytes del nibble alto
    HAL_I2C_Master_Transmit(&hi2c1, LCD_I2C_ADDRESS, data_high, 2, 100);
    
    // Retraso vital clave: da tiempo al controlador LCD para capturar el nibble alto y evita colapsar el bus I2C
    HAL_Delay(2);

    // ==========================================
    // Paso 2: enviar solo el nibble bajo, generando un pulso EN completo
    // ==========================================
    data_low[0] = lower_nibble | backlight | 0x04 | rs;  // EN = 1
    data_low[1] = lower_nibble | backlight | 0x00 | rs;  // EN = 0
    // Enviar los 2 bytes del nibble bajo
    HAL_I2C_Master_Transmit(&hi2c1, LCD_I2C_ADDRESS, data_low, 2, 100);
    
    // Esperar un poco después de enviar un byte completo
    HAL_Delay(2);
}

Además, por seguridad, ajusta ligeramente el orden en main.c:
Elimina la línea lcd_clear();, ya que en el último paso de lcd_init() ya incluimos la instrucción de limpieza. Limpiar dos veces seguidas podría causar acumulación de órdenes fallidas. En tu función main, deja esa parte así:

  /* USER CODE BEGIN 2 */
  lcd_init();                // Inicializar LCD1602
  // lcd_clear();            // Comenta o elimina esta línea
  lcd_set_cursor(0, 0);      // Posicionar cursor en primera fila, primera columna
  lcd_send_string("Hello STM32!");
  /* USER CODE END 2 */

Tras compilar y grabar, si la pantalla queda completamente en blanco (los cuadros han desaparecido), gira suavemente el potenciómetro azul: seguro que los caracteres aparecerán.

Si esta vez logra encenderse correctamente, ¿te gustaría que te enseñe cómo modificar el I2C para usar DMA o interrupciones, eliminando así el uso de HAL_Delay y liberando valioso tiempo de ejecución en tu programa principal?