Программирование ESP32 с ESP-IDF в среде platformio #0

от автора

Привет, Хабр!
Совсем недавно мне в руки попала плата 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"  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 задачи, каждая задача будет на отдельном ядре.

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

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

#include "freertos/FreeRTOS.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: настроим выводы, организуем прерывания от кнопок и научимся обрабатывать события в режиме реального времени. Буду рад конструктивным замечаниям опытных разработчиков и любым вашим советам по улучшению материалов. До встречи в следующей части!


ссылка на оригинал статьи https://habr.com/ru/articles/918434/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *