Привет!
Это третья статья из цикла по 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");
}
}
Заключение
Если вы заметили неточности, ошибки или у вас есть предложения по улучшению статьи — обязательно отпишитесь в диалоге или в комментариях. Я с радостью подкорректирую материал.
После изучения очередей логично перейти к синхронизации доступа к общим данным и обработке событий. Далее мы рассмотрим использование мьютексов и семафоров.
Автор: nikitatm333
