Tolerancia a Fallos y Software Crítico: Cuando el Bug no es una Opción

Introducción: El costo de un crash en el mundo real

Hola estimados lectores de este blog ingenieril, quise hacer un artículo de Toyota y un error grave que cometió hace algunos años en algunos de sus autos, pero como el tema es complejo, primero quise hacer este artículo explicando qué es la tolerancia a fallos, el software crítico y cómo los sistemas deben ser seguros y con máxima confiabilidad. Pero primero hagamos un ejercicio de imaginación…

En el software tradicional, un error crítico (también llamado bug) suele terminar en un log de excepciones, un código de estado HTTP 500 o una alerta en tu sistema de monitoreo. Si tu API de pagos se cae durante tres segundos, es un problema molesto y potencialmente problemático, sí, pero imagina lo siguiente: lo que falla no es una API de pagos sino que el código que gestiona el control de estabilidad de un automóvil falla durante tres segundos… allí las consecuencias son físicas o fatales.

En la ingeniería de sistemas críticos (safety-critical systems), asumimos una premisa fundamental: el hardware fallará, la memoria se corromperá y el entorno físico interferirá.

Por ello, la meta del desarrollo embebido crítico no es solo escribir código "sin bugs", sino diseñar arquitecturas tolerantes a fallos capaces de detectar sus propios errores y volver a un estado seguro antes de que ocurra un accidente.

1. El enemigo invisible: Interferencias y Bit Flips

A diferencia de un servidor en un centro de datos con temperatura controlada, un microcontrolador automotriz o industrial opera en un entorno hostil: vibración, cambios drásticos de temperatura e interferencias electromagnéticas (EMI).

Un pulso electromagnético generado por el motor de arranque o el encendido de un motor puede inducir pequeñas variaciones de voltaje en las líneas de la memoria RAM. Esto provoca lo que en la industria se conoce como un SEU (Single Event Upset) o simplemente un Bit Flip: un 0 se convierte en 1 (o viceversa) en la memoria física del chip.

Memoria RAM Normal:        0 0 0 0 0 0 0 0  (Valor: 0 - Acelerador Cerrado)
Bit Flip por radiación/EMI: 0 0 0 0 0 0 0 1  (Valor: 1 - Acelerador Abierto)

Si ese bit alterado pertenecía a una variable global que indicaba si el pedal de aceleración estaba presionado, el software actuará en consecuencia, creyendo legítimamente que el usuario pisó el pedal.

¿Cómo nos defendemos?

  1. Memoria ECC (Error-Correcting Code): El hardware incluye bits adicionales de paridad para detectar y corregir automáticamente alteraciones en la RAM.
  2. Redundancia por Software: Guardar variables críticas por duplicado o triplicado (o almacenar su complemento XOR) y verificar que coincidan antes de tomar una decisión física.

2. El perro guardián: Watchdog Timers (WDT)

Un Watchdog Timer es un temporizador por hardware, independiente del procesador principal, cuyo único trabajo es contar hacia atrás desde un valor determinado (por ejemplo, 100 milisegundos).

Si el contador llega a cero, el hardware del Watchdog fuerza un reset eléctrico del microcontrolador para reiniciar el sistema.

Para evitar que el sistema se reinicie, el software debe "alimentar al perro" (kick the watchdog) continuamente, reiniciando el contador antes de que expire.

El peligro de los Watchdogs ingenuos

Un error de diseño muy común es colocar la instrucción de "alimentar al watchdog" dentro de una interrupción periódica por timer o en una tarea secundaria con prioridad baja.

Si la tarea principal (por ejemplo, la que calcula la trayectoria o lee los sensores) se congela o muere por un desbordamiento de memoria, la interrupción del timer puede seguir ejecutándose alegremente y continuar alimentando al watchdog.

El procesador seguirá vivo, el watchdog creerá que todo está bien, pero el sistema estará "zombie" e incapaz de responder. Un verdadero Task-Level Watchdog exige que todas las tareas críticas confirmen que están vivas y saludables antes de resetear el temporizador de hardware.

3. Fail-Safe y Fail-Operational: La estrategia ante la falla

Cuando el sistema detecta que una variable está corrupta o que un sensor entregó un valor físicamente imposible, entra en juego el concepto de Fail-Safe (Fallo Seguro).

  • Fail-Safe: Si ocurre un fallo indebidamente grave, el sistema entra en un estado predecible que minimiza el daño, aunque pierda funcionalidad.
    • Ejemplo: Si el software de una caldera detecta una lectura errónea de temperatura, corta el paso de gas por completo y enciende un LED de error.
  • Fail-Operational: Se utiliza en aviación o conducción autónoma donde "apagar todo" no es una opción segura. Requiere múltiples canales de procesamiento en paralelo (redundancia modular triple) para votar y continuar operando incluso si un microcontrolador completo falla.

¿Qué significa "votar"?
Es una validación por mayoría. Un circuito compara en milisegundos los datos de los tres procesadores, adopta el resultado idéntico (regla del "2 contra 1") y descarta de inmediato el erróneo.

En automoción, el mecanismo Fail-Safe clásico para el acelerador electrónico es desactivar la corriente del motor del acelerador para que un resorte mecánico fuerce la mariposa a cerrarse por defecto.

4. Estándares de Codificación: El caso de MISRA C

En el desarrollo de software convencional confiamos en linters o guías de estilo para mantener la legibilidad. En la industria automotriz y aeroespacial, los estándares de código existen para prevenir comportamientos indefinidos (Undefined Behavior) en C.

El estándar más famoso es MISRA C (creado por la Motor Industry Software Reliability Association).

MISRA C no es una recomendación estética; es un subconjunto restringido del lenguaje C que prohíbe expresamente características peligrosas:

  1. Sin asignación dinámica de memoria (malloc / free prohibidos): Evita la fragmentación de la RAM y la posibilidad de que un puntero nulo cuelgue el sistema.
  2. Sin recursión: Garantiza que el tamaño máximo del stack se pueda calcular de forma exacta antes de compilar.
  3. Restricción severa de variables globales: Obliga a encapsular el estado para evitar lecturas/escrituras concurrentes no coordinadas.
  4. Verificación de valores de retorno: No se permite ignorar el resultado de ninguna función o llamada a registros de hardware.

Conclusión: El lienzo perfecto para el caso Toyota

Dominar estos conceptos nos permite entender la diferencia entre un código que "funciona en el camino feliz" y un código diseñado para no matar a nadie cuando el mundo real falla.

Cuando revisas la teoría, los principios parecen claros:

  • Evitar el desbordamiento de pila.
  • No depender ciegamente de variables globales compartidas.
  • Implementar Watchdogs que monitoreen la salud real de cada hilo.
  • Diseñar un hardware con redundancia que imponga un Fail-Safe mecánico.

Sin embargo, a finales de la década de 2000, uno de los fabricantes de automóviles más grandes e importantes del mundo se enfrentó a un problema masivo de aceleración involuntaria. Cuando los peritos forenses de software abrieron el firmware del sistema ETCS-i de Toyota, descubrieron que prácticamente cada una de estas reglas había sido violada.

En el próximo y último artículo de esta serie, analizaremos en detalle el caso Toyota Throttle-gate: el análisis de código fuente que cambió para siempre la historia del software embebido.

Si esta entrada te ha gustado crack, ya sabes compártela!

Créditos de imagen de portada: Foto de Sam Loyd en Unsplash

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *