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 1Implementation
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);
}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
--------------------------------------------------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));
}
}Typical output:
Task Absolute %
IDLE 9800500 98%
WiFi 100000 1%
Blink 50000 <1%
MathTask 50000 <1%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
StackDepthinxTaskCreate.
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
NULLhelps 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.