Chapter 10: FreeRTOS Task Creation and Management
Chapter Objectives
Upon completing this chapter, you will be able to:
- Create FreeRTOS tasks using
xTaskCreate
and understand all its parameters. - Create tasks pinned to specific CPU cores using
xTaskCreatePinnedToCore
on multi-core targets. - Effectively use task handles (
TaskHandle_t
) to manage specific tasks. - Pass parameters to tasks during creation.
- Delete tasks safely using
vTaskDelete
, understanding the need for resource cleanup. - Temporarily stop and restart tasks using
vTaskSuspend
andvTaskResume
. - Dynamically change the priority of a running task using
vTaskPrioritySet
. - Retrieve information about tasks, such as their state, priority, and stack usage.
- Implement robust task lifecycle management in your applications.
Introduction
In the previous chapter, we introduced the core concepts of FreeRTOS – tasks, the scheduler, context switching, and basic timing. We learned that tasks allow us to structure our application into concurrent units of execution. Now, we delve deeper into the practical mechanics of working with these tasks.
This chapter focuses on the lifecycle of a task: how it’s born (created), how it can be controlled while running (suspended, resumed, priority changed), and how it ends (deleted). Mastering these operations is essential for building dynamic, efficient, and manageable embedded applications. We’ll explore the specific ESP-IDF FreeRTOS functions that allow you to precisely control task creation, execution, and termination, including how to pass information to tasks and how to manage them individually using task handles.
Theory
Managing tasks goes beyond simply creating them. FreeRTOS provides a rich API to control their execution flow, priority, and existence.
Task Creation Revisited: xTaskCreate
We saw xTaskCreate
in the previous chapter, but let’s examine its parameters in detail:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // Pointer to the task entry function.
const char * const pcName, // Descriptive name for the task (for debugging).
const configSTACK_DEPTH_TYPE usStackDepth, // Stack size in BYTES for ESP-IDF.
void * const pvParameters, // Pointer to parameters to pass to the task.
UBaseType_t uxPriority, // Priority (0 to configMAX_PRIORITIES - 1).
TaskHandle_t * const pxCreatedTask // Pointer to store the handle of the created task (optional).
);
Parameter | Type | Description | Key Considerations |
---|---|---|---|
pvTaskCode | TaskFunction_t |
Pointer to the C function that implements the task’s code. | Must be of type void FuncName(void *pvParameters) . Task functions should never return; they typically contain an infinite loop and block periodically. |
pcName | const char * const |
A descriptive name for the task, used for debugging and tracing. | Max length defined by configMAX_TASK_NAME_LEN (e.g., 16 chars). |
usStackDepth | configSTACK_DEPTH_TYPE (uint32_t for ESP-IDF) |
The size of the task’s stack. | In ESP-IDF, this is specified in BYTES, not words. Minimum depends on usage, use configMINIMAL_STACK_SIZE as a baseline and uxTaskGetStackHighWaterMark() to tune. Insufficient stack leads to crashes. |
pvParameters | void * const |
A void pointer that is passed as the argument to the task function (pvTaskCode ). |
Used to pass initial data or configuration to the task. Can be NULL if no parameters are needed. Task must cast it to the correct type. |
uxPriority | UBaseType_t |
The priority at which the task will run. | Ranges from 0 (lowest) to configMAX_PRIORITIES - 1 (highest). Higher numbers indicate higher priority. |
pxCreatedTask | TaskHandle_t * const |
Optional pointer to a variable of type TaskHandle_t . If not NULL , the handle of the created task will be stored here. |
Essential if you need to manage the task later (delete, suspend, change priority, etc.). Can be NULL if the handle is not needed. |
Return Value | BaseType_t |
Returns pdPASS if the task was created successfully. Returns errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY if the stack or Task Control Block (TCB) could not be allocated (usually due to insufficient heap memory). |
Core Affinity: xTaskCreatePinnedToCore
On multi-core ESP32 variants (like the original ESP32 and ESP32-S3), you might want to ensure a specific task always runs on a particular CPU core. This is called core affinity.
BaseType_t xTaskCreatePinnedToCore(
TaskFunction_t pvTaskCode,
const char * const pcName,
const uint32_t usStackDepth, // Still in BYTES for ESP-IDF
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask,
const BaseType_t xCoreID // The core to pin the task to (0 or 1).
);
This function is identical to xTaskCreate
except for the last parameter:
xCoreID
: Specifies the CPU core the task should run on. Use0
for Core 0,1
for Core 1. You can also usetskNO_AFFINITY
to allow the task to run on either core (which is the default behavior ofxTaskCreate
).
Why Pin to a Core?
- Interrupt Handling: Some ISRs might be configured to run on a specific core. A task interacting heavily with that ISR might benefit from running on the same core to reduce cross-core communication overhead.
- Performance: For CPU-bound tasks, dedicating a core can maximize throughput. You might run communication tasks on one core and heavy processing on the other.
- Peripheral Access: While most peripherals can be accessed from either core, certain low-level operations or specific drivers might have core-specific considerations.
Tip: On single-core ESP32 variants (S2, C3, C6, H2),
xTaskCreatePinnedToCore
still exists for API compatibility, but thexCoreID
parameter is effectively ignored, and tasks will always run on the single available core (Core 0).
Task Handles (TaskHandle_t
)
The TaskHandle_t
is an opaque type (effectively a pointer) that uniquely identifies a task instance. Think of it as the task’s ID card. You obtain it from the pxCreatedTask
parameter of the creation functions. You need this handle to perform operations on that specific task, such as deleting, suspending, resuming, or changing its priority.
Task Deletion: vTaskDelete
Tasks consume resources (stack memory, TCB). When a task is no longer needed, it should be deleted to free these resources.
void vTaskDelete( TaskHandle_t xTaskToDelete );
xTaskToDelete
: The handle of the task to be deleted.- Deleting Other Tasks: If you provide the handle of another task, FreeRTOS will attempt to delete that task.
- Deleting the Calling Task: If you pass
NULL
, the task callingvTaskDelete
will delete itself. This is a common way for a task to terminate cleanly after completing its work.
Warning: Deleting a task is abrupt. The task is removed from the scheduler immediately. Crucially,
vTaskDelete
only frees the resources allocated by FreeRTOS itself (stack and TCB). If the task allocated any other resources (e.g., heap memory usingmalloc
, opened files, acquired mutexes, hardware peripherals), these resources will not be automatically released. This leads to resource leaks. The task being deleted must release all such resources before callingvTaskDelete
on itself or being deleted by another task.
stateDiagram-v2 [*] --> Create: Task Creation Create --> Ready: Task Initialized Ready --> Running: Task Scheduled Running --> Ready: Task Yields/Preempted Running --> Blocked: Resource Unavailable Running --> Suspended: Task Suspended Blocked --> Ready: Resource Available Suspended --> Ready: Task Resumed Create --> Deleted: vTaskDelete Ready --> Deleted: vTaskDelete Running --> Deleted: vTaskDelete Blocked --> Deleted: vTaskDelete Suspended --> Deleted: vTaskDelete Deleted --> [*]: Task Memory Freed
Task Suspension and Resumption
Sometimes, you need to temporarily stop a task without deleting it, perhaps to disable a feature or wait for a specific system condition.
vTaskSuspend( TaskHandle_t xTaskToSuspend )
: Moves the specified task (or the calling task ifNULL
is passed, though less common) into the Suspended state. A suspended task is completely ignored by the scheduler until it is explicitly resumed. It consumes no CPU time but still retains its stack and TCB.vTaskResume( TaskHandle_t xTaskToResume )
: Moves a task from the Suspended state back to the Ready state. If the resumed task has a higher priority than the currently running task, a context switch may occur immediately.
Note: Suspension is different from the Blocked state. A blocked task is waiting for a specific event (timeout, queue data, semaphore) and will automatically become Ready when that event occurs. A suspended task waits for explicit resumption via
vTaskResume
.
There’s also an ISR-safe version: xTaskResumeFromISR()
.
Task Priority Management
You can dynamically change a task’s priority after it has been created.
vTaskPrioritySet( TaskHandle_t xTask, UBaseType_t uxNewPriority )
: Changes the priority of the specified task (xTask
, or the calling task ifNULL
) touxNewPriority
. TheuxNewPriority
must be within the valid range (0 toconfigMAX_PRIORITIES - 1
). Changing priority can affect the scheduling order immediately.uxTaskPriorityGet( TaskHandle_t xTask )
: Returns the current priority of the specified task (xTask
, or the calling task ifNULL
).
Why Change Priority?
- Temporarily boost a task’s priority to handle a critical event quickly.
- Lower a task’s priority when it’s performing background activities.
- Implement more complex scheduling or resource allocation schemes.
Function | Purpose | Key Parameter(s) | Important Notes |
---|---|---|---|
vTaskDelete() | Deletes a task, freeing its stack and TCB. | TaskHandle_t xTaskToDelete |
Pass task’s handle to delete another task. Pass NULL to delete the calling task. Task MUST free any other allocated resources (heap, mutexes) BEFORE deletion to prevent leaks. |
vTaskSuspend() | Moves a task into the Suspended state. | TaskHandle_t xTaskToSuspend |
Suspended tasks are ignored by the scheduler. Pass task’s handle or NULL for calling task (less common). Retains stack/TCB. |
vTaskResume() | Moves a Suspended task back to the Ready state. | TaskHandle_t xTaskToResume |
If resumed task has higher priority than current, context switch may occur. Use xTaskResumeFromISR() from an interrupt. |
vTaskPrioritySet() | Dynamically changes a task’s priority. | TaskHandle_t xTask , UBaseType_t uxNewPriority |
Pass task’s handle (or NULL for calling task) and the new priority. Can affect scheduling immediately. |
uxTaskPriorityGet() | Retrieves the current priority of a task. | TaskHandle_t xTask |
Pass task’s handle or NULL for the calling task. Returns UBaseType_t . |
Getting Task Information
FreeRTOS provides functions to query the status and properties of tasks, which is invaluable for debugging.
uxTaskGetStackHighWaterMark( TaskHandle_t xTask )
: Returns the minimum amount of free stack space (in bytes for ESP-IDF) the task has had since it started. PassNULL
for the calling task. A low value indicates potential stack overflow risk.eTaskGetState( TaskHandle_t xTask )
: Returns the current state of the task (e.g.,eRunning
,eReady
,eBlocked
,eSuspended
,eDeleted
) as an enumerated typeeTaskState
.pcTaskGetName( TaskHandle_t xTask )
: Returns a pointer to the name string of the task.vTaskList( char * pcWriteBuffer )
: (RequiresconfigUSE_TRACE_FACILITY
andconfigUSE_STATS_FORMATTING_FUNCTIONS
) Formats a table containing details of all current tasks (name, state, priority, stack HWM, task number) into the provided character buffer. Useful for system snapshots.vTaskGetRunTimeStats( char * pcWriteBuffer )
: (RequiresconfigGENERATE_RUN_TIME_STATS
andconfigUSE_STATS_FORMATTING_FUNCTIONS
) Formats a table showing how much CPU time each task has consumed. Requires configuration of a high-frequency timer for statistics collection. Excellent for performance analysis.
Function | Purpose | Key Parameter(s) / Return | Notes & Requirements |
---|---|---|---|
uxTaskGetStackHighWaterMark() | Gets the minimum amount of free stack space a task has had. | TaskHandle_t xTask (or NULL for current task). Returns UBaseType_t (bytes in ESP-IDF). |
Crucial for detecting potential stack overflows. A low value is a warning. |
eTaskGetState() | Gets the current state of a task. | TaskHandle_t xTask . Returns eTaskState enum (e.g., eRunning , eBlocked ). |
Useful for debugging task behavior and interactions. |
pcTaskGetName() | Gets a pointer to the task’s descriptive name. | TaskHandle_t xTask . Returns char * . |
Returns the name string given during task creation. |
vTaskList() | Generates a formatted table of all current tasks and their states. | char * pcWriteBuffer (buffer to write table into). |
Requires configUSE_TRACE_FACILITY and configUSE_STATS_FORMATTING_FUNCTIONS to be enabled in FreeRTOSConfig.h (via menuconfig). Provides a system snapshot. |
vTaskGetRunTimeStats() | Generates a formatted table showing CPU time consumed by each task. | char * pcWriteBuffer . |
Requires configGENERATE_RUN_TIME_STATS and configUSE_STATS_FORMATTING_FUNCTIONS. Also needs a high-frequency timer configured for stats collection (ESP-IDF usually handles this). Excellent for performance profiling. |
Practical Examples
Let’s put these concepts into practice.
Example 1: Task Parameters and Handles
This example creates a task, passes a simple integer ID to it via pvParameters
, and uses a task handle to later retrieve its stack high water mark from app_main
.
Code:
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
static const char *TAG = "TASK_PARAMS";
// Task function that receives a parameter
void worker_task(void *pvParameter)
{
int task_id = (int)pvParameter; // Cast the void* back to an int
ESP_LOGI(TAG, "Worker Task %d started.", task_id);
for(int i = 0; i < 5; i++) {
ESP_LOGI(TAG, "Worker Task %d running iteration %d...", task_id, i);
vTaskDelay(pdMS_TO_TICKS(1000)); // Delay 1 second
}
ESP_LOGI(TAG, "Worker Task %d finishing.", task_id);
// This task will finish its loop and then be implicitly deleted
// because it returns (or we could call vTaskDelete(NULL)).
// For simplicity here, we let it return. ESP-IDF handles this.
}
void app_main(void)
{
TaskHandle_t xWorkerTaskHandle = NULL; // Variable to store the task handle
ESP_LOGI(TAG, "App Main: Creating worker task.");
// Pass the integer '1' as the parameter.
// Note: Direct casting of integers to void* can be problematic on some architectures,
// but is generally acceptable for small integers on ESP32.
// For complex data, pass a pointer to a struct.
BaseType_t result = xTaskCreate(
worker_task,
"Worker1",
2048,
(void *)1, // Pass task ID 1 as parameter
5,
&xWorkerTaskHandle // Store the handle in our variable
);
if (result != pdPASS) {
ESP_LOGE(TAG, "Failed to create worker task.");
return;
}
// Check if the handle was obtained
if (xWorkerTaskHandle != NULL) {
ESP_LOGI(TAG, "Worker task created successfully with handle %p", xWorkerTaskHandle);
// Let the worker task run for a bit
vTaskDelay(pdMS_TO_TICKS(6000)); // Wait 6 seconds
// Get info about the worker task using its handle
UBaseType_t stack_hwm = uxTaskGetStackHighWaterMark(xWorkerTaskHandle);
ESP_LOGI(TAG, "Worker Task Stack High Water Mark: %u bytes", stack_hwm);
eTaskState state = eTaskGetState(xWorkerTaskHandle);
ESP_LOGI(TAG, "Worker Task State: %d (eReady=%d, eBlocked=%d, eDeleted=%d)",
state, eReady, eBlocked, eDeleted); // Task might be deleted by now
} else {
ESP_LOGE(TAG, "Failed to get task handle even though task creation reported success?");
}
ESP_LOGI(TAG, "App Main finished.");
// app_main exits, but the worker task might still be running until it finishes.
// The FreeRTOS scheduler continues as long as any tasks exist.
}
Build, Flash, Monitor.
Observe:
- The worker task starts and logs its ID (1).
- It runs for 5 iterations.
app_main
waits and then uses the stored handle (xWorkerTaskHandle
) to query the stack HWM and state of the worker task. Note that by the timeapp_main
queries the state, the worker task might have already finished and been deleted.
Example 2: Task Deletion (Self and Other)
This example demonstrates deleting another task using its handle and a task deleting itself.
Code:
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include <stdlib.h> // For malloc/free
static const char *TAG = "TASK_DELETE";
TaskHandle_t xVictimTaskHandle = NULL; // Global handle for the task to be deleted
// Task that will be deleted by another task
void victim_task(void *pvParameter)
{
ESP_LOGI(TAG, "Victim Task: Waiting to be deleted...");
// Simulate doing something, maybe holding a resource (bad practice if deleted externally!)
void *resource = malloc(100); // Allocate memory
if (!resource) ESP_LOGE(TAG, "Victim failed to allocate memory!");
while(1) {
// Normally this task would do work, but here it just waits
vTaskDelay(pdMS_TO_TICKS(1000));
// IMPORTANT: If this task is deleted externally, 'resource' will be leaked!
}
// This part is unreachable if deleted externally
free(resource); // Proper cleanup
vTaskDelete(NULL); // Self-delete
}
// Task that deletes the victim task and then itself
void killer_task(void *pvParameter)
{
ESP_LOGI(TAG, "Killer Task: Started.");
vTaskDelay(pdMS_TO_TICKS(3000)); // Wait 3 seconds
if (xVictimTaskHandle != NULL) {
ESP_LOGW(TAG, "Killer Task: Deleting Victim Task!");
vTaskDelete(xVictimTaskHandle);
xVictimTaskHandle = NULL; // Invalidate the handle after deletion
ESP_LOGI(TAG, "Killer Task: Victim task delete requested.");
} else {
ESP_LOGE(TAG, "Killer Task: Victim task handle is NULL!");
}
vTaskDelay(pdMS_TO_TICKS(1000)); // Wait a bit more
ESP_LOGI(TAG, "Killer Task: Preparing to delete self.");
// Perform any necessary cleanup for killer_task itself here
ESP_LOGI(TAG, "Killer Task: Deleting self now.");
vTaskDelete(NULL); // Delete self
// Code below this line will not execute
ESP_LOGE(TAG, "Killer Task: Should not reach here!");
}
void app_main(void)
{
ESP_LOGI(TAG, "App Main: Creating tasks.");
xTaskCreate(victim_task, "Victim", 2048, NULL, 5, &xVictimTaskHandle);
if (xVictimTaskHandle == NULL) {
ESP_LOGE(TAG, "Failed to create victim task.");
return;
}
xTaskCreate(killer_task, "Killer", 2048, NULL, 6, NULL); // No handle needed for killer
ESP_LOGI(TAG, "App Main: Tasks created. Monitoring...");
// app_main can idle or exit
}
Build, Flash, Monitor.
Observe:
- Both tasks start.
- After 3 seconds, the Killer Task deletes the Victim Task.
- Notice the Victim Task stops logging. Crucially, the memory allocated by
malloc
invictim_task
is leaked because it wasn’t freed before deletion. - After another second, the Killer Task deletes itself and stops logging.
Example 3: Suspend and Resume
Control one task’s execution from another using suspend/resume.
Code:
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
static const char *TAG = "TASK_SUSPEND";
TaskHandle_t xWorkerHandle = NULL;
// Worker task that prints periodically
void worker_task_suspend(void *pvParameter) {
int counter = 0;
ESP_LOGI(TAG, "Worker Task: Started.");
while(1) {
counter++;
ESP_LOGI(TAG, "Worker Task: Running, count = %d", counter);
vTaskDelay(pdMS_TO_TICKS(500)); // Print every 0.5 seconds
}
}
// Control task that suspends/resumes the worker
void control_task(void *pvParameter) {
ESP_LOGI(TAG, "Control Task: Started.");
vTaskDelay(pdMS_TO_TICKS(2000)); // Let worker run for 2 seconds
while(1) {
if (xWorkerHandle != NULL) {
ESP_LOGW(TAG, "Control Task: Suspending worker...");
vTaskSuspend(xWorkerHandle);
ESP_LOGI(TAG, "Control Task: Worker suspended. Waiting 5 seconds...");
vTaskDelay(pdMS_TO_TICKS(5000)); // Keep suspended for 5 seconds
ESP_LOGW(TAG, "Control Task: Resuming worker...");
vTaskResume(xWorkerHandle);
ESP_LOGI(TAG, "Control Task: Worker resumed. Waiting 5 seconds...");
vTaskDelay(pdMS_TO_TICKS(5000)); // Let it run for 5 seconds
} else {
ESP_LOGE(TAG, "Control Task: Worker handle is NULL!");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
}
void app_main(void) {
ESP_LOGI(TAG, "App Main: Creating tasks.");
xTaskCreate(worker_task_suspend, "WorkerSuspend", 2048, NULL, 5, &xWorkerHandle);
if (xWorkerHandle == NULL) {
ESP_LOGE(TAG, "Failed to create worker task.");
return;
}
xTaskCreate(control_task, "ControlSuspend", 2048, NULL, 6, NULL);
ESP_LOGI(TAG, "App Main: Tasks created.");
}
Build, Flash, Monitor.
Observe:
- The Worker Task starts printing every 0.5 seconds.
- After 2 seconds, the Control Task suspends the Worker. Worker Task logs stop.
- After 5 seconds of suspension, the Control Task resumes the Worker. Worker Task logs resume.
- The cycle repeats.
Example 4: Pinning to Cores (Dual-Core ESP32/S3 only)
Demonstrates creating two tasks pinned to different cores.
Code:
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
static const char *TAG = "CORE_PINNING";
// Simple task function
void core_specific_task(void *pvParameter) {
int core_id = xPortGetCoreID();
int task_num = (int)pvParameter;
ESP_LOGI(TAG, "Task %d started on Core %d.", task_num, core_id);
// Simulate some work or just loop
uint64_t counter = 0;
while(1) {
counter++;
if ((counter % 200000000) == 0) { // Print periodically to show it's alive
ESP_LOGI(TAG, "Task %d (Core %d) still running...", task_num, core_id);
}
// Yield slightly to prevent watchdog issues if loop is too tight
// vTaskDelay(1); // Optional: uncomment if watchdog triggers
}
}
void app_main(void) {
ESP_LOGI(TAG, "App Main: Creating pinned tasks.");
// Create Task 1 pinned to Core 0
xTaskCreatePinnedToCore(
core_specific_task,
"Core0Task",
2048,
(void *)1, // Task ID 1
5,
NULL,
0 // Pin to Core 0
);
ESP_LOGI(TAG, "Task 1 (Core 0) created.");
// Create Task 2 pinned to Core 1
// Note: On single-core chips, this will still run on Core 0.
xTaskCreatePinnedToCore(
core_specific_task,
"Core1Task",
2048,
(void *)2, // Task ID 2
5,
NULL,
1 // Pin to Core 1
);
ESP_LOGI(TAG, "Task 2 (Core 1) created.");
ESP_LOGI(TAG, "App Main: Pinned tasks created.");
// app_main can idle
}
Build, Flash, Monitor.
Observe:
- Task 1 logs that it started on Core 0.
- Task 2 logs that it started on Core 1 (on dual-core chips). On single-core chips, it will also report Core 0.
- Both tasks run concurrently, printing their periodic messages.
Variant Notes
- Core Pinning: As mentioned,
xTaskCreatePinnedToCore
only has a functional effect on multi-core variants (ESP32, ESP32-S3). On single-core variants (ESP32-S2, C3, C6, H2), tasks will always execute on Core 0 regardless of thexCoreID
parameter specified. - API Consistency: All other task management functions (
xTaskCreate
,vTaskDelete
,vTaskSuspend
,vTaskResume
,vTaskPrioritySet
, etc.) have consistent behavior across all ESP32 variants supported by ESP-IDF v5.x. Performance characteristics will differ based on the core type and clock speed, but the API usage remains the same.
Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
---|---|---|
Forgetting Task Cleanup Before Deletion Task deleted (self or other) while still holding resources. |
Resource leaks (heap memory exhaustion, peripheral conflicts); Deadlocks (if mutex held); System instability over time. | Implement robust cleanup: task must free malloc ‘d memory, release mutexes/semaphores, close files/sockets, de-init hardware BEFORE vTaskDelete(NULL) or being deleted. Consider signaling a task to self-clean and delete. |
Using Invalid Task Handles Using an uninitialized, NULL , or stale (deleted task) handle. |
Crashes (illegal memory access); API calls fail or have no effect; Unpredictable behavior. | Initialize handles to NULL . Check xTaskCreate return value. Set handle to NULL after deleting the referenced task. Always verify handle is not NULL before use. |
Suspend/Resume Deadlocks Task A suspends Task B, then Task A waits for an action only Task B can perform. |
System hangs; relevant tasks stop progressing. | Analyze task dependencies carefully. Use suspend/resume sparingly. Prefer event groups, queues, or notifications for complex synchronization. |
Incorrect Stack Size (usStackDepth )Too small (overflow) or excessively large (RAM waste). |
Guru Meditation Errors (stack canary, illegal access); Random crashes. Wasted RAM if too large. | ESP-IDF: size is in BYTES. Start generous, use uxTaskGetStackHighWaterMark() to tune. Enable stack overflow checks in menuconfig. Add safety margin to HWM. |
Passing Pointers to Local (Stack) Variables as Task Parameters Creating function’s stack frame (with parameter data) becomes invalid before new task uses it. |
Created task reads garbage data or crashes when dereferencing pvParameters . |
Ensure pointed-to data has valid lifetime. Use pointers to static/global data, heap-allocated data (task then responsible for free ), or persistent objects. For simple integers, direct cast (void *)my_int is often okay on ESP32. |
Task Function Returns A task function (passed to xTaskCreate ) executes a return statement. |
Behavior can be undefined or lead to a crash. In ESP-IDF, if app_main returns, its task is cleaned up. Other tasks returning might cause issues. |
Task functions should contain an infinite loop (while(1) ) and block periodically. If a task needs to terminate, it should call vTaskDelete(NULL) after cleaning up its resources. |
Not Checking xTaskCreate Return ValueAssuming task creation always succeeds. |
If heap is exhausted, task creation fails (errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY ). Subsequent use of uninitialized handle causes crashes. |
Always check if xTaskCreate (or pinned version) returned pdPASS . Handle creation failure gracefully (e.g., log error, retry, safe shutdown). |
Exercises
- Task Parameter Struct:
- Define a
struct
containing anint delay_ms;
andchar message[20];
. - In
app_main
, create an instance of this struct on the heap (malloc
). Populate it (e.g., delay 1500ms, message “Hello from Struct”). - Create a task, passing a pointer to this heap-allocated struct as
pvParameters
. - The task function should cast
pvParameters
back to the struct pointer, read thedelay_ms
andmessage
, print the message in a loop using the specified delay, and finallyfree
the struct memory before deleting itself (vTaskDelete(NULL)
).
- Define a
- Task Handle Array:
- Create an array of
TaskHandle_t
(e.g., size 3). - Create 3 instances of the same simple worker task function (e.g., prints “Task X running” with X being 1, 2, or 3 passed as a parameter).
- Store the handle of each created task in the array.
- After creating all tasks, loop through the handle array and print the Stack High Water Mark for each of the 3 tasks using their handles.
- Create an array of
- Interactive Task Control:
- Create a worker task that prints a counter every second. Store its handle.
- Create a “monitor” task that waits for input from the serial console (using
stdin
or a simple UART polling mechanism if preferred). - If the monitor task receives ‘s’, it suspends the worker task.
- If it receives ‘r’, it resumes the worker task.
- If it receives ‘p’, it changes the worker task’s priority (e.g., toggles between 5 and 7) using
vTaskPrioritySet
. - If it receives ‘d’, it deletes the worker task (and should probably stop trying to control it afterwards!).
- Core Load Simulation (Dual-Core):
- On an ESP32/S3, create two tasks performing floating-point calculations in a tight loop (e.g.,
volatile double x = 1.23; while(1) { x = sin(x) * cos(x); }
). - Task A: Pin to Core 0, Priority 5.
- Task B: Pin to Core 1, Priority 5.
- Create a third, low-priority task (Priority 1, no affinity) that simply prints “Low priority task running” every 2 seconds using
vTaskDelay
. - Observe if the low-priority task still gets scheduled and prints its message, demonstrating that even with both cores busy, the RTOS scheduler allows lower-priority tasks some time (due to tick interrupts/scheduler calls).
- On an ESP32/S3, create two tasks performing floating-point calculations in a tight loop (e.g.,
- Dynamic Priority for Critical Section:
- Create Task A (Priority 5) that loops, printing “Task A: Background work…” with a 500ms delay.
- Create Task B (Priority 7) that loops, printing “Task B: High priority check…” with a 1000ms delay.
- Modify Task A: Occasionally (e.g., every 5 loops), it needs to perform a “critical update”. Before the update, it calls
vTaskPrioritySet(NULL, 8)
to raise its own priority above Task B. It prints “Task A: CRITICAL UPDATE START”. It performs a short delay (vTaskDelay(pdMS_TO_TICKS(100))
) simulating the work. It prints “Task A: CRITICAL UPDATE END”. It then callsvTaskPrioritySet(NULL, 5)
to lower its priority back down. - Observe how Task A can temporarily preempt Task B during its critical update phase.
Summary
- Tasks are created using
xTaskCreate
(runs on any core) orxTaskCreatePinnedToCore
(runs on a specific core on multi-core chips). - Key creation parameters include the task function, name, stack size (bytes in ESP-IDF), parameters (
void*
), priority, and an optional pointer to store theTaskHandle_t
. - The
TaskHandle_t
is essential for managing specific tasks after creation. - Tasks should release any allocated resources (heap memory, mutexes, peripherals) before being deleted.
vTaskDelete(handle)
deletes another task;vTaskDelete(NULL)
deletes the calling task. Cleanup before deleting!vTaskSuspend(handle)
pauses a task;vTaskResume(handle)
restarts it.vTaskPrioritySet(handle, new_priority)
changes a task’s priority dynamically.- Functions like
uxTaskGetStackHighWaterMark
,eTaskGetState
, andpcTaskGetName
use the task handle to provide valuable debugging information. - Proper task lifecycle management is critical for robust and leak-free applications.
Further Reading
- ESP-IDF Programming Guide: Task API (FreeRTOS): https://docs.espressif.com/projects/esp-idf/en/v5.4/esp32/api-reference/system/freertos_idf.html#task-api (Select your target chip)
- FreeRTOS API Reference: Task Control: https://www.freertos.org/a00020.html
- FreeRTOS API Reference: Task Utilities: https://www.freertos.org/a00021.html
- Mastering the FreeRTOS Real Time Kernel: (Official FreeRTOS guide/book) – Chapters on Task Management.
