DÍA 150 DE 365

Fase 3: Comunicaciones y Redes Industriales

📖 Teoría a Estudiar

La Escena del Crimen: El Stack Frame

Cuando la CPU sufre un error fatal, sabe que va a perder el control. En sus últimos nanosegundos de lucidez, el procesador hace algo increíble: agarra sus 8 registros internos más importantes y los empuja (Pushes) hacia la memoria RAM (El Stack/Pila) formando un Stack Frame. Luego de guardar esta foto instantánea, salta a la función HardFault_Handler.

Los 8 registros guardados son:

Extracción mediante Ensamblador y Polling I2C

Dado que el compilador de C altera el Stack al entrar a una función, no podemos escribir el `HardFault_Handler` en C puro. Debemos usar Ensamblador (Assembly) para capturar el Puntero de Pila (Stack Pointer) virgen y luego pasárselo a C.

Una vez que el código C tiene el puntero, extraerá el registro PC y el LR y los enviará a la memoria EEPROM. Advertencia Crítica: Como estamos dentro de un fallo de sistema masivo, las interrupciones normales (SysTick, DMA) están congeladas. El driver I2C para guardar en la EEPROM debe estar escrito en modo Polling Bloqueante (sin depender de interrupciones), o de lo contrario el guardado fallará.

ARQUITECTURA POST-MORTEM: EXTRACCIÓN DEL STACK FRAME CPU (HardFault) *puntero_nulo = 5; CRITICAL ERROR! Auto-Push RAM (Stack Pila) SP[7]: xPSR SP[6]: PC (Falla) SP[5]: LR (Origen) SP[4]: R12 SP[3]: R3 SP[2]: R2 SP[1]: R1 SP[0]: R0 Stack Pointer (Se lo pasamos a C) Analizador en Lenguaje C uint32_t pc = stack[6]; uint32_t lr = stack[5]; eeprom_escribir(pc, lr); I2C (Bloqueante) EEPROM 0xA144, 0x0802B NVIC_SystemReset() -> Reboot

⚙️ Ejercicios Prácticos

Ejercicio 1: Reemplazaremos el manejador genérico de errores (que es un simple while(1); infinito). Utilizaremos un envoltorio Naked en Ensamblador para que el compilador no altere la RAM, y deduciremos si se usó el Main Stack (MSP) o el Process Stack (PSP). Pasaremos este puntero a una función en C que extraerá los registros y los escribirá en una zona reservada de la EEPROM o en los Backup Registers antes de suicidar el sistema.


/**
 * @file hardfault_logger.c
 * @brief Caja Negra (Flight Recorder) para volcado Post-Mortem del CPU
 */

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

// Direcciones reservadas en EEPROM / BKP (Día 142)
#define ADDR_BKP_LOG_PC  RTC->BKP10R
#define ADDR_BKP_LOG_LR  RTC->BKP11R
#define ADDR_BKP_STATUS  RTC->BKP12R
#define CRASH_FLAG       0xDEADDEAD

/**
 * @brief Manejador C del HardFault (Recibe el puntero de la pila)
 * @param sp Puntero al arreglo de 8 variables guardado en RAM por el hardware
 */
void HardFault_Handler_C(uint32_t *sp) {
    
    // Extracción de la "Escena del Crimen" desde el Stack Frame
    uint32_t r0  = sp[0];
    uint32_t r1  = sp[1];
    uint32_t r2  = sp[2];
    uint32_t r3  = sp[3];
    uint32_t r12 = sp[4];
    uint32_t lr  = sp[5]; // Link Register (Quién nos llamó antes de fallar)
    uint32_t pc  = sp[6]; // Program Counter (La línea exacta de código que falló)
    uint32_t psr = sp[7];
    
    (void)r0; (void)r1; (void)r2; (void)r3; (void)r12; (void)psr; // Omitir warnings
    
    // ====================================================
    // SALVAMENTO DE EMERGENCIA (Bóveda BKP)
    // Usamos el BKP porque el I2C/EEPROM podría requerir interrupciones,
    // y en un HardFault el NVIC está bloqueado.
    // ====================================================
    
    // 1. Romper el seguro del BKP (Día 142)
    RCC->APB1ENR |= RCC_APB1ENR_PWREN;
    PWR->CR |= PWR_CR_DBP;
    
    // 2. Grabar los registros letales
    ADDR_BKP_LOG_PC = pc;
    ADDR_BKP_LOG_LR = lr;
    
    // 3. Levantar la bandera de desastre para el próximo arranque
    ADDR_BKP_STATUS = CRASH_FLAG;
    
    // ====================================================
    // REINICIO FORZADO DEL SISTEMA (No nos quedamos colgados)
    // ====================================================
    NVIC_SystemReset(); 
}

/**
 * @brief Envoltorio en Ensamblador ARM (Naked) para interceptar el Stack Pointer.
 *        Remplaza al HardFault_Handler por defecto provisto por ST.
 */
__attribute__((naked)) void HardFault_Handler(void) {
    __asm volatile (
        // Testeamos el bit 2 del Link Register (EXC_RETURN)
        // para saber si el sistema usó el Main Stack o el Process Stack.
        " TST LR, #4 \n" 
        " ITE EQ \n" 
        " MRSEQ R0, MSP \n" // Si es 0, el SP está en MSP, guardarlo en R0
        " MRSNE R0, PSP \n" // Si no, el SP está en PSP, guardarlo en R0
        
        // R0 ahora contiene el puntero a la pila. 
        // Llamamos a la función C (R0 se pasa automáticamente como el 1er argumento)
        " B HardFault_Handler_C \n" 
    );
}

int main(void) {
    
    // 1. INICIALIZACIÓN
    // inicializar_relojes();
    // habilitar_acceso_backup_domain();
    
    // 2. LECTURA DE LA CAJA NEGRA EN EL ARRANQUE
    if (ADDR_BKP_STATUS == CRASH_FLAG) {
        
        uint32_t fallo_en_pc = ADDR_BKP_LOG_PC;
        uint32_t llamado_por = ADDR_BKP_LOG_LR;
        
        // El procesador se reinició debido a una falla fatal.
        // Convertimos las direcciones de memoria a texto y las enviamos al Servidor.
        // LOG_ERROR_CRITICO("Sistema recuperado de HardFault", fallo_en_pc);
        
        // Limpiamos la bandera para no reportarlo en el siguiente reinicio normal
        ADDR_BKP_STATUS = 0;
    }
    
    while(1) {
        // Ejecución normal de la máquina...
        // Modbus, Sensor de Gas, Impresora.
    }
    
    return 0;
}
        

🚀 Avance del Proyecto Tesis: Despachador de Gas LP

Objetivo del día: Trazabilidad de Fallos de Sistema y Diagnóstico Remoto.

Imagina que tras instalar el nuevo Firmware OTA del Día 147, uno de tus Despachadores en la estación de Monterrey empieza a reiniciarse aleatoriamente cada ciertas horas. Si el código fuente original tuviera el `while(1)` infinito por defecto, la máquina se congelaría y tendrías que volar a Monterrey con un ST-LINK y una laptop para conectarte físicamente al procesador en el patio de gas e intentar reproducir el fallo. Una pesadilla logística.

Al implementar el **HardFault Logger Post-Mortem**, tu máquina captura el desastre automáticamente. Salva el `Program Counter (PC)` en la bóveda de la batería `VBAT`, y realiza un reinicio de sistema (Soft Reboot). Cuando la máquina vuelve a despertar, detecta la bandera `0xDEADDEAD`, lee el `PC`, lo formatea usando el Logger de Telemetría (Día 131) y envía por la red RS485 Modbus el mensaje: *"Error Crítico en 0x080045A2"*. Desde la comodidad de tu oficina, compilas tu código, revisas el archivo `.map` o la instrucción ensambladora exacta que vive en `0x080045A2`, descubres que hay un arreglo desbordado en la función de la impresora, haces la corrección en C, y envías el parche OTA solucionando el problema globalmente en 5 minutos.

📝 Resumen del Día