- DIY
- A
ESP32 programming with ESP-IDF in the PlatformIO environment #0
Hello tekkix! Not long ago, I got my hands on an ESP32 board. Previously, I had worked with the ESP8266 and even created a simple web application on it in Station mode. I did all of that in Arduino IDE and was happy to find an extension that helped me organize my project – PlatformIO. It was in PlatformIO that I first discovered the ESP-IDF framework and started to gradually dive deeper into this topic.
This article opens a cycle of my "cheat sheets" for all beginners who, like me, want to dive into the world of real-time systems based on FreeRTOS. There are already many series of materials on ESP-IDF and FreeRTOS on tekkix, but my goal is to share my development experience with this framework in simple terms. Today, we will break down basic terminology and write "Hello, world!". Let's go!
What are real-time systems?
Real-time system (RTOS) is a software environment in which it is critically important to complete tasks within strictly defined deadlines. RTOS ensures that important tasks are executed exactly at the scheduled time.
Why is RTOS needed?
At first glance, when developing simple firmware, a single while(1)
loop might suffice, where buttons are checked, an LED blinks, and communication events are handled. However, when there are multiple events (for example, simultaneous UART, sensor, and Wi-Fi operations), this approach quickly turns into a chaotic monotonic loop — tasks start "bumping" into each other, delays occur, and hard-to-catch errors emerge. Vladimir Medintsev.
Without RTOS — you have to manually manage flags, timeouts, and states in a single thread:
while (1) {
if (millis() - lastLED >= ledPeriod) { blinkLED(); lastLED = millis(); }
checkButtons();
processUART();
if (WiFi.ready()) handleWiFi();
}
With RTOS — each function is separated into its own task with priorities, synchronization, and deterministic execution:
xTaskCreate(buttonTask, "Buttons", 2048, NULL, 10, NULL);
xTaskCreate(ledTask, "LED", 2048, NULL, 5, NULL);
xTaskCreate(uartTask, "UART", 2048, NULL, 7, NULL);
xTaskCreate(wifiTask, "WiFi", 4096, NULL, 8, NULL);
RTOS provides:
Clean and modular code, where each task does its important job.
Priority execution — critical tasks (for example, buttons) are assigned a separate priority. Higher priority tasks can preempt lower priority tasks.
Predictable behavior without hang-ups, even under high loads.
Key characteristics of RTOS:
Determinism
The reaction time to an external event (e.g., an interrupt from a sensor) is limited and predictable.Priorities
Tasks can have different priorities, and the scheduler ensures that more "important" tasks always run first.Preemptive scheduling
If a higher-priority task becomes available at any moment, the system can "interrupt" the current task and switch to the higher-priority one.Low latency
The delay between a task request and its execution remains within the responsiveness required by the application.
ESP-IDF
ESP‑IDF (Espressif IoT Development Framework) — this is the official development environment from Espressif for ESP32 series microcontrollers (as well as ESP32‑Sx, ESP32‑C3, etc.). It provides everything necessary to create reliable, multitasking, networked, and secure applications "out of the box." In ESP-IDF, the FreeRTOS kernel is already built in and fully integrated into the system. In ESP‑IDF, interaction with hardware (GPIO, I²C, SPI, UART, ADC, PWM, etc.) is organized through a set of APIs tightly integrated with FreeRTOS.
Let's introduce some general terminology:
1. Tasks
Separate "threads" of execution within the FreeRTOS OS. Each task gets its own stack, name, and priority.
Each task is created using
xTaskCreate()
orxTaskCreatePinnedToCore()
.
2. Task Handle
A pointer to the task (
TaskHandle_t
).Used to manage the task after creation: pause, resume, or delete it.
TaskHandle_t taskHandle; // Task descriptor (handle)
xTaskCreate(..., &taskHandle); // Save the handle
vTaskSuspend(taskHandle); // Pause the task
3. Task Priority
Each task has a priority (an integer).
The FreeRTOS scheduler always runs the task with the highest available priority.
4. Scheduler
Task scheduler in ESP‑IDF is part of the built-in FreeRTOS kernel, which determines which of your tasks will run at any given time.
5. Delay
Used to pause a task for a specified amount of time.
The task "sleeps" without blocking the core.
vTaskDelay(pdMS_TO_TICKS(1000)); // 1000 ms delay
6. Queues
A mechanism for data exchange between tasks or from ISR to a task.
You can send/receive data (e.g., structures, numbers, bytes).
// Create a queue with 10 elements, with fixed sizes
QueueHandle_t queue = xQueueCreate(10, sizeof(int));
// Insert a new item into the queue
// Blocking the task on overflow until space becomes available or timeout occurs.
xQueueSend(queue, &value, portMAX_DELAY);
// Remove (collect) an item from the queue
// Blocking the task on an empty queue until data becomes available or timeout occurs.
xQueueReceive(queue, &value, portMAX_DELAY);
7. Semaphore
A signaling mechanism: one task can "signal" another.
There are binary (0 or 1) and counting (e.g., resource availability counters) semaphores.
8. Mutex
A special type of semaphore designed for mutual exclusion — to prevent tasks from interfering with each other when accessing shared resources (e.g., UART or SPI).
It can be ordinary (
xSemaphoreCreateMutex
) or recursive (xSemaphoreCreateRecursiveMutex
).
Setting up and configuring PlatformIO for ESP‑IDF
PlatformIO is a cross-platform development environment for embedded systems, integrating with VS Code, CLion, Atom, and other IDEs. It simplifies managing SDKs, dependencies, and building projects, including those based on ESP‑IDF.
In VS Code extensions, install PlatformIO. After restarting VS Code, the PlatformIO panel will appear. To create a new project, click the PlatformIO icon in the sidebar (PIO Home).
Select New Project. In the Board field, choose your board (I have nodemcu-32s
). In the Framework section, select ESP-IDF.
A platformio.ini
file is created in the root. The minimal configuration for ESP32 with ESP‑IDF looks like this:
[env:nodemcu-32s]
platform = espressif32
board = nodemcu-32s
framework = espidf
monitor_speed = 115200
Also, in PIO Home, all tools for building, uploading, and debugging our microcontroller are provided:
Practice
Let's start writing a simple task: this task will notify us that it has started and then print "Hello, World!" on the next line.
TaskHandle_t Hello = NULL; // Task handle
// Task function Hello_Task
void Hello_Task(void *arg){
while(1){
printf("Task started!\n");
printf("Hello, World!\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
Hello
- the task handle. As mentioned above, without a handle, we cannot directly control a specific task. In this example, we're not going to manage the task's lifecycle (removal, stopping tasks), so we assign NULL.
Hello_Task
- the task function. It contains an infinite loop; without it, the task would immediately end and be removed by the scheduler. The macropdMS_TO_TICKS(1000)
converts 1000 ms to ticks.
Keep in mind, tasks are created using xTaskCreate
xTaskCreate(
Hello_Task, // pointer to the task function
"Hello, World!", // task name (for debugging)
4096, // stack size
NULL, // argument passed to the function (not needed here)
10, // task priority
&Hello // pointer where the task handle will be written
);
Result:
#include "freertos/FreeRTOS.h"
TaskHandle_t Hello = NULL; // Task handle
// Task function Hello_Task
void Hello_Task(void *arg){
while(1){
printf("Task started!\n");
printf("Hello, World!\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void app_main() {
// Create the task
xTaskCreate(Hello_Task, "Hello, World!", 4096, NULL, 10, &Hello);
}
Cores
The ESP32 has 2 cores under the hood: PRO_CPU
(core 0) and APP_CPU
(core 1). We can explicitly specify which core a task will use when creating it. Let's create 2 tasks, each running on a separate core.
Within the ESP-IDF documentation, cores Core 0 and Core 1 are sometimes referred to as PRO_CPU and APP_CPU respectively. These aliases reflect the typical distribution of tasks in applications.
Usually, tasks responsible for protocol handling such as Wi‑Fi or Bluetooth are assigned to core 0 (PRO_CPU), while tasks related to the rest of the application are assigned to core 1 (APP_CPU).
#include "freertos/FreeRTOS.h"
TaskHandle_t Task_1_Handle = NULL; // Task 1 handle
TaskHandle_t Task_2_Handle = NULL; // Task 2 handle
// First task to be pinned to core 0
void task_core0(void *arg) {
while (1) {
printf("First task running on core %d\n", xPortGetCoreID());
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// Second task to be pinned to core 1
void task_core1(void *arg) {
while (1) {
printf("Second task running on core %d\n", xPortGetCoreID());
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void app_main() {
// Create the first task and pin it to core 0 (PRO_CPU)
xTaskCreatePinnedToCore(
task_core0, // Task function
"TaskCore0", // Task name
2048, // Stack size
NULL, // Argument
5, // Priority
&Task_1_Handle, // Handle
0 // Core 0
);
// Create the second task and pin it to core 1 (APP_CPU)
xTaskCreatePinnedToCore(
task_core1, // Task function
"TaskCore1", // Task name
2048, // Stack size
NULL, // Argument
5, // Priority
&Task_2_Handle, // Handle
1 // Core 1
);
}
In the next part, we will dive deeper into GPIO and ISR: we will configure the pins, set up interrupts for buttons, and learn to handle events in real-time mode. I welcome constructive feedback from experienced developers and any advice you may have for improving the materials. See you in the next part!
Write comment