DÍA 188 DE 365

Fase 4: FreeRTOS y Conectividad IoT

📖 Teoría a Estudiar

El Espejismo de la Instrucción Atómica

Cuando escribes dinero = dinero + 1; en lenguaje C, asumes que la acción ocurre en un solo paso instantáneo. Pero el compilador traduce esa línea a 3 instrucciones en lenguaje Ensamblador (Assembly) para el procesador ARM Cortex:

  1. LDR (Load): Extrae el valor actual de `dinero` desde la memoria RAM hacia un registro interno de la CPU (Ej. `R1`).
  2. ADD (Add): Suma 1 al registro interno `R1` usando la Unidad Aritmético Lógica (ALU).
  3. STR (Store): Escribe el nuevo valor del registro `R1` de vuelta en la memoria RAM.

El Impacto de la Preempción

Una Condición de Carrera ocurre cuando dos tareas comparten la misma variable global. Imagina que la Tarea A extrae el valor `500` a su registro y le suma 1 (ahora tiene `501` en su cabeza). Pero justo antes de ejecutar la instrucción STR (Store) para guardarlo, ¡PUM! El Tick de FreeRTOS dispara un Cambio de Contexto.

La Tarea B entra a la CPU. Lee la memoria RAM original, la cual sigue valiendo `500`. Le suma 1 (`501`) y la guarda en la RAM. Luego, el RTOS le devuelve el control a la Tarea A. Esta despierta, recuerda que en su registro tenía el número `501` y lo guarda en la RAM.

¡El desastre ocurrió! Ambas tareas sumaron 1 (el resultado debió ser `502`), pero la RAM terminó valiendo `501`. Un dato entero ha sido borrado del universo.

ANATOMÍA DE UNA CONDICIÓN DE CARRERA (RACE CONDITION) TAREA A (Reg. R1) Memoria RAM Compartida TAREA B (Reg. R2) TIEMPO → litros = 10 1. LDR R1, RAM R1 = 10 2. ADD R1, #1 R1 = 11 CONTEXT SWITCH! (Tarea A pausada antes de guardar) 1. LDR R2, RAM R2 = 10 2. ADD R2, #1 R2 = 11 litros = 11 CONTEXT SWITCH! 3. STR R1, RAM litros = 11 ERROR FATAL! Dos incrementos ocurrieron, pero el resultado es 11 en vez de 12.

⚙️ Ejercicios Prácticos

Ejercicio 1: Vamos a escribir un código malicioso (anti-patrón) para probar este concepto empíricamente en nuestro simulador. Crearemos una variable global dinero_total. Crearemos dos Hilos (Manguera A y Manguera B). Ambos intentarán sumarle 1 peso a la variable de manera simultánea 1,000,000 de veces. Matemáticamente, el resultado debería ser $2,000,000. Pero debido a la Condición de Carrera, veremos cómo el dinero se desvanece en el aire.


/**
 * @file race_condition.c
 * @brief Demostración destructiva de acceso concurrente no protegido (Data Tearing)
 */

#include "stm32f401xe.h"
#include "cmsis_os2.h"
#include <stdio.h>

// =======================================================
// EL RECURSO COMPARTIDO (Vulnerable)
// =======================================================
// 'volatile' evita que el compilador optimice la variable en un registro, 
// forzando siempre la lectura/escritura a la memoria RAM.
volatile uint32_t dinero_total = 0;

// Cantidad de incrementos que hará cada manguera
#define CICLOS_DE_VENTA 1000000

// =======================================================
// HILO 1: MANGUERA A
// =======================================================
void Thread_MangueraA(void *argument) {
    for (uint32_t i = 0; i < CICLOS_DE_VENTA; i++) {
        
        // ¡ZONA DE PELIGRO!
        // Esta línea es un Read-Modify-Write.
        // Si el Kernel hace preempción aquí a la mitad, el dato se corrompe.
        dinero_total++; 
        
    }
    // Hilo terminado, se suspende
    osThreadSuspend(osThreadGetId());
}

// =======================================================
// HILO 2: MANGUERA B
// =======================================================
void Thread_MangueraB(void *argument) {
    for (uint32_t i = 0; i < CICLOS_DE_VENTA; i++) {
        
        // La misma variable, el mismo peligro.
        dinero_total++; 
        
    }
    // Hilo terminado, se suspende
    osThreadSuspend(osThreadGetId());
}

// =======================================================
// INICIALIZACIÓN
// =======================================================
int main(void) {
    SystemClock_Config();
    osKernelInitialize();
    
    // ATRIBUTOS: A propósito, les daremos EXACTAMENTE la misma prioridad.
    // Esto forzará al Kernel de FreeRTOS a hacer "Round-Robin" (turnarlos a la fuerza)
    // cada 1 milisegundo (Tick), incrementando brutalmente la probabilidad de chocar.
    const osThreadAttr_t attr_mangueras = {
        .stack_size = 512,
        .priority = osPriorityNormal, 
    };
    
    osThreadNew(Thread_MangueraA, NULL, &attr_mangueras);
    osThreadNew(Thread_MangueraB, NULL, &attr_mangueras);
    
    osKernelStart();
    
    // RESULTADO EMPÍRICO ESPERADO EN PRODUCCIÓN:
    // Si imprimes 'dinero_total' al final, NO será 2,000,000.
    // Terminará siendo un número impredecible y errático como 1,345,821.
    // Has perdido cientos de miles de pesos debido a la sobreescritura de contexto.
    
    while (1);
}
        

🚀 Avance del Proyecto Tesis: Despachador de Gas LP

Objetivo del día: Identificación Temprana de Fallas de Arquitectura.

El peor error que puede tener un software no es uno que hace que la máquina se reinicie (eso es fácil de notar y arreglar). El peor error es un Fallo Silencioso. Una Condición de Carrera no lanza pantallas azules, no activa alarmas de hardware ni arroja errores de compilador. Simplemente, los datos se desvanecen.

Imagínate que tu Despachador de Gas LP atiende a dos vehículos mediante un modelo de doble manguera. El Hilo de la Manguera A procesa $15.00 MXN en gas, y en el exacto instante en que está sumando el dinero al `Corte_de_Caja_Total`, el Hilo de la Manguera B lo interrumpe y suma $20.00 MXN. Si ocurre la Condición de Carrera, la RAM sobrescribirá el total y desaparecerán $15.00 de la contabilidad de la gasolinera. Esto te metería en problemas legales severos de metrología. Hoy aprendiste a diagnosticar la enfermedad; mañana (Día 189) inventaremos la cura usando candados criptográficos llamados Mutexes.

📝 Resumen del Día