El controlador HD44780 de la LCD necesita tiempo para asimilar los datos. Cuando le envías el pulso Enable (EN), el fabricante exige que lo mantengas estable al menos unos microsegundos y que no envíes el siguiente comando muy rápido. Al usar un bucle `for` para imprimir "Hola Mundo", la función obliga al procesador a quedarse sentado esperando pacientemente (usando retardos) a que la LCD procese la 'H' antes de enviar la 'o'. En tiempo de CPU, una pausa de 2 milisegundos equivale a desperdiciar 168,000 ciclos de reloj en un STM32 a 84MHz.
La solución Bare-Metal es dividir la escritura en Rebanadas de Tiempo (Time-Slices) utilizando una Máquina de Estados (State Machine) gobernada por nuestra variable milisegundos_globales del SysTick.
En lugar de una función que imprima todo de golpe, crearemos un Búfer Circular de Transmisión (FIFO). Cuando tu código principal quiera imprimir "Hola", simplemente copiará esa palabra en el búfer y seguirá su camino. En el bucle principal, una tarea en segundo plano llamada lcd_procesar_tareas() tomará una letra del búfer, la enviará por I2C, y cambiará su propio estado a "Esperando 2ms". En la siguiente iteración del Súper Bucle, la función verá que aún no pasan los 2ms y retornará instantáneamente en 1 ciclo de reloj, permitiendo que el CPU atienda la telemetría.
Ejercicio 1: Transforma el driver bloqueante del Día 125. Implementaremos un Búfer Circular (FIFO) para almacenar los caracteres que deseamos imprimir. Luego, crearemos la función lcd_procesar_tareas() gobernada por un switch-case y la variable de tiempo del sistema (SysTick). Esta función entrará y saldrá instantáneamente hasta que la pantalla termine visualmente de renderizar el mensaje de forma asíncrona.
/**
* @file i2c_lcd_async.c
* @brief Driver I2C LCD 20x4 No Bloqueante (Time-Slicing)
*/
#include "stm32f401xe.h"
#include <stdint.h>
#include <stdbool.h>
#define LCD_BUFFER_SIZE 128
#define LCD_I2C_ADDR 0x4E
// Variables externas requeridas
extern volatile uint32_t milisegundos_globales; // Del SysTick (Día 98)
extern void lcd_enviar_i2c(uint8_t dato); // Envío físico I2C (Día 125)
// --- FIFO BÚFER CIRCULAR ---
char lcd_fifo[LCD_BUFFER_SIZE];
uint8_t fifo_head = 0;
uint8_t fifo_tail = 0;
// --- ESTADOS DE LA MÁQUINA (ENUM) ---
typedef enum {
STATE_IDLE,
STATE_SEND_HIGH_NIBBLE_EN,
STATE_WAIT_HIGH_NIBBLE,
STATE_SEND_LOW_NIBBLE_EN,
STATE_WAIT_LOW_NIBBLE
} LCD_State_t;
LCD_State_t estado_lcd = STATE_IDLE;
uint32_t tiempo_espera_lcd = 0;
uint8_t caracter_actual = 0;
/**
* @brief Función para el Súper Bucle.
* Añade el string a la cola y retorna inmediatamente (0 ms delay).
*/
void lcd_escribir_async(const char* string) {
while (*string) {
uint8_t next_head = (fifo_head + 1) & (LCD_BUFFER_SIZE - 1);
if (next_head != fifo_tail) { // Si hay espacio en el búfer
lcd_fifo[fifo_head] = *string;
fifo_head = next_head;
}
string++;
}
}
/**
* @brief El corazón del HMI. Debe llamarse en cada iteración del while(1).
* No bloquea el CPU. Se administra a sí mismo en el tiempo.
*/
void lcd_procesar_tareas(void) {
// Si estamos en un retardo asíncrono, salimos instantáneamente
if ((milisegundos_globales - tiempo_espera_lcd) < 2 && estado_lcd != STATE_IDLE) {
return;
}
// Banderas de control de la pantalla (RS=1 Datos, BL=1 Backlight)
const uint8_t CTRL_BITS = 0x01 | 0x08;
switch (estado_lcd) {
case STATE_IDLE:
// ¿Hay caracteres pendientes en la cola por imprimir?
if (fifo_tail != fifo_head) {
caracter_actual = lcd_fifo[fifo_tail];
fifo_tail = (fifo_tail + 1) & (LCD_BUFFER_SIZE - 1);
estado_lcd = STATE_SEND_HIGH_NIBBLE_EN;
}
break;
case STATE_SEND_HIGH_NIBBLE_EN:
// Enviamos 4 bits altos con el pin Enable=1
lcd_enviar_i2c((caracter_actual & 0xF0) | CTRL_BITS | 0x04);
tiempo_espera_lcd = milisegundos_globales;
estado_lcd = STATE_WAIT_HIGH_NIBBLE;
break;
case STATE_WAIT_HIGH_NIBBLE:
// Bajamos el Enable para que la LCD procese la mitad de la letra
lcd_enviar_i2c((caracter_actual & 0xF0) | CTRL_BITS);
tiempo_espera_lcd = milisegundos_globales;
estado_lcd = STATE_SEND_LOW_NIBBLE_EN;
break;
case STATE_SEND_LOW_NIBBLE_EN:
// Enviamos 4 bits bajos con el pin Enable=1
lcd_enviar_i2c(((caracter_actual << 4) & 0xF0) | CTRL_BITS | 0x04);
tiempo_espera_lcd = milisegundos_globales;
estado_lcd = STATE_WAIT_LOW_NIBBLE;
break;
case STATE_WAIT_LOW_NIBBLE:
// Bajamos el Enable terminando la letra.
lcd_enviar_i2c(((caracter_actual << 4) & 0xF0) | CTRL_BITS);
tiempo_espera_lcd = milisegundos_globales;
estado_lcd = STATE_IDLE; // ¡Letra completa! Volver a buscar más.
break;
}
}
int main(void) {
// SysTick_Init();
// inicializar_i2c1();
// lcd_inicializar_bloqueante_boot(); // El encendido sí requiere delays.
while(1) {
// --- 1. Tareas Rápidas (Decisiones lógicas, RS485, Sensores) ---
// ejecutar_telemetria();
// vigilar_caudalimetro();
// --- 2. Enviar información a la Pantalla de forma asíncrona ---
// (En la vida real usarías banderas para no saturar la FIFO en cada ciclo)
if (venta_completada) {
lcd_escribir_async("Venta Terminada! ");
venta_completada = false;
}
// --- 3. Ejecutor (Dispatcher) de la LCD ---
// Esta función toma menos de 1 microsegundo si no le toca hacer nada,
// garantizando que el While(1) siga girando a velocidad extrema.
lcd_procesar_tareas();
}
return 0;
}
Objetivo del día: Determinismo de Tiempo Real y Concurrencia de Sistema.
Un ingeniero de sistemas junior desarrolla en modo "Procedural": *Paso 1: Lee botón. Paso 2: Imprime pantalla (espera 40ms). Paso 3: Revisa red (espera 20ms).* Ese enfoque convierte un microcontrolador de 84MHz en una máquina lenta y torpe, perdiendo pulsos críticos de la turbina metrológica durante las pausas.
Al refactorizar tu Interfaz Humano-Máquina (HMI) hacia una arquitectura No Bloqueante guiada por Máquina de Estados (State Machine), lograste que tu Cortex-M atienda la pantalla "a pedacitos" intercalados con el escaneo de seguridad. Ahora, mientras la lenta pantalla Hitachi de los años 80 se toma su dulce tiempo para encender sus cristales líquidos, tu STM32 procesa miles de ecuaciones de fluidos y atiende interrupciones sin sudar. Esta es la diferencia absoluta entre código aficionado y Firmware Industrial de Nivel 3.
delay_ms dentro del bucle while(1) principal es el enemigo número uno del rendimiento en Bare-Metal (Bloqueo de CPU).switch-case).milisegundos_globales impulsada por la interrupción atómica del SysTick es el metrónomo universal que le permite a las máquinas de estados saber cuándo avanzar al siguiente estado sin detener la ejecución de otras funciones.