Привет, Хабр!

Совсем недавно мне в руки попала плата ESP32 (NodeMCU‑32S). Ранее я уже работал с ESP8266 и даже создавал на ней небольшое веб‑приложение в режиме Station. Делал я всё это в Arduino IDE и был рад обнаружить расширение, которое позволяло организовать мой проект (да и просто в VS Code удобнее работать) — PlatformIO. Именно в PlatformIO я в первый раз увидел фреймворк ESP-IDF и начал потихоньку углубляться в эту тему.

Framework: ESP-IDF
Framework: ESP-IDF

Данная статья открывает цикл моих «шпаргалок» для всех новичков, которые, как и я, хотят погрузиться в мир систем реального времени на базе FreeRTOS. На просторах Хабра уже есть множество серий материалов по ESP‑IDF и FreeRTOS, но моя цель — простым языком поделиться собственным опытом разработки с помощью этого фреймворка. Сегодня мы разберём базовую терминологию и напишем «Hello, world!». Поехали!

Что же такое системы реального времени?

Система реального времени (RTOS) — это программное окружение, в котором критически важно выполнение задач в строго заданные сроки. RTOS следят за тем, чтобы важные задачи запускались точно в назначенный момент.

Зачем вообще нужна RTOS?

На первый взгляд, при разработке простой прошивки можно обойтись одним циклом while(1), в котором по очереди проверяются кнопки, мигает светодиод и обрабатываются события связи. Но уже при наличии нескольких событий (например, одновременной работы с UART, датчиками и Wi‑Fi) такой подход быстро становится хаотичным однотонным циклом — задачи начинают «спотыкаться» друг о друга, возникают задержки и трудно уловимые ошибки. Владимир Мединцев.

Без RTOS — вы вынуждены вручную следить за флагами, тайм-аутами и состояниями в одном потоке:

while (1) {
  if (millis() - lastLED >= ledPeriod) { blinkLED(); lastLED = millis(); }
  checkButtons();
  processUART();
  if (WiFi.ready()) handleWiFi();
}

С RTOS — каждая функция выделена в отдельную задачу с приоритетами, синхронизацией и детерминированным запуском:

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 обеспечивает:

  • Чистый и модульный код, где каждая задача делает свои важные дела.

  • Приоритетное исполнение — критичным задачам (например, кнопкам) отведён отдельный приоритет. Задачи с наивысшим приоритетом могут перебить задачи с более низким приоритетом.

  • Предсказуемое поведение без зависаний, даже при высоких нагрузках.

Ключевые характеристики RTOS:

  • Детерминизм
    Время реакции на внешнее событие (например, прерывание от датчика) ограничено и предсказуемо.

  • Приоритеты
    Задачи могут иметь разный приоритет, и планировщик гарантирует, что более «важные» задачи всегда выполняются раньше.

  • Планирование с предвосхищением (preemptive)
    Если в любой момент появляется более приоритетная задача, система может «прервать» текущую и переключиться на неё.

  • Малые задержки (latency)
    Задержка между запросом на выполнение задачи и началом её работы остаётся в пределах отзывчивости, требуемой приложению.

ESP‑IDF

ESP‑IDF (Espressif IoT Development Framework) — это официальная среда разработки от компании Espressif для микроконтроллеров серии ESP32 (а также ESP32‑Sx, ESP32‑C3 и др.). Она предоставляет всё необходимое для создания надёжных, многозадачных, сетевых и безопасных приложений «из коробки». В ESP-IDF ядро FreeRTOS уже встроено и полностью интегрировано в систему. В ESP‑IDF взаимодействие с “железом” (GPIO, I²C, SPI, UART, ADC, PWM и др.) организовано через набор API, тесно интегрированных с FreeRTOS.

Приведем немного общей терминологии:

1. Задачи (Tasks)

  • Отдельные «нити» исполнения внутри ОС FreeRTOS. Каждая задача получает свой стек, имя и приоритет.

  • Каждая задача создаётся с помощью xTaskCreate() или xTaskCreatePinnedToCore().

2. Хэндл задачи (Task Handle)

  • Указатель на задачу (TaskHandle_t).

  • Используется, чтобы управлять задачей после создания: приостановить, возобновить, удалить.

TaskHandle_t taskHandle;       // Дескриптор (хэндл) задачи
xTaskCreate(..., &taskHandle); // Сохраняем хэндл
vTaskSuspend(taskHandle);      // Приостанавливаем задачу

3. Приоритет задачи

  • Каждая задача имеет приоритет (целое число).

  • Планировщик FreeRTOS всегда запускает задачу с наивысшим доступным приоритетом.

4. Планировщик

Планировщик задач в ESP‑IDF — это часть встроенного ядра FreeRTOS, которая решает, какая из ваших “тасок” (задач) будет выполняться в каждый момент времени.

5. Задержка (Delay)

  • Используется для приостановки задачи на заданное количество времени.

  • Задача "спит", а не блокирует ядро.

vTaskDelay(pdMS_TO_TICKS(1000)); // задержка 1000 мс

6. Очереди (Queues)

  • Механизм обмена данными между задачами или из ISR в задачу.

  • Можно отправлять/принимать данные (например, структуры, числа, байты).

// Создание очереди на 10 элементов, с фиксированными размерами
QueueHandle_t queue = xQueueCreate(10, sizeof(int));

// Вставка нового элемента в очерель
// блокируя задачу при переполнении до тех пор, пока место не освободится или не выйдет таймаут.
xQueueSend(queue, &value, portMAX_DELAY);

// Удаление (сбор) элемента из очереди
//блокируя задачу при пустой очереди до появления данных или до истечения таймаута.
xQueueReceive(queue, &value, portMAX_DELAY);

7. Семафор (Semaphore)

  • Сигнальный механизм: одна задача может «сигналить» другой.

  • Бывают бинарные (0 или 1) и счётные (например, счетчик доступных ресурсов).

8. Мьютекс (Mutex)

  • Специальный тип семафора, предназначенный для взаимоисключения — чтобы задачи не мешали друг другу при доступе к общим ресурсам (например, UART или SPI).

  • Может быть обычным (xSemaphoreCreateMutex) или рекурсивным (xSemaphoreCreateRecursiveMutex).


Установка и настройка PlatformIO для ESP‑IDF

PlatformIO — кросс‑платформенная среда разработки для встраиваемых систем, которая интегрируется с VS Code, CLion, Atom и другими IDE. Она упрощает управление SDK, зависимостями и сборкой проектов, в том числе на базе ESP‑IDF.

В расширениях VS Code устанавливаем PlatformIO. После перезапуска VS Code появится панель PlatformIO. Чтобы создать новый проект нажимаем на иконку PlatformIO в боковой панели (PIO Home).

PIO Home
PIO Home

Выбираем New Project. В поле Board выберете Вашу плату (у меня этоnodemcu-32s). В разделе Framework выбираем ESP-IDF.

Создание проекта
Создание проекта

В корне создаётся файл platformio.ini. Минимальная конфигурация для ESP32 с ESP‑IDF выглядит так:

[env:nodemcu-32s]
platform = espressif32
board = nodemcu-32s
framework = espidf
monitor_speed = 115200

Также в PIO Home Нам представлены все инструменты для сборки, загрузки и отладки нашего МК:

Набор инструментов
Набор инструментов

Практика

Приступим к написанию простой задачи: эта задача будет уведомлять нас о том, что она запущена и следующей строкой печатать "Hello, World!".

TaskHandle_t Hello = NULL; // Дескриптор (хэндл) задачи

// Функция‑задача Hello_Task
void Hello_Task(void *arg){
    while(1){
        printf("Задача запущена!\n");
        printf("Hello, World!\n");
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

Hello - дескриптор (хэндл) задачи. Как было сказано выше, без хэндла мы не сможем напрямую управлять конкретной задачей. В данном примере мы не будем управлять жизненным циклом задачи (удаление, остановка задач), поэтому присваиваем NULL.

Hello_Task - функция задача. Содержит бесконечный цикл, без него задача сразу завершилась бы и была удалена планировщиком. Макрос pdMS_TO_TICKS(1000) конвертирует 1000 мс в количество тиков.

Напомним, что задачи создаются с помощьюxTaskCreate

xTaskCreate(
  Hello_Task,        // указатель на функцию‑задачу
  "Hello, World!",   // имя задачи (для отладки)
  4096,              // размер стека
  NULL,              // аргумент, передаваемый в функцию (здесь не нужен)
  10,                // приоритет задачи
  &Hello             // указатель, в который запишут дескриптор задачи
);

Результат:

#include "freertos/FreeRTOS.h"
#include "freertos/task.h

TaskHandle_t Hello = NULL; // Дескриптор (хэндл) задачи

// Функция‑задача Hello_Task
void Hello_Task(void *arg){
    while(1){
        printf("Задача запущена!\n");
        printf("Hello, World!\n");
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void app_main() {
    // Создание задачи
    xTaskCreate(Hello_Task, "Hello, World!", 4096, NULL, 10, &Hello);
}
Hello, World!
Hello, World!

Ядра

У ESP32 под капотом 2 ядра: PRO_CPU (ядро 0) и APP_CPU (ядро 1). Мы можем явно прописать при создании задачи какое ядро она будет использовать. Создадим 2 задачи, каждая задача будет на отдельном ядре. Функция xPortGetCoreID() поможет нам узнать текущее ядро, на котором выполняется задача.

В рамках ESP-IDF ядра Core 0 и Core 1 иногда обозначаются как PRO_CPU и APP_CPU соответственно. Эти псевдонимы отражают типичное распределение задач в приложениях.

Обычно задачи, отвечающие за обработку протоколов, таких как Wi‑Fi или Bluetooth, закрепляются за ядром 0 (PRO_CPU), а задачи, связанные с остальной частью приложения, — за ядром 1 (APP_CPU).

#include "freertos/FreeRTOS.h"
#include "freertos/task.h

TaskHandle_t Task_1_Handle = NULL; // Дескриптор (хэндл) задачи 1
TaskHandle_t Task_2_Handle = NULL; // Дескриптор (хэндл) задачи 2

// Первая задача, которая будет прикреплена к ядру 0
void task_core0(void *arg) {
    while (1) {
        printf("Запущена первая задача на ядре %d\n", xPortGetCoreID());
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

// Вторая задача, которая будет прикреплена к ядру 1
void task_core1(void *arg) {
    while (1) {
        printf("Запущена вторая задача на ядре %d\n", xPortGetCoreID());
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void app_main() {
    // Создаём первую задачу и прикрепляем её к ядру 0 (PRO_CPU)
    xTaskCreatePinnedToCore(
        task_core0,            // функция‑задача
        "TaskCore0",           // имя задачи
        2048,                  // размер стека
        NULL,                  // аргумент
        5,                     // приоритет
        &Task_1_Handle,        // хэндл
        0                      // ядро 0
    );

    // Создаём вторую задачу и пинним её к ядру 1 (APP_CPU)
    xTaskCreatePinnedToCore(
        task_core1,            // функция‑задача
        "TaskCore1",           // имя задачи
        2048,                  // размер стека
        NULL,                  // аргумент
        5,                     // приоритет
        &Task_2_Handle,        // хэндл
        1                      // ядро 1
    );
  }
Работа на двух ядрах
Работа на двух ядрах

В следующей части мы подробно поработаем с GPIO и ISR: настроим выводы, организуем прерывания от кнопок и научимся обрабатывать события в режиме реального времени. Буду рад конструктивным замечаниям опытных разработчиков и любым вашим советам по улучшению материалов. До встречи в следующей части!

Комментарии (13)


  1. zbot
    15.06.2025 19:40

    #include "freertos/FreeRTOS.h"
    TaskHandle_t Task_1_Handle = NULL; // Дескриптор (хэндл) задачи 1
    TaskHandle_t Task_2_Handle = NULL; // Дескриптор (хэндл) задачи 2

    в этот код надо добавить:

    #include "freertos/task.h"


    1. nikitatm333 Автор
      15.06.2025 19:40

      Добрый день! Спасибо, проверю и исправлю!


  1. Alex_Crack
    15.06.2025 19:40

    Спасибо. Информация последовательна и количество перерабатываемо. Мне, как человеку, начинающему с isp-idf, полезно. Надеюсь, цикл статей не закончится после первой, как десятки других.


    1. nikitatm333 Автор
      15.06.2025 19:40

      Добрый день! После Ваших слов теперь точно буду продолжать)


  1. zorket
    15.06.2025 19:40

    Просто и понятно


  1. klepko36
    15.06.2025 19:40

    Отличная статья, тоже буду ждать продолжения!


  1. 0x6b73ca
    15.06.2025 19:40

    Может быть на начальном этапе это не так важно но думаю стоит сказать что размер стека в классическом freeRTOS указывается в словах но в espidf указывается в байтах, я когда перешел на esp32 с stm32 то не знал этого и не понимал почему задачи требуют на столько большого стека


    1. nikitatm333 Автор
      15.06.2025 19:40

      Обязательно будет!


  1. 0x6b73ca
    15.06.2025 19:40

    Ещё Espressif добавили в FreeRTOS функции с постфиксом WithCaps которые позволяют выделять стек во внешней ОЗУ (и не только это но это важно) без чего не обойтись в случаях работы с графическими библиотеками типа lvgl, кстати советовал бы включить её в будущие статьи так как она доволи простая в использовании особенно если есть опыт в css, и достаточно мощная, можно рисовать вполне себе хорошие интерфейсы, поддерживает даже сенсорные дисплеи, так сильно интересней будет учиться, мигать светодиодом и выводить строчки на консоль быстро надоедает


    1. nikitatm333 Автор
      15.06.2025 19:40

      Окей, посмотрю
      Спасибо


  1. Eugeniy2014
    15.06.2025 19:40

    Не подскажите?Поставил VS Code Platformio ,но при выборе платы в проекте крутится кружок ожидания и платы не прогружаются.Мысль,что дело в админке,что не дает соединиться с интернетом.Антивирус отключал,но все равно нет выбора плат.


    1. nikitatm333 Автор
      15.06.2025 19:40

      Я не сталкивался с таким, но могу предположить несколько вариантов:
      1) Проблема действительно в правах доступа

      2) Можно попробовать обновится, в терминале VSCode:
      pio upgrade (либо platformio upgrade)
      pio update (либо platformio update)
      Также можно посмотреть список плат в ручную: pio boards
      ----------------------------------------------------------------------------------
      Если жалуется на pio, то можно попробовать установить в ручную
      Установите pytho, а затем попробуйте:
      pip install -U platformio
      Проверьте версию:
      platformio --version. Если все ок то обновляемся (upgrade и update) и смотрим список плат pio boards (либо platformio boards). Если все ок можно перезапустить VSCode и попробовать снова.

      Если ошибки все еще летят то смотреть логи


    1. Yakamoz
      15.06.2025 19:40

      Попробуйте с vpn, мне помогло. Есть подозрение, что проблема связана с cloudflare.