Introduction to FreeRTOS on ESP32
In this tutorial series, we’ll explore FreeRTOS on ESP32 using ESP-IDF, starting with the basics. FreeRTOS is a real-time operating system that enables multitasking on microcontrollers, allowing you to run multiple tasks concurrently without blocking the CPU.
In this first tutorial of the ESP32 RTOS Series, we’ll cover the key concepts of FreeRTOS, explain why it is useful on the ESP32, and demonstrate a simple LED blink task. By the end of this post, you’ll have a solid understanding of tasks, delays, and the structure of a FreeRTOS project in ESP-IDF.

What is FreeRTOS?
FreeRTOS is a lightweight real-time operating system (RTOS) designed for microcontrollers, including the ESP32. It allows your microcontroller to run multiple independent tasks concurrently without blocking each other.
FreeRTOS schedules tasks according to their priority and timing requirements, ensuring that critical operations are executed on time. This is useful for IoT projects, robotics, and embedded systems, where multiple processes need to run simultaneously, such as reading sensors, controlling actuators, and communicating over Wi-Fi or Bluetooth.
Understanding Real-Time Operating Systems
A real-time operating system (RTOS) is an operating system that guarantees deterministic behavior for executing tasks. This means tasks run predictably within a known time frame, which is crucial for applications where timing matters.
Key features of an RTOS:
- Multitasking: Run multiple tasks “simultaneously” on a single CPU.
- Task Priorities: Assign priorities to tasks so critical operations run first.
- Time Management: Control task execution with precise delays and timers.
- Resource Sharing: Use semaphores and mutexes to prevent conflicts between tasks.
Example analogy:
Imagine your ESP32 is like a small office. A normal program is like a single worker handling one task at a time. FreeRTOS allows multiple workers (tasks) to work concurrently, coordinating efficiently so that the most important tasks get done first.
How FreeRTOS Works on Microcontrollers
On microcontrollers like the ESP32, FreeRTOS functions by scheduling tasks on the CPU. Even though the ESP32 has only a few cores, FreeRTOS makes it appear as if multiple tasks are running at the same time.
How it works in simple terms:
- Tasks are defined as independent functions.
- FreeRTOS maintains a task list with their states (running, ready, blocked).
- A scheduler decides which task runs at any moment based on priority and timing.
- Delays and waiting mechanisms allow tasks to pause without blocking the CPU, letting other tasks run.
On the ESP32, FreeRTOS is already integrated into ESP-IDF, so when you create tasks in your app_main()
, FreeRTOS automatically manages scheduling and multitasking. This allows you to focus on writing functional tasks without worrying about low-level timing management.
Why Use FreeRTOS on ESP32?
The ESP32 is a powerful microcontroller with built-in Wi-Fi and Bluetooth, making it ideal for IoT and embedded applications. However, as your projects grow more complex, using the traditional “super loop” approach—where everything runs sequentially inside a while(1)
loop—can quickly become messy and difficult to manage.
This is where FreeRTOS comes in. It provides a structured and efficient way to handle multiple tasks simultaneously, ensuring each part of your application runs smoothly without interfering with others.
Advantages for IoT and Embedded Projects
FreeRTOS brings several key advantages that make it ideal for real-world IoT and embedded systems:
- True Multitasking
- Run multiple tasks in parallel, such as reading sensors, sending data to the cloud, and updating displays—all without blocking each other.
- Better Code Organization
- Separate different functionalities into independent tasks instead of putting everything inside a single loop. This makes your code modular, easier to read, and simpler to maintain.
- Precise Timing Control
- Use
vTaskDelay()
or timers to control the timing of tasks accurately, without relying on busy loops ordelay()
functions that block other operations.
- Use
- Improved Responsiveness
- High-priority tasks (e.g., safety checks, interrupts, or critical sensors) can run immediately, while lower-priority tasks wait, ensuring time-sensitive operations are never delayed.
- Scalability
- As your project grows, adding new features becomes easier. You can simply create new tasks and adjust priorities, without rewriting existing code logic.
These benefits make FreeRTOS especially useful for IoT devices, where multiple peripherals, sensors, and communication protocols must work together seamlessly.
Built-in FreeRTOS Support in ESP-IDF
One of the biggest advantages of using ESP32 is that FreeRTOS comes pre-integrated with ESP-IDF, the official development framework from Espressif.
- There’s no need to download or configure FreeRTOS separately—it’s part of the core system.
- When you create tasks using
xTaskCreate()
in your ESP-IDF project, they are automatically scheduled and managed by FreeRTOS. - ESP-IDF also provides additional FreeRTOS APIs and debugging tools to monitor task performance, stack usage, and system behavior.
This built-in support means you can focus directly on application logic – defining tasks, setting priorities, and leveraging queues or semaphores – without worrying about integrating a third-party RTOS.
Key Concepts of FreeRTOS
Before we start coding with FreeRTOS on the ESP32, it’s important to understand a few fundamental concepts. These form the core of how FreeRTOS manages multiple operations efficiently in real-time.
Tasks — Creating and Scheduling
In FreeRTOS, tasks are like independent functions that run in parallel, each performing a specific job. For example, you might have one task reading sensor data, another sending data to the cloud, and another controlling an LED.
You create a task using the xTaskCreate()
or xTaskCreatePinnedToCore()
function in ESP-IDF. Each task has:
- A priority, which determines when and how often it runs compared to other tasks.
- A stack size, which defines how much memory it uses.
- A task function, where you define the task’s behaviour.
FreeRTOS uses a preemptive scheduler to decide which task runs at any moment. The task with the highest priority is always given CPU time. If two tasks have the same priority, the scheduler will switch between them in a round-robin fashion.
This multitasking mechanism allows your ESP32 to handle multiple operations without blocking, making your applications smoother and more responsive.
Delays — vTaskDelay and vTaskDelayUntil
In a regular Arduino-style loop, using delay()
blocks the CPU completely, stopping all other code from running. In FreeRTOS, this problem is solved using vTaskDelay()
and vTaskDelayUntil()
.
vTaskDelay(ticks)
Suspends the task for a specific number of ticks (usually 1 tick = 1 ms on ESP32). While this task is waiting, other tasks are free to run.vTaskDelayUntil()
Useful when you want tasks to run at fixed, periodic intervals, regardless of when the delay was called. This helps maintain precise timing, especially in applications like sensor sampling or real-time control loops.
Queues — Task Communication
When multiple tasks run simultaneously, they often need to share data. FreeRTOS provides queues as a safe and efficient way to pass data between tasks.
For example, you can have:
- A sensor task pushing new readings into a queue.
- A communication task reading those values from the queue and sending them to the cloud.
Queues act like first-in-first-out (FIFO) buffers. They ensure data is passed in order and prevent race conditions, that can occur when tasks access the shared variables directly.
In ESP-IDF, you can create a queue using xQueueCreate()
, and then use xQueueSend()
or xQueueReceive()
to transfer data. Queues also support blocking, so a task can wait until new data arrives.
Semaphores & Mutexes — Resource Management
When multiple tasks need to use a shared resource (like a UART port, an I²C bus, or even a shared variable), there’s a risk of conflicts and data corruption. FreeRTOS provides semaphores and mutexes to manage this.
- Binary Semaphores
Work like a signaling mechanism. One task can “give” the semaphore, and another can “take” it. This is useful for synchronizing tasks or signaling that an event has occurred (like an ISR notifying a task). - Mutexes (Mutual Exclusion)
Used to protect shared resources. Only one task can hold the mutex at a time. If another task tries to take it, it will wait until the mutex is released.
Using semaphores and mutexes ensures that shared resources are accessed safely, preventing data corruption and ensuring smooth multitasking on the ESP32.
Understanding Tasks in FreeRTOS
In FreeRTOS, a task is a small, independent program that runs inside your main application. Each task performs a specific job — for example, blinking an LED, reading a sensor, or sending data over Wi-Fi.
FreeRTOS allows the ESP32 to handle multiple tasks at once. This is called multitasking. Instead of writing one long loop with delays, you split your code into several smaller tasks that run together without blocking each other.
Every task in FreeRTOS has its own stack, priority, and state. The scheduler manages these tasks and decides which one runs at any given time. It switches between tasks so quickly that it looks like they’re all running simultaneously.

Here’s how you can think of it:
- Each task is like a worker focusing on one job.
- The scheduler is the manager who decides which worker should act next.
- Tasks don’t interfere with each other; they just wait for their turn to use the CPU.
By dividing your application into multiple tasks, you make it more organized, responsive, and real-time capable.
Creating a FreeRTOS Task in ESP32
In FreeRTOS on ESP-IDF, a task is a small, independent function that runs under the FreeRTOS scheduler. Each task performs one job — for example, blinking an LED, reading a sensor, or sending data over Wi-Fi.
ESP-IDF runs FreeRTOS by default, so you can use all its APIs directly. Creating multiple tasks helps you organize your code, avoid delays, and make your ESP32 project responsive and scalable.
The xTaskCreate() Function
The main API to create a task is xTaskCreate()
. It registers your task with the FreeRTOS kernel and sets parameters such as stack size, priority, and task handle.
Here’s the syntax:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // Task function
const char * const pcName, // Task name (for debugging)
const uint32_t usStackDepth, // Stack size in words
void *pvParameters, // Optional parameters
UBaseType_t uxPriority, // Task priority
TaskHandle_t *pvCreatedTask // Task handle (optional)
);
If the function returns pdPASS
, the task was created successfully. Otherwise, if the system runs out of heap memory, the function will fail to create the task.
Example – Two LED Tasks with Different Blink Rates
Let’s create two FreeRTOS tasks to blink two LEDs at different speeds. This shows true multitasking on ESP32 using the ESP-IDF framework.
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#define LED1 GPIO_NUM_2
#define LED2 GPIO_NUM_4
// ----- Task 1: Blink LED1 every 500 ms -----
void blink_task_1(void *pvParameters)
{
gpio_reset_pin(LED1);
gpio_set_direction(LED1, GPIO_MODE_OUTPUT);
while (1)
{
gpio_set_level(LED1, 1); // Turn LED ON
vTaskDelay(pdMS_TO_TICKS(500)); // Wait 500 ms
gpio_set_level(LED1, 0); // Turn LED OFF
vTaskDelay(pdMS_TO_TICKS(500)); // Wait 500 ms
}
}
// ----- Task 2: Blink LED2 every 200 ms -----
void blink_task_2(void *pvParameters)
{
gpio_reset_pin(LED2);
gpio_set_direction(LED2, GPIO_MODE_OUTPUT);
while (1)
{
gpio_set_level(LED2, 1); // Turn LED ON
vTaskDelay(pdMS_TO_TICKS(200)); // Wait 200 ms
gpio_set_level(LED2, 0); // Turn LED OFF
vTaskDelay(pdMS_TO_TICKS(200)); // Wait 200 ms
}
}
// ----- app_main: Entry point in ESP-IDF -----
void app_main(void)
{
printf("Starting FreeRTOS tasks...\n");
// Create both tasks
xTaskCreate(blink_task_1, "Blink1", 2048, NULL, 1, NULL);
xTaskCreate(blink_task_2, "Blink2", 2048, NULL, 1, NULL);
}
Code Breakdown
1. GPIO Setup
Each task begins by configuring its GPIO pin:
gpio_reset_pin(LED1);
gpio_set_direction(LED1, GPIO_MODE_OUTPUT);
This ensures the LED pin is ready for output. Doing this inside the task keeps each task self-contained and modular.
2. Infinite Loop for Task Operation
The while(1)
loop ensures each task runs forever. Tasks never return — instead, they sleep using vTaskDelay()
to give CPU time to other tasks.
while (1) {
gpio_set_level(LED1, 1);
vTaskDelay(pdMS_TO_TICKS(500));
gpio_set_level(LED1, 0);
vTaskDelay(pdMS_TO_TICKS(500));
}
Each delay converts milliseconds to RTOS ticks with pdMS_TO_TICKS()
. This keeps timing accurate even if the tick rate changes in FreeRTOSConfig.h
.
3. Non-Blocking Delays
vTaskDelay()
is a non-blocking delay. Unlike delay()
in the MCU, it doesn’t freeze the CPU — it simply tells the scheduler, “Pause this task for X ticks and let others run.” This is the foundation of multitasking in FreeRTOS.
4. Task Creation in app_main()
All FreeRTOS tasks are started inside app_main()
.
xTaskCreate(blink_task_1, "Blink1", 2048, NULL, 1, NULL);
xTaskCreate(blink_task_2, "Blink2", 2048, NULL, 1, NULL);
Each task receives:
- Function name – pointer to the task code
- Readable name – useful for debugging
- Stack size – 2048 words (~8 KB)
- Parameters – none in this case
- Priority – both are priority 1 (same level)
- Handle – set to NULL since we don’t need to control the task later
When both tasks have the same priority, FreeRTOS time-slices the CPU between them, making both LEDs blink independently and smoothly.
How It Works
The FreeRTOS scheduler alternates CPU time between blink_task_1
and blink_task_2
. When one task calls vTaskDelay()
, it moves to the blocked state, and the scheduler immediately switches to the other task.
This rapid switching gives the impression of parallel execution — a hallmark of real-time multitasking on the ESP32.
Execution Results – FreeRTOS Task Output
The GIF below shows the two FreeRTOS tasks running concurrently on the ESP32.
Each LED blinks at a different rate — one slow and one fast — demonstrating how the FreeRTOS scheduler efficiently switches between tasks without blocking the CPU.
Understanding the FreeRTOS Scheduler
The FreeRTOS Scheduler is the part of the system that decides which task runs and when. It manages all active tasks and switches between them efficiently. This makes your ESP32 run multiple operations as if they were happening at the same time.
Task States
Each task in FreeRTOS can be in one of several states:
- Running: The task currently using the CPU.
- Ready: The task is waiting for CPU time.
- Blocked: The task is waiting for a delay or an event (like a semaphore).
- Suspended: The task is paused until resumed.
- Deleted: The task is removed from memory.
The scheduler constantly checks these states. It only runs tasks that are in the Ready state.
Context Switching
The scheduler performs a context switch whenever it changes which task is running. During a context switch, FreeRTOS saves the current task’s data and loads the next task’s data from memory. This process happens very quickly — usually every few milliseconds.
Context switching allows multiple tasks to share the same CPU without interfering with each other. It’s what makes FreeRTOS feel truly multitasking.
Preemptive Scheduling
FreeRTOS uses preemptive scheduling by default. This means a higher-priority task can interrupt a lower-priority one at any time. The scheduler instantly switches control to the more important task.
This behaviour ensures that time-critical tasks — like reading sensors or handling Wi-Fi packets — always get CPU time first. Lower-priority tasks resume automatically once the high-priority work is done.
Task Priorities
Every task has a priority level. Higher priority means higher importance. The scheduler always runs the highest-priority ready task.
For example:
- A Wi-Fi task may have priority
3
. - A blink task may have priority
1
.
If both are ready, the scheduler gives CPU time to the Wi-Fi task first. When the Wi-Fi task delays or blocks, the blink task runs.
You can adjust task priorities with vTaskPrioritySet()
in ESP-IDF.
Visualizing the Scheduler
The diagram below shows how the FreeRTOS Scheduler switches between multiple tasks on the ESP32.
Task Priority and Stack Size
In FreeRTOS, every task has two important parameters — priority and stack size. These determine how often a task runs and how much memory it uses. Understanding both helps you create stable, efficient ESP32 applications.
Task Priority
Priority defines how important a task is to the system. FreeRTOS uses integer values for priorities — a higher number means higher importance. By default, priority 0
is the lowest, and there’s no upper limit (though ESP-IDF usually supports up to 25 levels).
The scheduler always runs the highest-priority task that’s ready to execute. If two tasks share the same priority, the scheduler switches between them in round-robin fashion.
Example
- Task A (priority 2): Reads sensor data.
- Task B (priority 1): Blinks an LED.
If both are ready, Task A runs first. When Task A blocks or delays, Task B runs next.
Setting the right priorities ensures that critical tasks get CPU time when needed, while background tasks run only when the system is free.
Stack Size
Each task also needs stack memory. This is the private space a task uses to store variables, return addresses, and function calls. If the stack is too small, your program may crash or behave unpredictably.
You define stack size in words, not bytes, when creating a task with xTaskCreate()
. For ESP32 (32-bit architecture), one word = 4 bytes.
Example
xTaskCreate(taskFunction, "TaskA", 2048, NULL, 2, NULL);
Here, the stack size is 2048 words (≈8 KB). This is usually enough for most small tasks, but tasks with heavy computation or large local variables may need more.
You can monitor stack usage with:
uxTaskGetStackHighWaterMark(taskHandle);
This helps you detect if a task is close to running out of stack space.
Conclusion
In this tutorial, we learned the core concepts of FreeRTOS on the ESP32. You now understand what tasks are and how they let your program handle multiple operations at once. We also explored how the FreeRTOS scheduler manages these tasks, switching between them efficiently through context switching and preemptive scheduling.
We learned how task priority affects which task runs first, and how stack size ensures each task has enough memory to operate safely. With this knowledge, you can now create stable, multitasking ESP32 applications using FreeRTOS.
This was your first step into real-time multitasking. You’ve set the foundation for building more complex and responsive systems.
Next Tutorial:
In the next tutorial, we’ll explore how the FreeRTOS scheduler actually decides which task runs — and how you can tune priorities for real-time performance on the ESP32.
Browse More ESP32 Tutorials
ESP32 SPI Tutorial Part 2: Read Data from Slave
ESP32 WiFi Station Mode with ThingSpeak Data Logging Using Espressif IDE
ESP32 SPI LCD with Graphics using LVGL
ESP32 SPI Tutorial Part 1: Configure & Transmit
ESP32 UART Tutorial with ESP-IDF – TX/RX, Pin Setup & Code Example
ESP32 MQTT Client: Publish and Subscribe to Topics Using Espressif IDE
Interface SPI LCD with ESP32 – Step-by-Step Guide
ESP32 FreeRTOS Project Download
Info
You can help with the development by DONATING Below.
To download the project, click the DOWNLOAD button.
FAQs – FreeRTOS Basics on ESP32
There’s no strict limit to the number of tasks you can create. The actual limit depends on available RAM and each task’s stack size. Since each task consumes stack memory, keeping tasks lightweight and using shared queues or semaphores helps manage memory efficiently. Most ESP32 projects run comfortably with 5–20 tasks.
No, FreeRTOS is part of the ESP-IDF by default and is optimized for the ESP32 architecture. The scheduler adds minimal overhead — just a few microseconds per context switch. If tasks yield properly and avoid blocking calls, the system remains responsive and efficient.
Yes. FreeRTOS is a soft real-time operating system, which means it can meet most timing requirements for embedded applications. You can use task priorities, vTaskDelayUntil(), or hardware timers to ensure critical code runs with consistent timing. For ultra-precise operations, consider using interrupts alongside tasks.
ESP-IDF includes several debugging tools such as System View, FreeRTOS Trace, and runtime functions like uxTaskGetSystemState()
and vTaskList()
. These tools help you analyze CPU load, stack usage, and task states in real time — essential for optimizing multitasking applications.
If a task overflows its stack, the system can crash or behave unpredictably. To prevent this, enable stack overflow checking in menuconfig
and monitor stack usage using uxTaskGetStackHighWaterMark()
. It’s good practice to allocate extra stack space for tasks that use large local variables or functions like printf()
.
Search This Site
Subscribe
