Chapter 11: Task Priorities and Scheduling

Chapter Objectives

Upon completing this chapter, you will be able to:

  • Explain the core principles of the FreeRTOS scheduling algorithm (priority-based, preemptive).
  • Understand the significance and range of task priorities in FreeRTOS.
  • Describe how higher-priority tasks preempt lower-priority tasks.
  • Explain time-slicing and how tasks of the same priority share processing time.
  • Identify the role and characteristics of the FreeRTOS Idle Task.
  • Implement the optional Idle Hook function for background processing.
  • Apply basic strategies for assigning task priorities in an embedded application.
  • Recognize potential scheduling problems like starvation and the importance of yielding.
  • Understand how scheduling operates on single-core vs. multi-core ESP32 variants.

Introduction

In the previous chapters, we learned how to create and manage individual FreeRTOS tasks. We saw that tasks allow us to run multiple pieces of code concurrently. However, on a system with one or two CPU cores, tasks cannot truly run simultaneously; they must share the CPU time. The scheduler is the component responsible for deciding which task runs at any given moment.

Understanding how the scheduler makes these decisions is fundamental to designing predictable and responsive real-time systems. The primary mechanism the FreeRTOS scheduler uses is task priority. Assigning appropriate priorities ensures that critical operations happen promptly, while less important activities run in the background. This chapter explores the FreeRTOS scheduling algorithm, the concept of task priorities, preemption, time-slicing, and the special Idle task, providing you with the knowledge to control the flow of execution in your ESP32 applications.

Theory

The FreeRTOS scheduler dictates the order and timing of task execution based on a few core principles.

The FreeRTOS Scheduler Algorithm

As mentioned previously, FreeRTOS employs a fixed-priority preemptive scheduling algorithm, optionally augmented with time-slicing (round-robin) for tasks of equal priority.

Fixed-Priority:

Each task is assigned a priority level when it’s created, and this priority normally doesn’t change unless explicitly altered by the application using vTaskPrioritySet().

Preemptive:

This is a key characteristic. If a task becomes ready to run (e.g., its vTaskDelay expires, or it receives data it was waiting for) and it has a higher priority than the task currently running on a core, the scheduler will immediately stop the lower-priority task (pausing it exactly where it was) and switch to the higher-priority task. The lower-priority task is preempted.

%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%%
sequenceDiagram
    participant CPU
    participant Scheduler
    participant Task_A [Task A (Low Prio)]
    participant Task_B [Task B (High Prio)]

    rect rgb(219, 234, 254)
        Note over CPU, Task_A: Task A (Low Priority) is Running
        CPU->>Task_A: Executes
    end

    Task_B->>Scheduler: Becomes Ready (e.g., vTaskDelay ends)
    Scheduler-->>CPU: Higher priority task (Task B) is Ready!
    CPU->>Task_A: Preempt! Save Context
    Task_A-->>CPU: Context Saved

    rect rgb(209, 250, 229)
        Note over CPU, Task_B: Scheduler switches to Task B (High Priority)
        CPU->>Task_B: Executes
        Task_B->>Task_B: Performs its work
    end
    alt Task B Completes or Blocks
        Task_B->>Scheduler: Blocks (e.g., vTaskDelay, waits for event)
    else Task B Runs to Completion (if short)
        Task_B->>Scheduler: Finishes its current run cycle (less common for typical tasks)
    end

    Scheduler-->>CPU: Task B no longer running, check for next highest priority task
    CPU->>Task_A: Restore Context
    Task_A-->>CPU: Context Restored

    rect rgb(219, 234, 254)
        Note over CPU, Task_A: Task A (Low Priority) Resumes Running
        CPU->>Task_A: Executes from where it left off
    end

    %% Styling - Not directly applicable in sequence diagrams in this manner,
    %% but using notes and rects for visual grouping.
    %% Colors used in rect: Light Blue for Low Prio, Light Green for High Prio

Time-Slicing (Round-Robin):

What happens if multiple tasks with the same highest priority are ready to run? The scheduler allows each of these tasks to run for one time slice before preemptively switching to the next ready task at that same priority level. This ensures that equal-priority tasks share the CPU time fairly. A time slice is typically equal to one RTOS tick period.

%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%%
sequenceDiagram
    participant CPU
    participant Scheduler
    participant Task_X [Task X (Prio 5)]
    participant Task_Y [Task Y (Prio 5)]

    Note over Scheduler: Task X and Task Y are Ready (Same Highest Priority: 5)

    rect rgb(219, 234, 254) # Task X color
        CPU->>Task_X: Scheduler dispatches Task X
        Task_X->>Task_X: Runs for 1 Time Slice (e.g., 1 RTOS Tick)
    end
    Scheduler-->>CPU: Time Slice for Task X ended

    rect rgb(222, 230, 249) # Task Y color (slightly different blue)
        CPU->>Task_Y: Scheduler dispatches Task Y
        Task_Y->>Task_Y: Runs for 1 Time Slice
    end
    Scheduler-->>CPU: Time Slice for Task Y ended

    rect rgb(219, 234, 254) # Task X color
        CPU->>Task_X: Scheduler dispatches Task X again
        Task_X->>Task_X: Runs for 1 Time Slice
    end
    Scheduler-->>CPU: Time Slice for Task X ended

    Note over CPU, Scheduler: ...and so on, cycling between Task X and Task Y.

Task Priorities

  • Range: Priorities are represented by unsigned integers, ranging from 0 (the lowest priority) up to configMAX_PRIORITIES - 1 (the highest priority).
  • configMAX_PRIORITIES: This constant is defined in FreeRTOSConfig.h (accessible via menuconfig in ESP-IDF under Component config -> FreeRTOS). Its value determines the number of available priority levels. A typical value for ESP32 projects is 25. Increasing this value consumes slightly more RAM.
  • Assignment: The scheduler only considers priority. It doesn’t care how long a task has been waiting or how much work it has done. If multiple tasks are ready, the one with the numerically highest priority value runs.
Concept Description Details
Priority Range Defines the spectrum of possible priority levels for tasks. Priorities are unsigned integers from 0 (lowest) to configMAX_PRIORITIES – 1 (highest).
configMAX_PRIORITIES A compile-time constant in FreeRTOS configuration. Determines the total number of distinct priority levels available. Typically set to a value like 5, 16, 25, or 32 in ESP-IDF projects (default often 25). Configurable via menuconfig (Component config → FreeRTOS → Max Task priorities).
Priority Assignment How the scheduler uses priorities. The scheduler always attempts to run the highest-priority task that is in the Ready state. Numerically higher values mean higher effective priority.
Idle Task Priority The priority of the system’s Idle Task(s). The Idle Task always runs at priority 0 (tskIDLE_PRIORITY). Application tasks should generally use priorities greater than 0.
Impact of configMAX_PRIORITIES System resource usage. A higher value for configMAX_PRIORITIES consumes slightly more RAM as FreeRTOS needs to maintain data structures (like lists of ready tasks) for each priority level.

The RTOS Tick (configTICK_RATE_HZ)

  • The scheduler relies on a periodic interrupt called the RTOS tick. The frequency of this interrupt is set by configTICK_RATE_HZ in menuconfig (defaults often 100Hz or 1000Hz).
  • Tick Functions: Each tick interrupt allows FreeRTOS to:
    • Decrement timers for tasks blocked on vTaskDelay or vTaskDelayUntil.
    • Move tasks whose delays have expired from the Blocked state to the Ready state.
    • Implement time-slicing by potentially switching between equal-priority tasks at the end of their slice.
  • Trade-offs:
Tick Rate (configTICK_RATE_HZ) Advantages Disadvantages Typical Values & Use Cases
Higher Tick Rate
(e.g., 1000 Hz / 1ms tick)
  • Finer time resolution for delays (vTaskDelay) and timeouts.
  • Faster detection of tasks becoming ready from timed waits.
  • More responsive time-slicing for equal-priority tasks.
  • Increased CPU overhead due to more frequent tick interrupts.
  • More frequent context switches can add overhead.
  • Potentially higher power consumption if CPU wakes more often.
Suitable for applications requiring precise short delays or fast reactions to timeouts. Common in control systems or high-frequency data sampling. Default in some ESP-IDF configurations.
Lower Tick Rate
(e.g., 100 Hz / 10ms tick)
  • Reduced CPU overhead from tick interrupts.
  • Fewer context switches solely due to time-slicing.
  • Can contribute to lower power consumption in idle periods (especially if tickless idle is not fully effective).
  • Coarser time granularity; minimum delay is one tick period.
  • Slower response to timeouts.
  • Time-slicing is less frequent, potentially making same-priority tasks feel less responsive to each other if they don’t yield.
Suitable for applications where millisecond precision is not critical, and reducing CPU overhead or power is more important. Default in some ESP-IDF configurations.
The optimal tick rate depends on the specific application’s requirements for timing precision versus CPU overhead and power consumption. Configurable via menuconfig.

The Idle Task and the Idle Hook

What happens when no application tasks are ready to run (i.e., all tasks are either Blocked or Suspended)? Does the CPU just stop? No. FreeRTOS automatically creates at least one Idle Task per core when the scheduler starts.

  • Priority: The Idle Task always runs at the lowest possible priority (tskIDLE_PRIORITY, which is 0).
  • Purpose: Its primary job is simply to ensure something is always running when no other application task can. It typically executes an infinite loop.
  • Cleanup: The Idle Task is also responsible for freeing the memory (stack and TCB) allocated to tasks that have been deleted. When vTaskDelete() is called, the task’s resources are marked for cleanup, but the actual freeing happens later in the context of the Idle Task. This avoids the need for complex cleanup logic within the deletion function itself.
  • The Idle Hook (vApplicationIdleHook): FreeRTOS allows you to define an optional function called vApplicationIdleHook(). If enabled via configUSE_IDLE_HOOK in menuconfig, the Idle Task will call this function once per iteration of its loop.
    • Restrictions: The Idle Hook function must not block (e.g., call vTaskDelay with a non-zero delay or wait indefinitely for a queue/semaphore). It should perform very short, quick operations.
    • Uses: Common uses include putting the CPU into a low-power state, performing very low-priority background calculations, incrementing system statistics, or toggling a “system alive” LED.

Tip: On multi-core systems (ESP32, ESP32-S3), there is an Idle Task running on each core when that core has no other work to do.

flowchart TD
    %% Styling definitions
    classDef start fill:#9de24f,stroke:#333,stroke-width:2px
    classDef process fill:#87CEFA,stroke:#333,stroke-width:1px
    classDef decision fill:#FFD700,stroke:#333,stroke-width:1px
    classDef idleTask fill:#FF9999,stroke:#333,stroke-width:1px
    classDef hook fill:#DDA0DD,stroke:#333,stroke-width:1px
    
    A[Scheduler Starts] --> B["Idle Task Created (per core)"]
    B --> C{Are any application tasks Ready?}
    C -- Yes --> D[Run highest priority Ready task]
    C -- No --> E["Run Idle Task (lowest priority)"]

    E --> F[Infinite Loop]
    F --> G[Clean up deleted tasks' memory]
    F --> H{"Is Idle Hook enabled?<br>(configUSE_IDLE_HOOK)"}
    
    H -- Yes --> I["Call vApplicationIdleHook()"]
    I --> J[Perform short non-blocking ops:<br>- Enter low power mode<br>- Background stats<br>- Toggle LED]

    H -- No --> K[Skip Idle Hook]
    
    %% Apply styles
    class A start
    class B,D,E,F,G,K process
    class C,H decision
    class I,J hook

Priority Assignment Strategies

Choosing the right priorities is crucial for system behavior. There are no hard rules, but here are some general guidelines:

Priority Category Typical Use Cases Characteristics & Considerations
Highest Priorities
(e.g., configMAX_PRIORITIES – 1 down to configMAX_PRIORITIES – N)
  • Critical interrupt handlers deferred work (if not done in ISR).
  • Emergency stop conditions / safety critical responses.
  • Hard real-time control loops with very tight deadlines.
  • Time-sensitive communication protocol acknowledgements.
Use sparingly. Tasks at this level must be short, efficient, and block very quickly. High risk of starving lower-priority tasks if not designed carefully. Should rarely perform complex computations or I/O.
Medium Priorities
(Middle range of available priorities)
  • Main application logic and state machines.
  • Standard communication tasks (Wi-Fi, Bluetooth, MQTT).
  • Sensor data acquisition and processing.
  • User interface event handling (if moderately responsive).
This is where most application tasks will reside. Differentiate priorities within this band based on relative urgency and deadlines. Tasks should still yield regularly via vTaskDelay or by waiting on synchronization objects.
Low Priorities
(Above Idle Priority, e.g., 1 up to M)
  • Background data logging or telemetry.
  • Non-critical system monitoring.
  • Infrequent or non-time-sensitive UI updates (e.g., display refresh).
  • File system operations that can tolerate delays.
  • Tasks triggered by the Idle Hook for extended work.
These tasks run when higher-priority tasks are blocked. Suitable for activities that are not time-critical and can be deferred. Ensure they do not unintentionally block medium or high priority tasks via shared resources without proper mutexes.
Lowest Priority (0)
(tskIDLE_PRIORITY)
  • Reserved for the FreeRTOS Idle Task(s).
Application tasks should generally not be assigned priority 0. The Idle Task handles system cleanup (e.g., freeing memory of deleted tasks) and can run the Idle Hook. If an application task is at priority 0, it will only run when absolutely nothing else, including the Idle Task’s own processing, can run.
General Guideline: Assign priorities based on task urgency and timing deadlines, not just perceived “importance”. A task with a short, hard deadline is more urgent than a background task, even if the background task is vital for long-term operation.

Rule of Thumb: Assign priorities based on the urgency and timing requirements of the task, not just perceived importance. A task that needs to react within 10ms is more urgent than one that updates a log file every 5 seconds, even if the log file seems “important”.

Potential Problems

  • Starvation: A low-priority task might never get CPU time if higher-priority tasks are constantly running without blocking sufficiently. Ensure all tasks, especially higher-priority ones, block periodically using vTaskDelay or waiting on synchronization primitives.
  • Priority Inversion: (Briefly introduced here, detailed in later chapters). A high-priority task needs a resource (like a mutex) currently held by a low-priority task. Before the low-priority task can release the resource, a medium-priority task preempts it. The high-priority task is now effectively blocked by the medium-priority task. FreeRTOS Mutexes have mechanisms (priority inheritance) to mitigate this.

Multi-Core Scheduling (ESP32, ESP32-S3)

  • By default, each core runs its own instance of the FreeRTOS scheduler independently.
  • Core 0 runs the highest-priority Ready task eligible to run on Core 0.
  • Core 1 runs the highest-priority Ready task eligible to run on Core 1.
  • If a task is created with xTaskCreate (no affinity), it can potentially run on either core, chosen by the scheduler based on core availability and priority.
  • If a task is created with xTaskCreatePinnedToCore, it can only run on the specified core.
  • Preemption and time-slicing occur independently on each core based on the tasks ready to run on that core.

Practical Examples

Let’s observe the scheduler in action.

Example 1: Demonstrating Preemption

A high-priority task interrupts a lower-priority task.

Code:

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

static const char *TAG = "PREEMPTION";

// Low priority task
void low_priority_task_preempt(void *pvParameter) {
    ESP_LOGI(TAG, "Low Prio Task: Started.");
    volatile uint64_t counter = 0; // Volatile to prevent optimization
    while(1) {
        counter++;
        // Simulate doing continuous work
        if ((counter % 10000000) == 0) { // Print occasionally
             ESP_LOGI(TAG, "Low Prio Task: Working (%llu)...", counter);
        }
         // No vTaskDelay here - this task tries to hog the CPU
    }
}

// High priority task
void high_priority_task_preempt(void *pvParameter) {
    ESP_LOGI(TAG, "High Prio Task: Started.");
    while(1) {
        ESP_LOGW(TAG, "High Prio Task: Running!");
        vTaskDelay(pdMS_TO_TICKS(1000)); // Block for 1 second
    }
}

void app_main(void) {
    ESP_LOGI(TAG, "App Main: Starting preemption demo.");

    // Create low priority task (Priority 5)
    xTaskCreate(low_priority_task_preempt, "LowPrio", 3072, NULL, 5, NULL);

    // Create high priority task (Priority 6)
    xTaskCreate(high_priority_task_preempt, "HighPrio", 2048, NULL, 6, NULL);

    ESP_LOGI(TAG, "App Main: Tasks created.");
}

Build, Flash, Monitor.

Observe:

  • The “Low Prio Task” starts working and prints its messages.
  • Every 1 second, the “High Prio Task” wakes up (its vTaskDelay finishes). Because it has a higher priority (6 vs 5), it immediately preempts the Low Prio Task.
  • You see the “High Prio Task: Running!” message.
  • The High Prio Task then calls vTaskDelay again, entering the Blocked state.
  • Only now can the Low Prio Task resume execution exactly where it was interrupted.
  • You see the Low Prio Task continue its work until it’s preempted again one second later.

Example 2: Demonstrating Time-Slicing

Two tasks at the same priority level share CPU time.

Code:

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

static const char *TAG = "TIME_SLICING";

// Task function for time-slicing demo
void time_slice_task(void *pvParameter) {
    char *task_name = (char *)pvParameter;
    volatile uint64_t counter = 0;
    ESP_LOGI(TAG, "%s: Started.", task_name);
    while(1) {
        counter++;
        // Simulate continuous work - Print very frequently to see interleaving
        if ((counter % 1000000) == 0) {
             ESP_LOGI(TAG, "%s: Count %llu", task_name, counter / 1000000);
        }
         // No vTaskDelay here to demonstrate time-slicing
         // In a real app, tasks should yield/block!
    }
}

void app_main(void) {
    ESP_LOGI(TAG, "App Main: Starting time-slicing demo.");

    // Create two tasks at the SAME priority (e.g., 5)
    xTaskCreate(time_slice_task, "Task A", 2048, "Task A", 5, NULL);
    xTaskCreate(time_slice_task, "Task B", 2048, "Task B", 5, NULL);

    ESP_LOGI(TAG, "App Main: Tasks created.");
    // Let app_main idle, maybe delete it
    vTaskDelete(NULL);
}

Configure Tick Rate: For better visualization, you might want to set a slightly higher tick rate in menuconfig (e.g., CONFIG_FREERTOS_HZ=1000). Rebuild if changed.

Build, Flash, Monitor.

Observe:

  • You should see the log messages from “Task A” and “Task B” interleaved.
  • Neither task runs continuously for a long time. The scheduler forces a context switch between them roughly every RTOS tick because they are the highest-priority ready tasks and have the same priority.
  • This demonstrates that even without explicit blocking (vTaskDelay), tasks at the same priority level share the CPU.

Example 3: Implementing the Idle Hook

Toggle an LED or print a message from the Idle Hook.

Enable Idle Hook: Run idf.py menuconfig. Go to Component config -> FreeRTOS -> Enable CONFIG_USE_IDLE_HOOK. Save and exit.

Code:

C
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "driver/gpio.h" // For LED control

static const char *TAG = "IDLE_HOOK";

#define BLINK_GPIO CONFIG_BLINK_GPIO // Configure via menuconfig or define here

// Application's Idle Hook function
// IMPORTANT: This function MUST NOT BLOCK.
void vApplicationIdleHook(void) {
    // Example 1: Simple print statement (can flood console if no other tasks run)
    // ets_printf("."); // Use ets_printf for minimal overhead in idle

    // Example 2: Toggle an LED
    // static bool led_state = false;
    // led_state = !led_state;
    // gpio_set_level(BLINK_GPIO, led_state);

    // Example 3: Enter light sleep (more advanced, requires power management config)
    // esp_light_sleep_start();

    // Add a small delay or yield if idle hook is computationally intensive
    // to prevent watchdog timeout, though ideally it should be very short.
    // ets_delay_us(100); // Small delay
}

// A simple task that runs for a while and then blocks
void busy_task(void *pvParameter) {
    ESP_LOGI(TAG, "Busy Task: Running for 5 seconds...");
    vTaskDelay(pdMS_TO_TICKS(5000)); // Run for 5 seconds
    ESP_LOGI(TAG, "Busy Task: Now blocking indefinitely.");
    while(1) {
        vTaskDelay(pdMS_TO_TICKS(10000)); // Block for 10 seconds
    }
}

void app_main(void) {
    ESP_LOGI(TAG, "App Main: Starting Idle Hook demo.");

    #ifdef BLINK_GPIO
    // Configure LED GPIO if using Example 2
    gpio_reset_pin(BLINK_GPIO);
    gpio_set_direction(BLINK_GPIO, GPIO_MODE_OUTPUT);
    ESP_LOGI(TAG, "Idle Hook will toggle GPIO %d", BLINK_GPIO);
    #endif

    // Create a task that will eventually block, allowing idle hook to run more
    xTaskCreate(busy_task, "BusyTask", 2048, NULL, 1, NULL); // Low priority task

    ESP_LOGI(TAG, "App Main: Task created. Idle Hook should run when BusyTask blocks.");
}

Configure GPIO: If using the LED example, set CONFIG_BLINK_GPIO in menuconfig (Example Configuration) or define BLINK_GPIO directly.

Build, Flash, Monitor.

Observe:

  • While the busy_task is running (first 5 seconds), the Idle Hook will likely run less frequently (only when busy_task is briefly blocked by vTaskDelay or preempted by higher priority system tasks).
  • After busy_task enters its long vTaskDelay, it blocks for extended periods. Now, the Idle Task gets much more CPU time.
  • If using the print statement, you’ll see . printed frequently. If using the LED, it should start blinking rapidly (rate depends on idle task loop speed and any delays added). This shows the Idle Hook executing when no other application tasks are ready.

Variant Notes

  • Core Scheduling: As discussed, the primary difference is how scheduling operates on single-core (S2, C3, C6, H2) versus dual-core (ESP32, S3) devices. On dual-core systems, each core schedules independently from the pool of tasks ready to run on that core (considering affinity). On single-core systems, there’s just one scheduler instance managing all tasks.
  • Idle Task: On dual-core systems, each core has its own Idle Task. If CONFIG_USE_IDLE_HOOK is enabled, vApplicationIdleHook will be called by the Idle Task of whichever core becomes idle.
  • Performance: Context switch times and the overhead of the tick interrupt will vary slightly depending on the CPU architecture (Xtensa vs RISC-V) and clock speed, but the scheduling logic remains the same.

Common Mistakes & Troubleshooting Tips

Mistake / Issue Symptom(s) Troubleshooting / Solution
Priority Starvation
High-priority tasks run continuously without blocking.
Lower-priority tasks never run or run very rarely; system unresponsive to inputs handled by them; watchdog timer may reset. Ensure ALL tasks, especially high-priority ones, have a blocking call (vTaskDelay, queue/semaphore wait, etc.) in their loops. Even vTaskDelay(1) yields. Analyze CPU load (vTaskGetRunTimeStats). Re-evaluate if task truly needs to run so constantly.
Assigning All Tasks Same (Medium) Priority Lack of responsiveness for time-sensitive operations; critical events delayed by time-slicing with non-critical tasks. Differentiate priorities based on urgency and timing deadlines. Use higher priorities for tasks needing faster response.
Blocking in Idle Hook (vApplicationIdleHook)
Calling blocking functions or long computations in Idle Hook.
Idle Task stops proper execution; deleted task cleanup may fail; watchdog may trigger; power saving may be impacted. Keep Idle Hook extremely short and non-blocking. For significant work, use Idle Hook to trigger a separate low-priority task (e.g., via semaphore). Use ets_delay_us for microsecond hardware delays if essential.
Misunderstanding Time-Slicing
Assuming it guarantees equal CPU time or occurs between different priorities.
Incorrect assumptions about task execution timing. Time-slicing is only for tasks of the same priority when they are the highest-priority ready tasks. Preemption by higher-priority tasks always takes precedence.
Ignoring Priority Inversion
Not using priority inheritance mutexes for resources shared between different priority tasks.
High-priority tasks occasionally miss deadlines or become unresponsive when accessing shared resources; erratic system behavior under load. Use FreeRTOS Mutexes (which implement priority inheritance) to protect shared resources. Design to minimize resource lock times. (More in Mutex chapter).
Incorrectly Setting configMAX_PRIORITIES
Setting it too low (not enough distinct levels) or unnecessarily high (wastes RAM).
Too low: Cannot differentiate task urgencies effectively. Too high: Wastes a small amount of RAM for scheduler data structures. Choose a value that allows sufficient granularity for your application’s needs. ESP-IDF default (e.g., 25) is often adequate. Adjust in menuconfig if needed.
Assigning Application Tasks to Priority 0 Task will only run when absolutely nothing else (including Idle Task’s own work like cleanup) can run. May lead to unexpected behavior or starvation. Priority 0 (tskIDLE_PRIORITY) is reserved for the Idle Task(s). Application tasks should generally have priorities > 0.

Exercises

  1. Priority Inversion Teaser:
    • Create Task H (Priority 7), Task M (Priority 6), Task L (Priority 5).
    • Task L: Loops, occasionally “acquires” a simulated resource (just sets a global flag resource_held = true;) for 500ms, then “releases” it (resource_held = false;), delaying 100ms between attempts.
    • Task H: Loops, tries to run only if resource_held == false. Prints “Task H running” when it can. Delays 50ms.
    • Task M: Loops, performing “busy work” (e.g., ets_delay_us(5000);) without accessing the resource. Prints “Task M running” occasionally. Delays 70ms.
    • Observe: Task H should run frequently when the resource is free. When Task L holds the resource, Task H blocks. But notice that Task M (medium priority) keeps running, preventing Task L (low priority) from running quickly to release the resource needed by Task H. This simulates priority inversion. (We will solve this later with mutexes).
  2. Time Slice Counter:
    • Create two tasks (Task X, Task Y) at the same priority (e.g., 5).
    • Create two global volatile uint32_t counters, counterX and counterY.
    • Task X: Enters while(1) loop, continuously increments counterX.
    • Task Y: Enters while(1) loop, continuously increments counterY.
    • Create a third, lower-priority task (e.g., Priority 3) that wakes up every 2 seconds (vTaskDelay), prints the current values of counterX and counterY, and then resets both counters to 0.
    • Observe the printed values. They should be roughly similar (though likely not identical due to scheduling details), showing that both tasks got significant CPU time thanks to time-slicing. How do the counts change if you set CONFIG_FREERTOS_HZ to 100 vs 1000?
  3. Idle Hook CPU Load Indicator:
    • Implement the Idle Hook (vApplicationIdleHook, enable CONFIG_USE_IDLE_HOOK).
    • Inside the hook, increment a global volatile uint64_t idle_counter;.
    • Create a task that wakes up every 5 seconds, prints the value of idle_counter, and resets it to 0.
    • Run the system with only this monitoring task. Note the typical idle count per 5 seconds.
    • Now, add another task that performs significant work (e.g., a calculation loop) but also includes vTaskDelay calls. Observe how the reported idle_counter value decreases, indicating less time spent in the Idle state.
  4. Starvation Demonstration:
    • Create Task A (Priority 6) with a while(1) loop that prints “Task A running…” but never calls vTaskDelay or any other blocking function.
    • Create Task B (Priority 5) with a while(1) loop that prints “Task B running…” and calls vTaskDelay(pdMS_TO_TICKS(1000));.
    • Run the code. Observe that Task B’s message never gets printed (or prints only once if Task A yields briefly during startup). Task A starves Task B.
    • Modify Task A to include vTaskDelay(pdMS_TO_TICKS(10)); inside its loop. Observe that Task B now gets a chance to run.

Summary

  • FreeRTOS uses a fixed-priority, preemptive scheduler. Higher-priority tasks interrupt lower-priority tasks immediately when they become ready.
  • Priorities range from 0 (lowest) to configMAX_PRIORITIES - 1 (highest).
  • Time-slicing (round-robin) occurs between tasks of the same priority when they are the highest-priority ready tasks, ensuring they share CPU time based on the RTOS tick (configTICK_RATE_HZ).
  • The Idle Task (priority 0) runs when no other application tasks are ready. It performs cleanup of deleted tasks.
  • The optional Idle Hook (vApplicationIdleHook) allows running short, non-blocking code during idle periods, useful for low-power modes or background checks.
  • Assigning priorities requires considering task urgency and timing requirements to ensure responsiveness and prevent starvation.
  • On multi-core systems, each core schedules independently based on task affinity and priority.
  • Potential issues include starvation, priority inversion (addressed later), and misuse of the Idle Hook.

Further Reading

Leave a Comment

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

Scroll to Top