Introduction to FreeRTOS on ESP32

Chapter 9: Introduction to FreeRTOS on ESP32

Chapter Objectives

Upon completing this chapter, you will be able to:

  • Explain the purpose and benefits of using a Real-Time Operating System (RTOS) like FreeRTOS.
  • Define core RTOS concepts: Tasks, Scheduler, Task States, Context Switching, and Ticks.
  • Understand how FreeRTOS is integrated into the ESP-IDF framework.
  • Create, run, and manage multiple tasks with different priorities using the FreeRTOS API in ESP-IDF.
  • Utilize FreeRTOS functions for task delays and basic time management (vTaskDelay, vTaskDelayUntil).
  • Query basic information about running tasks, including their stack usage.
  • Recognize the importance of task synchronization and communication (to be covered in later chapters).
  • Identify common pitfalls when working with tasks in an RTOS environment.

Introduction

In previous chapters, we explored the hardware foundations of the ESP32 and how code gets onto the chip and starts running. However, modern embedded systems often need to perform multiple activities seemingly simultaneously. Consider a Wi-Fi connected sensor node: it might need to read sensor data, process it, manage the Wi-Fi connection, respond to network requests, and perhaps update a display – all concurrently.

Trying to manage these activities in a single, sequential loop (often called a “super-loop” or “bare-metal scheduling”) quickly becomes complex, inefficient, and difficult to maintain. This is where a Real-Time Operating System (RTOS) comes in. ESP-IDF uses FreeRTOS as its underlying operating system, providing features to manage concurrent operations, timing, and communication in a structured way. This chapter introduces the fundamental concepts of FreeRTOS and how to use its basic features within your ESP-IDF projects.

Theory

What is an RTOS?

An RTOS is specialized operating system software designed for applications that need to respond to events within predictable, often strict, time constraints (real-time). Unlike desktop operating systems (like Windows, macOS, or Linux) which prioritize overall throughput and fairness, an RTOS prioritizes determinism and responsiveness.

Key benefits of using an RTOS include:

Benefit Description
Concurrency Allows multiple functions or parts of an application (Tasks) to appear to run simultaneously, improving system responsiveness and utilization.
Modularity Helps break down complex embedded applications into smaller, independent, and more manageable units (Tasks), simplifying development and maintenance.
Resource Management Provides standardized mechanisms (like mutexes and semaphores) to safely share hardware resources (peripherals, memory) and data between tasks, preventing conflicts.
Timing Control Offers precise control over task execution timing, delays, and periodic activities, crucial for real-time applications with deadlines.
Responsiveness & Preemption Enables high-priority tasks to preempt (interrupt) lower-priority tasks to respond quickly to critical events and meet strict deadlines.
Simplified Design Abstracts away the complexities of low-level task scheduling and context switching, allowing developers to focus on application logic.
Scalability & Maintainability Makes it easier to add new features (as new tasks) and maintain the existing codebase due to the structured, decoupled nature of tasks.

Core FreeRTOS Concepts

FreeRTOS is a popular, open-source, scalable RTOS designed for embedded systems. ESP-IDF integrates a version of FreeRTOS tailored for the ESP32’s architecture (including multi-core support). Here are the essential concepts:

Tasks (Threads)

A Task is the fundamental unit of execution in FreeRTOS. Think of it as an independent function with its own stack (for local variables, function calls, context) and priority level.You can create multiple tasks, each responsible for a specific part of your application’s functionality (e.g., a task for reading sensors, a task for handling Wi-Fi, a task for updating a display).Task States: A task can be in one of several states at any given time:

Task State Description Can it Execute?
Running The task is currently being executed by a CPU core. On a multi-core system, multiple tasks can be in the Running state simultaneously (one per core). Yes, actively using CPU.
Ready The task is able to run (not waiting for any event) but is not currently running because a higher-priority task is running, or all available CPU cores are occupied by other tasks of equal or higher priority. Yes, waiting for CPU time.
Blocked The task is waiting for a specific event to occur. Examples: waiting for a timer to expire (due to vTaskDelay or vTaskDelayUntil), waiting for data to arrive in a queue, waiting for a semaphore or mutex to become available. No, cannot run until event occurs.
Suspended The task has been explicitly put into a dormant state by a call to vTaskSuspend(). It will not be scheduled for execution, regardless of its priority or available events, until it is explicitly resumed by a call to vTaskResume() or vTaskResumeFromISR(). No, explicitly paused.
%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%%
graph TD
    subgraph Task States
        RNG[Running]
        RDY[Ready]
        BLK[Blocked]
        SUS[Suspended]
    end

    RDY -- Scheduler Dispatch --> RNG;
    RNG -- Preemption by Higher Priority Task --> RDY;
    RNG -- Time Slice End (Equal Priority) --> RDY;
    RNG -- Waiting for Event (vTaskDelay, Queue Recv, Semaphore Take) --> BLK;
    BLK -- Event Occurred (Timer Expired, Data Arrived, Semaphore Given) --> RDY;
    RNG -- vTaskSuspend --> SUS;
    RDY -- vTaskSuspend --> SUS;
    BLK -- vTaskSuspend (while blocked, then event occurs) --> SUS; 
    SUS -- vTaskResume --> RDY;
    SUS -- vTaskResumeFromISR --> RDY;

    %% Styling
    classDef runningNode fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46; 
    classDef readyNode fill:#DBEAFE,stroke:#2563EB,stroke-width:2px,color:#1E40AF; 
    classDef blockedNode fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#92400E; 
    classDef suspendedNode fill:#FEE2E2,stroke:#DC2626,stroke-width:2px,color:#991B1B; 

    class RNG runningNode;
    class RDY readyNode;
    class BLK blockedNode;
    class SUS suspendedNode;

    linkStyle default color:#1F2937,stroke-width:1px;

Scheduler

The Scheduler is the heart of the RTOS. It decides which Ready task should enter the Running state on each available CPU core. FreeRTOS implements a priority-based, preemptive scheduler:

  • Priority-Based: Each task is assigned a priority. The scheduler will always try to run the highest-priority task that is in the Ready state.
  • Preemptive: If a task with a higher priority than the currently running task becomes Ready (e.g., its delay finishes, or it receives needed data), the scheduler will immediately stop the lower-priority task (even if it hasn’t finished its work) and switch to the higher-priority task.

Time-Slicing (Round-Robin): If multiple tasks at the same highest priority level are Ready, the scheduler will run each one for a fixed period, called a time slice or tick, before switching to the next Ready task of the same priority. This provides a fair share of CPU time among equal-priority tasks.

%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%%
graph TD
    A[System Tick / Event Occurs] --> B{Scheduler Invoked};
    B --> C{Any Task Became Ready or Unblocked?};
    C -- No --> E["Continue Current Task(s)"];
    C -- Yes --> D{"Find Highest Priority Ready Task(s)"};
    D --> F{Is a Higher Priority Task Ready than Currently Running Task on a Core?};
    F -- Yes (Preemption) --> G[Context Switch: Save Current, Load Higher Priority Task];
    F -- No --> H{"Multiple Ready Tasks at Same Highest Priority? (for a core)"};
    H -- Yes (Time Slicing / Round Robin) --> I[Context Switch: Save Current, Load Next Equal Priority Task in List];
    H -- No --> E;
    G --> J["New Task(s) Running"];
    I --> J;
    E --> K[Wait for Next Tick / Event];
    J --> K;

    %% Styling
    classDef startNode fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef processNode fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef decisionNode fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    classDef endNode fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;
    classDef checkNode fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B; 


    class A startNode;
    class B,D processNode;
    class C,F,H decisionNode;
    class G,I checkNode; 
    class E,J endNode; 
    class K processNode; 

    linkStyle default color:#1F2937,stroke-width:1px;

Context Switching:

  • When the scheduler decides to stop one task and start another (due to preemption or time-slicing), it performs a Context Switch.
  • This involves saving the complete state (CPU registers, stack pointer, etc.) of the outgoing task and loading the state of the incoming task. This allows the incoming task to resume exactly where it left off. Context switching introduces a small overhead but is essential for multitasking.

Ticks and Time Management:

  • The RTOS maintains a system Tick – a periodic interrupt occurring at a configurable rate (defined by CONFIG_FREERTOS_HZ in menuconfig, often 100Hz or 1000Hz).
  • This tick serves as the base for RTOS timekeeping. Functions like vTaskDelay use ticks to measure time intervals. The precision of delays and timeouts is limited by the tick rate.
  • FreeRTOS provides functions to convert between milliseconds and ticks (pdMS_TO_TICKS()).

Inter-Task Communication (ITC) and Synchronization:

Since tasks run quasi-independently, they often need ways to communicate data or synchronize their actions to avoid conflicts when accessing shared resources (like variables or hardware peripherals).

FreeRTOS provides several mechanisms for this, including:

Mechanism Primary Purpose Brief Description Common Use Case
Queues Passing data between tasks. Thread-safe FIFO (First-In, First-Out) buffers. Tasks can send data to a queue, and other tasks can receive data from it, blocking if the queue is empty (on receive) or full (on send). Producer task generates data, consumer task processes it. Sending sensor readings from one task to another for processing/logging.
Semaphores (Binary) Signaling and basic synchronization. Can be thought of as a flag that is either available (‘taken’) or unavailable (‘given’). Tasks can wait (block) until a semaphore is given. Notifying a task that an event has occurred (e.g., ISR signals a task). Basic lock for a shared resource (though mutexes are often better for this).
Semaphores (Counting) Controlling access to multiple instances of a resource. Maintains a count. Tasks ‘take’ (decrement) the semaphore to access a resource and ‘give’ (increment) it back when done. Blocks if count is zero on a take. Managing a pool of limited resources, like network connections or buffers.
Mutexes (Mutual Exclusion Semaphores) Protecting shared resources from simultaneous access (mutual exclusion). Special binary semaphores designed to prevent race conditions. Only one task can hold a mutex at a time. Incorporates priority inheritance to help prevent priority inversion. Ensuring only one task can write to a global variable or access a hardware peripheral at any given moment.
Event Groups Synchronizing on multiple events or conditions. Allows tasks to wait for a combination of one or more events (represented by bits in an event group) to occur. Supports waiting for ALL specified bits or ANY specified bit. A task waiting for both Wi-Fi connection AND an SD card to be ready before proceeding.
Task Notifications Direct, lightweight task-to-task signaling and data passing. Each task has a 32-bit notification value. One task can directly notify another, optionally sending a value. Faster than queues/semaphores for simple direct notifications. An ISR quickly notifying a specific task, or one task signaling another to perform an action.
These mechanisms will be explored in detail in later chapters.

FreeRTOS in ESP-IDF

  • Automatic Startup: You don’t need to manually initialize or start the FreeRTOS scheduler. The ESP-IDF startup code does this before calling your app_main function.
  • app_main is a Task: Your app_main function itself runs as a FreeRTOS task (with a configurable priority and stack size set via menuconfig).
  • Configuration: Many FreeRTOS options (tick rate, maximum priorities, kernel configuration, stack checking, etc.) are configured using idf.py menuconfig under Component config -> FreeRTOS.
  • Dual-Core Support (ESP32, ESP32-S3): ESP-IDF’s FreeRTOS port handles the dual-core nature of these chips. You can create tasks and let the scheduler manage them across both cores, or you can explicitly pin tasks to specific cores (Core Affinity) using xTaskCreatePinnedToCore. By default, tasks can run on either core.

Practical Examples

Let’s create some tasks using the FreeRTOS API within ESP-IDF.

Example 1: Creating Multiple Tasks

This example creates two tasks that print messages to the console at different intervals and with different priorities.

Code: Create a new C file (e.g., multi_task.c) in your project’s main directory or modify your main.c.

C
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"

static const char *TAG = "MULTI_TASK";

// Task function for the high-priority task
void high_priority_task(void *pvParameter)
{
    int count = 0;
    ESP_LOGI(TAG, "High Priority Task started on core %d", xPortGetCoreID());
    while(1) {
        count++;
        ESP_LOGI(TAG, "High Priority Task running (%d)...", count);
        // Delay for 1000 milliseconds (1 second)
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

// Task function for the low-priority task
void low_priority_task(void *pvParameter)
{
    int count = 0;
    ESP_LOGI(TAG, "Low Priority Task started on core %d", xPortGetCoreID());
    while(1) {
        count++;
        // This task tries to print more frequently but will be preempted
        // by the high-priority task when it becomes ready.
        ESP_LOGI(TAG, "Low Priority Task running (%d)...", count);
        // Delay for 500 milliseconds (0.5 seconds)
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

void app_main(void)
{
    ESP_LOGI(TAG, "App Main started on core %d", xPortGetCoreID());

    // Wait a moment before creating tasks to ensure app_main setup completes (optional)
    vTaskDelay(pdMS_TO_TICKS(100));

    // Create the high-priority task
    // xTaskCreate(function, name, stack_size, parameters, priority, task_handle)
    xTaskCreate(
        high_priority_task,    // Function that implements the task.
        "HighPriTask",         // Text name for the task (debugging).
        2048,                  // Stack size in words (on ESP32, stack size is in bytes!). Adjust as needed. Check CONFIG_FREERTOS_TASK_STACK_SIZE_CHECK.
        NULL,                  // Parameter passed into the task - NULL for no parameters.
        10,                    // Priority of the task (0 is lowest, configMAX_PRIORITIES - 1 is highest).
        NULL                   // Task handle - NULL if not needed.
    );
    ESP_LOGI(TAG, "High Priority Task created.");

    // Create the low-priority task
    xTaskCreate(
        low_priority_task,
        "LowPriTask",
        2048,
        NULL,
        5,                     // Lower priority than the high-priority task.
        NULL
    );
    ESP_LOGI(TAG, "Low Priority Task created.");

    // app_main task can continue doing other things or just exit/delete itself
    // If app_main exits, the other tasks will continue running.
    ESP_LOGI(TAG, "app_main finished creating tasks. It could now exit or do other work.");
    // For this example, let app_main just idle.
    // Note: If app_main returns, its task is deleted automatically by ESP-IDF.
    // If you want app_main to persist, it needs its own loop or blocking call.
     while(1) {
         vTaskDelay(pdMS_TO_TICKS(10000)); // Idle loop for app_main task
     }
}

Build: idf.py build

Flash: idf.py -p <YOUR_PORT> flash

Monitor: idf.py -p <YOUR_PORT> monitor

Observe:

  • You’ll see logs from both tasks printing.
  • Notice that the “High Priority Task” log appears consistently every second.
  • The “Low Priority Task” log appears roughly every 0.5 seconds, but it will be interrupted whenever the high-priority task becomes ready to run (after its 1-second delay expires). You might see the low-priority task print once or twice, then the high-priority task prints, then the low-priority task resumes. This demonstrates preemption.
  • Note the core ID reported by each task. On dual-core chips, they might run on different cores unless pinned.

Example 2: Precise Timing with vTaskDelayUntil

vTaskDelay delays for a duration relative to when it was called. If other tasks or interrupts cause jitter, the actual period between executions might vary. vTaskDelayUntil is used for fixed-frequency execution.

Code: Modify app_main and add a new task function.

C
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_timer.h" // For high-resolution timer

static const char *TAG_DELAY = "TASK_DELAY";

// Task using vTaskDelay (relative delay)
void relative_delay_task(void *pvParameter)
{
    ESP_LOGI(TAG_DELAY, "Relative Delay Task started.");
    while(1) {
        long long time_before = esp_timer_get_time();
        ESP_LOGI(TAG_DELAY, "Relative Task executing...");
        // Simulate some work that takes variable time
        vTaskDelay(pdMS_TO_TICKS(50 + (esp_random() % 50))); // Work for 50-100ms
        long long time_after = esp_timer_get_time();
        ESP_LOGI(TAG_DELAY, "Relative Task work took %lld us", time_after - time_before);

        // Delay for 1 second relative to NOW
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

// Task using vTaskDelayUntil (fixed frequency)
void fixed_frequency_task(void *pvParameter)
{
    ESP_LOGI(TAG_DELAY, "Fixed Frequency Task started.");
    TickType_t xLastWakeTime;
    const TickType_t xFrequency = pdMS_TO_TICKS(1000); // Target frequency: 1 second

    // Initialise the xLastWakeTime variable with the current time.
    xLastWakeTime = xTaskGetTickCount();

    while(1) {
        long long time_before = esp_timer_get_time();
        ESP_LOGI(TAG_DELAY, "Fixed Freq Task executing...");
        // Simulate some work that takes variable time
        vTaskDelay(pdMS_TO_TICKS(50 + (esp_random() % 50))); // Work for 50-100ms
         long long time_after = esp_timer_get_time();
        ESP_LOGI(TAG_DELAY, "Fixed Freq Task work took %lld us", time_after - time_before);

        // Wait for the next cycle.
        vTaskDelayUntil(&xLastWakeTime, xFrequency);
        // Note: xLastWakeTime is updated automatically by vTaskDelayUntil
    }
}


void app_main(void)
{
    ESP_LOGI(TAG_DELAY, "Starting tasks with different delay types.");

    xTaskCreate(relative_delay_task, "RelativeDelay", 3072, NULL, 5, NULL);
    xTaskCreate(fixed_frequency_task, "FixedFrequency", 3072, NULL, 5, NULL);

    // Allow tasks to run
    while(1) {
         vTaskDelay(pdMS_TO_TICKS(10000));
     }
}

Build, Flash, Monitor.

Observe:

  • Both tasks aim for a 1-second execution cycle.
  • Look closely at the timestamps (if enabled in logging) or the perceived regularity of the “Executing…” messages.
  • The “Relative Delay Task” execution points will drift over time because its 1-second delay starts after its variable work completes. If the work takes 100ms, the cycle is 1100ms. If it takes 50ms, the cycle is 1050ms.
  • The “Fixed Frequency Task” execution points should remain much more consistent, occurring almost exactly every 1000ms (plus jitter), regardless of how long the work took (as long as work + overhead < 1000ms). vTaskDelayUntil calculates the delay needed to wake up at the next fixed interval based on the xLastWakeTime.

Example 3: Getting Task Information (Stack High Water Mark)

Monitoring stack usage is crucial to prevent overflows. The “High Water Mark” is the minimum amount of free stack space a task has had since it started.

Code: Modify app_main and add a task function

C
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"

static const char *TAG_STACK = "STACK_CHECK";

// Function to consume stack space
void recursive_function(int depth, volatile uint8_t buffer[100]) {
    // Volatile buffer to prevent optimization
    buffer[depth % 100] = depth;
    ESP_LOGD(TAG_STACK, "Recursion depth: %d, Stack HWM: %u bytes", depth, uxTaskGetStackHighWaterMark(NULL));
    if (depth > 0) {
        recursive_function(depth - 1, buffer);
    }
}

void stack_check_task(void *pvParameter)
{
    ESP_LOGI(TAG_STACK, "Stack Check Task started.");
    UBaseType_t hwm_initial; // Stack High Water Mark in bytes

    // Get initial HWM (NULL means current task)
    hwm_initial = uxTaskGetStackHighWaterMark(NULL);
    ESP_LOGI(TAG_STACK, "Initial Stack High Water Mark: %u bytes", hwm_initial);

    // Allocate a reasonably large buffer on the stack
    volatile uint8_t local_buffer[512];
    // Use volatile to prevent compiler optimizing it away
    local_buffer[0] = 1;
    local_buffer[511] = 2;

    UBaseType_t hwm_after_alloc = uxTaskGetStackHighWaterMark(NULL);
    ESP_LOGI(TAG_STACK, "Stack HWM after local_buffer alloc: %u bytes (Used approx %u bytes)",
             hwm_after_alloc, hwm_initial - hwm_after_alloc);

    // Call a function that uses more stack
    ESP_LOGI(TAG_STACK, "Calling recursive function...");
    // Enable DEBUG logging for TAG_STACK to see recursive calls: idf.py set-target esp32 && idf.py menuconfig -> Component config -> Log output -> Default log verbosity (Debug)
    // Be careful with recursion depth and buffer size to avoid actual overflow!
    recursive_function(5, local_buffer); // Modest recursion depth

    UBaseType_t hwm_after_func = uxTaskGetStackHighWaterMark(NULL);
    ESP_LOGI(TAG_STACK, "Stack HWM after recursive_function: %u bytes (Used approx %u bytes)",
             hwm_after_func, hwm_initial - hwm_after_func);

    ESP_LOGI(TAG_STACK, "Stack Check Task finished checks. Looping.");

    while(1) {
        vTaskDelay(pdMS_TO_TICKS(5000));
    }
}

void app_main(void)
{
    ESP_LOGI(TAG_STACK, "Starting Stack Check Task.");

    // Create the task with a specific stack size
    xTaskCreate(stack_check_task, "StackCheck", 3072, NULL, 5, NULL); // 3072 bytes stack

     // Allow task to run
    while(1) {
         vTaskDelay(pdMS_TO_TICKS(10000));
     }
}

Build, Flash, Monitor. (You might want to enable DEBUG log level for the STACK_CHECK tag in menuconfig to see the recursive calls).

Observe:

  • Note the initial high water mark.
  • See how it decreases after allocating local_buffer.
  • Observe further decrease (potentially) during the execution of recursive_function.
  • The final HWM shows the peak stack usage during the task’s run so far. If this value gets close to zero, you risk a stack overflow.

Tip: In ESP-IDF, the stack size parameter in xTaskCreate and xTaskCreatePinnedToCore is specified in bytes, unlike standard FreeRTOS which often uses words. Always allocate sufficient stack, considering local variables, function call depth, and context switch overhead. Use the stack checking features in menuconfig (Component config -> FreeRTOS -> Enable stack overflow checking) during development.

Variant Notes

The core concepts and APIs of FreeRTOS presented here apply consistently across all ESP32 variants supported by ESP-IDF (ESP32, S2, S3, C3, C6, H2).

Feature/Aspect General Applicability Variant-Specific Notes & Considerations
Core FreeRTOS Concepts & API Consistent across all ESP32 variants (ESP32, S2, S3, C3, C6, H2). Functions like xTaskCreate, vTaskDelay, queue/semaphore APIs are the same. The underlying implementation is tailored by ESP-IDF for each specific architecture (Xtensa LX6/LX7, RISC-V).
CPU Core Count Varies. Dual-Core (ESP32, ESP32-S3): FreeRTOS runs in SMP mode. Tasks can run on Core 0 or Core 1. Use xTaskCreatePinnedToCore() for core affinity.
Single-Core (ESP32-S2, C3, C6, H2): All tasks naturally run on the single available core. xTaskCreatePinnedToCore() will still work but only allows pinning to core 0 (or the only available core).
Performance & Timing Conceptually similar, but absolute performance differs. Context switch times, interrupt latencies, and overall task execution speed can vary due to:
  • CPU architecture (Xtensa vs. RISC-V)
  • CPU clock speed (configurable, e.g., 80MHz, 160MHz, 240MHz)
  • Cache size and architecture
  • Memory speed
Configuration Defaults (menuconfig) Many defaults are shared. Default task stack sizes for system tasks, or default app_main priority/stack might have slight variations in the default SDK configurations for different targets. Always verify in menuconfig for your specific target.
Interrupt Handling FreeRTOS-safe ISRs are required. Specific interrupt controller details and interrupt numbers differ per chip. ESP-IDF HAL abstracts some of this, but low-level interrupt knowledge is variant-specific.
Tickless Idle Supported for power saving. The effectiveness and implementation details of tickless idle power saving might vary slightly based on the power management capabilities of each variant.
For the introductory concepts in this chapter, these differences are largely transparent when using standard FreeRTOS APIs. Advanced optimization or hardware-specific interactions may require deeper variant knowledge.

Common Mistakes & Troubleshooting Tips

Mistake / Issue Symptom(s) Troubleshooting / Solution
Stack Overflow
Insufficient stack size for a task.
Guru Meditation Error (LoadProhibited, StoreProhibited, IllegalInstruction); Stack canary errors if enabled; Seemingly random crashes. Enable FreeRTOS stack checking in menuconfig (CONFIG_FREERTOS_CHECK_STACKOVERFLOW). Monitor uxTaskGetStackHighWaterMark(). Increase task stack size (in bytes for ESP-IDF). Analyze code for large local variables or deep recursion; move to heap/static if possible.
Priority Inversion
High-priority task blocked by low-priority task holding a resource, while a medium-priority task runs.
High-priority tasks unresponsive or miss deadlines; system sluggish. Use Mutexes (which implement Priority Inheritance) instead of binary semaphores for protecting shared resources. Design to minimize resource holding times.
Busy-Waiting
Using a loop to poll a flag/condition without blocking.
One CPU core at 100% utilization; other tasks on that core starve or run slowly; high power consumption; watchdog timeout. Replace with blocking calls: vTaskDelay, vTaskDelayUntil, queue receives, semaphore takes, event group waits, task notifications.
Race Conditions
Multiple tasks access shared data/resource without proper protection.
Intermittent failures; corrupted data; unpredictable behavior that varies between runs. Hard to debug. Identify shared resources. Protect access using Mutexes or Critical Sections (taskENTER_CRITICAL(), taskEXIT_CRITICAL()). Use atomic operations where applicable. Minimize shared data or use queues for safe data transfer.
Non-Blocking Task Loop
A task’s main while(1) loop never calls a blocking function or delay.
If it’s the highest priority ready task, it starves all lower-priority tasks on its core. Watchdog timer may trigger a reset. Ensure every persistent task loop contains a blocking call (e.g., waiting on a queue/semaphore) or a delay (vTaskDelay(1) is enough to yield).
Incorrect Stack Size Unit
Assuming stack size in xTaskCreate is in words.
Likely leads to stack overflow if a small number is provided. Remember: In ESP-IDF, task stack size for xTaskCreate and xTaskCreatePinnedToCore is specified in BYTES.
Accessing Peripherals from Multiple Tasks Unprotected Unpredictable peripheral behavior, corrupted peripheral state, crashes. Use mutexes to guard access to shared hardware peripherals if multiple tasks need to configure or use them. Alternatively, dedicate one task to manage a specific peripheral and use queues to communicate with it.

Exercises

  1. LED Blinker Tasks:
    • If your board has built-in LEDs (or connect external ones), create two tasks.
    • Task 1: Blinks LED 1 ON for 200ms, OFF for 800ms (1-second period). Priority 5.
    • Task 2: Blinks LED 2 ON for 500ms, OFF for 500ms (1-second period). Priority 5.
    • Use gpio_set_level() and vTaskDelay(). Configure GPIOs appropriately. Observe both LEDs blinking concurrently.
  2. Priority Execution Order:
    • Create three tasks (Task A, Task B, Task C).
    • Task A: Priority 7. In a loop, prints “Task A Running” and delays for 1000ms.
    • Task B: Priority 5. In a loop, prints “Task B Running” and delays for 1000ms.
    • Task C: Priority 6. In a loop, prints “Task C Running” and delays for 1000ms.
    • Run the code and observe the order of messages in the monitor. Which task runs most consistently?
    • Modify the code: Change Task B’s priority to 8. Re-flash and observe the new execution order.
  3. Dynamic Stack Check:
    • Create a task similar to “Example 3: Getting Task Information”.
    • Inside the task loop (after the initial checks), add code that allocates a variable-sized buffer on the stack based on some input (e.g., a value received via a queue, or just incrementing size in the loop).
    • Before and after the allocation, print the uxTaskGetStackHighWaterMark().
    • Observe how the HWM changes dynamically within the loop. Add checks to prevent allocating too much and causing a real overflow. (This exercise foreshadows inter-task communication).

Summary

  • An RTOS (like FreeRTOS) manages tasks, scheduling, and resources in real-time embedded systems, enabling concurrency and modularity.
  • Tasks are independent functions with their own stacks and priorities. They exist in states like Running, Ready, Blocked, Suspended.
  • The Scheduler determines which Ready task runs, using priority-based preemption and time-slicing for equal priorities.
  • Context Switching allows tasks to resume where they left off.
  • The system Tick provides the base for RTOS timekeeping (vTaskDelay, vTaskDelayUntil).
  • ESP-IDF integrates FreeRTOS, handles its initialization, and runs app_main as a task. Configuration is done via menuconfig.
  • Creating tasks (xTaskCreate, xTaskCreatePinnedToCore) and managing delays (vTaskDelay, vTaskDelayUntil) are fundamental operations.
  • Monitoring Stack High Water Mark (uxTaskGetStackHighWaterMark) is crucial to prevent overflows.
  • Avoid common pitfalls like stack overflows, priority inversion, busy-waiting, race conditions, and non-blocking task loops.
  • Inter-Task Communication (Queues, Semaphores, Mutexes) is essential for coordination and will be covered next.

Further Reading

Leave a Comment

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

Scroll to Top