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

Очередь — это потокобезопасная структура FIFO (первый пришел, первый вышел), используемая для обмена данными между задачами или из прерываний (ISR) в задачи. В ESP-IDF все операции с очередями выполняются через стандартный FreeRTOS API.
Безопасная передача данных
Очередь копирует данные, поэтому несколько задач могут безопасно обмениваться структурами или числами, не опасаясь гонок.
Синхронизация
Задача может блокироваться, ожидая прихода сообщения, вместо «активного ожидания» (polling).
Передача из ISR
Из обработчика прерывания можно вызывать специальные версии функций отправки в очередь, гарантируя минимальный ISR-код.
xQueueCreate(uxQueueLength, uxItemSize);
Назначение: создаёт новую очередь и возвращает её хэндл (дескриптор). Вы не знаете и не видите, как устроена очередь «внутри» — вместо этого у вас есть лишь её хэндл. Все дальнейшие операции (отправка, приём, удаление) вы выполняете, передавая этот хэндл в API-функции.
Параметры:
uxQueueLength — максимальное количество элементов, которые очередь может содержать.
uxItemSize — размер одного элемента в байтах.
Возвращает:
Хэндл созданной очереди (QueueHandle_t), или NULL при ошибке (например, недостаточно памяти).
xQueueSend(xQueue, *pvItemToQueue, xTicksToWait);
Назначение: копирует pvItemToQueue в конец очереди xQueue.
Параметры:
xQueue — хэндл очереди.
pvItemToQueue — указатель на данные, которые будут скопированы в очередь.
xTicksToWait — максимальное время в тиках ожидать места, если очередь полна (portMAX_DELAY — ждать бесконечно).
Возвращает:
pdTRUE — элемент успешно поставлен в очередь.
errQUEUE_FULL — не удалось поместить элемент за отведённое время.
xQueueSendFromISR(xQueue, *pvItemToQueue, *pxHigherPriorityTaskWoken);
Назначение: отправляет элемент в очередь из контекста ISR.
Параметры:
xQueue — хэндл очереди.
pvItemToQueue — указатель на данные для копирования.
pxHigherPriorityTaskWoken — указатель на флаг, который устанавливается в pdTRUE, если отправка разблокировала задачу с более высоким приоритетом (требуется portYIELD_FROM_ISR).
Возвращает:
pdTRUE —элемент успешно поставлен в очередь.
errQUEUE_FULL — не удалось поместить элемент за отведённое время.
xQueueReceive(xQueue, *pvBuffer, xTicksToWait);
Назначение: извлекает первый элемент из очереди xQueue и копирует его в pvBuffer.
Параметры:
xQueue — хэндл очереди.
pvBuffer — указатель на буфер , куда скопируется элемент.
xTicksToWait — максимальное время ожидания, если очередь пуста (portMAX_DELAY — ждать бесконечно).
Возвращает:
pdTRUE — элемент успешно получен и удалён из очереди.
pdFALSE — по истечении таймаута в очереди не оказалось данных.
xQueueReceiveFromISR(xQueue, *pvBuffer, *pxHigherPriorityTaskWoken);
Назначение: извлекает элемент из очереди в ISR.
Параметры:
xQueue — хэндл очереди.
pvBuffer — указатель на буфер для приёма данных.
pxHigherPriorityTaskWoken — флаг, как и в xQueueSendFromISR.
Возвращает:
pdTRUE — элемент успешно получен и удалён из очереди.
pdFALSE — по истечении таймаута в очереди не оказалось данных.
Прежде чем приступать к практике я предлагаю Вам вкратце рассмотреть API Logging [1]. Это удобный инструмент, который упростит нам работу.
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
В качестве подопытного берем наш пример из прошлой [2] статьи для мигания светодиодом:
#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 // указатель, в который запишут дескриптор задачи
);
}
Думаю вам все станет понятнее на простом и рабочем примере. Предлагаю создать очередь, например, на 5 целочисленных элементов:
static QueueHandle_t int_queue = NULL; // Дескриптор очереди, возвращаемый при создании.
int_queue = xQueueCreate(5, sizeof(int)); // Соаздание очереди на 5 элементов
Теперь придумаем 2 сущности (задачи), которые будут работать с этой очередью. Допустим, одна из этих сущностей - Producer будет класть (по крайней мере пытаться это сделать) в очередь элемент, а вторая - Consumer будет забирать элемент из очереди и печатать его.
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 мс.
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");
}
}
В прошлой статье мы работали с ISR и в комментариях правильно подметили, что есть несколько проблем в нашем коде [3]:
Нет гарантии согласованности.
Нет масштабируемости: если понадобится передавать что-то более сложное (не просто флаг), придётся придумывать собственные механизмы блокировок.
Трудно расширять: нельзя легко различать «какое» событие пришло.
Конечно, эти проблемы проявляются в более сложных сценариях, но в данном случае использование очереди будет как раз кстати.
Предлагаю реализовать так: в обработчике прерывания (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
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/esp32/423404
Ссылки в тексте:
[1] Logging: https://docs.espressif.com/projects/esp-idf/en/v4.4/esp32/api-reference/system/log.html?utm_source=chatgpt.com#logging-library
[2] прошлой: https://habr.com/ru/articles/919362/
[3] коде: https://habr.com/ru/articles/919362/#:~:text=%D0%BF%D0%BE%D0%BC%D0%BE%D1%89%D1%8C%D1%8E%20IRAM_ATTR.-,%D0%9F%D1%80%D0%B0%D0%BA%D1%82%D0%B8%D0%BA%D0%B0,-%D0%9D%D0%B0%D1%88%D0%B0%20%D1%86%D0%B5%D0%BB%D1%8C%20%E2%80%94%20%D1%80%D0%B5%D0%B0%D0%BB%D0%B8%D0%B7%D0%BE%D0%B2%D0%B0%D1%82%D1%8C
[4] Источник: https://habr.com/ru/articles/920636/?utm_campaign=920636&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.