HomeESP32 TutorialsFreeRTOS on ESP32 (PART 2): Understanding the Scheduler and Task Management

Understanding the FreeRTOS Scheduler on ESP32 using ESP-IDF

This is Part 2 of our ESP32 FreeRTOS tutorial series using ESP-IDF. In the previous part, we learned how to create and manage multiple tasks in FreeRTOS.
We saw how each task runs independently and how priorities affect task execution.

Now, in this part, we will understand the FreeRTOS Scheduler — the core feature that controls how these tasks actually run. The scheduler decides which task should use the CPU and for how long. It handles task switching, manages priorities, and ensures smooth multitasking on the ESP32’s dual-core processor.

We will explore:

  • How the FreeRTOS scheduler works in ESP32.
  • Different task states and their transitions.
  • What context switching means and how it happens.
  • The difference between preemptive and cooperative scheduling.
  • And how time slicing works on ESP32’s two cores.

After this tutorial, you will have a clear idea of how the ESP32 FreeRTOS scheduler keeps multiple tasks running efficiently in the background.

Understanding the FreeRTOS Scheduler on ESP32 using ESP-IDF

What is the FreeRTOS Scheduler in ESP32?

The FreeRTOS Scheduler is the core part of the FreeRTOS operating system. It manages all tasks created in your program and decides which one should run at any given moment. In simple words, the scheduler acts as a task manager for the ESP32.

Each task in FreeRTOS gets a specific priority and its own share of CPU time. The scheduler continuously checks which tasks are ready to run and which are waiting. Then, based on their priority and system timing, it selects the next task to execute.

Without the scheduler, all tasks would try to run at once, and the CPU would not know which one to handle first. So, the scheduler brings order and control to multitasking on the ESP32.

Role of the Scheduler in FreeRTOS

The main role of the scheduler is to control the execution flow of all tasks in the system. It ensures that the highest priority ready task always gets CPU time. If a new task with higher priority becomes ready, the scheduler immediately switches to it. This process is called context switching.

The scheduler also handles task delays, blocking, and suspension. When a task waits for a delay or an event, the scheduler pauses it and moves it to the appropriate state. At the same time, it gives the CPU to another task that is ready to run.

In short, the scheduler:

  • Keeps track of all active and waiting tasks.
  • Selects the next task based on priority and state.
  • Handles switching between tasks smoothly.
  • Maximizes CPU utilization without user intervention.

On the ESP32, the scheduler runs on both cores, allowing true parallel task execution. This is what makes the ESP32 FreeRTOS system powerful for real-time applications.

Diagram showing FreeRTOS Scheduler selecting ready tasks and dispatching them to Core 0 and Core 1 on ESP32, with Ready, Blocked, and Suspended task states and a tick timer

Why Task Scheduling is Important in ESP32 Projects

Task scheduling is crucial in any multitasking system, especially on the ESP32. Most embedded projects involve several operations running together — such as sensor reading, Wi-Fi communication, and data display. If all these operations run in a single loop, the system becomes slow and unresponsive.

By using the FreeRTOS scheduler, you can assign each task a specific role and priority. For example, Wi-Fi handling can run at a higher priority, while LED blinking can stay at a lower one. This ensures critical tasks always get CPU time, even when the system is busy.

Proper scheduling also improves real-time performance, reduces latency, and saves power. When no tasks are ready, the scheduler puts the CPU in an idle state, helping conserve energy — an important factor for IoT devices.

Understanding FreeRTOS Task States on ESP32

FreeRTOS tasks move through different states during execution. Understanding these task states helps you design efficient ESP32 applications. Each task can be in ready, running, blocked, or suspended state at any time. The scheduler uses these states to decide which task should execute next.

Ready State

A task is in the ready state when it is prepared to run but is waiting for the CPU. All tasks start in this state after creation. The scheduler keeps track of ready tasks and selects one based on priority. If multiple tasks have the same priority, time slicing allows each task to run for a short period in turn.


Running State

A task enters the running state when the scheduler assigns it CPU time. Only one task per core can run at a time.

While running, the task executes its code until:

  • Its time slice ends, or
  • A higher-priority task becomes ready, or
  • It blocks or suspends itself.

On the ESP32, with two cores, one task can run on each core simultaneously. This allows true parallel execution, improving performance for multitasking applications.


Blocked and Suspended States

A task moves to the blocked state when it waits for an event or a delay to complete. For example, a task can block while waiting for a queue message, semaphore, or timer. Blocked tasks do not consume CPU time.

The suspended state is similar, but the task stays inactive until explicitly resumed. You can suspend a task using vTaskSuspend() and resume it with vTaskResume(). These states help manage tasks efficiently and prevent CPU wastage.


Transition Between Task States

Tasks often move between states during execution.
For example:

  • A ready task becomes running when the scheduler selects it.
  • A running task moves to blocked if it waits for a resource.
  • A suspended task becomes ready when resumed.
Diagram showing FreeRTOS task state transitions on ESP32. Arrows indicate Ready to Running when scheduler selects a task, Running to Blocked when waiting for a resource, and Suspended to Ready when resumed.

Understanding these transitions helps you design responsive and reliable ESP32 FreeRTOS systems. It ensures that high-priority tasks get CPU time when needed, while low-priority tasks wait efficiently.

Context Switching in FreeRTOS (ESP-IDF)

In FreeRTOS, multiple tasks share the CPU. The scheduler manages when each task runs and when it pauses. The act of switching the CPU’s execution from one task to another is called context switching. It’s a core mechanism that enables multitasking on the ESP32.

What is Context Switching?

Context switching is the process of:

  1. Saving the current state (context) of a running task, and
  2. Restoring the previously saved context of the next task to run.

This “context” typically includes:

  • CPU registers (general-purpose and special-purpose)
  • Stack pointer (SP)
  • Program counter (PC)
  • Processor status registers
  • Floating-point registers (if used)

When the scheduler switches tasks, it ensures that each task can resume exactly where it left off, as if it had never been paused.

Without context switching, multiple tasks would overwrite each other’s CPU state, causing data corruption and system crashes.


How FreeRTOS Saves and Restores Task Context

FreeRTOS automates the entire context switch process.

Here’s what happens step by step:

  1. Tick interrupt or event triggers the scheduler (for example, a timer tick or a task becoming ready).
  2. The current task’s context (registers, stack pointer, program counter) is pushed onto its stack.
  3. The scheduler selects the next ready task based on priority and state.
  4. The next task’s context is popped from its stack and restored to the CPU registers.
  5. The CPU resumes execution from that task’s saved program counter.

On the ESP32, which has dual cores (Core 0 and Core 1), FreeRTOS maintains separate schedulers — one per core.
Each core can independently switch tasks, and tasks are pinned to a specific core unless explicitly created as pinned-to-core = tskNO_AFFINITY.


When Does Context Switching Occur?

FreeRTOS performs context switches in several cases:

  • Preemption: A higher-priority task becomes ready to run.
  • Time slicing: Two tasks of equal priority share CPU time in round-robin fashion.
  • Blocking: A task waits for an event, delay, or semaphore.
  • ISR yield: An interrupt handler unblocks a task with higher priority.

Context Switching Overhead on ESP32

Context switching is not “free” — it consumes CPU cycles for saving/restoring registers and updating stacks.

  • Each switch introduces a few microseconds of latency (depending on clock frequency).
  • Very frequent switches can cause performance degradation due to overhead.
  • However, FreeRTOS is highly optimized to keep this minimal — especially with ESP-IDF’s efficient ISR and tick handling.

Tips to optimize performance:

  • Keep task code short and efficient.
  • Avoid unnecessary blocking (like long vTaskDelay() calls).
  • Use appropriate priorities to avoid excessive preemption.
  • Consider pinning critical tasks to a specific core to reduce scheduler contention.

Example Timeline of Context Switching

Let’s say you have three tasks:

TaskPriorityDescription
Task A2Sensor Reading
Task B1UART Communication
Task C3Control Loop

Example Scenario:

  1. Task C (priority 3) runs first.
  2. It blocks while waiting for an event (like sensor data ready).
  3. Task A (priority 2) becomes the highest ready task → scheduler switches context.
  4. After some ticks, Task C unblocks and preempts Task A.
  5. Scheduler performs another context switch back to Task C.
Context switching using different triggers in FreeRTOS on ESP32.

This diagram shows how FreeRTOS manages context switching on the ESP32. Each coloured bar represents a task running on Core 0 or Core 1. The vertical dashed lines marking represents the context switch points triggered by tick interrupts, higher-priority tasks, or task blocking. The inset box illustrates the task’s saved CPU state (PC, SP, Registers) during a switch.

Preemptive vs Cooperative Scheduling in FreeRTOS

FreeRTOS supports two main scheduling methods: preemptive and cooperative. These methods determine how the scheduler decides which task runs next. Understanding the difference is essential for building efficient ESP32 multitasking applications.

How Preemptive Scheduling Works in ESP32

In preemptive scheduling, the scheduler can interrupt a running task. It immediately switches to a higher-priority task if one becomes ready. This ensures that critical tasks always run on time.

On the ESP32, preemptive scheduling works on both cores. Even if a low-priority task is running, the scheduler can preempt it for a higher-priority task. This is the default scheduling method in FreeRTOS and is ideal for real-time applications.


When to Use Cooperative Scheduling

In cooperative scheduling, a task runs until it yields control voluntarily. The scheduler does not force a running task to stop. Tasks must call functions like taskYIELD() or wait for delays/events to let other tasks run.

Cooperative scheduling is simpler and reduces context-switching overhead. It is suitable for applications where tasks have predictable execution times and timing is not critical. However, one long-running task can block others if it does not yield.

FeaturePreemptive SchedulingCooperative Scheduling
Task SwitchingScheduler can interrupt a running task anytimeTask runs until it yields control voluntarily
ResponsivenessHigh — critical tasks get CPU immediatelyModerate — depends on tasks yielding in time
Context SwitchingMore frequent; slightly higher CPU overheadLess frequent; lower CPU overhead
ComplexitySlightly more complex to manageSimpler and easier to implement
Use CaseReal-time applications where timing is criticalSimple, predictable systems without strict timing
ESP32 Dual-Core SupportWorks efficiently on both coresWorks on both cores but relies on tasks to yield
Risk of Task BlockingLow — high-priority tasks preempt lower onesHigh — one long-running task can block others

Choosing the Right Scheduling Mode for Your Application

Choosing between preemptive and cooperative scheduling depends on your ESP32 project:

  • Use preemptive scheduling when tasks have critical timing requirements.
  • Use cooperative scheduling for simpler systems with predictable task execution.
  • Consider task priorities, CPU usage, and real-time constraints when deciding.
  • For most ESP32 FreeRTOS applications, preemptive scheduling provides better responsiveness.

Time Slicing on ESP32 Dual Core

FreeRTOS allows multiple tasks of the same priority to share CPU time fairly. This process is called time slicing. Time slicing ensures that no single task hogs the CPU, improving ESP32 multitasking performance.

How Time Slicing Works in FreeRTOS

FreeRTOS divides CPU time into small, regular intervals called ticks. Each tick is generated by a hardware timer interrupt, typically at a frequency defined by the configuration constant:

#define configTICK_RATE_HZ 1000  // 1ms per tick

When multiple tasks have the same priority, the scheduler:

  1. Chooses one task to run for a tick interval.
  2. After a tick interrupt, if time slicing is enabled (configUSE_TIME_SLICING = 1), it moves to the next ready task of the same priority.
  3. This continues in a round-robin manner, ensuring all tasks get equal CPU time.

This happens automatically when preemption is enabled (configUSE_PREEMPTION = 1).

Example: Equal Priority Tasks

Let’s assume you have three tasks:

TaskPriorityFunction
Task A2Handle network packets
Task B2Process sensor data
Task C2Update display

Note that All tasks have priority 2.

Execution pattern (single core):

Tick 0 → Task A
Tick 1 → Task B
Tick 2 → Task C
Tick 3 → Task A
Tick 4 → Task B
...

Even though they have equal priority, each one gets a fair time slot to execute. This ensures responsiveness and fairness in CPU usage.


Task Execution Across Both Cores (Core 0 and Core 1)

The ESP32 has two CPU cores. FreeRTOS can run one task on each core at the same time. You can assign tasks to specific cores using xTaskCreatePinnedToCore().

Tasks on separate cores run independently, but the scheduler still handles priority and time slicing on each core. This enables true parallel execution and improves the performance of multitasking ESP32 applications.

Example:

TaskPriorityPinned Core
Task A2Core 0
Task B2Core 1
Task C2Core 0
Task D1Core 1

In this setup:

  • Core 0 slices between Task A and Task C (equal priority = 2).
  • Core 1 runs Task B (priority 2) and Task D (priority 1), where Task B preempts Task D due to higher priority.

This allows true parallelism — Core 0 and Core 1 execute different tasks simultaneously.


Setting Tick Rate and Its Impact on Task Timing

The tick rate determines how often the scheduler runs. It is defined in FreeRTOSConfig.h using configTICK_RATE_HZ.

A higher tick rate means tasks switch more frequently. This improves responsiveness but increases context switching overhead. A lower tick rate reduces overhead but may cause slower task reaction times.

Tick RateTick DurationTypical Use CaseImpact
1000 Hz1 msHigh responsiveness (e.g., control loops, fast I/O)More context switches, higher CPU overhead
100 Hz10 msGeneral applicationsBalanced performance
10 Hz100 msLow power, slow systemsLower overhead, slower responsiveness

The diagram shows how FreeRTOS schedules tasks of the same priority across the ESP32’s dual cores.

FreeRTOS time slicing on the ESP32 dual-core processor

Each colored block represents a task executing on a core. Vertical dashed lines indicate tick interrupts, where the scheduler performs time slicing. Core 0 and Core 1 run their tasks independently, allowing true parallel multitasking.

How the FreeRTOS Scheduler Runs on ESP32

The FreeRTOS scheduler runs continuously in the background. It uses tick interrupts to check task states and manage execution. The scheduler decides which task should run based on priority, state, and core assignment.

Tick Interrupt and Task Switching

A tick interrupt occurs at regular intervals defined by configTICK_RATE_HZ. Each tick allows the scheduler to update task states and perform context switching if needed. If a higher-priority task becomes ready, the scheduler preempts the running task immediately. This ensures that critical tasks get CPU time on schedule.


Priority-Based Scheduling on Dual Cores

The ESP32 has two cores, each capable of running tasks independently. FreeRTOS uses priority-based scheduling on each core. You can assign tasks to a specific core using xTaskCreatePinnedToCore(). The scheduler checks ready tasks on both cores and executes the highest-priority task on each core. This allows efficient multitasking and better CPU utilization.


Real Example of Scheduler Behavior

Imagine you have three tasks:

  1. Task A – high priority (sensor reading)
  2. Task B – medium priority (Wi-Fi communication)
  3. Task C – low priority (LED blinking)

When Task A becomes ready, the scheduler immediately preempts Task B or C. Task B runs only when Task A is blocked or delayed. Task C runs last, during idle CPU time.

Timeline showing FreeRTOS scheduler preempting a medium-priority task with a high-priority task; low-priority task runs only in idle time.

This diagram illustrates how the FreeRTOS scheduler manages tasks of different priorities on a single core. Task B (medium priority) starts running first. When Task A (high priority) becomes ready at tick 2, it immediately preempts Task B. Task A runs until it blocks, after which Task B resumes. Task C (low priority) runs only during CPU idle time when higher-priority tasks are not ready.

The vertical dashed lines represent system ticks, showing the timing of task switches. This visualization helps understand how FreeRTOS ensures high-priority tasks get CPU time immediately while lower-priority tasks run opportunistically.

Debugging and Monitoring the FreeRTOS Scheduler

Efficient multitasking on the ESP32 depends on the scheduler running smoothly. Monitoring and debugging the scheduler is crucial to ensure tasks execute correctly, system resources are sufficient, and your application remains responsive.

Without proper monitoring, issues such as timing glitches, CPU overload, or stack overflows may go unnoticed, potentially causing crashes or unpredictable behaviour.

Using FreeRTOS Trace or ESP-IDF System View

FreeRTOS Trace and ESP-IDF SystemView are powerful tools to visualize task execution:

  • FreeRTOS Trace:
    • Records events like task creation, deletion, and state changes.
    • Shows context switches and time spent in each task.
    • Helps identify bottlenecks where tasks consume excessive CPU time.
  • ESP-IDF SystemView:
    • Provides a real-time timeline of task execution on the ESP32.
    • Displays preemption events, interrupts, and tick timing.
    • Allows detailed inspection of both cores on dual-core ESP32s.

Practical Tip: Visualizing the task timeline can reveal tasks that hog the CPU, block others unnecessarily, or are starved due to priority issues.


Checking Task Runtime and Stack Usage

FreeRTOS offers built-in functions to monitor task health:

  • Stack usage: uxTaskGetStackHighWaterMark(TaskHandle_t xTask);
    • Returns the minimum remaining stack space.
    • Helps detect tasks at risk of stack overflow.
  • Task runtime stats: vTaskGetRunTimeStats(char *pcWriteBuffer);
    • Shows CPU time consumed by each task.
    • Useful for balancing task execution and identifying heavy CPU consumers.

Example: If a sensor reading task consumes 80% of CPU time while a display task gets 5%, you may need to adjust priorities or optimize code to maintain responsiveness.


Tips to Optimize Task Scheduling Performance

  1. Keep tasks short and efficient
    • Avoid long loops or delays inside a task.
    • Break long operations into smaller chunks to allow other tasks to run.
  2. Assign appropriate priorities
    • Critical tasks like sensor reading or safety checks → high priority.
    • Non-critical tasks like logging or LED updates → lower priority.
  3. Use time slicing wisely
    • For tasks of the same priority, ensure that frequent context switching does not introduce excessive overhead.
  4. Avoid long blocking operations without yielding
    • Use vTaskDelay(), vTaskDelayUntil(), or wait on semaphores.
    • Blocking without yielding prevents other tasks from running, causing system sluggishness.
  5. Adjust tick rate carefully
    • Higher configTICK_RATE_HZ → better responsiveness but more context-switch overhead.
    • Lower tick rate → less overhead but slower reaction times.
  6. Regularly monitor with SystemView/Trace
    • Detect subtle timing issues early.
    • Ensure tasks are executing within their expected time budgets.

Practical Example

Imagine a project with:

  • Task A: Reads sensors every 10 ms (high priority).
  • Task B: Sends Wi-Fi data (medium priority).
  • Task C: Updates LEDs (low priority).

Monitoring with SystemView shows:

  • Task A preempts B and C correctly.
  • Task B sometimes blocks too long → Wi-Fi packets delayed.
  • Task C executes during idle CPU time.

From this, you might optimize Task B to break long operations into smaller chunks or adjust priority to improve performance.

Timeline of FreeRTOS tasks on ESP32 showing execution blocks, CPU usage, stack warnings, and preemption events.

This diagram illustrates how FreeRTOS schedules tasks of different priorities on the ESP32 and how monitoring tools help identify performance issues. Each lane represents a task: Task A (high priority, sensor reading), Task B (medium priority, Wi-Fi communication), and Task C (low priority, LED updates). Coloured blocks show when each task is running.
The CPU usage bars indicate the percentage of processor time consumed by each task, while the stack warning icon highlights tasks at risk of stack overflow. The arrow shows a preemption event where Task A interrupts Task B. Monitoring task execution, CPU usage, and stack levels helps optimize system performance and prevent crashes.

Conclusion

In this tutorial, we explored the FreeRTOS scheduler on ESP32 using ESP-IDF. We covered task states, context switching, preemptive vs cooperative scheduling, and time slicing on dual cores.

Understanding the scheduler helps you design efficient and responsive ESP32 applications. You can manage task priorities, optimize CPU usage, and ensure critical tasks run on time.

In the next part, we will learn inter-task communication using queues, semaphores, and event groups to safely share data between tasks.

Browse More ESP32 Tutorials

1 2

FAQs – FreeRTOS Basics on ESP32

Subscribe
Notify of

0 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments