DÍA 153 DE 365

Fase 3: Comunicaciones y Redes Industriales

📖 Teoría a Estudiar

Concurrencia: La Ilusión del Tiempo Real

Un microcontrolador de un solo núcleo (Single-Core) solo puede ejecutar una instrucción a la vez. Sin embargo, gracias al sistema de interrupciones (NVIC), puede pausar el programa principal (Súper Bucle), ejecutar código de emergencia, y regresar. Esto crea la ilusión de concurrencia.

El peligro mortal ocurre con los Recursos Compartidos. En nuestra placa, la LCD, el Reloj DS3231 y la Memoria EEPROM viven en los mismos dos cables físicos: el bus I2C. Si dos partes del código intentan usar ese bus al mismo tiempo (ej. el Main actualiza la LCD, y una Interrupción intenta leer el Reloj), los datos se mezclan y el hardware I2C se bloquea eternamente esperando confirmaciones cruzadas.

Mutex Bare-Metal (Mutual Exclusion)

Para proteger un recurso compartido, usamos una variable llamada "Semáforo Mutex". Antes de que cualquier código toque el bus I2C, debe preguntar: "¿El bus está ocupado?". Si la respuesta es no, el código bloquea el bus (Toma el Mutex), hace su transmisión, y al terminar libera el bus (Suelta el Mutex).

Pero hay una trampa: verificar y bloquear una variable en C (if(bus==0) bus=1;) toma varios ciclos de reloj. Si justo a la mitad de ese chequeo ocurre una interrupción, ambas partes podrían creer que el bus está libre. Para evitar esto, el chequeo del Mutex debe ser Atómico (ininterrumpible), lo cual logramos apagando brevemente las interrupciones del CPU.

ARQUITECTURA DE ARBITRAJE: MUTEX Y DELEGACIÓN DE TAREAS Sin Gestión (Caos) Main() [EEPROM] ISR [MODBUS] I2C1 BUS Hardware Lockup (Crash) Delegación y Mutex Atómico Main() [EEPROM] 🔑 (Toma Mutex) I2C1 BUS ISR [MODBUS] ¿Mutex=Libre? NO (Delega a Cola) Job Queue (RAM) [0]: Escribir 10.5 [1]: Vacío El Main() procesará la cola cuando suelte la llave.

Job Queue (Cola de Tareas Diferidas)

¿Qué hace la Interrupción si el bus I2C está bloqueado por el Main? No puede usar la instrucción while(bus_ocupado);, porque si la interrupción detiene al CPU esperando que el Main suelte la llave... ¡el Main nunca la soltará porque la interrupción no lo deja ejecutarse! Es un Deadlock (Abrazo Mortal).

La regla de oro industrial: Las interrupciones jamás acceden a buses compartidos. Si la interrupción de Modbus quiere guardar algo en EEPROM, simplemente deposita el dato en una Cola (Ring Buffer o variables de bandera) en la RAM. Y se despide. Cuando el Súper Bucle esté libre, revisará esa cola y hará el guardado físico en su propio tiempo.

⚙️ Ejercicios Prácticos

Ejercicio 1: Implementaremos el objeto `Mutex_t` usando desactivación atómica de interrupciones para garantizar su inmunidad. Luego, crearemos el **Gestor de Tareas de EEPROM**. El módulo Modbus usará la función `eeprom_encolar_escritura()` en lugar de escribir físicamente. El `main()` procesará las escrituras pendientes gota a gota.


/**
 * @file multi_bus_manager.c
 * @brief Gestor de Colisiones (Mutex Atómico) y Cola de Delegación I2C
 */

#include "stm32f401xe.h"
#include <stdbool.h>
#include <stdint.h>

// =========================================================================
// 1. MUTEX ATÓMICO (Hardware Lock)
// =========================================================================

typedef struct {
    volatile bool locked;
} Mutex_t;

Mutex_t i2c_bus_mutex = { .locked = false };

/**
 * @brief Intenta tomar la llave del Mutex. 
 *        Aísla el chequeo desactivando las interrupciones para que 
 *        nadie interrumpa la comprobación.
 * @return true si logró tomar la llave, false si alguien más la tiene.
 */
bool Mutex_TryLock(Mutex_t *mutex) {
    bool pude_tomarlo = false;
    
    // ZONA CRÍTICA: Apagar interrupciones para una lectura/escritura atómica
    __disable_irq(); 
    
    if (mutex->locked == false) {
        mutex->locked = true;  // Tomamos la llave
        pude_tomarlo = true;
    }
    
    // ZONA SEGURA: Restaurar interrupciones
    __enable_irq(); 
    
    return pude_tomarlo;
}

/**
 * @brief Suelta la llave del Mutex para que otros lo puedan usar.
 */
void Mutex_Unlock(Mutex_t *mutex) {
    mutex->locked = false; 
    // Un simple booleano a falso es atómico por naturaleza en ARM de 32 bits
}

// =========================================================================
// 2. COLA DE DELEGACIÓN (Job Queue) PARA EEPROM
// =========================================================================

#define EEPROM_QUEUE_SIZE 10

typedef struct {
    uint16_t direccion;
    uint32_t dato;
} EEPROM_Job_t;

// Ring Buffer (Cola Circular) para los trabajos pendientes
volatile EEPROM_Job_t eeprom_queue[EEPROM_QUEUE_SIZE];
volatile uint8_t eq_head = 0; // Por donde inserta la Interrupción
volatile uint8_t eq_tail = 0; // Por donde saca el Main

/**
 * @brief La interrupción (Ej. Modbus) llama a esta función para delegar 
 *        el guardado. Es ultra-rápida (RAM pura) y jamás bloquea.
 */
bool eeprom_encolar_escritura(uint16_t dir, uint32_t valor) {
    uint8_t next_head = (eq_head + 1) % EEPROM_QUEUE_SIZE;
    
    // Si la cola está llena, rechazamos el trabajo (Buffer Overflow)
    if (next_head == eq_tail) return false;
    
    eeprom_queue[eq_head].direccion = dir;
    eeprom_queue[eq_head].dato = valor;
    
    eq_head = next_head;
    return true;
}

/**
 * @brief El Main llama a esta función incesantemente para vaciar la cola.
 *        Solo accede al hardware I2C si el Mutex está libre.
 */
void eeprom_procesar_cola_tick(void) {
    
    // Si hay trabajos pendientes en la RAM...
    if (eq_head != eq_tail) {
        
        // Pedimos la llave del hardware I2C
        if (Mutex_TryLock(&i2c_bus_mutex)) {
            
            // ¡Tenemos la llave! Extraemos el trabajo
            uint16_t dir = eeprom_queue[eq_tail].direccion;
            uint32_t val = eeprom_queue[eq_tail].dato;
            
            // ----------------------------------------------------
            // EJECUCIÓN FÍSICA BLOQUEANTE
            // I2C_Escribir_Bloqueante(dir, val); 
            // ----------------------------------------------------
            
            // Avanzamos la cola
            eq_tail = (eq_tail + 1) % EEPROM_QUEUE_SIZE;
            
            // ¡VITAL! Soltamos la llave para no asfixiar a otros módulos (Ej. LCD)
            Mutex_Unlock(&i2c_bus_mutex);
        }
    }
}

// =========================================================================
// EJEMPLO DE FLUJO CONCURRENTE
// =========================================================================

// Simulación de la Rutina de Recepción RS485 Modbus (ISR/DMA IDLE)
void USART2_Modbus_ISR(void) {
    // ... Parsea el mensaje (Día 139) ...
    // Si el Servidor Central ordena actualizar el Precio del Gas:
    
    uint16_t direccion_precio = 0x0010;
    uint32_t nuevo_precio = 1055;
    
    // EN LUGAR DE: I2C_Escribir_Bloqueante(...) que causaría un colapso...
    // DELEGAMOS:
    eeprom_encolar_escritura(direccion_precio, nuevo_precio);
    
    // Salimos de la interrupción en nanosegundos
}

int main(void) {
    
    // inicializar_hardware();
    
    while(1) {
        
        // 1. Despachador atiende la LCD (También usa I2C)
        // Solo actualiza la pantalla si el Mutex está libre
        if (bandera_actualizar_pantalla) {
            if (Mutex_TryLock(&i2c_bus_mutex)) {
                // LCD_Imprimir("Precio: 10.55");
                Mutex_Unlock(&i2c_bus_mutex);
                bandera_actualizar_pantalla = false;
            }
        }
        
        // 2. Procesamos 1 trabajo de EEPROM si hay pendientes
        eeprom_procesar_cola_tick();
        
        // 3. Dormimos la CPU para ahorrar energía (Día 144)
        // __WFI();
    }
    
    return 0;
}
        

🚀 Avance del Proyecto Tesis: Despachador de Gas LP

Objetivo del día: Sincronización Bare-Metal y Erradicación de Deadlocks.

Un error clásico al presentar una tesis de ingeniería es entregar una placa que funciona perfecto frente a los jueces durante 5 minutos, pero que en el campo real se congela "misteriosamente" una vez a la semana. Estos "misterios" en el 90% de los casos son **Condiciones de Carrera (Race Conditions)**: interrupciones colisionando con el flujo principal por el acceso a un mismo cable de datos (I2C/SPI).

Al implementar el **Mutex Atómico** y la **Job Queue** (Cola de Delegación), has aislado completamente el motor de hardware de las interrupciones asíncronas del mundo exterior. Si un camión gasero se conecta a tu Despachador y le inyecta 50 comandos Modbus por segundo, tu máquina no colapsará tratando de escribir en la memoria. Simplemente encolará los 50 trabajos en la RAM en fracciones de nanosegundo, y tu Súper Bucle, con la paciencia y cadencia que le caracteriza, tomará el Mutex, escribirá el dato 1, soltará el Mutex, actualizará la pantalla LCD, volverá a tomar el Mutex y escribirá el dato 2. Tráfico digital perfecto, sin necesidad de incorporar las sobrecargas de memoria de un sistema operativo FreeRTOS.

📝 Resumen del Día