ESP32 с FreeRTOS: очереди (Queues) для межзадачной коммуникации (Arduino IDE)

В этом руководстве вы узнаете, как использовать очереди FreeRTOS для безопасной и эффективной коммуникации между задачами на ESP32, используя Arduino IDE. Очереди позволяют обмениваться данными между задачами безопасным способом. Мы рассмотрим основные принципы работы очередей и проведём вас через три практических примера, показывающих, как передавать данные между задачами.

ESP32 с FreeRTOS: очереди для межзадачной коммуникации (Arduino IDE)

Новичок в FreeRTOS? Начните с этого руководства: ESP32 with FreeRTOS (Arduino IDE) – Getting Started Guide: Creating Tasks.

Коммуникация между задачами FreeRTOS

Во FreeRTOS существуют различные способы межзадачной коммуникации и синхронизации данных между задачами, подходящие для разных целей.

  • Очереди (Queues): позволяют задачам отправлять и получать данные, такие как показания датчиков, состояния кнопок или другие данные, как мы увидим в примерах, рассмотренных в этом руководстве. Здесь мы сосредоточимся на очередях FreeRTOS.

  • Семафоры (Semaphores) – это сигнальные инструменты во FreeRTOS, используемые для координации задач. Они часто используются для обозначения того, что произошло событие или ресурс готов. Они не переносят данные, только счётчик (или флаг) для запуска действий после того, как что-то произошло. Мы рассмотрим это в будущем руководстве.

  • Мьютексы (Mutexes) (взаимные исключения) используются для защиты общих ресурсов, позволяя только одной задаче обращаться к ним одновременно. Это предотвращает конфликты в параллельных операциях, которым нужно обращаться к одним и тем же данным. В отличие от очередей, мьютексы фокусируются на доступе к ресурсам, а не на передаче данных. Мы рассмотрим это в будущем руководстве.

Что такое очереди FreeRTOS?

Очереди FreeRTOS – это потокобезопасные структуры данных, которые позволяют задачам отправлять и получать данные для синхронизированной коммуникации в многозадачной среде.

Они гарантируют, что данные передаются безопасно между задачами без конфликтов или потери данных.

Очереди особенно полезны, когда одной задаче нужно отправить данные, а другой задаче нужно обработать их позже – например, задача датчика отправляет показания задаче логирования или для обработки и отображения на экране. Таким образом, каждая задача может работать со своей скоростью, не мешая другой.

Основы очередей FreeRTOS

Вот некоторые базовые концепции создания и обработки очередей в вашем коде Arduino IDE для ESP32:

Создание очереди

Используйте xQueueCreate(size, item_size) для создания очереди – size соответствует количеству элементов, которые могут находиться в очереди, а item_size – это размер в байтах кучи (heap), выделенной для каждого элемента в очереди.

Отправка данных в очередь

Для отправки данных в очередь используйте xQueueSend() для добавления данных в очередь. Или используйте xQueueSendFromISR(), если отправляете данные из ISR (процедуры обработки прерывания).

Получение данных из очереди

Функция xQueueReceive() читает данные из очереди, если они доступны.


Пример 1: Использование очередей для управления яркостью светодиода

ESP32 со светодиодом и потенциометром

В этом примере мы создадим две задачи. Одна задача будет отправлять данные в очередь. Другая задача будет читать данные из очереди.

Мы будем управлять яркостью светодиода с помощью потенциометра. Это базовый пример, который вы, вероятно, видели раньше, для изучения аналогового чтения и ШИМ. В данном случае мы используем этот базовый пример как отправную точку для объяснения очередей.

Мы создадим две задачи со следующими именами:

  • potTask: читает значение с потенциометра и отправляет его в очередь.

  • LEDBrightnessTask: проверяет, есть ли значения для получения из очереди, и соответственно регулирует яркость светодиода.

Необходимые компоненты

Для этого проекта вам понадобятся следующие компоненты:

Схема подключения

Для этого примера подключите светодиод и потенциометр к вашей плате ESP32. Мы подключаем светодиод к GPIO 2 и потенциометр к GPIO 15.

ESP32 подключён к светодиоду и потенциометру

Код – Управление яркостью светодиода (базовый пример очередей)

Следующий код создаёт две задачи. Одна читает значения с потенциометра и отправляет их в очередь. Другая задача читает значения из очереди и соответственно управляет яркостью светодиода.

Вы можете скопировать следующий код в Arduino IDE и загрузить его на вашу плату.

/*
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-freertos-queues-inter-task-arduino/
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
#define POT_PIN 15
#define LED_PIN 2
#define PWM_FREQ 5000
#define PWM_RESOLUTION 12  // 12-bit (0–4095)
#define QUEUE_SIZE 5

// Create an handle for the queue
QueueHandle_t potQueue = NULL;

void potTask(void *parameter) {
  for (;;) {
    uint16_t potValue = analogRead(POT_PIN);  // Read 0–4095
    xQueueSend(potQueue, &potValue, portMAX_DELAY);  // Send to queue
    Serial.printf("potTask: Sent pot value %u\n", potValue);
    vTaskDelay(100 / portTICK_PERIOD_MS);  // 100ms
  }
}

void LEDBrightnessTask(void *parameter) {
  for (;;) {
    uint16_t potValue;
    if (xQueueReceive(potQueue, &potValue, portMAX_DELAY)) {
      uint16_t brightness = potValue;
      ledcWrite(LED_PIN, brightness);
      Serial.printf("LEDBrightnessTask: Set brightness %u\n", brightness);
    }
  }
}

void setup() {
  Serial.begin(115200);

  // Setup PWM for LED
  ledcAttach(LED_PIN, PWM_FREQ, PWM_RESOLUTION);

  // Create queue (5 items, each uint16_t)
  potQueue = xQueueCreate(QUEUE_SIZE, sizeof(uint16_t));
  if (potQueue == NULL) {
    Serial.println("Failed to create queue!");
    while (1);
  }

  // Create tasks
  xTaskCreatePinnedToCore(
    potTask,
    "potTask",
    3000,  // Task stack
    NULL,
    1,
    NULL,
    1  // Core 1
  );

  xTaskCreatePinnedToCore(
    LEDBrightnessTask,
    "LEDBrightnessTask",
    3000,
    NULL,
    1,
    NULL,
    1
  );
}

void loop() {
  // Empty
}

Посмотреть исходный код

Как работает код?

Давайте рассмотрим код, чтобы понять, как он работает.

Определение GPIO для светодиода и потенциометра

Сначала определяем GPIO пины для светодиода и потенциометра.

#define POT_PIN 15
#define LED_PIN 2

Свойства ШИМ

Затем определяем частоту ШИМ и разрешение для регулировки яркости светодиода. Мы устанавливаем 12-битное разрешение (такое же разрешение по умолчанию для аналогового чтения).

#define PWM_FREQ 5000
#define PWM_RESOLUTION 12  // 12-bit (0–4095)

Если вы новичок в ШИМ с ESP32, ознакомьтесь с этим руководством, чтобы узнать больше: ESP32 PWM with Arduino IDE (Analog Output).

Размер очереди FreeRTOS

Определяем размер очереди FreeRTOS. Это количество элементов, которое может содержать очередь. В данном случае мы устанавливаем его равным 5.

#define QUEUE_SIZE 5

В нашем случае это хорошая оценка, потому что элементы, отправленные в очередь, будут почти немедленно потреблены другой задачей.

Дескриптор очереди

Создаём дескриптор для очереди. Так мы будем ссылаться на нашу очередь во всём коде. Мы будем называть очередь potQueue.

// Create an handle for the queue
QueueHandle_t potQueue = NULL;

Функция potTask

Создаём функцию для задачи потенциометра: potTask. Это задача, которая будет читать значения с потенциометра.

void potTask(void *parameter) {
  for (;;) {
    uint16_t potValue = analogRead(POT_PIN);  // Read 0–4095
    xQueueSend(potQueue, &potValue, portMAX_DELAY);  // Send to queue
    Serial.printf("potTask: Sent pot value %u\n", potValue);
    vTaskDelay(100 / portTICK_PERIOD_MS);  // 100ms
  }
}

Мы начинаем с чтения значения с потенциометра и сохраняем его в переменной типа uint16_t с именем potValue.

uint16_t potValue = analogRead(POT_PIN);  // Read 0–4095

Чтобы узнать больше о чтении аналоговых значений с ESP32, ознакомьтесь с этим руководством: ESP32 ADC – Read Analog Values with Arduino IDE.

Отправка элемента в очередь

Затем мы используем функцию xQueueSend() для отправки элемента в очередь. Мы отправляем potValue в potQueue следующим образом.

xQueueSend(potQueue, &potValue, portMAX_DELAY);  // Send to queue

Последний параметр, portMAX_DELAY, означает, что задача будет ждать неопределённо долго, пока в очереди не появится место для нового элемента. Если вы не хотите ждать и хотите вернуться немедленно, передайте 0 вместо него.

vTaskDelay

Затем мы ждём 100 миллисекунд между каждым чтением.

vTaskDelay(100 / portTICK_PERIOD_MS);  // 100ms

В задачах FreeRTOS мы должны использовать функцию vTaskDelay() вместо обычной функции Arduino delay(). Она принимает в качестве аргумента количество тиков ожидания. В случае ESP32 один тик соответствует 1мс = portTICK_PERIOD_MS.

LEDBrightnessTask

Следующие строки создают функцию для задачи управления яркостью светодиода. Она называется LEDBrightnessTask.

void LEDBrightnessTask(void *parameter) {
  for (;;) {
    uint16_t potValue;
    if (xQueueReceive(potQueue, &potValue, portMAX_DELAY)) {
      uint16_t brightness = potValue;
      ledcWrite(LED_PIN, brightness);
      Serial.printf("LEDBrightnessTask: Set brightness %u\n", brightness);
    }
  }
}

Мы создаём локальную переменную potValue, которая будет получать значения из очереди.

uint16_t potValue;
Получение значений из очереди

Для получения значений из очереди мы можем использовать функцию xQueueReceive(). Передаём дескриптор очереди в качестве аргумента, указатель на переменную, куда должно быть сохранено значение, и следует ли ждать доступности элемента (portMAX_DELAY) или вернуться немедленно (передать 0).

if (xQueueReceive(potQueue, &potValue, portMAX_DELAY)) {
Управление яркостью светодиода

Поскольку мы управляем ШИМ с 12-битным разрешением и используем разрешение по умолчанию для ШИМ (12 бит), яркость будет такой же, как potValue.

uint16_t brightness = potValue;

Наконец, мы можем использовать функцию ledcWrite() для регулировки яркости светодиода.

ledcWrite(LED_PIN, brightness);

setup()

В setup() инициализируем Serial Monitor на скорости 115200 бод.

Serial.begin(115200);

Настраиваем свойства ШИМ.

ledcAttach(LED_PIN, PWM_FREQ, PWM_RESOLUTION);
Создание очереди

Создаём саму очередь с помощью функции xQueueCreate(). Она принимает в качестве аргумента размер очереди и размер каждого элемента в байтах. Поскольку мы храним переменные uint16_t, мы передаём размер uint16_t, который составляет 2 байта (16 бит).

// Create queue (5 items, each uint16_t)
potQueue = xQueueCreate(QUEUE_SIZE, sizeof(uint16_t));
if (potQueue == NULL) {
  Serial.println("Failed to create queue!");
  while (1);
}
Создание задач

Затем создаём задачи для обработки потенциометра и управления яркостью светодиода. Мы назначаем обе задачи на ядро 1.

// Create tasks
xTaskCreatePinnedToCore(
  potTask,
  "potTask",
  3000,  // Task stack
  NULL,
  1,
  NULL,
  1  // Core 1
);

xTaskCreatePinnedToCore(
  LEDBrightnessTask,
  "LEDBrightnessTask",
  3000,
  NULL,
  1,
  NULL,
  1
);

Мы рассмотрели, как создавать задачи, в этом предыдущем руководстве: ESP32 with FreeRTOS (Arduino IDE) – Getting Started: Create Tasks

loop()

Функция loop() пуста, потому что FreeRTOS управляет задачами.

void loop() {
  // Empty
}

Демонстрация

Загрузите код на вашу плату. Нажмите встроенную кнопку RST, чтобы ESP32 начал выполнять код.

Затем откройте Serial Monitor на скорости 115200 бод. Вы должны получить что-то подобное.

ESP32 FreeRTOS задачи с очередями - демонстрация в Serial Monitor

Поверните потенциометр и наблюдайте, как яркость светодиода увеличивается и уменьшается соответственно.

ESP32 управление яркостью светодиода с помощью потенциометра

Пример 2: Очереди FreeRTOS – Обмен данными между тремя задачами

Данные могут быть переданы между несколькими задачами FreeRTOS. В предыдущем примере мы передавали данные от одной задачи к другой. Но мы можем передавать данные скольким угодно задачам.

Этот пример является модификацией предыдущего проекта с добавлением третьей задачи. Эта третья задача будет отвечать за получение значений потенциометра из очереди и отображение значений вместе с меткой времени в Serial Monitor. Я также убрал все выводы Serial из других задач.

Это простой пример, который показывает, как вы можете использовать очереди для обмена данными между несколькими задачами.

Если мы хотим, чтобы несколько задач получали одни и те же данные, нам нужно наполнять две (или более) отдельные очереди одинаковыми значениями. Тогда каждая задача будет получать данные из своей собственной очереди. Нам нужно делать это, потому что во FreeRTOS элемент очереди может быть удалён только один раз, что означает, что только одна задача может получить каждое значение из одной очереди.

Схема подключения

Оставьте ту же схему со светодиодом и потенциометром из предыдущего примера.

Код: Очереди FreeRTOS – Три задачи

Скопируйте следующий код в Arduino IDE.

/*
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-freertos-queues-inter-task-arduino/
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
#define POT_PIN 15
#define LED_PIN 2
#define PWM_FREQ 5000
#define PWM_RESOLUTION 12  // 12-bit (0–4095)
#define QUEUE_SIZE 5

QueueHandle_t potQueue = NULL;
QueueHandle_t sMonitorQueue = NULL;

void SensorTask(void *parameter) {
  for (;;) {
    uint16_t potValue = analogRead(POT_PIN);  // Read 0–4095
    xQueueSend(potQueue, &potValue, portMAX_DELAY);  // Send to queue
    xQueueSend(sMonitorQueue, &potValue, portMAX_DELAY);  // Send to queue
    vTaskDelay(300 / portTICK_PERIOD_MS);  // 300ms
  }
}

void LEDBrightnessTask(void *parameter) {
  for (;;) {
    uint16_t potValue;
    if (xQueueReceive(potQueue, &potValue, portMAX_DELAY)) {
      uint16_t brightness = potValue;
      ledcWrite(LED_PIN, brightness);
    }
  }
}

void SerialLoggerTask(void *parameter) {
  for (;;) {
    uint16_t potValue;
    if (xQueueReceive(sMonitorQueue, &potValue, portMAX_DELAY)) {
      Serial.printf("SerialLoggerTask: Pot value %u at %lu ms\n", potValue, millis());
    }
  }
}

void setup() {
  Serial.begin(115200);

  // Setup PWM for LED
  ledcAttach(LED_PIN, PWM_FREQ, PWM_RESOLUTION);

  // Create queue (5 items, each uint16_t)
  potQueue = xQueueCreate(QUEUE_SIZE, sizeof(uint16_t));
  if (potQueue == NULL) {
    Serial.println("Failed to create queue!");
    while (1);
  }

  // Create queue (5 items, each uint16_t)
  sMonitorQueue = xQueueCreate(QUEUE_SIZE, sizeof(uint16_t));
  if (sMonitorQueue == NULL) {
    Serial.println("Failed to create queue!");
    while (1);
  }

  // Create tasks with one parameter per line
  xTaskCreatePinnedToCore(
    SensorTask,             // Task function
    "SensorTask",           // Task name
    3000,                   // Stack size (bytes)
    NULL,                   // Task parameters
    1,                      // Priority
    NULL,                   // Task handle
    1                       // Core ID
  );

  xTaskCreatePinnedToCore(
    LEDBrightnessTask,      // Task function
    "LEDBrightnessTask",    // Task name
    3000,                   // Stack size (bytes)
    NULL,                   // Task parameters
    1,                      // Priority
    NULL,                   // Task handle
    1                       // Core ID
  );

  xTaskCreatePinnedToCore(
    SerialLoggerTask,       // Task function
    "SerialLoggerTask",     // Task name
    3000,                   // Stack size (bytes)
    NULL,                   // Task parameters
    1,                      // Priority
    NULL,                   // Task handle
    1                       // Core ID
  );
}

void loop() {
  // Empty
}

Посмотреть исходный код

Как работает код?

Этот код очень похож на предыдущий пример, но с дополнительной очередью и дополнительной задачей.

По сути, мы создаём ещё одну очередь с именем sMonitorQueue.

QueueHandle_t potQueue = NULL;
QueueHandle_t sMonitorQueue = NULL;

В SensorTask() мы наполняем обе очереди значениями потенциометра.

xQueueSend(potQueue, &potValue, portMAX_DELAY);  // Send to queue
xQueueSend(sMonitorQueue, &potValue, portMAX_DELAY);  // Send to queue

Мы добавляем дополнительную задачу – SerialLoggerTask().

void SerialLoggerTask(void *parameter) {
  for (;;) {
    uint16_t potValue;
    if (xQueueReceive(sMonitorQueue, &potValue, portMAX_DELAY)) {
      Serial.printf("SerialLoggerTask: Pot value %u at %lu ms\n", potValue, millis());
    }
  }
}

Эта задача получает potValue из sMonitorQueue.

if (xQueueReceive(sMonitorQueue, &potValue, portMAX_DELAY)) {

И затем отображает его в Serial Monitor вместе с меткой времени.

Serial.printf("SerialLoggerTask: Pot value %u at %lu ms\n", potValue, millis());

Вам также нужно создать эту новую задачу в setup().

xTaskCreatePinnedToCore(
  SerialLoggerTask,       // Task function
  "SerialLoggerTask",     // Task name
  3000,                   // Stack size (bytes)
  NULL,                   // Task parameters
  1,                      // Priority
  NULL,                   // Task handle
  1                       // Core ID
);

А также дополнительную очередь.

// Create queue (5 items, each uint16_t)
  sMonitorQueue = xQueueCreate(QUEUE_SIZE, sizeof(uint16_t));
  if (sMonitorQueue== NULL) {
    Serial.println("Failed to create queue!");
    while (1);
  }

Демонстрация

Результат будет аналогичен предыдущему примеру, но теперь у вас есть SerialLoggerTask, отображающая значения и метку времени в Serial Monitor.

ESP32 с задачей FreeRTOS, выводящей данные в Serial Monitor - демонстрация

Этот простой пример демонстрирует, что вы можете передавать данные скольким угодно задачам с помощью нескольких очередей.


Пример 3: Отображение данных датчика на экране (передача структуры данных)

ESP32 отображение показаний датчика BME280 на OLED дисплее

В этом примере мы отобразим показания датчика BME280 на OLED дисплее, используя задачи FreeRTOS. Процесс разделён на две основные задачи:

SensorTask:

  • Читает температуру, влажность и давление с датчика BME280

  • Отправляет данные датчика в очередь

DisplayTask:

  • Получает данные датчика из очереди

  • Отображает показания на OLED экране

Цель этого примера – продемонстрировать, как передавать структуру данных, содержащую несколько переменных (температура, влажность и давление), между задачами.

Необходимые компоненты

Для этого примера вам понадобятся следующие компоненты:

Схема подключения

Подключите BME280 и OLED дисплей к стандартным I2C пинам ESP32.

Схема подключения ESP32 с OLED дисплеем и BME280 к стандартным I2C пинам

Вам также может быть интересно прочитать: ESP32 I2C Communication: Set Pins, Multiple Bus Interfaces and Peripherals (Arduino IDE).

BME280

BME280

ESP32

VIN

3V3

GND

GND

SCL

GPIO 22

SDA

GPIO 21

OLED Display

OLED Display

ESP32

VIN

3V3

GND

GND

SCL

GPIO 22

SDA

GPIO 21

Код – Отображение данных датчика на экране с помощью очередей FreeRTOS

Скопируйте следующий код в Arduino IDE.

/*
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-freertos-queues-inter-task-arduino/
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_BME280.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_ADDRESS 0x3C

#define QUEUE_SIZE 5

Adafruit_BME280 bme;

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

QueueHandle_t sensorQueue = NULL;

typedef struct {
  float temperature;
  float humidity;
  float pressure;
} SensorData;

void SensorTask(void *parameter) {
  if (!bme.begin(0x76)) {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1);
  }
  for (;;) {
    SensorData data;
    data.temperature = bme.readTemperature();
    data.humidity = bme.readHumidity();
    data.pressure = bme.readPressure() / 100.0F;
    xQueueSend(sensorQueue, &data, portMAX_DELAY);
    Serial.print("SensorTask: Sent Temp=");
    Serial.print(data.temperature, 1);
    Serial.print("°C, Hum=");
    Serial.print(data.humidity, 1);
    Serial.print("%, Pres=");
    Serial.print(data.pressure, 1);
    Serial.println("hPa");
    vTaskDelay(2000 / portTICK_PERIOD_MS);  // 2s
  }
}

void DisplayTask(void *parameter) {
  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
    Serial.println("OLED init failed!");
    while (1);
  }
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);

  for (;;) {
    SensorData data;
    if (xQueueReceive(sensorQueue, &data, portMAX_DELAY)) {

      // Display on OLED
      display.clearDisplay();
      display.setCursor(0, 0);
      display.print("Temperature: ");
      display.print(data.temperature, 1);
      display.print(" ");
      display.cp437(true);
      display.write(167);
      display.println("C");
      display.setCursor(0, 20);
      display.print("Humidity: ");
      display.print(data.humidity, 1);
      display.println(" %");
      display.setCursor(0, 40);
      display.print("Pressure: ");
      display.print(data.pressure, 1);
      display.println(" hPa");
      display.display();
    }
  }
}

void setup() {
  Serial.begin(115200);

  // Starts I2C on the board default's  I2C pins
  Wire.begin();

  // Create queue
  sensorQueue = xQueueCreate(QUEUE_SIZE, sizeof(SensorData));
  if (sensorQueue == NULL) {
    Serial.println("Failed to create queue!");
    while (1);
  }

  // Create tasks
  xTaskCreatePinnedToCore(
    SensorTask,             // Task function
    "SensorTask",           // Task name
    4000,                   // Stack size (bytes)
    NULL,                   // Task parameters
    1,                      // Priority
    NULL,                   // Task handle
    1                       // Core ID
  );

  xTaskCreatePinnedToCore(
    DisplayTask,            // Task function
    "DisplayTask",          // Task name
    4000,                   // Stack size (bytes)
    NULL,                   // Task parameters
    1,                      // Priority
    NULL,                   // Task handle
    1                       // Core ID
  );
}

void loop() {
  // Empty
}

Посмотреть исходный код

Как работает код?

На данном этапе вы должны быть знакомы с большей частью кода.

Для отправки нескольких переменных в очередь мы создаём структуру, которая будет содержать температуру, влажность и давление. Мы называем этот тип структуры SensorData.

В качестве альтернативы вы также можете использовать JSON-объекты, но я считаю, что структуры проще в использовании для данного сценария.

typedef struct {
  float temperature;
  float humidity;
  float pressure;
} SensorData;

Функция SensorTask отвечает за получение текущих показаний датчика и отправку их в очередь. Мы создаём переменную data, которая представляет собой структуру данных типа SensorData (ту, которую мы создали ранее).

SensorData data;

Затем мы добавляем значения в структуру данных следующим образом.

data.temperature = bme.readTemperature();
data.humidity = bme.readHumidity();
data.pressure = bme.readPressure() / 100.0F;

Теперь, когда мы заполнили структуру данных последними показаниями датчика, мы можем отправить её в очередь, так же как в предыдущих примерах, используя функцию xQueueSend().

xQueueSend(sensorQueue, &data, portMAX_DELAY);

В функции DisplayTask мы получаем данные из очереди и отображаем их на OLED экране.

void DisplayTask(void *parameter) {
  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
    Serial.println("OLED init failed!");
    while (1);
  }
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);

  for (;;) {
    SensorData data;
    if (xQueueReceive(sensorQueue, &data, portMAX_DELAY)) {

      // Display on OLED
      display.clearDisplay();
      display.setCursor(0, 0);
      display.print("Temperature: ");
      display.print(data.temperature, 1);
      display.print(" ");
      display.cp437(true);
      display.write(167);
      display.println("C");
      display.setCursor(0, 20);
      display.print("Humidity: ");
      display.print(data.humidity, 1);
      display.println(" %");
      display.setCursor(0, 40);
      display.print("Pressure: ");
      display.print(data.pressure, 1);
      display.println(" hPa");
      display.display();
    }
  }
}

Как и в предыдущих примерах, в setup() мы создаём очередь и задачи и назначаем их на ядро ESP32.

void setup() {
  Serial.begin(115200);

  // Starts I2C on the board default's  I2C pins
  Wire.begin();

  // Create queue
  sensorQueue = xQueueCreate(QUEUE_SIZE, sizeof(SensorData));
  if (sensorQueue == NULL) {
    Serial.println("Failed to create queue!");
    while (1);
  }

  // Create tasks
  xTaskCreatePinnedToCore(
    SensorTask,             // Task function
    "SensorTask",           // Task name
    4000,                   // Stack size (bytes)
    NULL,                   // Task parameters
    1,                      // Priority
    NULL,                   // Task handle
    1                       // Core ID
  );

  xTaskCreatePinnedToCore(
    DisplayTask,            // Task function
    "DisplayTask",          // Task name
    4000,                   // Stack size (bytes)
    NULL,                   // Task parameters
    1,                      // Priority
    NULL,                   // Task handle
    1                       // Core ID
  );
}

Функция loop() пуста, потому что FreeRTOS управляет задачами.

void loop() {
  // Empty
}

Демонстрация

Загрузите предыдущий код на вашу плату ESP32. После загрузки нажмите встроенную кнопку RST, чтобы плата начала выполнять код.

Откройте Serial Monitor на скорости 115200 бод. Каждые две секунды будут отображаться обновлённые показания датчика.

ESP32 Sensor Task (FreeRTOS) отображается в Serial Monitor каждые две секунды

Экран обновляется с той же скоростью, с которой новые показания отправляются в очередь.

ESP32 отображение показаний датчика BME280 на OLED дисплее с использованием очередей FreeRTOS

Заключение

В этом руководстве вы узнали, как использовать очереди FreeRTOS для безопасной передачи данных от одной задачи к другой. Мы показали вам три различных примера, демонстрирующих, как легко использовать очереди для коммуникации между задачами. После понимания этих базовых примеров идея состоит в том, чтобы применить эту концепцию к более сложным проектам, где одновременный доступ к значениям переменных может быть критичен для программы.

Надеемся, вы нашли это руководство полезным. У нас есть ещё руководства в этой серии FreeRTOS, которые вам могут понравиться:

Узнайте больше об ESP32 с нашими ресурсами:


Источник: Rui Santos & Sara Santos – Random Nerd Tutorials