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:
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á.
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;
}
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.
NVIC_SystemReset().