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 and vTaskResume.
  • 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:

C
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.

C
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. Use 0 for Core 0, 1 for Core 1. You can also use tskNO_AFFINITY to allow the task to run on either core (which is the default behavior of xTaskCreate).

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 the xCoreID 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.

C
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 calling vTaskDelete 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 using malloc, 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 calling vTaskDelete 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 if NULL 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 if NULL) to uxNewPriority. The uxNewPriority must be within the valid range (0 to configMAX_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 if NULL).

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. Pass NULL 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 type eTaskState.
  • pcTaskGetName( TaskHandle_t xTask ): Returns a pointer to the name string of the task.
  • vTaskList( char * pcWriteBuffer ): (Requires configUSE_TRACE_FACILITY and configUSE_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 ): (Requires configGENERATE_RUN_TIME_STATS and configUSE_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:

C
#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 time app_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:

C
#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 in victim_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:

C
#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:

C
#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 the xCoreID 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 Value
Assuming 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

  1. Task Parameter Struct:
    • Define a struct containing an int delay_ms; and char 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 the delay_ms and message, print the message in a loop using the specified delay, and finally free the struct memory before deleting itself (vTaskDelete(NULL)).
  2. 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.
  3. 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!).
  4. 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).
  5. 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 calls vTaskPrioritySet(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) or xTaskCreatePinnedToCore (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 the TaskHandle_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, and pcTaskGetName use the task handle to provide valuable debugging information.
  • Proper task lifecycle management is critical for robust and leak-free applications.

Further Reading

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top