FREERTOS image for ESP32 ESP32-IDF tutorials

FreeRTOS on ESP32: Tasks, Delays, and Resource Management

FreeRTOS: Unlocking the Power of Multitasking on ESP32

Managing multiple tasks on an embedded system can be challenging, especially when everything seems to demand attention at the same time. This is where FreeRTOS steps in, simplifying multitasking and making your ESP32 projects more powerful and efficient. In this lesson, you’ll learn what makes FreeRTOS so essential, how it works, and how to use its key features.

By the end, you’ll have a solid understanding of FreeRTOS concepts such as the kernel, tasks, delays, and resource sharing, allowing you to create responsive and well-organized projects.

Chapter 1: Why FreeRTOS and What is the Kernel?

When working with bare-metal programming, you have direct control over the hardware. While this simplicity works for small tasks, it quickly becomes overwhelming for larger projects. For example, manually coordinating delays, task switching, and resource sharing can lead to cluttered and unreliable code.

Enter FreeRTOS

FreeRTOS is a lightweight operating system that eliminates these challenges. At its core lies the kernel, which acts like the brain of your ESP32, managing tasks and ensuring they run efficiently.

Key Responsibilities of the Kernel

  1. Task Scheduling: The kernel decides which task runs next based on its priority and state.
  2. Timer Management: It tracks time to handle delays and ensure tasks resume at the correct moment.
  3. Resource Coordination: The kernel prevents conflicts when tasks share hardware resources, such as GPIOs or memory.

With FreeRTOS, you don’t need to manually juggle tasks. Instead, the kernel does it for you, enabling multitasking without the complexity of bare-metal programming.

Chapter 2: Creating and Deleting Tasks

FreeRTOS revolves around tasks, which are independent units of execution. Think of them as small programs running in parallel. The kernel ensures these tasks work together seamlessly.

Creating a Task

To create a task, use xTaskCreate or xTaskCreatePinnedToCore. These functions register the task with the kernel, specifying how it should behave.

Syntax:

BaseType_t xTaskCreatePinnedToCore(
    TaskFunction_t pxTaskCode,         // Pointer to the function that implements the task
    const char * const pcName,         // Name of the task, useful for debugging and monitoring
    const configSTACK_DEPTH_TYPE usStackDepth, // Amount of stack memory allocated for the task
    void * const pvParameters,         // Pointer to parameters to pass to the task (NULL if not needed)
    UBaseType_t uxPriority,            // Priority of the task (higher number = higher priority)
    TaskHandle_t * const pxCreatedTask,// Optional handle to reference the created task
    const BaseType_t xCoreID           // Core to run the task on (Core 0, Core 1, or tskNO_AFFINITY)
);
C

Example:

// Task function to blink an LED
void blink_task(void *pvParameters) {
    // Infinite loop for the task
    while (1) {
        gpio_set_level(GPIO_NUM_2, 1); // Turn the LED ON
        vTaskDelay(500 / portTICK_PERIOD_MS); // Wait 500ms (non-blocking delay) other tasks can run during this time
        gpio_set_level(GPIO_NUM_2, 0); // Turn the LED OFF
        vTaskDelay(500 / portTICK_PERIOD_MS); //Wait 500ms; other tasks can run during this time
    }
}

void app_main() {
    // Create a FreeRTOS task pinned to Core 0
    xTaskCreatePinnedToCore(
        blink_task,      // Pointer to the task function to execute
        "Blink Task",    // Name of the task, useful for debugging
        2048,            // Stack size allocated for the task (2048 words)
        NULL,            // Parameters passed to the task (none in this case)
        1,               // Task priority (1 is a low priority, higher numbers are more critical)
        NULL,            // Task handle (optional, NULL if not needed)
        0                // Core to pin the task (0 for Core 0)
    );
}
C


Here, the blink_task function runs in its own thread, independently blinking an LED.

Deleting a Task

Tasks can be deleted when they’re no longer needed, freeing resources for other operations.

Example:

void some_task(void *pvParameters) {
    // Print a message indicating the task is running
    printf("Task running...\n");
    
    // Delay the task for 2000 milliseconds (2 seconds)
    vTaskDelay(2000 / portTICK_PERIOD_MS);
    
    // Delete the task itself after the delay
    vTaskDelete(NULL); // NULL indicates the current task
}
C

Understanding Priorities

Tasks with higher priorities run before those with lower priorities. If tasks share the same priority, they split CPU time equally. For example:

  • A high-priority task might handle real-time sensor data.
  • A low-priority task could blink an LED.

Core Selection

On the ESP32, you can assign tasks to specific cores (Core 0 or Core 1) or let the kernel decide. Assigning tasks strategically helps balance workload across the two cores.

Chapter 3: Delays and Pausing Tasks

Managing timing is a critical aspect of multitasking. FreeRTOS introduces non-blocking delays, allowing one task to wait while others continue to run. This is achieved using the kernel’s timer system.

Using Delays

The vTaskDelay function pauses a task for a specified time, measured in ticks. This function does not stop the scheduler; it only delays the specific task. Other tasks with equal or higher priority can run during the delay.

Syntax:

void vTaskDelay(const TickType_t xTicksToDelay);
C

Example:

void blink_task(void *pvParameters) {
    // Infinite loop to keep the task running
    while (1) {
        // Set GPIO_NUM_2 to high (turn the LED on)
        gpio_set_level(GPIO_NUM_2, 1);
        
        // Delay for 500 milliseconds
        vTaskDelay(500 / portTICK_PERIOD_MS);
        
        // Set GPIO_NUM_2 to low (turn the LED off)
        gpio_set_level(GPIO_NUM_2, 0);
        
        // Delay for 500 milliseconds
        vTaskDelay(500 / portTICK_PERIOD_MS);
    }
}
C

Here, the vTaskDelay function pauses the task for 500 milliseconds, but the kernel continues to run other tasks during this time.

Why FreeRTOS Delays Are Better

  1. Non-blocking: Other tasks can continue while one task waits.
  2. Kernel Timer System: Delays are tracked accurately using the system tick rate.
  3. Efficient Multitasking: Tasks no longer block each other.

Pausing Tasks

Sometimes, you may need to pause a task indefinitely. FreeRTOS provides vTaskSuspend and vTaskResume for this purpose.

Example:

// Handle to manage the task
TaskHandle_t myTaskHandle;

// Task function to perform periodic actions
void controlled_task(void *pvParameters) {
    // Infinite loop to keep the task running
    while (1) {
        // Print a message indicating the task is running
        printf("Task running...\n");

        // Delay the task for 1000 milliseconds (1 second)
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}

// Main application function
void app_main() {
    // Create a new task
    // - Task function: controlled_task
    // - Name: "Controlled Task"
    // - Stack size: 2048 bytes
    // - Parameters: NULL (no specific parameters passed)
    // - Priority: 1 (low priority)
    // - Task handle: myTaskHandle (to manage this task later)
    xTaskCreate(controlled_task, "Controlled Task", 2048, NULL, 1, &myTaskHandle);

    // Allow the created task to run for 5000 milliseconds (5 seconds)
    vTaskDelay(5000 / portTICK_PERIOD_MS);

    // Suspend the task (pause execution)
    vTaskSuspend(myTaskHandle);

    // Wait for another 5000 milliseconds while the task is suspended
    vTaskDelay(5000 / portTICK_PERIOD_MS);

    // Resume the task (continue execution)
    vTaskResume(myTaskHandle);
}
C

Chapter 4: Mutexes and Semaphores

When multiple tasks access shared resources, such as GPIOs or communication buses, conflicts can occur. FreeRTOS offers mutexes and semaphores to handle this.

Mutex: Ensuring Exclusive Access

A mutex ensures that only one task can access a resource at a time.

Example:

// Declare a mutex to protect shared resources
SemaphoreHandle_t myMutex;

// Task 1: Attempts to use a shared resource protected by the mutex
void task1(void *pvParameters) {
    while (1) {
        // Try to take the mutex. Wait indefinitely if it's not available
        if (xSemaphoreTake(myMutex, portMAX_DELAY)) {
            // Critical section: Safe to use the shared resource
            printf("Task 1 using resource\n");

            // Simulate resource usage by delaying for 1000 milliseconds
            vTaskDelay(1000 / portTICK_PERIOD_MS);

            // Release the mutex, allowing other tasks to access the resource
            xSemaphoreGive(myMutex);
        }
    }
}

// Task 2: Similar to Task 1, it tries to use the shared resource
void task2(void *pvParameters) {
    while (1) {
        // Try to take the mutex. Wait indefinitely if it's not available
        if (xSemaphoreTake(myMutex, portMAX_DELAY)) {
            // Critical section: Safe to use the shared resource
            printf("Task 2 using resource\n");

            // Simulate resource usage by delaying for 1000 milliseconds
            vTaskDelay(1000 / portTICK_PERIOD_MS);

            // Release the mutex, allowing other tasks to access the resource
            xSemaphoreGive(myMutex);
        }
    }
}

// Main function: Initializes the mutex and creates tasks
void app_main() {
    // Create a mutex to synchronize access to shared resources
    myMutex = xSemaphoreCreateMutex();

    // Create Task 1
    // - Task function: task1
    // - Name: "Task 1"
    // - Stack size: 2048 bytes
    // - Parameters: NULL (no specific parameters passed)
    // - Priority: 1 (low priority)
    // - Task handle: NULL (not storing the handle for this task)
    xTaskCreate(task1, "Task 1", 2048, NULL, 1, NULL);

    // Create Task 2
    // - Same configuration as Task 1
    xTaskCreate(task2, "Task 2", 2048, NULL, 1, NULL);
}
C


Semaphore: Managing Multiple Access

Semaphores allow multiple tasks to access a resource up to a specified limit. For instance, a counting semaphore can manage several tasks by accessing a shared resource.

Chapter 5: Queues for Task Communication in FreeRTOS

In multitasking systems, tasks often need to share information or synchronize their operations. FreeRTOS provides queues as a simple and efficient way to enable communication between tasks while maintaining thread safety.

A queue is essentially a FIFO (First-In-First-Out) buffer where one task can send data, and another task can receive it.

Why Use Queues?

  • Decoupling Tasks: Queues allow tasks to work independently and share data only when necessary.
  • Thread Safety: FreeRTOS queues handle synchronization, ensuring no data corruption occurs when multiple tasks access them.
  • Flexible Communication: Tasks can send and receive any type of data, such as integers, strings, or complex structures.

Creating a Queue

To create a queue, use the xQueueCreate function:

//Parameters:
// - uxQueueLength: The maximum number of items the queue can hold.
// - uxItemSize: The size of each item in bytes (e.g., sizeof(int), sizeof(float)).
//
// Returns:
// - A handle to the created queue (QueueHandle_t) if successful.
// - NULL if the queue could not be created due to insufficient memory.
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize);
C
Example:
codeQueueHandle_t myQueue;

// Create a queue to hold 10 integers
myQueue = xQueueCreate(10, sizeof(int));
C

Sending Data to a Queue

To send data to a queue, use the xQueueSend function:

BaseType_t xQueueSend(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait);
C
  • xQueue: Handle to the queue.
  • pvItemToQueue: Pointer to the data being sent.
  • xTicksToWait: Maximum time to wait if the queue is full (use portMAX_DELAY to wait indefinitely).

Example:

int data = 42; // Data to send

// Send the data to the queue
xQueueSend(myQueue, &data, portMAX_DELAY);
C

Receiving Data from a Queue

To receive data from a queue, use the xQueueReceive function:

BaseType_t xQueueReceive(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait);
C
  • xQueue: Handle to the queue.
  • pvBuffer: Pointer to the buffer where the received data will be stored.
  • xTicksToWait: Maximum time to wait if the queue is empty.

Example:

int receivedData; // Variable to store the data received from the queue

// Attempt to receive data from the queue
// - myQueue: Handle to the queue to receive data from
// - &receivedData: Pointer to the variable where the received data will be stored
// - portMAX_DELAY: Wait indefinitely until an item is available in the queue
if (xQueueReceive(myQueue, &receivedData, portMAX_DELAY)) {
    // If data is successfully received, print it
    printf("Received data: %d\n", receivedData);
}
C

Wrapping It Up

FreeRTOS transforms the way you manage tasks and resources on the ESP32. Let’s recap:

  • The kernel organizes tasks, handles timing, and ensures smooth multitasking.
  • You can create, delete, and prioritize tasks, even assigning them to specific cores.
  • Delays in FreeRTOS are non-blocking, allowing tasks to run independently.
  • Mutexes and semaphores prevent conflicts when sharing resources.

These concepts are foundational to understanding and using FreeRTOS effectively. Now, it’s your turn to experiment and build amazing multitasking projects with your ESP32! 😊

Stay tuned for the next part of our FreeRTOS series!
In the upcoming lesson, we’ll dive into real-world examples with detailed explanations and provide homework tasks to solidify your understanding. Get ready to take your FreeRTOS skills to the next level!

Useful links:

FreeRTOS Official Documentation: Comprehensive guides and references for FreeRTOS.

ESP-IDF FreeRTOS Documentation: Detailed information on using FreeRTOS within Espressif’s ESP-IDF framework.

Real-Time Operating System (RTOS) – Wikipedia: An overview of RTOS concepts and implementations.