Как стать автором
Обновить

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

Уровень сложностиСредний
Время на прочтение8 мин
Количество просмотров2.7K

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

Это третья статья из цикла по 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

Заключение

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

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

Теги:
Хабы:
+18
Комментарии9

Публикации

Ближайшие события