freertos-debugging-vtasklist-uso-cpu

Depurar FreeRTOS con vTaskList y estadísticas de CPU

  • 6 min

La depuración en FreeRTOS es el conjunto de técnicas para observar tareas, memoria, tiempos y bloqueos en un sistema concurrente.

Cuando un RTOS falla, suele hacerlo de formas poco evidentes: el sistema se reinicia, una tarea deja de responder mientras las demás siguen vivas o la conexión WiFi se vuelve inestable.

Depurar un sistema concurrente es difícil porque no podemos detenerlo fácilmente (si pones un breakpoint, los timers siguen contando, el Watchdog salta y el WiFi se desconecta).

Por suerte, FreeRTOS incluye herramientas de introspección que nos permiten hacer una “radiografía” al sistema en tiempo real. Hoy vamos a aprender a usar vTaskList y las estadísticas de ejecución.

El administrador de tareas: vTaskList

Imagina que pudieras abrir el “Administrador de Tareas” de Windows o el monitor de actividad de Mac, pero dentro de tu ESP32. Eso es exactamente lo que hace la función vTaskList().

Esta función toma una instantánea del sistema y genera una tabla de texto con información importante sobre cada tarea.

Configuración previa

Para que esto funcione, FreeRTOS debe estar compilado con ciertas opciones activadas en FreeRTOSConfig.h.

  • En Arduino-ESP32, comprueba la configuración de la versión instalada, porque las librerías precompiladas pueden habilitar o deshabilitar estas funciones.
  • En un port configurable, necesitas:
#define configUSE_TRACE_FACILITY 1
#define configUSE_STATS_FORMATTING_FUNCTIONS 1
Copied!

Implementación

La función necesita un buffer donde escribir el informe, pero su API no recibe el tamaño. Debemos reservar margen suficiente para todas las tareas o usar uxTaskGetSystemState() y formatear nosotros la salida si necesitamos control estricto.

void TareaMonitor(void *pvParameters) {
  char buffer[1024]; // Ajustar según el número de tareas y sus nombres.

  for(;;) {
    // 1. Tomamos una instantánea del sistema
    // FreeRTOS rellenará el buffer con la tabla
    vTaskList(buffer);

    // 2. Imprimimos el resultado
    Serial.println("--------------------------------------------------");
    Serial.println("Nombre      Estado   Prio    Stack    Num");
    Serial.println("--------------------------------------------------");
    Serial.print(buffer);
    Serial.println("--------------------------------------------------");

    // Repetimos cada 5 segundos
    vTaskDelay(pdMS_TO_TICKS(5000));
  }
}

void setup() {
  Serial.begin(115200);
  
  // Creamos algunas tareas de ejemplo para ver algo interesante
  xTaskCreate(TareaBlink, "Blink", 2048, NULL, 1, NULL);
  xTaskCreate(TareaWiFi,  "WiFi",  4096, NULL, 2, NULL);

  // Creamos la tarea monitor
  xTaskCreate(TareaMonitor, "Monitor", 2048, NULL, 1, NULL);
}
Copied!

Interpretando la Salida

Si ejecutas el código, verás algo parecido a esto en el monitor serie:

Nombre      Estado   Prio    Stack    Num
--------------------------------------------------
WiFi          R       2      1500     2
Blink         B       1      1800     3
Monitor       X       1      1024     4
IDLE          R       0      1010     1
--------------------------------------------------
Copied!

Vamos a descifrar las columnas:

Nombre: El nombre que le dimos en xTaskCreate.

Estado: Una letra que indica cómo estaba la tarea en ese instante:

  • X (Running): Ejecutándose en el instante de la captura.
  • R (Ready): Lista para ejecutarse.
  • B (Blocked): Esperando (delay, cola, semáforo).
  • S (Suspended): Suspendida explícitamente.
  • D (Deleted): Marcada para borrar (esperando a la IDLE).

Prio: La prioridad actual.

Stack (High Water Mark): ¡Esta es la columna más importante!

  • No indica la memoria usada, sino la mínima memoria libre histórica.
  • En ESP-IDF se expresa en bytes; en FreeRTOS estándar se expresa en unidades de StackType_t.
  • Un valor pequeño requiere atención, pero el margen correcto depende de los peores caminos de ejecución y de la carga futura.

Num: Un ID único autoincremental que asigna el sistema.

Medir la carga de CPU con vTaskGetRunTimeStats

vTaskList nos dice cómo están las tareas, pero no cuánto trabajan. Si el sistema va lento, necesitamos saber qué tarea está acaparando la CPU.

Para esto existe vTaskGetRunTimeStats. Esta función nos devuelve el tiempo absoluto y relativo (%) que cada tarea ha pasado ejecutándose.

Para medir esto, FreeRTOS necesita una fuente de tiempo de ejecución bastante más rápida que el tick y las opciones configGENERATE_RUN_TIME_STATS y configUSE_STATS_FORMATTING_FUNCTIONS. No des por hecho que las librerías precompiladas las incluyen.

Ejemplo de uso

El funcionamiento es idéntico a vTaskList: le pasamos un buffer.

void TareaStats(void *pvParameters) {
  char bufferStats[512];

  for(;;) {
    vTaskGetRunTimeStats(bufferStats);
    
    Serial.println("------ USO DE CPU ------");
    Serial.println("Tarea        Absoluto      %");
    Serial.print(bufferStats);
    
    vTaskDelay(pdMS_TO_TICKS(10000));
  }
}
Copied!

Salida típica:

Tarea        Absoluto      %
IDLE          9800500     98%
WiFi           100000      1%
Blink           50000     <1%
MathTask        50000     <1%
Copied!

Un porcentaje alto en Idle indica que ese núcleo ha tenido bastante tiempo sin trabajo de mayor prioridad. Un porcentaje bajo no es malo por sí mismo: compáralo con los plazos, las colas pendientes y la latencia que necesita la aplicación.

vTaskList() y vTaskGetRunTimeStats() formatean bastante texto y suspenden la planificación durante parte de la captura. Son herramientas de diagnóstico, no funciones para ejecutar continuamente en producción.

Errores comunes

Para cerrar el curso, recopilamos los “pantalazos azules” más comunes que encontraréis en el monitor serie del ESP32 y qué significan.

Guru Meditation Error: Stack Canary Watchpoint Triggered

  • Causa: Una tarea ha escrito fuera de su memoria asignada. Stack Overflow.
  • Solución: Mira cuál fue la última tarea activa (el propio error suele decirlo) y auméntale el StackDepth en xTaskCreate.

Task Watchdog Got Triggered (TWDT)

  • Causa: Una tarea lleva demasiado tiempo ejecutándose sin ceder el control (Starvation). La tarea IDLE no ha podido ejecutarse para “alimentar al perro”.
  • Solución: Busca bucles o secciones largas y haz que la tarea se bloquee mediante una cola, semáforo, notificación o vTaskDelay. taskYIELD() solo cede ante tareas de la misma prioridad y puede seguir dejando sin CPU a Idle.

LoadProhibited / StoreProhibited

  • Causa: El programa intenta acceder a una dirección inválida por un puntero nulo, sin inicializar, ya liberado o corrupto.
  • Solución: Revisa la traza y la vida de los punteros. Comprobar NULL ayuda, pero no detecta un puntero colgante.

Visualización de trazas

Lo que hemos visto es depuración por puerto serie. Es útil, pero intrusiva (el propio Serial.print afecta al tiempo de ejecución).

Herramientas como SEGGER SystemView permiten registrar eventos del kernel y visualizarlos en una línea temporal. La integración y el método de captura dependen del chip, del framework y de la sonda de depuración disponible.

Una traza permite ver cambios de contexto, ISR y operaciones de sincronización sin depender únicamente de mensajes serie. Su configuración es más avanzada, pero resulta muy útil para estudiar jitter, bloqueos y orden de ejecución.