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

от автора

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

Это третья статья из цикла по ESP-IDF. Ранее мы разобрали стек задач, работу с GPIO и прерывания. Теперь перейдём к очередям FreeRTOS — мощному инструменту для безопасного обмена данными между ISR и задачами. Поехали!


Теория работы с очередями (Queues) в FreeRTOS / ESP-IDF

Очередь — это потокобезопасная структура FIFO (первый пришел, первый вышел), используемая для обмена данными между задачами или из прерываний (ISR) в задачи. В ESP-IDF все операции с очередями выполняются через стандартный FreeRTOS API.

Зачем нужны очереди?

  • Безопасная передача данных
    Очередь копирует данные, поэтому несколько задач могут безопасно обмениваться структурами или числами, не опасаясь гонок.

  • Синхронизация
    Задача может блокироваться, ожидая прихода сообщения, вместо «активного ожидания» (polling).

  • Передача из ISR
    Из обработчика прерывания можно вызывать специальные версии функций отправки в очередь, гарантируя минимальный ISR-код.


Основные API работы с очередями

1. Создание очереди

xQueueCreate(uxQueueLength, uxItemSize);

Назначение: создаёт новую очередь и возвращает её хэндл (дескриптор). Вы не знаете и не видите, как устроена очередь «внутри» — вместо этого у вас есть лишь её хэндл. Все дальнейшие операции (отправка, приём, удаление) вы выполняете, передавая этот хэндл в API-функции.

Параметры:

  • uxQueueLength — максимальное количество элементов, которые очередь может содержать.

  • uxItemSize — размер одного элемента в байтах.

Возвращает:

  • Хэндл созданной очереди (QueueHandle_t), или NULL при ошибке (например, недостаточно памяти).

2. Копирование в конец очереди

xQueueSend(xQueue, *pvItemToQueue, xTicksToWait);

Назначение: копирует pvItemToQueue в конец очереди xQueue.

Параметры:

  • xQueue — хэндл очереди.

  • pvItemToQueue — указатель на данные, которые будут скопированы в очередь.

  • xTicksToWait — максимальное время в тиках ожидать места, если очередь полна (portMAX_DELAY — ждать бесконечно).

Возвращает:

  • pdTRUE — элемент успешно поставлен в очередь.

  • errQUEUE_FULL — не удалось поместить элемент за отведённое время.

3. Копирование в конец очереди (в контексте ISR)

xQueueSendFromISR(xQueue, *pvItemToQueue, *pxHigherPriorityTaskWoken);

Назначение: отправляет элемент в очередь из контекста ISR.

Параметры:

  • xQueue — хэндл очереди.

  • pvItemToQueue — указатель на данные для копирования.

  • pxHigherPriorityTaskWoken — указатель на флаг, который устанавливается в pdTRUE, если отправка разблокировала задачу с более высоким приоритетом (требуется portYIELD_FROM_ISR).

Возвращает:

  • pdTRUE —элемент успешно поставлен в очередь.

  • errQUEUE_FULL — не удалось поместить элемент за отведённое время.

4. Извлечение элемента из очереди

xQueueReceive(xQueue, *pvBuffer, xTicksToWait);

Назначение: извлекает первый элемент из очереди xQueue и копирует его в pvBuffer.

Параметры:

  • xQueue — хэндл очереди.

  • pvBuffer — указатель на буфер , куда скопируется элемент.

  • xTicksToWait — максимальное время ожидания, если очередь пуста (portMAX_DELAY — ждать бесконечно).

Возвращает:

  • pdTRUE — элемент успешно получен и удалён из очереди.

  • pdFALSE — по истечении таймаута в очереди не оказалось данных.

5. Извлечение элемента из очереди (в контексте ISR)

xQueueReceiveFromISR(xQueue, *pvBuffer, *pxHigherPriorityTaskWoken);

Назначение: извлекает элемент из очереди в ISR.

Параметры:

  • xQueue — хэндл очереди.

  • pvBuffer — указатель на буфер для приёма данных.

  • pxHigherPriorityTaskWoken — флаг, как и в xQueueSendFromISR.

Возвращает:

  • pdTRUE — элемент успешно получен и удалён из очереди.

  • pdFALSE — по истечении таймаута в очереди не оказалось данных.


Логирование

Прежде чем приступать к практике я предлагаю Вам вкратце рассмотреть API Logging. Это удобный инструмент, который упростит нам работу.

ESP-IDF предоставляет мощную и при этом простую в использовании систему логирования, основанную на макросах:

ESP_LOGE(TAG, "Error! code=%d", err_code);    // Error   (E) ESP_LOGW(TAG, "Warning: %s", warn_msg);       // Warning (W) ESP_LOGI(TAG, "Info: init complete");         // Info    (I) ESP_LOGD(TAG, "Debug: x=%d, y=%d", x, y);     // Debug   (D) ESP_LOGV(TAG, "Verbose: raw data=%02X", b);   // Verbose (V)

Уровень

Описание

ERROR

Критические сбои

WARN

Потенциальные проблемы

INFO

Ключевые события (старт, стоп, …)

DEBUG

Детальная отладка

VERBOSE

Максимальная детализация

  • TAG (обычно static const char* TAG = "MyModule";) помогает группировать сообщения из разных частей кода и фильтровать их независимо.

  • В итоговом логе каждая строка автоматически дополнится временем, уровнем и тегом:

I (1234) MyModule: Info: init complete

В качестве подопытного берем наш пример из прошлой статьи для мигания светодиодом:

#include "freertos/FreeRTOS.h" #include "freertos/task.h"            #include "driver/gpio.h"   #include "esp_log.h"             #define LED_GPIO GPIO_NUM_2 // Порт светодиода static const char* TAG = "blink_task"; // Тэг  TaskHandle_t Blink_Handle = NULL; // Дескриптор задачи Blink  void Blink_Task(void *arg){     esp_rom_gpio_pad_select_gpio(LED_GPIO); // "переключение" выбранного физического контакта в режим GPIO     gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT); // Устанавливаем направление как выход          bool led_on = false;     while (1) {         led_on = !led_on;         gpio_set_level(LED_GPIO, led_on);         ESP_LOGI(TAG, "LED is now %s", led_on ? "ON" : "OFF");         vTaskDelay(pdMS_TO_TICKS(500));     } }   void app_main() {     xTaskCreate(         Blink_Task,     // указатель на функцию‑задачу         "BLINK",        // имя задачи (для отладки)         4096,           // размер стека         NULL,           // аргумент, передаваемый в функцию (здесь не нужен)         10,             // приоритет задачи         &Blink_Handle   // указатель, в который запишут дескриптор задачи     ); }
Логирование светодиода

Логирование светодиода

Практика Queue

Пример 1

Думаю вам все станет понятнее на простом и рабочем примере. Предлагаю создать очередь, например, на 5 целочисленных элементов:

static QueueHandle_t int_queue = NULL; // Дескриптор очереди, возвращаемый при создании. int_queue = xQueueCreate(5, sizeof(int)); // Соаздание очереди на 5 элементов

Теперь придумаем 2 сущности (задачи), которые будут работать с этой очередью. Допустим, одна из этих сущностей — Producer будет класть (по крайней мере пытаться это сделать) в очередь элемент, а вторая — Consumer будет забирать элемент из очереди и печатать его.

Задача-производитель (Producer)

void producer_task(void *arg) {     int counter = 0;     while (1) {         if (xQueueSend(int_queue, &counter, pdMS_TO_TICKS(100)) == pdTRUE) {             ESP_LOGI(TAG, "Sent: %d", counter);             counter++;         } else {             ESP_LOGW(TAG, "Queue full, could not send");         }         vTaskDelay(pdMS_TO_TICKS(1000));     } } 
  • counter — локальная переменная, инкрементируется после каждой успешной отправки (counter++).

  • xQueueSend пытается поместить текущее значение counter в очередь int_queue.

  • pdMS_TO_TICKS(100)максимальное время, в течение которого задача-производитель будет блокироваться, ожидая свободного места в очереди, если она в момент отправки уже полна.

    • Если в очереди есть хотя бы один свободный слот, xQueueSend вернёт pdTRUE немедленно, и задача продолжит работу, не дожидаясь 100 мс.

    • Если очередь полна, таска перейдёт в состояние Blocked и будет ждать появления места до тех самых 100 мс.

      • Если за эти 100 мс слот освободится (взять элемент из очереди), xQueueSend поместит ваш элемент в очередь и вернёт pdTRUE.

      • Если же за 100 мс очередь так и останется полной, функция вернёт pdFALSE (ошибка errQUEUE_FULL), и вы увидите в логе «Queue full, could not send».

  • vTaskDelay(pdMS_TO_TICKS(1000)) — задача засыпает на 1000 мс.

Задача-потребитель (Consumer)

void consumer_task(void *arg) {     int received;     while (1) {         if (xQueueReceive(int_queue, &received, portMAX_DELAY) == pdTRUE) {             ESP_LOGI(TAG, "Received: %d", received);         }     } } 
  • Задача ждёт (portMAX_DELAY — бесконечно) пока в очередь не придёт новый элемент.

  • Как только xQueueReceive возвращает pdTRUE, в received скопировано значение — его и печатаем.

  • Цикл повторяется, задача снова блокируется в ожидании.

Результат

#include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/queue.h" #include "esp_log.h"  static const char *TAG = "queue_example";  // Хэндл очереди static QueueHandle_t int_queue = NULL; // Хэндл очереди задачи producer_task (опционально) TaskHandle_t Producer_Handle = NULL;  // Хэндл очереди задачи consumer_task (опционально) TaskHandle_t Consumer_Handle = NULL;   // Producer: каждые 1000 мс кладёт в очередь увеличивающийся счётчик void producer_task(void *arg) {     int counter = 0;     while (1) {         if (xQueueSend(int_queue, &counter, pdMS_TO_TICKS(100)) == pdTRUE) {             ESP_LOGI(TAG, "Sent: %d", counter);             counter++;         } else {             ESP_LOGW(TAG, "Queue full, could not send");         }         vTaskDelay(pdMS_TO_TICKS(1000));     } }  // Consumer: ждёт элемент из очереди и выводит его void consumer_task(void *arg) {     int received;     while (1) {         // ждем бесконечно, пока не придёт новое значение         if (xQueueReceive(int_queue, &received, portMAX_DELAY) == pdTRUE) {             ESP_LOGI(TAG, "Received: %d", received);         }     } }  void app_main(void) {     // Создаём очередь на 5 элементов типа int     int_queue = xQueueCreate(5, sizeof(int));     // Проверяем, что очередь успешно создана, иначе логируем ошибку и выходим     if (int_queue == NULL) {         ESP_LOGE(TAG, "Failed to create queue");         return;     }      /*         Создаём задачи, предварительно их проверив         Учимся логировать)     */     if (xTaskCreate(producer_task, "producer", 2048, NULL, 5, &Producer_Handle) != pdPASS) {         ESP_LOGE(TAG, "Failed to create producer task");     }     if (xTaskCreate(consumer_task, "consumer", 2048, NULL, 5, &Consumer_Handle) != pdPASS) {         ESP_LOGE(TAG, "Failed to create consumer task");     } }
queue_example

queue_example

Пример 2

В прошлой статье мы работали с ISR и в комментариях правильно подметили, что есть несколько проблем в нашем коде:

  • Нет гарантии согласованности.

  • Нет масштабируемости: если понадобится передавать что-то более сложное (не просто флаг), придётся придумывать собственные механизмы блокировок.

  • Трудно расширять: нельзя легко различать «какое» событие пришло.

Конечно, эти проблемы проявляются в более сложных сценариях, но в данном случае использование очереди будет как раз кстати.

Предлагаю реализовать так: в обработчике прерывания (gpio_isr_handler) мы передаём в очередь булево значение (true/false), а задача Blink_Task в бесконечном цикле блокируется на приёме из этой очереди и по каждому новому элементу инвертирует состояние светодиода. Это позволяет полностью исключить гонки при одновременном доступе ISR и таски к переменной состояния: ISR отвечает только за быструю доставку события в очередь, а вся логика переключения и управления GPIO сосредоточена в одном месте — в задаче, что гарантирует атомарность и упрощает отладку.

#include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/queue.h" #include "driver/gpio.h" #include "esp_attr.h" #include "esp_log.h"  #define LED_GPIO    GPIO_NUM_2     // Пин, к которому подключён светодиод #define BUTTON_GPIO GPIO_NUM_23    // Пин, к которому подключена кнопка  TaskHandle_t Blink_Handle = NULL;  // Дескриптор задачи Blink  static const char* TAG = "led_queue_no_yield"; // Для логирования  // Очередь для команд от ISR static QueueHandle_t led_queue = NULL;    // ISR-обработчик: шлёт команду в очередь static void IRAM_ATTR gpio_isr_handler(void *arg) {     uint8_t cmd = 1;     xQueueSendFromISR(led_queue, &cmd, NULL); }  // Задача, которая постоянно устанавливает уровень на LED_GPIO согласно state void Blink_Task(void *arg) {     // Инициализация     esp_rom_gpio_pad_select_gpio(LED_GPIO);     gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT);     gpio_set_level(LED_GPIO, 0);      uint8_t cmd;     bool state = false;     while (1) {         // Блокируемся до прихода команды, ждем portMAX_DELAY(без ограничений)         if(xQueueReceive(led_queue, &cmd, portMAX_DELAY) == pdTRUE){                 if (cmd == 1) {                     // Переключаем светодиод                     state = !state;                     gpio_set_level(LED_GPIO, state);                     ESP_LOGI(TAG, "LED toggled to %s", state ? "ON" : "OFF");             }         }     } }  void app_main() {         // 1) Создаём очередь на 10 элементов     led_queue = xQueueCreate(10, sizeof(uint8_t));     if (!led_queue) {         ESP_LOGE(TAG, "Queue creation failed");         return;     }      esp_rom_gpio_pad_select_gpio(BUTTON_GPIO);                  // "Переключение" выбранного физического контакта в режим GPIO     gpio_set_direction(BUTTON_GPIO, GPIO_MODE_INPUT);           // Настройка BUTTON_GPIO как цифровой вход     gpio_pullup_en(BUTTON_GPIO);                                // Подтяжка к VCC     gpio_set_intr_type(BUTTON_GPIO, GPIO_INTR_POSEDGE);         // Прерывание при нажатии (положительный фронт)     gpio_install_isr_service(ESP_INTR_FLAG_IRAM);               // Устанавливаем сервис ISR      gpio_isr_handler_add(BUTTON_GPIO, gpio_isr_handler, NULL);  // Регистрируем функцию-обработчик прерывания      // Создаём задачу, которая будет “мигать” LED в соответствии с state     if(xTaskCreate(Blink_Task, "BLINK", 2048, NULL, 5, &Blink_Handle) != pdPASS) {         ESP_LOGE(TAG, "Failed to create Blink_Task");     } }
ISR_QUEUE

ISR_QUEUE

Заключение

Если вы заметили неточности, ошибки или у вас есть предложения по улучшению статьи — обязательно отпишитесь в диалоге или в комментариях. Я с радостью подкорректирую материал.

После изучения очередей логично перейти к синхронизации доступа к общим данным и обработке событий. Далее мы рассмотрим использование мьютексов и семафоров.


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


Комментарии

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

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