¡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.
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?