Привет, Хабр!
Это третья статья из цикла по 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");
}
}

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