freertos-debugging-vtasklist-uso-cpu

Debug FreeRTOS with vTaskList and CPU Statistics

  • 6 min

The debugging in FreeRTOS is the set of techniques for observing tasks, memory, timings, and locks in a concurrent system.

When an RTOS fails, it often does so in non-obvious ways: the system restarts, a task stops responding while others remain alive, or the WiFi connection becomes unstable.

Debugging a concurrent system is difficult because we cannot easily stop it (if you set a breakpoint, timers keep running, the Watchdog triggers, and WiFi disconnects).

Fortunately, FreeRTOS includes introspection tools that allow us to take an “X-ray” of the system in real time. Today we’ll learn to use vTaskList and execution statistics.

The task manager: vTaskList

Imagine you could open Windows’ “Task Manager” or Mac’s Activity Monitor, but inside your ESP32. That is exactly what the vTaskList() function does.

This function takes a system snapshot and generates a text table with important information about each task.

Prerequisites

For this to work, FreeRTOS must be compiled with certain options enabled in FreeRTOSConfig.h.

  • In Arduino-ESP32, check the configuration of the installed version, as precompiled libraries may enable or disable these functions.
  • In a configurable port, you need:
#define configUSE_TRACE_FACILITY 1
#define configUSE_STATS_FORMATTING_FUNCTIONS 1
Copied!

Implementation

The function needs a buffer to write the report, but its API does not receive the size. We must reserve enough margin for all tasks or use uxTaskGetSystemState() and format the output ourselves if strict control is needed.

void MonitorTask(void *pvParameters) {
  char buffer[1024]; // Adjust according to the number of tasks and their names.

  for(;;) {
    // 1. Take a system snapshot
    // FreeRTOS will fill the buffer with the table
    vTaskList(buffer);

    // 2. Print the result
    Serial.println("--------------------------------------------------");
    Serial.println("Name        State  Prio    Stack   Num");
    Serial.println("--------------------------------------------------");
    Serial.print(buffer);
    Serial.println("--------------------------------------------------");

    // Repeat every 5 seconds
    vTaskDelay(pdMS_TO_TICKS(5000));
  }
}

void setup() {
  Serial.begin(115200);
  
  // Create some example tasks to see something interesting
  xTaskCreate(BlinkTask, "Blink", 2048, NULL, 1, NULL);
  xTaskCreate(WiFiTask,  "WiFi",  4096, NULL, 2, NULL);

  // Create the monitor task
  xTaskCreate(MonitorTask, "Monitor", 2048, NULL, 1, NULL);
}
Copied!

Interpreting the Output

If you run the code, you’ll see something like this on the serial monitor:

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

Let’s decipher the columns:

Name: The name we gave in xTaskCreate.

State: A letter indicating the task’s status at that moment:

  • X (Running): Executing at the time of the snapshot.
  • R (Ready): Ready to run.
  • B (Blocked): Waiting (delay, queue, semaphore).
  • S (Suspended): Explicitly suspended.
  • D (Deleted): Marked for deletion (waiting for IDLE).

Prio: The current priority.

Stack (High Water Mark): This is the most important column!

  • It does not indicate used memory, but the historical minimum free memory.
  • In ESP-IDF it is expressed in bytes; in standard FreeRTOS it is expressed in units of StackType_t.
  • A small value requires attention, but the correct margin depends on the worst-case execution paths and future load.

Num: A unique auto-incremental ID assigned by the system.

Measuring CPU load with vTaskGetRunTimeStats

vTaskList tells us how tasks are doing, but not how much they work. If the system is slow, we need to know which task is hogging the CPU.

For this, there is vTaskGetRunTimeStats. This function returns the absolute and relative (%) time each task has spent running.

To measure this, FreeRTOS needs a runtime clock source significantly faster than the tick, as well as the configGENERATE_RUN_TIME_STATS and configUSE_STATS_FORMATTING_FUNCTIONS options. Do not assume precompiled libraries include them.

Usage Example

Its usage is identical to vTaskList: pass it a buffer.

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

  for(;;) {
    vTaskGetRunTimeStats(bufferStats);
    
    Serial.println("------ CPU USAGE ------");
    Serial.println("Task         Absolute      %");
    Serial.print(bufferStats);
    
    vTaskDelay(pdMS_TO_TICKS(10000));
  }
}
Copied!

Typical output:

Task         Absolute      %
IDLE          9800500     98%
WiFi           100000      1%
Blink           50000     <1%
MathTask        50000     <1%
Copied!

A high percentage for Idle indicates that core has had significant time without higher-priority work. A low percentage is not inherently bad: compare it with deadlines, pending queues, and the latency required by the application.

vTaskList() and vTaskGetRunTimeStats() generate a lot of text and suspend scheduling during part of the capture. They are diagnostic tools, not functions to run continuously in production.

Common errors

To close the course, we compile the most common “blue screens” you’ll find on the ESP32 serial monitor and what they mean.

Guru Meditation Error: Stack Canary Watchpoint Triggered

  • Cause: A task has written outside its allocated memory. Stack Overflow.
  • Solution: Check which was the last active task (the error itself usually states it) and increase its StackDepth in xTaskCreate.

Task Watchdog Got Triggered (TWDT)

  • Cause: A task has been running for too long without yielding control (Starvation). The IDLE task has not been able to run to “feed the watchdog”.
  • Solution: Look for long loops or sections and make the task block via a queue, semaphore, notification, or vTaskDelay. taskYIELD() only yields to tasks of the same priority and can still leave Idle without CPU.

LoadProhibited / StoreProhibited

  • Cause: The program tries to access an invalid address due to a null, uninitialized, already freed, or corrupted pointer.
  • Solution: Review the trace and the lifecycle of the pointers. Checking for NULL helps but does not detect a dangling pointer.

Trace visualization

What we’ve seen is debugging via serial port. It is useful, but intrusive (the Serial.print itself affects execution time).

Tools like SEGGER SystemView allow recording kernel events and visualizing them on a timeline. Integration and capture methods depend on the chip, framework, and available debug probe.

A trace allows viewing context switches, ISRs, and synchronization operations without relying solely on serial messages. Its setup is more advanced, but very useful for studying jitter, lockups, and execution order.