01 · La puerta de servicio
La interfaz de depuración dentro del chip
Un microcontrolador con núcleo ARM Cortex-M incluye, junto a la CPU, un bloque de depuración que funciona de forma independiente del programa. Desde afuera se llega a él por un puerto de dos o cuatro pines, y a través de él se accede a un bus de acceso que ve el mismo mapa de memoria que la CPU (tema 28): la flash, la RAM, los registros de los periféricos y los propios registros del núcleo. Por eso un depurador puede leer y escribir cualquier dirección sin que el programa lo sepa, y puede detener el núcleo para inspeccionarlo con calma.
La CPU y el puerto de depuración son dos agentes que comparten el bus. Esa independencia explica casi todo lo que sigue: el depurador puede mirar la memoria con el programa corriendo, puede grabar la flash con el núcleo detenido, y, cuando detiene el núcleo, el resto del chip sigue funcionando: los temporizadores, la UART y el reloj del mundo exterior no se enteran de que el programa está en pausa. Esa última frase tiene consecuencias importantes en la sección 9.
02 · El estándar de los chips
JTAG: cuatro hilos y una máquina de estados
JTAG (nombre del grupo que lo definió, el estándar IEEE 1149.1) nació en los años noventa para probar las conexiones entre chips de una placa (boundary scan) y terminó siendo la interfaz de depuración universal. Usa cuatro señales (y una quinta opcional):
| Señal | Sentido (visto desde el chip) | Función |
|---|---|---|
| TCK | Entrada | Reloj: lo genera la sonda; cada flanco avanza todo un paso |
| TMS | Entrada | Selección de modo: en cada flanco de TCK, su valor decide el estado siguiente de la máquina de estados |
| TDI | Entrada | Datos hacia el chip |
| TDO | Salida | Datos desde el chip |
| nTRST (opcional) | Entrada | Reinicio asíncrono de la lógica JTAG |
Dentro de cada chip hay un controlador llamado TAP (Test Access Port), una máquina de estados de 16 estados que solo se maneja con TMS: nada de direcciones ni comandos, solo un bit por ciclo de reloj. Hay un camino para cargar una instrucción (el registro IR) y otro para mover datos por el registro que esa instrucción haya elegido (el registro DR): el IDCODE que identifica al chip, el BYPASS de un solo bit, o los registros de acceso al depurador ARM. En el estado Shift cada ciclo empuja un bit desde TDI hacia dentro y otro hacia fuera por TDO; en Update lo cargado se hace efectivo.
/* La máquina de estados del TAP, en forma de tabla: estado -> {siguiente con TMS=0, siguiente con TMS=1} */
typedef enum { TLR, RTI, SEL_DR, CAP_DR, SH_DR, EX1_DR, PAU_DR, EX2_DR, UP_DR,
SEL_IR, CAP_IR, SH_IR, EX1_IR, PAU_IR, EX2_IR, UP_IR } EstadoTap;
static const EstadoTap SIGUIENTE[16][2] = {
[TLR] = { RTI, TLR }, [RTI] = { RTI, SEL_DR },
[SEL_DR] = { CAP_DR, SEL_IR }, [CAP_DR] = { SH_DR, EX1_DR }, [SH_DR] = { SH_DR, EX1_DR },
[EX1_DR] = { PAU_DR, UP_DR }, [PAU_DR] = { PAU_DR, EX2_DR }, [EX2_DR] = { SH_DR, UP_DR }, [UP_DR] = { RTI, SEL_DR },
[SEL_IR] = { CAP_IR, TLR }, [CAP_IR] = { SH_IR, EX1_IR }, [SH_IR] = { SH_IR, EX1_IR },
[EX1_IR] = { PAU_IR, UP_IR }, [PAU_IR] = { PAU_IR, EX2_IR }, [EX2_IR] = { SH_IR, UP_IR }, [UP_IR] = { RTI, SEL_DR },
};
/* Cinco ciclos seguidos con TMS=1 llevan a Test-Logic-Reset desde cualquier estado: así arranca toda sonda. */
La Parte 2 del programa de Python construye esa tabla y busca por anchura los caminos más cortos. Para leer el IDCODE de un chip desde el reset hacen falta 4 ciclos (TMS = 0, 1, 0, 0) hasta el estado de desplazamiento, 32 ciclos para sacar los bits y 2 para volver al reposo: 38 ciclos de TCK en total. La virtud más grande de JTAG aparece al conectar varios chips en cadena: la salida TDO de uno va a la entrada TDI del siguiente, y una sola sonda con cuatro hilos los atiende a todos. Con tres chips, leer los tres IDCODE son 4 + 96 + 2 = 102 ciclos; para hablar con uno solo, los otros se ponen en BYPASS y cada uno agrega apenas un bit de retardo al registro.
03 · El hermano de dos hilos
SWD: dos hilos
ARM definió, para los Cortex-M, una alternativa que usa solamente dos pines: SWCLK (reloj, lo genera la sonda) y SWDIO (datos, bidireccional). En un microcontrolador con pocos pines, ahorrar dos de los cuatro de JTAG vale mucho, y por eso SWD es la opción habitual en los Cortex-M. Usa el mismo bloque de acceso de depuración (el DAP) que JTAG, pero con un protocolo de paquetes en lugar de una máquina de estados: cada transacción tiene tres fases, petición, respuesta y datos.
| Fase | Bits | Contenido |
|---|---|---|
| Petición (la manda la sonda) | 8 | Inicio (1), APnDP (¿acceso al puerto de acceso o al de depuración?), RnW (¿lectura o escritura?), A[2], A[3] (la dirección del registro), paridad, parada (0) y park (1) |
| Giro del bus | 1 | El pin SWDIO cambia de dueño |
| Respuesta (el chip) | 3 | ACK: OK, WAIT (ocupado, reintentar) o FAULT |
| Datos | 32 + 1 | La palabra de 32 bits y su bit de paridad; quien los manda depende de si es lectura o escritura (con un giro del bus antes de la escritura) |
| Giro final | 1 | El bus vuelve a la sonda |
La Parte 1 del programa arma las peticiones. El paquete para leer el IDCODE del puerto de depuración es el byte 0xA5: inicio 1, APnDP 0, RnW 1, A[3:2] = 00, paridad 1 (para que el número de unos sea par), parada 0 y park 1, enviado desde el bit menos significativo. Escribir el registro TAR del puerto de acceso es 0x8B y leer el DRW, 0x9F. Cada transacción ocupa 8 + 1 + 3 + 32 + 1 + 1 = 46 ciclos de SWCLK, y leer memoria significa repetirlas: se escribe la dirección en TAR una vez y luego se lee DRW con autoincremento, una palabra por transacción.
| Reloj SWCLK | Leer 4 KB | Rendimiento | Leer 128 KB |
|---|---|---|---|
| 1 MHz | 47,2 ms | 87 KB/s | 1,51 s |
| 4 MHz | 11,8 ms | 347 KB/s | 0,38 s |
| 10 MHz | 4,7 ms | 867 KB/s | 0,15 s |
Son mejores casos: en la práctica la sonda, el USB y el programa de la PC agregan demoras, y la grabación de la flash tarda mucho más que la lectura porque la propia flash es lenta de escribir (tema 40). Pero la cuenta da una idea correcta: a 4 MHz el volcado de toda la RAM de un equipo chico lleva una fracción de segundo. Si el reloj es demasiado rápido para el cableado o para el reloj del núcleo, aparecen errores (ACK incorrecto, paridad): bajar la velocidad de SWCLK es el primer remedio ante una conexión inestable.
| Aspecto | JTAG | SWD |
|---|---|---|
| Pines | 4 o 5 (TCK, TMS, TDI, TDO, nTRST) | 2 (SWCLK, SWDIO) |
| Varios chips en una cadena | Sí, con una sonda | Un solo punto de acceso por conexión (con selección por dirección en SWD multipunto) |
| Núcleos que lo soportan | Casi todos: ARM, RISC-V, DSP, FPGA | Solo ARM (Cortex-M y otros) |
| Ciclos por acceso a un registro de 32 bits | Unos 40 (más 10 al cambiar de instrucción) | 46 |
| Boundary scan (probar conexiones de la placa) | Sí | No |
| Traza por un pin extra | TDO se comparte con SWO en algunos casos | Pin SWO opcional |
Para un Cortex-M suelto y una sola placa, SWD gana por los pines; para una placa con varios chips programables (un microcontrolador, una FPGA y un procesador) o para probar las conexiones en fábrica, JTAG. Casi todas las sondas hablan los dos, y muchos chips arrancan con ambos habilitados, compartiendo pines: SWDIO con TMS y SWCLK con TCK.
04 · Los cables
Conectores, cableado y reset
La conexión física es lo primero que falla, y la causa de la mayoría de los «no encuentra el dispositivo». El conector estándar de ARM para Cortex-M es un encabezado de 10 pines con paso de 1,27 mm (el de 20 pines con paso de 1,27 mm es la versión grande); en placas chicas se usan versiones sin conector, con almohadillas que se contactan con una pinza (Tag-Connect) o los pines cableados a una tira de 4 o 5 pasos de 2,54 mm.
| Pin | Señal | Para qué sirve |
|---|---|---|
| 1 | VTref | Tensión de referencia: la sonda adapta sus niveles a la tensión del equipo y detecta si está alimentado |
| 2 | SWDIO / TMS | Datos de SWD |
| 4 | SWCLK / TCK | Reloj |
| 6 | SWO / TDO | Salida de traza (SWD) o datos de salida (JTAG) |
| 8 | TDI | Datos de entrada (solo JTAG) |
| 10 | nRESET | Permite a la sonda reiniciar el equipo |
| 3, 5, 9 | GND | Masa; varias, para que el retorno sea corto |
Algunas reglas que ahorran horas: masa común y corta (un cable de masa largo es la causa clásica de errores intermitentes); cables cortos (los de menos de 10 a 15 cm admiten velocidades altas; los largos, no); VTref conectado, para que la sonda use los niveles correctos (una placa de 1,8 V con una sonda a 3,3 V puede dañarse); no alimentar la placa desde la sonda más allá de su capacidad (la sonda entrega poca corriente, y menos aún cuando el equipo arranca una radio, tema 36); y el pin de reset conectado, que permite a la sonda recuperar un chip cuyo programa deshabilitó los pines de depuración. Los pines de SWD no se deben reutilizar para otra función durante la etapa de desarrollo, y se debe poder acceder a ellos también en la placa final, aunque solo sea con almohadillas de prueba.
Una trampa conocida: un programa que reconfigura los pines de SWD como GPIO, o que entra en un modo de bajo consumo que apaga el reloj del bloque de depuración (tema 35), deja al chip sin conexión para la sonda. La salida es mantener presionado el reset mientras se conecta («connect under reset»), de manera que la sonda tome el control antes de que el programa llegue a ejecutar esa línea. Conviene saberlo antes de tener sobre la mesa un equipo «bloqueado».
05 · Controlar la ejecución
Puntos de interrupción, watchpoints y paso a paso
Con el núcleo detenido o en marcha, el depurador ofrece cuatro formas de controlar y observar la ejecución:
- Punto de interrupción (breakpoint): detiene el programa cuando el contador de programa llega a una dirección. Puede ser de hardware (un comparador dentro del chip, la unidad FPB en los Cortex-M3 y M4, que compara la dirección de captura de instrucciones con unas pocas direcciones configuradas, y funciona también con el código en flash) o de software (el depurador escribe en la posición una instrucción
BKPT; solo se puede hacer donde se puede escribir, es decir, en RAM, o reprogramando la flash, lo que es lento y desgasta la memoria). - Punto de observación de datos (watchpoint): detiene el programa cuando se lee o escribe una dirección de datos. Lo implementa la unidad DWT. Es la herramienta ideal para el error «una variable cambia sola»: se pone un watchpoint de escritura sobre la variable y el programa se detiene en la instrucción que la pisa (tema 44).
- Paso a paso (step): ejecuta una línea o una instrucción y vuelve a detenerse.
- Detención a pedido (halt): detiene el núcleo en el punto en que esté, para ver en dónde se colgó. Es lo primero que se hace con un equipo que «no responde».
| Núcleo | Breakpoints de hardware | Watchpoints (DWT) | Registro de fallas (CFSR) |
|---|---|---|---|
| Cortex-M0 / M0+ | hasta 4 | hasta 2 | No (solo HardFault) |
| Cortex-M3 / M4 | hasta 6 de código (más 2 de literales) | 4 comparadores | Sí |
La consecuencia práctica es que los breakpoints de hardware son pocos: seis en un M4. Con el código en flash, el depurador agota el cupo rápido; el IDE avisa cuando se pasa y, en el mejor de los casos, se ponen y se quitan sobre la marcha. Un breakpoint condicional («detenerse solo si i == 500») se implementa deteniendo el núcleo en cada paso por ahí y evaluando la condición desde la PC: puede ser miles de veces más lento que el código real, y en una rutina de 1 kHz vuelve imposible la ejecución normal. Para esos casos, el tema anterior ya dio la salida: un pin de prueba, un registro de eventos, o un watchpoint con una condición simple.
06 · Las herramientas
Sondas y programas
Entre la PC y los pines hay una sonda que traduce el USB a SWD o JTAG. La oferta habitual:
| Herramienta | Qué es | Comentario |
|---|---|---|
| ST-Link | Sonda de ST, integrada en las placas de desarrollo y también suelta | La más accesible para STM32; muchas placas permiten usarla con otros chips |
| J-Link (Segger) | Sonda profesional | Rápida, con soporte para muchos chips, RTT, y programa de pruebas; hay versiones educativas |
| CMSIS-DAP | Protocolo abierto de ARM para sondas de bajo costo | Muchas placas lo traen; funciona con OpenOCD y los IDE |
| OpenOCD | Programa de código abierto que habla con la sonda y ofrece un servidor para gdb | El puente habitual con herramientas abiertas |
| gdb (y su interfaz en el IDE) | El depurador propiamente dicho | Interpreta los símbolos del ejecutable (el archivo ELF), mostrando variables y líneas de código |
La sonda solo mueve bits; lo que le da sentido a los números es el archivo ELF con la información de depuración (la opción -g del compilador, tema 12): sin él, el depurador ve direcciones y bytes sueltos. Esa información de símbolos no se graba en el microcontrolador, vive en la PC: por eso se puede depurar un equipo con el ELF que corresponda exactamente al firmware grabado; un ELF distinto del binario que corre da una vista engañosa (variables que no coinciden, líneas que no son las que se ejecutan). Al usar versiones (tema 42), se guarda el ELF de cada versión liberada: sin él, un cuelgue en el campo apenas muestra direcciones.
# Dos terminales: OpenOCD habla con la sonda; gdb habla con OpenOCD (sintaxis típica; los nombres de los archivos cambian según la sonda y el chip)
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg
arm-none-eabi-gdb build/firmware.elf
(gdb) target extended-remote :3333
(gdb) monitor reset halt # reinicia y detiene el núcleo
(gdb) load # graba la flash con el ELF
(gdb) break procesar_muestra # breakpoint (de hardware si está en flash)
(gdb) watch contador # watchpoint: se detiene al escribir la variable
(gdb) continue
(gdb) info registers # R0-R12, SP, LR, PC, xPSR
(gdb) x/8wx $sp # 8 palabras desde el SP
(gdb) bt # pila de llamadas
07 · Mirar con el programa corriendo
Depurar sin detener: ITM, SWO y RTT
Los puntos de interrupción detienen el programa y, en un sistema de tiempo real, eso cambia lo que se quiere observar. Hay mecanismos para sacar información con el programa corriendo, con un costo de microsegundos, y el depurador es el canal:
- ITM / SWO (Cortex-M3 y superiores): el programa escribe a unos puertos de estímulo (registros especiales) y el hardware envía esos bytes por el pin SWO, empaquetados con un encabezado, hacia la sonda, sin intervención de la CPU. El núcleo solo gasta unos ciclos por escritura, y no espera la salida en la mayor parte de los casos. Requiere el pin SWO conectado y la velocidad configurada igual en el chip y en la sonda.
- RTT (Real-Time Transfer, de Segger): el programa escribe en un buffer circular en RAM que el depurador lee por SWD sin parar el núcleo. No usa un pin extra, solo SWD. El costo es una copia en RAM: microsegundos.
- Semihosting: la entrada y la salida estándar del programa se redirigen a la PC a través del depurador. Es cómodo, pero cada operación detiene el núcleo con una instrucción
BKPTmientras el depurador la atiende, y sin un depurador conectado el programa se cuelga: no se debe dejar en el firmware que se entrega. - Traza de instrucciones (ETM) en núcleos más grandes: graba la secuencia completa de instrucciones ejecutadas con un hardware dedicado y varios pines; es la herramienta más completa y la más costosa.
/* ITM: escribir un carácter en el puerto de estímulo 0 (CMSIS: ITM_SendChar) sin esperar un UART */
#include "core_cm4.h"
static void itm_texto(const char *s) { while (*s) ITM_SendChar((uint32_t)*s++); } /* espera solo si el FIFO está lleno */
/* RTT (biblioteca de Segger): un buffer en RAM que el depurador lee a su ritmo */
#include "SEGGER_RTT.h"
void inicio(void) { SEGGER_RTT_Init(); }
void registrar(int valor) { SEGGER_RTT_printf(0, "valor=%d\n", valor); } /* copia a RAM; no depende del reloj de un puerto serie */
La Parte 4 del programa compara con la consola por UART del tema anterior. Con SWO a 2 Mbaudios (un valor supuesto: depende de la sonda y del reloj del núcleo), el canal mueve 200 000 B/s (10 bits por byte). Cada byte escrito a un puerto de estímulo viaja con un encabezado, así que un mensaje de 40 caracteres ocupa 80 B si se escribe byte a byte y 50 B si se escribe de a palabras de 4 bytes: 2500 o 4000 mensajes por segundo, contra los 288 de una UART a 115 200 baudios. Y el costo en el núcleo, con un supuesto de 30 ciclos a 48 MHz, son unos 0,62 µs, unas 5555 veces menos que imprimir por la UART bloqueando. Es la técnica del tema 44 llevada al máximo: el registro diferido en un canal rápido, que no necesita ni UART.
08 · El caso típico
Analizar un HardFault
Cuando el procesador intenta algo imposible (leer una dirección que no existe, ejecutar una instrucción inválida, dividir por cero si se lo pidió), entra en una excepción de falla. Si no hay un manejador específico, se escala a HardFault, y el programa suele terminar en un bucle infinito del archivo de arranque, es decir, colgado. El síntoma, desde afuera, es solo «el equipo dejó de responder». Pero el chip guarda las pistas, y el depurador las lee:
- El cuadro de pila apilado por el hardware. Al entrar en la excepción, el núcleo guarda en la pila ocho palabras:
R0, R1, R2, R3, R12, LR, PC, xPSR. El PC apilado es la dirección de la instrucción que estaba en curso al producirse la falla, y es la pista principal: con el archivo.mapo el ELF se traduce a una función y una línea. - CFSR (Configurable Fault Status Register, solo en M3 y superiores): una palabra de bits que dice qué pasó: acceso a un lugar inexistente, instrucción indefinida, división por cero, acceso no alineado, uso de la FPU sin habilitarla, falla al apilar (desborde de pila).
- BFAR y MMFAR: la dirección que se intentó acceder, cuando el hardware la conoce (bit de validez en CFSR).
- HFSR: indica que la falla se escaló a HardFault (FORCED).
| Bit | Nombre | Qué sugiere |
|---|---|---|
| 9 y 15 | PRECISERR y BFARVALID | Acceso de datos a una dirección inválida, con la dirección en BFAR: puntero o índice mal |
| 10 | IMPRECISERR | Error de bus detectado tarde: el PC es aproximado |
| 12 | STKERR | Falló el apilado del cuadro: la pila se desbordó |
| 16 | UNDEFINSTR | Instrucción inexistente: se ejecutan datos (dirección de retorno pisada) |
| 17 | INVSTATE | Salto con el bit de Thumb en cero: puntero a función corrupto |
| 19 | NOCP | Instrucción de coprocesador (FPU) sin habilitar |
| 24 | UNALIGNED | Acceso no alineado (si se activó la trampa) |
| 25 | DIVBYZERO | División por cero (si se activó la trampa) |
El código para captar esa información en el propio equipo, sin depurador, es lo que convierte un cuelgue en el campo en un dato (tema 37): un manejador de HardFault que guarda el cuadro de pila y los registros en una zona de RAM que sobrevive al reinicio y que, tras reiniciar, se informa:
/* El HardFault_Handler elige la pila en uso (MSP o PSP, según el bit 2 de LR) y llama a una función en C con el puntero al cuadro apilado */
__attribute__((naked)) void HardFault_Handler(void) {
__asm volatile ("tst lr, #4 \n"
"ite eq \n"
"mrseq r0, msp \n"
"mrsne r0, psp \n"
"b hardfault_c \n");
}
typedef struct { uint32_t pc, lr, sp, cfsr, hfsr, bfar, mmfar; uint32_t sello; } InformeFalla;
static InformeFalla informe __attribute__((section(".noinit"))); /* sobrevive al reinicio (tema 29) */
void hardfault_c(uint32_t *marco) {
informe.pc = marco[6]; /* R0-R3, R12, LR, PC, xPSR: el PC apilado es el elemento 6 */
informe.lr = marco[5];
informe.sp = (uint32_t)marco;
informe.cfsr = SCB->CFSR; informe.hfsr = SCB->HFSR;
informe.bfar = SCB->BFAR; informe.mmfar = SCB->MMFAR;
informe.sello = 0xFA11ED00u; /* marca de que hay un informe válido */
#ifdef DEPURANDO
__BKPT(0); /* con depurador: detenerse aquí; sin él, nunca en el producto */
#endif
NVIC_SystemReset(); /* en el producto: reiniciar y reportar tras arrancar */
}
La Parte 3 del programa arma ocho fallas típicas con sus bits y las decodifica. Una escritura por un puntero a una dirección inexistente deja CFSR = 0x00008200 (PRECISERR y BFARVALID) y BFAR con la dirección culpable, con un PC apilado que, buscado en el mapa, cae en procesar_muestra+0x1A. Una división por cero deja 0x02000000 y apunta a calcular_promedio+0x14. Ejecutar datos deja 0x00010000 con un PC en la RAM (0x20000150): como ninguna función vive allí, se sabe que se ejecutó información en lugar de código, casi seguro una dirección de retorno pisada. Y un desborde de pila deja 0x00001000, con un SP fuera de la RAM. El laboratorio de más abajo permite recorrerlos todos.
09 · Cuando el equipo sale al mundo
El depurador y el producto final
Una puerta de servicio que permite leer y escribir cualquier posición de memoria es, para quien la use sin permiso, una puerta abierta: se lee el firmware y las claves (tema 43), se lo modifica y se instala uno malicioso. Por eso los fabricantes ofrecen formas de cerrarla, y el producto debe usarlas:
| Nivel | Efecto | Cuándo usarlo |
|---|---|---|
| Abierto | Se lee y se escribe todo | Desarrollo y primeras pruebas |
| Protección de lectura | La sonda no puede leer la flash; borrar total la reabre (borrando el firmware) | Productos de serie: protege la propiedad intelectual y las claves |
| Depuración deshabilitada de forma permanente | Ya no se conecta ningún depurador (en algunos chips, irreversible) | Productos de alto riesgo; se pierde la posibilidad de analizar fallas en el campo |
La decisión no es obvia: cerrar la puerta protege, pero impide analizar un equipo devuelto del campo. Una política razonable es dejar el nivel intermedio en producción y conservar el registro de eventos y el informe de falla del apartado anterior para el diagnóstico remoto (tema 37); y, si hace falta depurar un equipo cerrado, que el procedimiento sea un desbloqueo controlado (con un borrado total) o una puerta de depuración protegida por una clave (debug authentication, que ofrecen algunos fabricantes). El conector físico también se puede dejar sin poblar, aunque la protección verdadera es la del chip y no la del conector.
Dos efectos más del depurador, en el sentido contrario: con la sonda conectada, el microcontrolador consume más y puede no entrar en el modo de bajo consumo profundo, porque el bloque de depuración mantiene relojes encendidos (hay bits para permitir la depuración en reposo, con ese costo). Las medidas de consumo del tema 36 deben hacerse sin la sonda conectada; con ella, se mide otro sistema. Y, como se vio, watchdog y temporizadores se congelan con el núcleo detenido solo si se lo configura.
10 · Analogía con programación
Probar el modelo en Python
El programa reúne cuatro piezas: la construcción de los paquetes de petición de SWD con su paridad y el tiempo ideal de lectura de memoria; la máquina de estados del TAP de JTAG con una búsqueda del camino más corto de TMS; la decodificación de ocho HardFault a partir de los bits de CFSR y el PC apilado, traducido con una tabla de símbolos; y el ancho de banda de la traza por SWO comparado con el de la UART.
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 con SWD y JTAG: paquetes SWD, máquina de estados del TAP JTAG, decodificación de un HardFault y ancho de banda de la traza.
import bisect
# ---- Parte 1: el paquete de petición de SWD y el costo de leer memoria ----------------------------------------------------------------
def pedido_swd(apndp, rnw, a): # a = dirección de registro (0, 4, 8, 12): bits A[3:2]
a2, a3 = (a >> 2) & 1, (a >> 3) & 1
paridad = (apndp + rnw + a2 + a3) % 2 # paridad par sobre APnDP, RnW, A2 y A3
bits = [1, apndp, rnw, a2, a3, paridad, 0, 1] # inicio, APnDP, RnW, A2, A3, paridad, parada, park (se envía primero el bit 0)
return sum(b << i for i, b in enumerate(bits))
print("Parte 1: paquetes de petición de SWD (8 bits, se envía primero el menos significativo)")
for nombre, args in (("leer DP IDCODE (reg 0x0)", (0, 1, 0x0)), ("escribir AP TAR (reg 0x4)", (1, 0, 0x4)), ("leer AP DRW (reg 0xC)", (1, 1, 0xC))):
print(f" {nombre:<28} -> 0x{pedido_swd(*args):02X}")
CLK_TRANS = 8 + 1 + 3 + 32 + 1 + 1 # petición, giro, ACK, datos, paridad, giro
print(f" una transacción de lectura o escritura de 32 bits ocupa {CLK_TRANS} ciclos de SWCLK (8 + 1 + 3 + 32 + 1 + 1)")
def leer_memoria(n_bytes, mhz): # CSW y TAR se escriben una vez; DRW se lee con autoincremento; +1 lectura final de RDBUFF
palabras = n_bytes // 4
ciclos = (2 + palabras + 1) * CLK_TRANS
return ciclos / (mhz * 1e6)
for mhz in (1, 4, 10):
t4k, t128k = leer_memoria(4096, mhz), leer_memoria(131072, mhz)
print(f" SWCLK {mhz:>2} MHz: leer 4 KB en {t4k * 1000:6.1f} ms ({4096 / t4k / 1000:5.0f} KB/s); 128 KB en {t128k:5.2f} s (mejor caso, sin demoras de la PC ni de la sonda)")
# ---- Parte 2: la máquina de estados del TAP de JTAG ------------------------------------------------------------------------------------
T = { # estado: (siguiente con TMS=0, siguiente con TMS=1)
"TLR": ("RTI", "TLR"), "RTI": ("RTI", "SelDR"), "SelDR": ("CapDR", "SelIR"), "CapDR": ("ShDR", "Ex1DR"), "ShDR": ("ShDR", "Ex1DR"),
"Ex1DR": ("PauDR", "UpDR"), "PauDR": ("PauDR", "Ex2DR"), "Ex2DR": ("ShDR", "UpDR"), "UpDR": ("RTI", "SelDR"),
"SelIR": ("CapIR", "TLR"), "CapIR": ("ShIR", "Ex1IR"), "ShIR": ("ShIR", "Ex1IR"), "Ex1IR": ("PauIR", "UpIR"), "PauIR": ("PauIR", "Ex2IR"),
"Ex2IR": ("ShIR", "UpIR"), "UpIR": ("RTI", "SelDR")}
def camino(desde, hasta): # búsqueda en anchura del camino más corto de TMS
cola, visto = [(desde, [])], {desde}
while cola:
e, tms = cola.pop(0)
if e == hasta: return tms
for b in (0, 1):
s = T[e][b]
if s not in visto: visto.add(s); cola.append((s, tms + [b]))
print()
print(f"Parte 2: el TAP de JTAG tiene {len(T)} estados y se maneja solo con TMS en cada flanco de TCK")
c_idcode = camino("TLR", "ShDR")
print(f" de Test-Logic-Reset a Shift-DR: TMS = {c_idcode} ({len(c_idcode)} ciclos); tras reset el registro seleccionado es IDCODE")
def leer_idcodes(n): # n dispositivos en cadena, cada uno con su IDCODE de 32 bits
return len(c_idcode) + 32 * n + len(camino("Ex1DR", "RTI"))
for n in (1, 3):
print(f" leer el IDCODE de una cadena de {n} dispositivo(s): {len(c_idcode)} + {32 * n} + 2 = {leer_idcodes(n)} ciclos de TCK")
ir_bits = 4
c_ir = len(camino("RTI", "ShIR")) + ir_bits + len(camino("Ex1IR", "RTI"))
c_dr = len(camino("RTI", "ShDR")) + 35 + len(camino("Ex1DR", "RTI"))
print(f" acceso al ARM por JTAG: cambiar la instrucción ({ir_bits} bits) cuesta {c_ir} ciclos y un registro de 35 bits (3 de ACK, 32 de datos), {c_dr}; en SWD, {CLK_TRANS}")
cadena = 3
print(f" en una cadena de {cadena} chips, para hablar con uno, los otros {cadena - 1} se ponen en BYPASS: cada uno agrega 1 bit al registro de datos ({35 + cadena - 1} bits)")
print(" pines: JTAG usa TCK, TMS, TDI, TDO (y, a veces, nTRST) = 4 o 5; SWD usa SWCLK y SWDIO = 2 (más el reset y la masa)")
# ---- Parte 3: decodificar un HardFault a partir de CFSR y del PC apilado -----------------------------------------------------------------
BITS = {0: "IACCVIOL", 1: "DACCVIOL", 3: "MUNSTKERR", 4: "MSTKERR", 7: "MMARVALID", 8: "IBUSERR", 9: "PRECISERR", 10: "IMPRECISERR",
11: "UNSTKERR", 12: "STKERR", 15: "BFARVALID", 16: "UNDEFINSTR", 17: "INVSTATE", 18: "INVPC", 19: "NOCP", 24: "UNALIGNED", 25: "DIVBYZERO"}
POR_NOMBRE = {v: k for k, v in BITS.items()}
SIMBOLOS = [(0x08000190, "main"), (0x080001F0, "sensor_leer"), (0x08000A80, "procesar_muestra"), (0x08000B10, "calcular_promedio"),
(0x08000C40, "enviar_trama"), (0x08000CF0, "TIM2_IRQHandler"), (0x08000D80, "_etext")]
def simbolo(pc):
if not (0x08000000 <= pc < 0x08000D80): return "FUERA DE LA FLASH (¿se ejecutan datos?)"
i = bisect.bisect_right([a for a, _ in SIMBOLOS], pc) - 1
return f"{SIMBOLOS[i][1]}+0x{pc - SIMBOLOS[i][0]:X}"
CASOS = [("escritura por un puntero a una dirección inexistente", ["PRECISERR", "BFARVALID"], 0x08000A9A),
("división por cero (con DIV_0_TRP activado)", ["DIVBYZERO"], 0x08000B24),
("salto a una dirección sin el bit de Thumb", ["INVSTATE"], 0x08000C58),
("ejecución de datos: salto a la RAM", ["UNDEFINSTR"], 0x20000150),
("acceso no alineado (con UNALIGN_TRP activado)", ["UNALIGNED"], 0x08000AB2),
("desborde de pila hasta fuera de la RAM", ["STKERR"], 0x08000A88),
("escritura de un búfer, error impreciso", ["IMPRECISERR"], 0x08000D1C),
("instrucción de punto flotante con la FPU apagada", ["NOCP"], 0x08000AE0)]
print()
print("Parte 3: de CFSR y del PC apilado a una causa (cada caso arma el registro con sus bits y lo decodifica)")
for nombre, nombres, pc in CASOS:
cfsr = sum(1 << POR_NOMBRE[n] for n in nombres)
leidos = [BITS[b] for b in sorted(BITS) if cfsr >> b & 1]
assert leidos == sorted(nombres, key=lambda n: POR_NOMBRE[n])
print(f" {nombre}\n CFSR = 0x{cfsr:08X} -> {', '.join(leidos)}; PC apilado 0x{pc:08X} = {simbolo(pc)}")
print(" el cuadro apilado por el hardware al entrar a la excepción: R0, R1, R2, R3, R12, LR, PC, xPSR (8 palabras = 32 bytes), en ese orden desde el SP")
# ---- Parte 4: ancho de banda de la traza por SWO (ITM) frente a la UART ------------------------------------------------------------------
SWO_BAUD = 2_000_000 # SUPUESTO: SWO asíncrono a 2 Mbaudios (depende de la sonda y del reloj del núcleo)
bytes_s_swo = SWO_BAUD / 10
msg = 40
por_bytes = msg * 2 # cada byte escrito al puerto de estímulo viaja con un encabezado: 2 bytes
por_palabras = (msg // 4) * (1 + 4) # una palabra de 4 bytes: encabezado + 4 datos
print()
print("Parte 4: salida de texto por ITM/SWO (puerto de estímulo) frente a la UART")
print(f" SWO a {SWO_BAUD / 1e6:.0f} Mbaudios (SUPUESTO) mueve {bytes_s_swo:,.0f} B/s (10 bits por byte)".replace(",", " "))
print(f" un mensaje de {msg} caracteres ocupa {por_bytes} B si se escribe byte a byte y {por_palabras} B si se escribe de a 4 bytes")
print(f" mensajes por segundo: {bytes_s_swo / por_bytes:,.0f} byte a byte; {bytes_s_swo / por_palabras:,.0f} de a 4 bytes; la UART a 115 200 admite {115200 / 10 / msg:.0f}".replace(",", " "))
ciclos = (msg // 4) * 3 # SUPUESTO: 3 ciclos por escritura de palabra al registro de estímulo
print(f" costo en el núcleo (SUPUESTO: {ciclos} ciclos a 48 MHz): {ciclos / 48:.2f} µs, contra 3472 µs de imprimir por la UART bloqueando ({3472 / (ciclos / 48):,.0f} veces más)".replace(",", " "))
Resultado, parte 1: las peticiones SWD son 0xA5 (leer IDCODE), 0x8B (escribir TAR) y 0x9F (leer DRW); cada transacción ocupa 46 ciclos; leer 4 KB lleva 47,2 ms a 1 MHz, 11,8 ms a 4 MHz y 4,7 ms a 10 MHz (347 KB/s a 4 MHz), y 128 KB, 1,51 s, 0,38 s y 0,15 s. Parte 2: el TAP tiene 16 estados; de reset a desplazamiento hay 4 ciclos (TMS 0, 1, 0, 0); leer el IDCODE de 1 chip son 38 ciclos y de 3 chips en cadena, 102; un registro de 35 bits cuesta 40 ciclos y cambiar la instrucción de 4 bits, 10, contra 46 de una transacción SWD; con 3 chips, el registro de datos es de 37 bits. Parte 3: ocho fallas con CFSR 0x00008200, 0x02000000, 0x00020000, 0x00010000, 0x01000000, 0x00001000, 0x00000400 y 0x00080000, y PC apilados que se traducen a procesar_muestra+0x1A, calcular_promedio+0x14, enviar_trama+0x18, «fuera de la flash» (0x20000150), procesar_muestra+0x32, procesar_muestra+0x8, TIM2_IRQHandler+0x2C y procesar_muestra+0x60. Parte 4: SWO a 2 Mbaudios mueve 200 000 B/s: un mensaje de 40 caracteres ocupa 80 B byte a byte o 50 B de a 4 bytes (2500 o 4000 mensajes por segundo contra 288 de la UART); el costo en el núcleo (30 ciclos a 48 MHz, supuesto) es 0,62 µs, 5555 veces menos que imprimir.
Probá cambiar en la Parte 1 el valor de mhz o el tamaño de la lectura, agregar en la Parte 2 una cadena de diez chips y ver cuánto crece la lectura del IDCODE (el registro crece 32 bits por chip), o agregar en la Parte 3 un caso propio con otros bits de CFSR. La lección del programa es que cada registro del depurador es un conjunto de bits con significado, y que una tabla y unas pocas líneas bastan para convertir una palabra hexadecimal en un diagnóstico.
11 · Exploración interactiva
Diagnóstico de un HardFault
El laboratorio es el «depurador» que se abre cuando el equipo se colgó: elegís una de ocho fallas típicas y muestra lo que leerías con la sonda (CFSR y los bits activos, el PC apilado traducido con una tabla de símbolos de ejemplo, el SP y, cuando es válida, la dirección en BFAR), junto con la causa más probable y qué hacer. Los valores son de un firmware imaginario con 20 KB de RAM (0x20000000 a 0x20004FFF) y la flash a partir de 0x08000000.
¿Por qué se colgó?
Lo que se lee con el depurador
Causa probable
Qué hacer
Elegí una falla.
Recorrido guiado
- Con la falla 1 (escritura por un puntero a una dirección inexistente): CFSR =
0x00008200, con PRECISERR y BFARVALID. BFAR vale0xA0000000, la dirección que se intentó escribir, y el PC apilado cae enprocesar_muestra+0x1A. Con la función y la dirección, el puntero culpable está a un paso. - Elegí la falla 2 (división por cero): CFSR =
0x02000000(DIVBYZERO) y PC encalcular_promedio+0x14. No hay dirección en BFAR: el error no es de un acceso a memoria. - Con la falla 4 (ejecución de datos): CFSR =
0x00010000(UNDEFINSTR) y el PC es0x20000150, fuera de la flash. Es la pista más fuerte: el programa está ejecutando información de la RAM. La búsqueda ya no empieza en la función donde se detuvo (no existe) sino en quién pisó la dirección de retorno. - Con la falla 6 (desborde de pila): CFSR =
0x00001000(STKERR) y el SP vale0x1FFFFFE0, fuera de la RAM. Acá no hay que fiarse del PC apilado, porque el cuadro no se pudo guardar: se sospecha de la pila antes que del código. - Con la falla 7 (error impreciso): CFSR =
0x00000400y BFAR no es válido. El PC apilado es aproximado, porque la escritura quedó en un búfer y falló después de que la instrucción terminó. Para volver el error preciso hay que desactivar el almacenamiento intermedio de escrituras en los núcleos que lo permiten. - Recorré las fallas 3, 5 y 8 (INVSTATE, UNALIGNED y NOCP) y mirá que cada bit de CFSR corresponde a una causa distinta, de modo que con un solo registro se descarta la mayor parte del código. Los bits 17, 19 y 24 son un buen ejemplo: puntero a función, FPU sin habilitar y acceso no alineado.
Desafío: tu equipo es un Cortex-M0+, que no tiene CFSR. ¿Cómo se analiza un HardFault?
Sin CFSR no hay un registro que diga qué pasó, pero el cuadro de pila existe en todos los Cortex-M: el hardware apila R0-R3, R12, LR, PC y xPSR igual. Se obtiene el PC apilado con el mismo manejador (el cuadro está en el MSP o en el PSP según el bit 2 del LR de la excepción) y se mira qué instrucción está en esa dirección: desensamblándola con el ELF se ve si es un acceso a memoria, una división o un salto. Con eso, y el contenido de los registros apilados (R0-R3 suelen contener los punteros usados), casi siempre se deduce la causa. También sirve el LR (quién llamó), y el SP: ¿está dentro de la RAM? Los chips sin CFSR tienen menos tipos de falla distintos (por ejemplo, no tienen trampa de división por cero), de modo que los casos posibles son pocos.
12 · Método de trabajo
Método: depurar con el depurador
- Dejá el conector y los pines de depuración accesibles desde el diseño de la placa (VTref, SWDIO, SWCLK, GND, nRESET, SWO si se usa). Un diseño sin acceso a SWD pierde la herramienta más potente.
- Conectá bien antes de sospechar del chip: masa corta, VTref, reset, cables cortos y, ante un error de conexión, bajar la velocidad del reloj y probar «connect under reset».
- Compilá con símbolos (
-g) y conservá el ELF de cada versión liberada, junto con su mapa. - Si el equipo se colgó, no lo reinicies: detenelo con el depurador y mirá el PC, el SP y el LR, y el CFSR. Es información que se pierde con el reinicio.
- Preferí los watchpoints a los breakpoints condicionales y los registros sin detener (ITM, RTT, registro de eventos) cuando el tiempo importa. Un breakpoint condicional lento cambia el comportamiento.
- Congelá los temporizadores y el watchdog cuando el núcleo está detenido por el depurador, y recordá que al reanudar el tiempo salta.
- Instalá un manejador de HardFault que guarde el cuadro de pila y los registros de falla en RAM sin inicializar, para tener el mismo diagnóstico en el campo, sin depurador.
- Medí el consumo y los tiempos finales sin la sonda, con la configuración de producción.
- Cerrá la interfaz de depuración en el producto con el nivel de protección que corresponda (tema 43) y definí cómo se analizarán los equipos devueltos.
13 · Comprensión
Confusiones frecuentes
«Si detengo el programa, se detiene todo el equipo»
Solo el núcleo. Los temporizadores, la UART, el ADC y el watchdog siguen corriendo, y los sensores y la radio siguen en el mundo real. Al detener y reanudar, el programa ve saltar el tiempo y puede recibir datos que se acumularon. Los bits de congelación de la depuración existen por eso.
«SWD y JTAG son dos cosas completamente distintas»
Comparten el mismo bloque de acceso (el DAP) y las mismas funciones; cambia el protocolo de transporte: JTAG, con una máquina de estados y cuatro hilos; SWD, con paquetes y dos hilos. Una sonda moderna habla los dos, y el programa de depuración no nota la diferencia.
«Un breakpoint condicional sirve para cualquier cosa»
Si la condición se evalúa en la PC, el núcleo se detiene cada vez que pasa por ahí, y eso puede hacerlo miles de veces más lento. En una rutina de tiempo real es inutilizable. Para esos casos: un watchpoint, un pin de prueba o un registro de eventos.
«El depurador me muestra el valor real de la variable»
Con optimizaciones activas, el compilador puede mantener una variable solo en un registro, o eliminarla, y el depurador muestra «optimizado» o valores sin sentido. Y el ELF debe corresponder exactamente al binario grabado. Depurar con la optimización del producto es válido, pero hay que interpretar lo que se ve.
«El PC apilado siempre es la instrucción que falló»
Es así en los errores precisos. En un error de bus impreciso es aproximado, y en un desborde de pila (STKERR) puede no ser fiable porque el cuadro no se guardó. Mirar los bits de CFSR dice cuánto confiar en el PC.
«Con la sonda conectada, mido el consumo real»
No: la sonda mantiene relojes del bloque de depuración activos, impide algunos modos de bajo consumo y agrega corriente por sus propias conexiones. Las medidas de consumo se hacen sin la sonda (tema 36).
«Dejar el depurador abierto en producción no es un riesgo»
Permite leer el firmware y las claves, y escribir uno modificado. Se debe cerrar con la protección del chip (tema 43), y decidir de antemano cómo se analizarán las fallas de los equipos devueltos, por ejemplo con un informe de falla en RAM sin inicializar enviado por telemetría.
«Si el chip no responde a la sonda, está roto»
Casi siempre es otra cosa: un cable de masa largo, VTref sin conectar, una velocidad de SWCLK demasiado alta, o un programa que reconfiguró los pines de SWD o apagó el bloque de depuración. «Connect under reset» y una velocidad más baja suelen resolverlo.
14 · Práctica guiada
Ejercicios
1. Tiempo de lectura
¿Cuánto tarda, en el mejor caso, en leerse por SWD, con SWCLK a 2 MHz, una RAM de 16 KB? (Se escribe una vez CSW y TAR; luego una lectura de DRW por palabra y una lectura final de RDBUFF; 46 ciclos por transacción.)
Ver solución
16 KB = 4096 palabras de 4 bytes. Transacciones: 2 (CSW y TAR) + 4096 (DRW) + 1 (RDBUFF) = 4099. Ciclos: 4099 × 46 = 188 554. Tiempo: 188 554 / 2 000 000 = 0,0943 s ≈ 94 ms (unos 174 KB/s). En la práctica, algo más por las demoras de la sonda y del USB.
2. Leer un CFSR
Un equipo colgado muestra CFSR = 0x00008200 y BFAR = 0xA0000000. ¿Qué bits están activos y qué pasó? ¿Se puede confiar en el PC apilado?
Ver solución
0x8200 = 1000 0010 0000 0000 en binario: están activos el bit 15 (BFARVALID) y el bit 9 (PRECISERR). Fue un error de bus preciso en un acceso de datos a la dirección 0xA0000000 (que no corresponde a ninguna memoria): un puntero o un índice mal calculado. Como es preciso, el PC apilado es la instrucción culpable: se busca en el mapa y se mira qué puntero usa.
3. Cadena JTAG
Una placa tiene cuatro dispositivos JTAG en cadena. ¿Cuántos ciclos de TCK lleva leer los cuatro IDCODE desde el reset, y cuánto tiempo a 1 MHz?
Ver solución
4 ciclos (reset a desplazamiento) + 4 × 32 = 128 de desplazamiento + 2 (a reposo) = 134 ciclos. A 1 MHz: 134 µs. Cada dispositivo agrega 32 bits al registro de datos (la cadena se comporta como un solo registro largo).
4. Pocos breakpoints
Un Cortex-M4 con el código en flash tiene seis breakpoints de hardware. Se quiere vigilar ocho puntos distintos. ¿Qué opciones hay?
Ver solución
En flash solo caben seis a la vez. Opciones: activar y desactivar según la etapa (los de la inicialización se quitan al pasarla); reemplazar algunos puntos por pines de prueba o eventos de registro; usar watchpoints (cuatro más) cuando lo que importa es el dato; copiar el código de interés a RAM, donde sí valen los breakpoints de software; o aceptar que el IDE reprograme la flash para poner breakpoints de software (lento y con desgaste: no recomendable). Y, si el comportamiento es de tiempo real, la traza de eventos evita detener el programa.
5. Canal de traza
SWO a 4 Mbaudios (10 bits por byte). Mensajes de 24 caracteres. ¿Cuántos mensajes por segundo caben si se escriben byte a byte y si se escriben de a palabras de 4 bytes? (Cada escritura lleva un encabezado de 1 byte.)
Ver solución
Canal: 4 000 000 / 10 = 400 000 B/s. Byte a byte: 24 × 2 = 48 B por mensaje → 400 000 / 48 ≈ 8333 mensajes/s. De a 4 bytes: 6 palabras × 5 B = 30 B → 400 000 / 30 ≈ 13 333 mensajes/s. Escribir de a palabras es más eficiente: menos encabezados por dato. En ambos casos muy por encima de los 480 mensajes de 24 caracteres por segundo que admite una UART a 115 200 baudios (11 520 / 24).
6. El equipo que se reinicia con el depurador
Al poner un breakpoint en una función y pararse en él, el equipo se reinicia solo unos segundos después. Sin depurador no se reinicia. ¿Por qué ocurre y cómo se arregla?
Ver solución
Con el núcleo detenido, el watchdog sigue contando (tema 38): como nadie lo alimenta, vence y reinicia el equipo. Se arregla activando el bit de congelación del watchdog en depuración del microcontrolador (en la familia STM32, en DBGMCU), de modo que su cuenta se pare cuando el depurador detiene el núcleo. Lo mismo vale para los temporizadores cuyo comportamiento se quiere «parado» durante la pausa. Y hay que recordar quitar esa configuración, o verificarla, en la versión de producción.
15 · Síntesis
Ideas para recordar
- La interfaz de depuración es un segundo acceso al chip, independiente de la CPU (el DAP): ve el mismo mapa de memoria, puede leer y escribir cualquier dirección y detener el núcleo. Detener el núcleo no detiene los periféricos ni el watchdog, salvo que se configure su congelación.
- JTAG usa cuatro o cinco hilos y una máquina de estados de 16 estados manejada por TMS (38 ciclos para leer un IDCODE; 102 con tres chips en cadena). SWD usa dos hilos y paquetes de 46 ciclos por palabra de 32 bits (la petición de lectura del IDCODE es 0xA5). Comparten el mismo bloque; SWD gana en pines y JTAG en cadenas y en boundary scan.
- La conexión física es la causa de la mayoría de los fallos: masa corta, cables cortos, VTref, reset, velocidad de SWCLK moderada. Los pines de depuración deben seguir accesibles en la placa final, y se puede recuperar un chip «bloqueado» con connect under reset.
- Los breakpoints de hardware son pocos (hasta 6 en un M4, 4 en un M0) y son los únicos válidos en flash; los watchpoints (DWT) detienen el programa al leer o escribir un dato, y son la herramienta para «la variable cambia sola». Un breakpoint condicional es lento y vuelve inutilizable el tiempo real.
- Para mirar sin detener: ITM/SWO (200 000 B/s a 2 Mbaudios, miles de mensajes por segundo, unos 0,6 µs de costo), RTT (un buffer en RAM leído por SWD) o el registro de eventos; el semihosting detiene el núcleo y no debe quedar en el producto.
- Un HardFault se analiza con el cuadro apilado (el PC apilado es la pista principal), CFSR (qué pasó), BFAR/MMFAR (qué dirección), el SP y el LR. Un PC fuera de la flash indica que se ejecutan datos; un SP fuera de la RAM, desborde de pila; una falla imprecisa, un PC aproximado. Un manejador que guarde esos valores en RAM sin inicializar da el mismo diagnóstico en el campo.
- El ELF con símbolos de cada versión liberada es parte del producto: sin él, un cuelgue solo muestra direcciones. Y la interfaz de depuración se cierra en producción con la protección del chip, midiendo el consumo siempre sin la sonda conectada.
¿Listo para el tema siguiente?
Antes de avanzar, revisá un proyecto tuyo y respondé: ¿tiene el conector o los pines de SWD accesibles en la placa final?, ¿guardás el ELF de cada versión que entregás?, ¿qué hace tu HardFault_Handler: un bucle infinito o un informe que sobrevive al reinicio?, ¿el watchdog se congela con el depurador? Si el manejador es un bucle vacío, escribí el de este tema: son veinte líneas que convierten un cuelgue misterioso en un dato.
En el próximo tema usamos todo esto para estudiar los fallos típicos: el HardFault, la pila corrupta y las condiciones de carrera, y cómo reproducirlos y analizarlos con método.