Привет, Хабр!
Это третья статья из цикла по 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)
|
Уровень |
Описание |
|---|---|
|
|
Критические сбои |
|
|
Потенциальные проблемы |
|
|
Ключевые события (старт, стоп, …) |
|
|
Детальная отладка |
|
|
Максимальная детализация |
-
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"); } }
Пример 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"); } }
Заключение
Если вы заметили неточности, ошибки или у вас есть предложения по улучшению статьи — обязательно отпишитесь в диалоге или в комментариях. Я с радостью подкорректирую материал.
После изучения очередей логично перейти к синхронизации доступа к общим данным и обработке событий. Далее мы рассмотрим использование мьютексов и семафоров.
ссылка на оригинал статьи https://habr.com/ru/articles/920636/
Добавить комментарий