01 · El oficio
Depurar es un método, no una corazonada
El error más caro al depurar es cambiar cosas al azar hasta que «anda»: se pierde tiempo, se agregan defectos nuevos y, cuando por fin deja de fallar, nadie sabe por qué, de modo que probablemente vuelva. Depurar bien es aplicar un método científico en miniatura, un ciclo que se repite hasta explicar el síntoma:
- Reproducir. Un error que no se puede provocar a voluntad no se puede estudiar. Hay que encontrar las condiciones que lo disparan (una secuencia de entradas, una carga, una temperatura, un tiempo de funcionamiento) y, si es posible, automatizarlas. Un error esporádico se vuelve tratable cuando se logra que ocurra una de cada diez veces en lugar de una de cada diez mil.
- Observar sin perturbar. Antes de teorizar, reunir hechos: qué pasa exactamente, cuándo, con qué datos. Una observación precisa vale más que una teoría brillante.
- Formular una hipótesis concreta y refutable: «la interrupción del UART se pierde cuando el temporizador la retrasa más de 300 µs», no «algo anda mal con las interrupciones».
- Diseñar un experimento que pueda refutarla, cambiando una sola cosa por vez. Si el resultado la contradice, se descarta; si la confirma, se intenta refutarla de otra manera antes de darla por cierta.
- Corregir la causa, no el síntoma, y comprobar que el síntoma desaparece y que no apareció otro. Anotar lo que se aprendió (y agregar una prueba que habría detectado el error).
Hay una dificultad propia de los sistemas embebidos que condiciona la elección de las herramientas: observar cambia el sistema. Agregar una línea para imprimir un valor modifica el tiempo de ejecución, el uso de la pila y la distribución de la memoria. En un programa de tiempo real, eso puede hacer desaparecer el error (un error que se esfuma cuando se lo mira se llama, con humor, heisenbug, por el principio de Heisenberg) o crear uno nuevo. Por eso las técnicas de este tema se ordenan por su intrusividad, y lo habitual es empezar por las menos intrusivas:
| Técnica | Qué se ve | Costo en el programa | Cuándo conviene |
|---|---|---|---|
| LED | Un estado o un código de pocas posibilidades; que el equipo sigue vivo | Casi nulo | Primera señal de vida; errores graves sin ninguna otra salida |
| Pin de prueba + analizador lógico | Tiempos con precisión de microsegundos o mejor: duración de una ISR, latencia, orden de eventos, protocolos | Una escritura de un registro (pocos ciclos) | Problemas de tiempo y de concurrencia: la técnica más fiel |
| Registro de eventos en RAM | La secuencia reciente de lo que hizo el programa, con marcas de tiempo | Decenas de ciclos por evento | Errores que ocurren una vez cada tanto; análisis posterior a un cuelgue |
| Consola por la UART (printf) | Texto con valores y mensajes | Milisegundos si bloquea; poco si es diferida | Código fuera de las rutinas de tiempo crítico, con un buffer |
| Depurador (tema 45) | Cualquier variable, registro y punto del programa | Detiene el programa | Lógica que no depende del tiempo; análisis de un cuelgue |
02 · El instrumento más barato
LEDs: lo más simple que funciona
Un LED es la salida más barata de un sistema, y a veces la única disponible: no hace falta una UART configurada ni un conector. Sirve para tres cosas, con un grado creciente de información:
- Latido (heartbeat): un LED que cambia de estado cada medio segundo desde el programa principal. Si parpadea, el bucle principal sigue corriendo; si se congela, el programa se colgó. Es la señal de vida más elemental, y la primera que conviene tener en cualquier proyecto.
- Estado: con varios LED, mostrar en qué estado está el sistema (esperando, midiendo, transmitiendo, error). Útil para seguir el flujo a simple vista.
- Código de parpadeos: un número de destellos seguido de una pausa larga informa un código de error con un solo LED: tres destellos y pausa significa «error 3». Un técnico en el campo sin ninguna herramienta puede leerlo.
/* Latido: se llama en cada vuelta del bucle principal. Si el LED se congela, el bucle se colgó. */
void latido_tick(void) {
static uint32_t t0;
if ((int32_t)(ahora_ms() - t0) >= 500) { t0 = ahora_ms(); led_alternar(); }
}
/* Código de parpadeos sin bloquear: N destellos de 200 ms y una pausa de 1,5 s, que se repite */
static uint8_t codigo_pendiente, destello; static uint32_t t_led; static bool led_encendido;
void falla_codigo(uint8_t n) { codigo_pendiente = n; destello = 0; led_encendido = false; t_led = ahora_ms(); }
void falla_tick(void) { /* se llama desde el bucle principal, sin esperas */
if (codigo_pendiente == 0) return;
uint32_t espera = led_encendido ? 200 : (destello < codigo_pendiente ? 200 : 1500);
if ((int32_t)(ahora_ms() - t_led) < (int32_t)espera) return;
t_led = ahora_ms();
if (led_encendido) { led_apagar(); led_encendido = false; destello++; }
else if (destello < codigo_pendiente) { led_encender(); led_encendido = true; }
else { destello = 0; } /* terminó la pausa: empezar otra vez */
}
Dos advertencias sobre el latido. Primera: dónde se alterna importa. Si el LED lo cambia la interrupción de un temporizador, seguirá parpadeando aunque el programa principal esté colgado, y el latido mentirá (es el mismo error de alimentar un watchdog desde una ISR, tema 38). Se alterna en el camino que se quiere vigilar. Segunda: un LED consume (varios mA, tema 33) y en un equipo a batería no puede quedar encendido, ni parpadeando seguido, en el producto final: se deshabilita con una macro de compilación o se lo reduce a destellos cortos y espaciados.
03 · Mirar el tiempo
Pines de prueba y analizador lógico
La técnica más valiosa de la depuración sin depurador es la de los pines de prueba: se elige un pin de salida libre, se lo pone en alto al entrar a la rutina o a la sección que interesa y en bajo al salir, y se observa con un analizador lógico (o un osciloscopio). Es la técnica ideal para los problemas de tiempo porque casi no cambia lo que mide: cambiar un pin cuesta una escritura a un registro, uno o dos ciclos de reloj, tiempo despreciable frente a una ISR de decenas de microsegundos. Con un pin se mide, con precisión de microsegundos:
- La duración de una ISR o de una función: el ancho del pulso (tema 18).
- La latencia de una interrupción: la distancia entre el evento externo (otro pin) y el comienzo del pulso.
- El orden de los eventos: con varios pines (uno por tarea, por ISR, por sección crítica), el analizador muestra una línea de tiempo con todo en paralelo, y de un vistazo se ven interrupciones que se pisan, tareas que no corren o secciones críticas demasiado largas (tema 16).
- El estado de una máquina de estados: con tres o cuatro pines se codifica el número de estado en binario, y el analizador lo muestra como un bus (tema 24).
/* Pines de prueba en un puerto de salida: una sola escritura atómica (STM32: BSRR sube con los bits bajos y baja con los altos) */
#define DBG_ALTO(n) (GPIOB->BSRR = (1u << (n)))
#define DBG_BAJO(n) (GPIOB->BSRR = (1u << ((n) + 16u)))
#define DBG_ISR_TIM 0 /* PB0: dura lo que dura la ISR del temporizador */
#define DBG_SENSOR 1 /* PB1: dura lo que dura la lectura del sensor */
#define DBG_CRITICA 2 /* PB2: dura lo que están deshabilitadas las interrupciones */
#define DBG_ERROR 3 /* PB3: un pulso cada vez que se detecta una condición de error */
void TIM2_IRQHandler(void) {
DBG_ALTO(DBG_ISR_TIM);
borrar_bandera_tim2();
atender_temporizador();
DBG_BAJO(DBG_ISR_TIM);
}
/* Con el analizador: el ancho del pulso de PB0 es la duración de la ISR; la separación entre pulsos, su período; si falta un pulso, se perdió una interrupción. */
La Parte 1 del programa de Python analiza la captura de un analizador lógico de un pin que marca la ISR de un temporizador de 1 ms (60 interrupciones). Con solo las marcas de tiempo de los flancos se obtienen cosas que a ojo no se ven: un período mediano de 1000,5 µs, una duración mediana de la ISR de 120,1 µs (una carga del 12,0 %), y dos anomalías escondidas entre las 60 ejecuciones: la ISR número 45 duró 900 µs, siete veces más que las demás (y es un candidato a ser investigado: ¿qué hizo diferente?), y la ISR número 37 empezó con 399 µs de retraso (los períodos de 1400 y de 600 µs alrededor de ella lo delatan), algo propio de una sección crítica larga que mantuvo deshabilitadas las interrupciones. Ninguna de las dos cosas se vería con un printf, que además habría causado parte del problema.
| Instrumento | Qué hace bien | Límites |
|---|---|---|
| Analizador lógico (de 8 o 16 canales; los económicos por USB, con programas como PulseView/sigrok) | Muchos canales a la vez, captura larga, decodificación de protocolos (UART, I²C, SPI) y exportación de los datos para analizarlos con un programa | Solo niveles lógicos: no ve la forma analógica de la señal ni el ruido; la frecuencia de muestreo limita el detalle |
| Osciloscopio | La forma real de la señal: tiempos de subida, rebotes, ruido, hundimientos de la alimentación (tema 36) | Pocos canales (2 o 4); captura corta |
| Multímetro | Tensiones continuas, continuidad, resistencias: la comprobación rápida de que algo está donde debe | No ve nada rápido ni intermitente |
Una regla para elegir la frecuencia de muestreo de un analizador: debe ser bastante mayor que la señal más rápida que interesa. Para medir el ancho de un pulso con un error del 1 %, el intervalo entre muestras debe ser el 1 % del ancho: para la ISR de 120 µs, 1,2 µs o menos (cerca de 1 MS/s). Para ver un pulso espurio (glitch) de 100 ns, hacen falta decenas de MS/s. Con una frecuencia de muestreo baja, un pulso corto simplemente no aparece, y uno mira una captura limpia de un sistema que tiene un problema (el mismo efecto de muestreo que vimos en el tema 36).
Y una recomendación para el diseño del hardware, que se agradece al depurar: dejar puntos de prueba en la placa (un pin libre del microcontrolador, aunque sea con un conector sin poblar, y accesos a la masa, la alimentación, el reset y las líneas de comunicación) y, siempre que se pueda, un conector para el depurador (tema 45). Una placa sin ninguno de ellos obliga a soldar cables sobre los pines de un componente de paso fino, y cada cable es una fuente de errores nuevos.
04 · Hablarle al programador
La consola por la UART
Imprimir texto por una UART conectada a una PC (con un adaptador USB-serie) es la técnica más familiar, y por eso la más usada y la más abusada. Tiene un valor real: permite ver valores, mensajes y secuencias en un formato legible. Y un costo que se ignora por costumbre: enviar los bytes lleva tiempo, y el programa suele esperar a que salgan. La Parte 2 del programa calcula cuánto tarda una línea, a 10 bits por byte (inicio, 8 de datos y parada):
| Velocidad | 20 caracteres | 40 caracteres | 80 caracteres |
|---|---|---|---|
| 9600 baudios | 20 833 µs | 41 667 µs | 83 333 µs |
| 115 200 baudios | 1736 µs | 3472 µs | 6944 µs |
| 921 600 baudios | 217 µs | 434 µs | 868 µs |
Una línea de 40 caracteres a 115 200 baudios bloquea 3,5 milisegundos. Dentro de una interrupción de 1 ms, eso significa que la ISR dura más que su período y se pierden interrupciones; en un bucle de control de 5 ms, que la mitad del tiempo se va en imprimir. Imprimir cambia el sistema que se quiere observar, y la medición, además de lenta, es engañosa. Además, printf con coma flotante consume mucha pila y flash (temas 29 y 32), y puede ser la causa de un desborde de pila que no existía antes de agregarlo.
La solución es el registro diferido: el programa no espera a la UART. Escribe el mensaje en un buffer circular en RAM (unos microsegundos) y es la interrupción de transmisión de la UART, o el DMA, la que va sacando los bytes en segundo plano. El costo en el camino crítico pasa de milisegundos a microsegundos:
/* Registro diferido: el mensaje va a un buffer; la interrupción de la UART lo envía mientras el programa sigue */
#define LOG_TAM 512u /* potencia de dos */
static uint8_t log_buf[LOG_TAM];
static volatile uint16_t log_cab, log_col; /* cabeza: donde escribe el programa; cola: de donde saca la UART */
static volatile uint16_t log_perdidos;
void log_bytes(const char *s, size_t n) {
uint32_t pm = __get_PRIMASK(); __disable_irq(); /* varios contextos pueden registrar: sección crítica corta (tema 16) */
for (size_t i = 0; i < n; i++) {
if ((uint16_t)(log_cab - log_col) == LOG_TAM) { log_perdidos++; break; } /* buffer lleno: se descarta y se CUENTA */
log_buf[log_cab++ & (LOG_TAM - 1)] = (uint8_t)s[i];
}
USART1->CR1 |= USART_CR1_TXEIE; /* habilitar la interrupción de transmisor vacío: arranca el envío */
__set_PRIMASK(pm);
}
void USART1_IRQHandler(void) {
if (USART1->SR & USART_SR_TXE) {
if (log_col != log_cab) USART1->DR = log_buf[log_col++ & (LOG_TAM - 1)];
else USART1->CR1 &= ~USART_CR1_TXEIE; /* no queda nada: se apaga la interrupción */
}
}
/* Niveles de registro, con filtrado en compilación: lo que no se usa no cuesta ni una instrucción */
#define LOG_ERROR 1
#define LOG_INFO 2
#define LOG_DEBUG 3
#ifndef LOG_NIVEL
#define LOG_NIVEL LOG_INFO
#endif
#define LOG(nivel, ...) do { if ((nivel) <= LOG_NIVEL) log_formato(__VA_ARGS__); } while (0) /* log_formato usa snprintf y log_bytes */
Tres precauciones. El formateo (snprintf) no se hace en una interrupción: es lento y usa mucha pila; las ISR registran un evento (apartado siguiente), no texto. El buffer tiene un tamaño calculado, y una política de desborde: a 115 200 baudios el canal solo admite 11 520 B/s, es decir 288 mensajes de 40 caracteres por segundo, y una ráfaga de 20 mensajes de 40 caracteres generados en 1 ms (800 B) tarda 69 ms en salir y necesita un buffer de al menos 789 bytes; si no cabe, se descarta y se cuenta (log_perdidos) en vez de bloquear. Y el registro se deshabilita en el producto, con el nivel de compilación, porque mantiene la UART activa (tema 35), ocupa flash con los textos (tema 32) y puede filtrar información sensible (tema 43).
Existen alternativas que no pasan por una UART común, y que el tema 45 retoma con el depurador conectado: la salida ITM/SWO de los Cortex-M3/M4 y la RTT de Segger, que escriben en un buffer de RAM que el depurador lee sin detener el programa, con un costo de microsegundos.
05 · La caja negra de la depuración
El registro de eventos en RAM
Para los errores que ocurren de vez en cuando y que ningún instrumento está mirando en ese momento, la técnica más poderosa es un registro de eventos (trace) en RAM: un buffer circular donde el programa anota, en puntos clave, qué pasó y cuándo, con un costo de decenas de ciclos. Cuando el error ocurre (un cuelgue, un valor imposible, una aserción que falla), el contenido del buffer cuenta la historia inmediatamente anterior, como la caja negra de un avión (tema 37). No guarda texto, sino registros compactos: un instante, un identificador del evento y un argumento.
typedef struct { uint32_t t; uint16_t id; uint16_t arg; } EventoTraza; /* 8 bytes */
#define N_TRAZA 256u /* potencia de dos: 2 KB */
static EventoTraza traza[N_TRAZA] __attribute__((section(".noinit"))); /* sobrevive a un reinicio (tema 29) */
static volatile uint32_t traza_cab;
void traza_iniciar(void) { /* contador de ciclos del núcleo (Cortex-M3/M4) */
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
static inline void evt(uint16_t id, uint16_t arg) { /* se puede llamar desde cualquier contexto, ISR incluidas */
uint32_t pm = __get_PRIMASK(); __disable_irq();
EventoTraza *e = &traza[traza_cab++ & (N_TRAZA - 1)];
e->t = DWT->CYCCNT; e->id = id; e->arg = arg; /* tres escrituras: unos 30 ciclos en total */
__set_PRIMASK(pm);
}
/* Los identificadores se definen una sola vez (una macro de tabla, tema 32) y se traducen a texto en la PC, no en el equipo */
#define EVENTOS(X) X(EV_ISR_TIM, 1) X(EV_SENSOR_INICIO, 2) X(EV_SENSOR_FIN, 3) X(EV_COLA_LLENA, 7) X(EV_ESTADO, 8)
/* Uso */
void TIM2_IRQHandler(void) { evt(EV_ISR_TIM, 0); /* ... */ }
void sensor_leer(void) { evt(EV_SENSOR_INICIO, 0); /* ... */ evt(EV_SENSOR_FIN, valor); }
void maquina_cambiar(uint8_t nuevo) { evt(EV_ESTADO, nuevo); /* ... */ }
/* Tras un cuelgue o una aserción (tema 39): volcar el buffer por la UART o leerlo con el depurador, de más viejo a más nuevo */
La Parte 3 del programa simula un sistema que genera 2000 eventos y se cuelga en el último, con un buffer de 256 entradas de 8 bytes (2 KB en total). Después del cuelgue se conservan los últimos 256 eventos, del número 1744 al 1999, con las marcas de tiempo crecientes, y cubren los últimos 58,1 ms de funcionamiento; los cinco últimos antes del cuelgue son los que permiten ver qué rutinas habían corrido. El costo de registrar cada uno es del orden de 0,62 µs (con un supuesto de 30 ciclos a 48 MHz), contra los 3472 µs de imprimir una línea de 40 caracteres a 115 200 baudios: unas 5556 veces menos. Es la diferencia entre poder instrumentar el interior de una ISR y no poder.
Algunas decisiones de diseño: la marca de tiempo viene de un contador libre de alta resolución (el contador de ciclos DWT->CYCCNT de los Cortex-M3 y M4, que no existe en los M0; en ese caso, un temporizador de hardware); el tamaño se elige para cubrir la ventana que interesa (a 5000 eventos por segundo, 512 entradas de 8 bytes, 4 KB, cubren 102 ms); el registro se pone en la RAM sin inicializar para que un reinicio no lo borre y se pueda leer tras un fallo; y se vuelca por la UART (en un momento tranquilo o ante una falla), con el depurador, o se lo incorpora a la telemetría (tema 37). Y los eventos no tienen que ser muchos: unos pocos puntos bien elegidos (entrada y salida de las ISR, cambios de estado, errores) valen más que mil eventos sin criterio.
06 · Antes de culpar al software
Los instrumentos y lo primero que se revisa
Una gran parte de los errores que parecen de software son de hardware: una alimentación que se hunde, una masa floja, un reloj que no arrancó, un pin mal cableado. Antes de mirar el código, conviene revisar lo básico, en este orden, porque es lo más barato de comprobar y lo más frecuente:
| Qué se revisa | Cómo | Síntoma de que está mal |
|---|---|---|
| Alimentación | Multímetro en el chip; osciloscopio durante la actividad (picos de la radio, motores) | Reinicios aleatorios, comportamiento distinto con cargas diferentes, fallos al transmitir (tema 37) |
| Masa común | Continuidad entre las masas de todos los equipos conectados | Comunicación que solo funciona a veces, lecturas ruidosas |
| Reloj | Osciloscopio en el oscilador o en una salida de reloj | El programa no arranca, o todo anda a otra velocidad (la UART con basura) |
| Reset | Nivel del pin de reset (debe estar en alto) | El equipo se reinicia solo o no arranca |
| Pines y conexiones | Comparar con el esquema; continuidad hasta el pin del chip | Un periférico que «no responde»; dos pines intercambiados |
| Niveles lógicos | Osciloscopio en la señal: ¿llega a las tensiones de «alto» y «bajo» de cada chip? | Comunicación entre chips de 3,3 V y 5 V que falla |
| Resistencias de polarización (pull-ups de I²C, pines flotantes) | Medir la tensión en reposo de las líneas | I²C que no funciona; entradas que cambian solas (tema 35) |
| Relojes de los periféricos habilitados (tema 9) | Leer el registro de habilitación en el depurador, o probar con un pin | Un periférico que «no hace nada» aunque el código lo configure |
Un truco útil para decidir si un problema es de software o de hardware: un programa mínimo. Se escribe (o se toma de los ejemplos del fabricante) un programa de diez líneas que haga solo lo imprescindible con el periférico sospechoso: parpadear un LED, enviar un byte por la UART, leer un registro de identificación de un sensor. Si el programa mínimo funciona, el hardware está bien y el problema está en el programa grande; si no funciona, el problema es de hardware o de configuración, y se acota sin las miles de líneas que lo rodean. El esqueleto del tema 12 existe para eso.
07 · Leer el síntoma
Tipos de error y sus síntomas
Con experiencia, el patrón del síntoma sugiere el tipo de error, y por lo tanto la técnica con que investigarlo. Los más frecuentes en sistemas embebidos, y a qué apuntan:
| Síntoma | Causa probable | Cómo investigarlo |
|---|---|---|
| Falla esporádica, sin patrón, imposible de reproducir en la mesa | Una condición de carrera entre una ISR y el programa principal (temas 16 y 22) | Pines de prueba en las secciones críticas; registro de eventos; revisar los datos compartidos sin protección |
Falla que desaparece al agregar un printf o un retardo | Un error sensible al tiempo (carrera, falta de volatile) o a la pila (tema 29) | No usar printf: pin de prueba o registro de eventos; medir la pila; revisar volatile |
| Variables que cambian solas | Un desbordamiento de buffer, o de la pila (tema 31) | Un watchpoint con el depurador (tema 45); pintado de la pila; revisar los arreglos vecinos en el .map |
Funciona en -O0 y falla optimizado | Comportamiento indefinido, una variable sin inicializar o la falta de volatile (temas 16 y 32) | Compilar con todas las advertencias; revisar lo compartido con ISR; sanitizers en la PC |
| Un periférico no responde | Reloj del periférico sin habilitar, orden de configuración incorrecto, pines mal configurados (tema 9) | Programa mínimo; leer los registros con el depurador; analizador lógico en el bus |
| Reinicios aleatorios | Brown-out, watchdog, un acceso inválido (temas 37 y 38) | La causa de reinicio; el registro de eventos en .noinit; osciloscopio en la alimentación |
| Comunicación con basura | Velocidad incorrecta (reloj distinto del supuesto, tema 9), niveles o masa mal | Osciloscopio o analizador con decodificador: ¿qué baudios mide? |
| Se cuelga sin reiniciar | Un bucle sin salida, una espera infinita, un interbloqueo (temas 22 y 39) | Latido con LED; hitos de ejecución; el depurador al colgarse: ¿dónde está el PC? |
| Resultado ligeramente incorrecto pero estable | Una unidad o escala mal usada, un desborde de entero, un redondeo (tema 32) | Pruebas en la PC con casos conocidos (temas 25 y 27); imprimir en forma diferida las etapas intermedias |
El síntoma «desaparece cuando lo miro» merece un lugar propio, porque desorienta a quien empieza: no significa que el error se haya ido sino que la forma de mirar lo tapó. Casi siempre es un problema de tiempo: la instrumentación demora una sección lo suficiente como para que la carrera no se produzca, o cambia el uso de la pila o la ubicación de las variables. La reacción correcta no es celebrar, sino cambiar de instrumento por uno menos intrusivo (un pin, un registro de eventos) y confirmar que el error sigue existiendo y se puede ver.
08 · Acorralar al error
Estrategias para acorralar un error
Además de las herramientas, hay estrategias de búsqueda que ahorran horas:
- Bisección. Si el programa funcionaba en una versión anterior y falla en la actual, hay una versión intermedia en la que apareció el error. Se prueba la del medio: si falla, el error está entre la buena y la del medio; si no, entre la del medio y la mala. Con 64 versiones, seis pruebas bastan (log2 64), contra unas 32 en promedio probando una por una: la Parte 4 lo simula y encuentra la versión 41 en seis pruebas. Sirve con el control de versiones (
git bisectautomatiza el proceso), y también dentro del código: se desactiva la mitad de las funciones y se ve de qué lado queda el error. - Reducir el caso. Quitar piezas del sistema hasta quedarse con el programa más chico que todavía falla. Un error en un programa de 50 líneas es casi evidente; el mismo en uno de 5000, no. La reducción obliga además a entender qué es y qué no es relevante.
- Comparar lo que anda con lo que no: la versión buena y la mala, la placa que funciona y la que no, las condiciones en que falla y en que no. La diferencia más pequeña entre las dos es el mejor candidato.
- Aserciones e invariantes (tema 39): afirmar en el código lo que debería ser verdad. Un
ASSERTque salta convierte «algo raro pasó en algún momento» en «esta condición se violó, en este lugar». - Pasar el problema a la PC. La lógica del firmware, separada del hardware (temas 10, 25 y 27), se puede ejecutar en la computadora con herramientas mucho más potentes (depuradores completos, sanitizers, pruebas automáticas con entradas al azar), y los errores de lógica se encuentran allí en minutos.
- Explicárselo a alguien (o a un objeto: es el método del «patito de goma»). Al describir en voz alta qué hace cada línea y qué se espera, muchas veces el error aparece solo, porque obliga a pasar de «creo que hace esto» a «hace esto».
- Descansar. Un error que no se encuentra después de horas casi siempre se resuelve en minutos tras una pausa o al día siguiente: lo que cansa es el supuesto equivocado que uno no revisa.
Y dos hábitos que valen oro: anotar lo que se prueba y lo que se descubre (un cuaderno de laboratorio, un archivo de texto), para no repetir experimentos ni perder la pista; y escribir una prueba que reproduzca el error antes de corregirlo, de modo que quede protegido contra su regreso. La corrección sin la prueba es una promesa; con ella, un hecho.
09 · Comprobar que funciona
Verificación y diagnóstico
La instrumentación también se verifica, por dos razones: puede cambiar el comportamiento (y entonces las conclusiones valen poco) y puede quedarse en el producto (y entonces consume recursos o expone información).
| Qué verificar | Cómo | Resultado esperado |
|---|---|---|
| Que no altera lo que mide | Comparar el comportamiento (tiempos con un pin, tasa de errores) con y sin la instrumentación | Sin diferencias apreciables; si las hay, cambiar de técnica |
| El costo de cada técnica | Medir con un pin cuánto tarda un evt() o un LOG() | Dentro del presupuesto de la rutina (microsegundos en una ISR) |
| El buffer de registro no se desborda en silencio | Provocar una ráfaga y leer el contador de mensajes perdidos | El contador refleja lo perdido; nada se bloquea |
| El registro de eventos es legible tras un cuelgue | Provocar un cuelgue y reiniciar; leer el buffer | Los últimos eventos están y las marcas de tiempo son crecientes |
| La instrumentación no llega al producto | Compilar la configuración de producción: buscar los textos de registro en el binario (strings), el tamaño y el estado de los pines de prueba | Sin textos de depuración, sin LED parpadeando, pines de prueba en estado seguro (tema 35) |
| No hay información sensible en los registros | Revisar qué se imprime y qué contiene el registro de eventos | Sin claves, contraseñas ni datos del usuario (tema 43) |
| Síntoma | Causa probable | Qué hacer |
|---|---|---|
Al agregar un printf el equipo se vuelve lento o pierde interrupciones | La impresión bloquea hasta que salen los bytes | Registro diferido con buffer; eventos binarios; nada de texto en las ISR |
| El analizador lógico no muestra el pulso corto que se espera | La frecuencia de muestreo es baja para ese pulso | Subir la frecuencia (al menos 10 veces la de la señal); usar el disparo por flanco |
| El LED de latido sigue parpadeando con el equipo colgado | Se alterna desde una interrupción en lugar del bucle principal | Alternarlo en el camino que se quiere vigilar |
| La consola muestra caracteres basura | Velocidad distinta de la esperada (reloj, tema 9) o ruido en la línea | Verificar el reloj y la velocidad con un osciloscopio; masa común |
| El registro de eventos tiene marcas de tiempo desordenadas | Escrituras desde varios contextos sin protección, o un contador que da la vuelta | Sección crítica corta al registrar; aritmética de diferencias con el contador |
| Se pierden los mensajes del final antes de un reinicio | El buffer diferido no alcanzó a vaciarse | Vaciar el buffer antes de reiniciar (tema 37); o registrar en RAM sin inicializar |
| La depuración funciona pero la versión de producción falla | La instrumentación enmascaraba un error de tiempo; o la optimización cambia el comportamiento | Depurar con la configuración de producción y con instrumentación de bajo costo |
10 · Analogía con programación
Probar el modelo en Python
El programa reúne cuatro piezas del tema: el análisis de la captura de un pin de prueba (duración, período, latencia y anomalías de las 60 ejecuciones de una ISR); el costo de imprimir por la UART y el tamaño del buffer de registro necesario para una ráfaga; un registro de eventos circular en RAM que sobrevive a un cuelgue; y la bisección que encuentra, entre 64 versiones, la primera que introdujo un error.
Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.
# Depuración básica: leer una captura de un pin de prueba, el costo de imprimir por la UART, un registro de eventos en RAM y la búsqueda binaria de un error.
import random, statistics, math
# ---- Parte 1: analizar la captura de un analizador lógico (un pin que sube al entrar a la ISR y baja al salir) --------------
rnd = random.Random(5)
flancos = [] # (instante en µs, nivel) como los exporta un analizador lógico
for k in range(60):
inicio = k * 1000 + rnd.gauss(0, 2)
dur = rnd.gauss(120, 3)
if k == 37: inicio += 400 # una sección crítica retrasó la interrupción
if k == 45: dur = 900 # una ISR que tardó siete veces más
flancos += [(inicio, 1), (inicio + dur, 0)]
flancos.sort()
subidas = [t for t, n in flancos if n == 1]
bajadas = [t for t, n in flancos if n == 0]
duraciones = [b - s for s, b in zip(subidas, bajadas)]
periodos = [b - a for a, b in zip(subidas, subidas[1:])]
med_d, med_p = statistics.median(duraciones), statistics.median(periodos)
print("Parte 1: captura de un pin de prueba que marca la duración de la ISR (60 interrupciones)")
print(f" período mediano {med_p:.1f} µs (desvío {statistics.pstdev(periodos):.1f}); duración mediana de la ISR {med_d:.1f} µs; carga {med_d / med_p * 100:.1f} %")
print(f" duración máxima: {max(duraciones):.0f} µs; período mínimo {min(periodos):.0f} µs y máximo {max(periodos):.0f} µs")
raras_dur = [k for k, d in enumerate(duraciones) if d > 3 * med_d]
raros_per = [k for k, p in enumerate(periodos) if abs(p - med_p) > 200]
print(f" ISR con duración anormal (más de 3 veces la mediana): número {raras_dur}")
print(f" intervalos con período anormal (más de 200 µs de diferencia): entre las ISR {[(k, k + 1) for k in raros_per]} (el retraso de la ISR {raros_per[0] + 1} y el adelanto relativo de la siguiente)")
retraso = subidas[37] - 37 * 1000
print(f" latencia de la ISR 37 respecto de su instante esperado: {retraso:.0f} µs")
# ---- Parte 2: el costo de imprimir por la UART --------------------------------------------------------------------------------
print()
print("Parte 2: imprimir una línea por la UART, bloqueando hasta que sale el último byte (10 bits por byte)")
print(f" {'baudios':>8} | " + " | ".join(f"{n:>3} caracteres" for n in (20, 40, 80)))
for baud in (9600, 115_200, 921_600):
print(f" {baud:>8} | " + " | ".join(f"{n * 10 / baud * 1e6:>8.0f} µs " for n in (20, 40, 80)))
cap = 115_200 / 10
print(f" a 115 200 baudios el canal admite {cap:,.0f} B/s = {cap / 40:.0f} mensajes de 40 caracteres por segundo como máximo".replace(",", " "))
rafaga, largo = 20, 40
bytes_rafaga = rafaga * largo
print(f" una ráfaga de {rafaga} mensajes de {largo} caracteres generada en 1 ms ({bytes_rafaga} B) tarda {bytes_rafaga / cap * 1000:.0f} ms en salir: el buffer de registro debe tener al menos {bytes_rafaga - int(cap * 0.001)} B")
# ---- Parte 3: un registro de eventos circular en RAM -----------------------------------------------------------------------------
TAM_REG, N = 8, 256 # 8 bytes por evento, 256 eventos = 2 KB
buffer = [None] * N
cabeza = 0
t = 0
for i in range(2000): # el sistema genera 2000 eventos y se cuelga en el último
t += rnd.randint(40, 400) # instante en µs
buffer[cabeza % N] = (t, rnd.choice((1, 2, 3, 7)), i) # (marca de tiempo, identificador del evento, argumento)
cabeza += 1
ultimos = [buffer[(cabeza - 1 - k) % N] for k in range(N)][::-1] # de más viejo a más nuevo
print()
print(f"Parte 3: registro de eventos de {N} entradas de {TAM_REG} bytes = {N * TAM_REG} bytes, tras {cabeza} eventos y un cuelgue")
print(f" se conservan los últimos {len(ultimos)} eventos: del número {ultimos[0][2]} al {ultimos[-1][2]}; marcas de tiempo crecientes: {all(a[0] < b[0] for a, b in zip(ultimos, ultimos[1:]))}")
print(" los 5 últimos antes del cuelgue (instante µs, id, argumento):", ultimos[-5:])
ventana = ultimos[-1][0] - ultimos[0][0]
print(f" esos {N} eventos cubren los últimos {ventana / 1000:.1f} ms de funcionamiento")
CICLOS, MHZ = 30, 48
registrar_us = CICLOS / MHZ
imprimir_us = 40 * 10 / 115_200 * 1e6
print(f" costo de registrar un evento (SUPUESTO: {CICLOS} ciclos a {MHZ} MHz): {registrar_us:.2f} µs; imprimir una línea de 40 caracteres por la UART a 115 200 baudios: {imprimir_us:.0f} µs ({imprimir_us / registrar_us:,.0f} veces más)".replace(",", " "))
# ---- Parte 4: encontrar la versión que introdujo un error por bisección -------------------------------------------------------------
versiones = 64
mala_desde = 41 # la primera versión con el error (el programador no lo sabe)
pruebas = []
bajo, alto = 0, versiones - 1 # la 0 es buena y la última es mala
while alto - bajo > 1:
medio = (bajo + alto) // 2
esta_mala = medio >= mala_desde
pruebas.append((medio, "MALA" if esta_mala else "buena"))
if esta_mala: alto = medio
else: bajo = medio
print()
print(f"Parte 4: buscar entre {versiones} versiones cuál introdujo el error (la primera mala es la {mala_desde}, pero eso no se sabe)")
print(" pruebas por bisección:", ", ".join(f"v{v}={r}" for v, r in pruebas))
print(f" la versión que introdujo el error es la {alto}: {len(pruebas)} pruebas (log2({versiones}) = {math.log2(versiones):.0f}); probando una por una, en promedio {versiones / 2:.0f}")
Resultado, parte 1: de la captura de 60 interrupciones salen un período mediano de 1000,5 µs, una duración mediana de la ISR de 120,1 µs (12,0 % de carga) y dos anomalías: la ISR número 45 dura 900 µs (más de tres veces la mediana) y la 37 arranca con 399 µs de retraso (períodos de 1395 y de 598 µs a su alrededor). Parte 2: una línea de 40 caracteres bloquea 41 667 µs a 9600 baudios, 3472 µs a 115 200 y 434 µs a 921 600; a 115 200 baudios el canal admite 11 520 B/s (288 mensajes de 40 caracteres por segundo), y una ráfaga de 20 mensajes de 40 caracteres en 1 ms tarda 69 ms en salir y exige un buffer de al menos 789 B. Parte 3: tras 2000 eventos y un cuelgue, el buffer de 256 entradas de 8 bytes (2048 bytes) conserva los eventos 1744 a 1999, con marcas de tiempo crecientes, que cubren los últimos 58,1 ms; registrar un evento cuesta 0,62 µs (supuesto de 30 ciclos a 48 MHz) contra 3472 µs de imprimir: 5556 veces menos. Parte 4: la bisección encuentra la versión 41, entre 64, con seis pruebas (v31 buena, v47 mala, v39 buena, v43 mala, v41 mala, v40 buena).
Probá cambiar en la Parte 1 el valor de la duración de la ISR anómala o el instante del retraso y mirá que el análisis las sigue encontrando, o aumentá versiones a 1024 en la Parte 4 y verificá que bastan 10 pruebas. La lección del programa es que los datos de una captura bien tomada hablan: dos anomalías enterradas en 60 ejecuciones, que a ojo no se ven, aparecen con unas pocas líneas de análisis, y esas líneas se pueden reutilizar con cualquier captura de un analizador lógico exportada a un archivo.
11 · Exploración interactiva
El costo de mirar
El laboratorio simula una interrupción de 1 ms que dura 120 µs, y muestra qué le hace cada forma de instrumentarla: nada, un pin de prueba, un printf dentro de la ISR o un registro diferido a un buffer de 512 bytes. Elegís la velocidad de la UART, el largo de los mensajes y cuántos se imprimen por segundo. El laboratorio simula un segundo de funcionamiento y cuenta las interrupciones que se pierden, la carga del procesador y los mensajes que no caben en el canal; la línea de tiempo muestra las primeras ejecuciones (en naranja, las que duran más que el período).
Instrumentar una ISR sin romperla
Las primeras ejecuciones de la ISR (6 ms)
Elegí una forma de instrumentar.
Recorrido guiado
- Con «Sin instrumentar»: la ISR dura 120 µs en un período de 1000 µs (12 % de carga), no se pierde nada y no se ve nada. Es la referencia: el sistema sano y a oscuras.
- Elegí «Pin de prueba»: la ISR dura 120,2 µs (el pin cuesta 0,2 µs) y la carga sigue en 12 %. Casi no perturba, y con un analizador lógico se ve cada ejecución. Es la técnica para los problemas de tiempo.
- Elegí «printf dentro de la ISR» a 115 200 baudios y 40 caracteres, un mensaje por ISR: la ISR dura 3592 µs, casi cuatro veces su período; se pierden 720 de las 1000 interrupciones y el procesador está al 100 %. La línea de tiempo muestra bloques naranja que cubren varios períodos. La instrumentación destruyó el comportamiento que se quería observar.
- Con el mismo printf, bajá la tasa a 10 mensajes por segundo: la ISR que imprime sigue durando 3592 µs (y pierde dos interrupciones cada vez que lo hace: 20 de 1000 en total), pero ocurre una vez cada 100 ejecuciones. Y subí la velocidad a 921 600: la ISR baja a 554 µs y ya entra en el período sin perder ninguna. Imprimir en una ISR depende de cuánto y de qué tan rápido.
- Elegí «Registro diferido» con 1000 mensajes por segundo de 40 caracteres a 115 200 baudios: la ISR dura 122 µs (la copia al buffer cuesta 2 µs) y no se pierde ninguna interrupción, pero el canal solo admite 288 mensajes por segundo: unos 712 por segundo se pierden cuando el buffer de 512 B se llena. Se arregló el tiempo y apareció el límite del ancho de banda.
- Con el registro diferido, bajá los mensajes a 100 por segundo (o subí la velocidad a 921 600): ahora el canal alcanza y no se pierde nada. La conclusión es la de la Parte 2: el registro diferido protege el tiempo, pero hay que dimensionar el volumen de información que se manda y decidir qué se pierde cuando no cabe.
Desafío: ¿cómo registrarías lo que pasa dentro de una ISR de 1 kHz durante un minuto sin perder nada?
Hay que cambiar qué se registra, no solo cómo. A 1000 ISR por segundo, un mensaje de texto de 40 caracteres por ISR son 40 KB/s, imposible por una UART a 115 200 baudios (11,5 KB/s). Opciones: registrar eventos binarios de 8 bytes en lugar de texto (8 KB/s, que entran en el canal con margen, y el texto se arma en la PC con una tabla de identificadores); registrar solo lo anómalo (la ISR que duró más de lo esperado, la que llegó tarde), con un contador de ejecuciones normales; acumular en RAM un resumen (el máximo, el mínimo, un histograma de duraciones) y volcarlo cada segundo; o guardar en un buffer en RAM de mayor tamaño los eventos de ese minuto (60 000 eventos de 8 bytes son 480 KB, que no caben en la RAM de un microcontrolador chico, pero sí en una flash externa con el registro circular del tema 41, escribiendo por bloques desde una tarea de baja prioridad). Y la técnica que no gasta nada de eso: un pin de prueba y un analizador lógico capturando el minuto con la duración de cada ISR, y analizando la captura exportada con un programa como el de la Parte 1.
12 · Método de trabajo
Método: depurar con orden
- Reproducí el error y anotá cómo. Un error que se puede provocar se puede estudiar, y si es raro, buscá qué lo hace más frecuente.
- Revisá lo básico antes del código: alimentación, masa, reloj, reset, pines, niveles. Si hay dudas, un programa mínimo que ejercite el periférico sospechoso.
- Elegí el instrumento por la pregunta: tiempo → pin y analizador; secuencia → registro de eventos; valores → depurador (tema 45); señal analógica → osciloscopio. Siempre el menos intrusivo que responda.
- Prefiero lo diferido: nada de texto dentro de las ISR; un buffer en RAM y salida en segundo plano; contadores de lo perdido.
- Dejá el latido y los puntos de prueba desde el principio del proyecto: un LED de vida en el bucle principal, pines libres accesibles, un conector de depuración y un registro de eventos básico.
- Formulá hipótesis concretas y probalas con un experimento que cambie una sola cosa. Si el error desaparece al observarlo, cambiá de instrumento, no lo celebres.
- Acotá con estrategia: bisección entre versiones, reducción del caso, comparación entre lo que anda y lo que no, aserciones; y trasladá la lógica a la PC cuando se pueda.
- Corregí la causa, no el síntoma, y escribí una prueba que reproduzca el error para que no regrese.
- Anotá lo que aprendiste (qué era, cómo se encontró, cómo se evitará).
- Quitá o desactivá la instrumentación del producto, y verificá la configuración de producción: sin textos de depuración, sin LED parpadeando, con los pines de prueba en un estado seguro.
13 · Comprensión
Confusiones frecuentes
«Un printf es la forma más simple de depurar, y no le hace nada al programa»
Le hace mucho: una línea de 40 caracteres a 115 200 baudios bloquea 3,5 ms, una ISR de 1 ms que imprime pierde interrupciones, y printf consume pila y flash. La técnica que no perturba es un pin de prueba o un registro diferido. El printf bloqueante es aceptable solo en código sin requisitos de tiempo.
«Si el error desaparece cuando agrego la instrumentación, ya está resuelto»
Casi seguro que no: la instrumentación cambió el tiempo, la pila o la memoria, y escondió el error. Hay que repetir la observación con una herramienta menos intrusiva y confirmar que el error sigue ahí. Un error que «se arregla solo» al mirarlo es un error que volverá en el campo, cuando nadie mire.
«Los problemas esporádicos son mala suerte, no se pueden depurar»
Se pueden, con un registro de eventos que anote la secuencia reciente y se lea tras el fallo, y con estadística (cuántas veces, en qué condiciones). Casi siempre son condiciones de carrera o de tiempo, y tienen una causa determinista oculta. Lo difícil es capturarlos, no entenderlos.
«Antes de mirar el hardware hay que descartar todo el software»
Al revés: una alimentación que se hunde, una masa floja o un reloj mal configurado producen síntomas idénticos a los de un error de software, y se comprueban en minutos con un multímetro. Revisar lo básico primero ahorra días de búsqueda en el código equivocado.
«Un LED de latido siempre me dice si el programa está vivo»
Solo si el LED se alterna en el camino que se quiere vigilar. Si lo alterna la interrupción de un temporizador, parpadea aunque el programa principal esté colgado: es el mismo error de alimentar el watchdog desde una ISR. Dónde se pone la señal de vida define qué demuestra.
«Más mensajes de registro es mejor»
Más mensajes ocupan flash y consumen tiempo y energía, saturan el canal y entierran la información útil entre miles de líneas. Pocos puntos bien elegidos (entrada y salida de las ISR, cambios de estado, errores), con niveles que se filtran en compilación, valen más que un registro de todo.
«Corregí el error y ahora anda, no hace falta saber por qué»
Un cambio que arregla un síntoma sin que se entienda la causa suele esconderlo (el error sigue ahí, desplazado) o es casualidad. Hay que poder explicar qué estaba mal, por qué el cambio lo arregla y por qué no apareció otro, y agregar una prueba que lo habría detectado.
«La instrumentación de depuración se puede dejar en el producto, por si acaso»
Consume energía (un LED parpadeando, una UART activa), ocupa flash con los textos, puede filtrar información sensible (claves, datos del usuario) y, si abre una interfaz de control, ofrece una puerta a un atacante (tema 43). Se desactiva en la configuración de producción; lo que sí conviene dejar es el registro de eventos compacto y los contadores de salud (tema 37).
14 · Práctica guiada
Ejercicios
1. El costo de una línea
Un programa imprime una línea de 32 caracteres a 57 600 baudios, bloqueando, desde una interrupción que se dispara cada 1 ms. ¿Cuánto tarda en salir la línea? ¿Qué pasa con la interrupción? Si solo se imprimiera, sin otra cosa, ¿cuántas líneas por segundo caben?
Ver solución
32 caracteres × 10 bits / 57 600 baudios = 320 / 57 600 = 5,56 ms. La ISR dura más de cinco veces su período (1 ms): se pierden unas cuatro de cada cinco interrupciones, y el procesador queda ocupado casi todo el tiempo. El canal admite 57 600 / 10 = 5760 B/s; a 32 B por línea son 180 líneas por segundo como máximo. Imprimir una línea por cada ISR (1000 por segundo) es imposible por ese canal, aun con buffer: harían falta registros binarios mucho más compactos, o una UART más rápida.
2. El tamaño del buffer
Un sistema genera una ráfaga de 10 mensajes de 50 caracteres en 2 ms. La UART va a 115 200 baudios. ¿Cuántos bytes se generan, cuántos salen durante los 2 ms y qué tamaño mínimo debe tener el buffer? ¿Cuánto tarda en vaciarse?
Ver solución
Se generan 10 × 50 = 500 B. En 2 ms, la UART saca 11 520 B/s × 0,002 s ≈ 23 B. El buffer debe poder retener al menos 500 − 23 = 477 B; en la práctica, una potencia de dos mayor: 512 B (con muy poco margen: conviene 1024 B si pueden venir ráfagas mayores). Se vacía en 500 / 11 520 ≈ 43 ms. Si otra ráfaga llega antes de eso, el buffer se llena antes: por eso se cuenta lo perdido y se dimensiona con la ráfaga más grande esperada, no con la promedio.
3. La frecuencia de muestreo
Se quiere medir con un analizador lógico la duración de una ISR de 120 µs con un error del 1 %. ¿Qué intervalo entre muestras y qué frecuencia de muestreo mínimos hacen falta? ¿Y para ver un pulso espurio de 100 ns?
Ver solución
Un error del 1 % de 120 µs son 1,2 µs: el intervalo entre muestras debe ser, como máximo, de 1,2 µs, es decir, una frecuencia de al menos 1 / 1,2 µs ≈ 0,83 MS/s (en la práctica, 1 MS/s o más). Para un pulso de 100 ns, hace falta tomar al menos una muestra dentro del pulso: un intervalo de 100 ns o menos, o sea 10 MS/s como mínimo, y para verlo con confianza (varias muestras) 40 MS/s o más. Con un muestreo más lento, el pulso puede caer entre dos muestras y no aparecer en la captura: se mira una señal «limpia» que tiene un problema.
4. Cuánta historia guarda el registro de eventos
Un registro de eventos usa 8 bytes por evento y dispone de 4 KB de RAM. El sistema genera 5000 eventos por segundo. ¿Cuántos eventos caben y qué ventana de tiempo cubren? ¿Qué harías si el error aparece unos 500 ms después de su causa?
Ver solución
4096 / 8 = 512 eventos. A 5000 eventos por segundo, cubren 512 / 5000 = 0,102 s, unos 102 ms. Si el error aparece 500 ms después de su causa, el buffer ya sobrescribió la causa. Opciones: más espacio (unos 2500 eventos × 8 B = 20 KB, que quizá no haya), menos eventos (registrar solo los puntos clave: 500 eventos por segundo darían una ventana de 1 s con los mismos 4 KB), eventos filtrados por tipo, o congelar el registro cuando se detecta una condición cercana al error (así se conserva el contexto), o volcar a una memoria externa (tema 41).
5. Bisección
Se sabe que un error aparece en algún lugar de una historia de 1000 versiones. Cada prueba (compilar, grabar y probar) lleva 8 minutos. ¿Cuántas pruebas hacen falta por bisección? ¿Cuánto tiempo, comparado con probar una por una en promedio?
Ver solución
log2(1000) ≈ 9,97: 10 pruebas. A 8 minutos cada una: 80 minutos. Probando una por una, en promedio hay que recorrer la mitad: unas 500 pruebas × 8 min = 4000 min ≈ 66,7 horas. La bisección convierte una semana de trabajo en una hora y veinte. Requiere que el error sea reproducible de forma fiable (una prueba que a veces falla y a veces no engaña a la bisección: hay que repetirla las veces suficientes) y que todas las versiones intermedias compilen y se puedan probar.
6. El error que desaparece
Un equipo se cuelga una vez por hora. Se agrega un printf al principio de la ISR del UART para ver los datos, y desde entonces no se cuelga más. ¿Qué indica esto, y cómo seguirías?
Ver solución
Indica que el error depende del tiempo (o de la pila, o de la disposición de la memoria), y que el printf lo está tapando: lo más probable es una condición de carrera entre la ISR y el programa principal sobre datos compartidos, que el retardo del printf (milisegundos) vuelve casi imposible, o un desborde de pila que el printf cambia de lugar o de tamaño. No es un arreglo: el error sigue ahí. Para seguir: retirar el printf y observar con herramientas de bajo costo: un pin de prueba al entrar y salir de la ISR y de las secciones críticas del programa principal (ver si se pisan), un registro de eventos que se lea tras el cuelgue, la medición de la pila con pintado, y una revisión de cada dato compartido (tema 16: volatile, atomicidad). Y, si el hardware lo permite, el depurador conectado en el momento del cuelgue (tema 45) para ver dónde está el PC.
15 · Síntesis
Ideas para recordar
- Depurar es un método: reproducir, observar, formular una hipótesis concreta, probarla con un experimento que cambie una sola cosa, corregir la causa y escribir una prueba. Cambiar cosas al azar hasta que «anda» deja el error escondido.
- Observar cambia el sistema. Por eso las técnicas se eligen por su intrusividad y por la pregunta que responden: LED (señal de vida), pin de prueba y analizador lógico (tiempo), registro de eventos (secuencia), consola diferida (mensajes), depurador (valores; tema 45). Un error que desaparece al mirarlo no está resuelto: se cambia de instrumento.
- Los LED dan latido, estado y códigos de parpadeo; el latido debe alternarse en el camino vigilado (no en una ISR) y no debe quedar en el producto. Los pines de prueba cuestan una escritura de registro y con un analizador muestran duración, latencia y orden de eventos con precisión de microsegundos: la captura de la Parte 1 reveló una ISR 7 veces más larga y otra con 399 µs de retraso, invisibles con un
printf. - La consola por la UART bloquea: una línea de 40 caracteres a 115 200 baudios son 3,5 ms. Imprimir dentro de una ISR de 1 ms la rompe. La solución es el registro diferido (buffer circular y envío por interrupción o DMA), con un buffer dimensionado para la ráfaga, una política de desborde que cuenta lo perdido y niveles filtrados en compilación; el formateo, nunca en una ISR.
- El registro de eventos en RAM (instante, identificador, argumento: 8 bytes, unos 0,6 µs por evento, miles de veces más barato que imprimir) guarda la historia inmediata de un fallo, sobrevive a un reinicio en RAM sin inicializar y se vuelca después. El tamaño se elige para cubrir la ventana que interesa.
- Primero lo básico: alimentación, masa, reloj, reset, pines, niveles y resistencias de polarización, con multímetro y osciloscopio; y un programa mínimo para separar el hardware del software. Muchos errores «de software» son de hardware.
- El síntoma sugiere la causa: lo esporádico apunta a condiciones de carrera; lo que cambia al agregar un
printf, al tiempo o a la pila; las variables que cambian solas, a desbordes de memoria; lo que falla solo optimizado, a comportamiento indefinido o a unvolatilefaltante; los reinicios, al brown-out o al watchdog. - Las estrategias acotan el espacio de búsqueda: bisección entre versiones (10 pruebas para 1000 versiones en lugar de unas 500), reducción del caso, comparación de lo que anda con lo que no, aserciones, trasladar la lógica a la PC, explicar el problema en voz alta. Se anota todo, y la instrumentación se quita o se desactiva en la configuración de producción.
¿Listo para el tema siguiente?
Antes de avanzar, revisá un proyecto tuyo y respondé: ¿tiene un LED de latido en el bucle principal?, ¿hay un pin libre accesible para medir tiempos y un conector para el depurador?, ¿hay un printf dentro de alguna interrupción?, ¿qué registraría el equipo si se colgara ahora mismo? Si la respuesta a la última es «nada», agregá un registro de eventos de 256 entradas: es una hora de trabajo que vale años de depuración.
En el próximo tema usamos la herramienta más potente de la depuración: SWD y JTAG. Puntos de interrupción, inspección de registros y memoria, observación de variables sin detener el programa y trazas, con el depurador conectado al microcontrolador.