ESP32 с FreeRTOS (Arduino IDE) – Начало работы: Создание задач

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

FreeRTOS – это операционная система реального времени, которая позволяет ESP32 управлять несколькими задачами и запускать их одновременно плавно и эффективно. Она встроена в ESP32 и полностью интегрирована как с ядром Arduino, так и с Espressif IoT Development Framework (IDF).

ESP32 с FreeRTOS - Руководство по началу работы, Создание задач

В этом руководстве мы рассмотрим следующие темы:

Предварительные требования

Это руководство посвящено программированию ESP32 с использованием ядра Arduino. Прежде чем продолжить, у вас должно быть установлено ядро ESP32 Arduino в вашей Arduino IDE. Следуйте следующему руководству для установки ESP32 в Arduino IDE, если вы ещё этого не сделали.


Что такое FreeRTOS?

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

Логотип FreeRTOS

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

Это делает ваш код более организованным и отзывчивым, особенно когда ESP32 одновременно выполняет несколько задач, таких как чтение датчиков, обработка HTTP-запросов и отображение информации на экране, одновременно прослушивая прерывания.

Зачем FreeRTOS нужна для ESP32?

Операционная система реального времени FreeRTOS встроена в ESP32 и интегрирована в Espressif IDF и ядро Arduino. Она поддерживает:

  • Управление задачами: создание, приостановка, возобновление или удаление задач (рассматривается в этом руководстве).

  • Планирование: позволяет назначать приоритеты задачам, чтобы они выполнялись в определённом порядке (рассматривается в этом руководстве).

  • Межзадачная коммуникация: используя такие вещи, как очереди, семафоры и мьютексы, мы можем обеспечить бесперебойную связь между задачами без сбоев ESP32.

  • Поддержка двухъядерности: позволяет запускать задачи на ядре 0 или ядре 1 ESP32 (также рассматривается в этом руководстве, но для более подробного руководства по использованию двух ядер ESP32 ознакомьтесь с этим руководством: Как использовать ESP32 Dual Core с Arduino IDE).

Итак, подводя итог…

FreeRTOS полезна для ESP32, потому что она обеспечивает многозадачность, позволяя нескольким задачам, таким как чтение датчиков, Wi-Fi и обновление дисплея, работать без блокировки друг друга.

Она также позволяет использовать преимущества двухъядерного процессора ESP32, назначая задачи конкретным ядрам для повышения производительности.

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

Основные концепции FreeRTOS

Прежде чем перейти к практическим примерам, давайте рассмотрим некоторые базовые концепции, связанные с FreeRTOS:

  • Задачи (Tasks): задачи – это независимые функции, выполняемые одновременно, каждая со своим собственным стеком (выделенной памятью) и приоритетом. Задачи могут находиться в состояниях: Running (Выполняется), Ready (Готова), Blocked (Заблокирована) или Suspended (Приостановлена).

  • Планировщик (Scheduler): планировщик решает, какие задачи запускать, на основе их приоритетов. Это вытесняющий планировщик, что означает, что он может прервать задачу с более низким приоритетом в любой момент, чтобы запустить задачу с более высоким приоритетом, гарантируя, что критические задачи выполняются, как только они готовы.

  • Приоритеты (Priorities): более высокие числа означают более высокий приоритет (например: 1 = низкий, 5 = высокий).


1) Создание задач

Это самый простой пример, в котором мы покажем, как создать задачу FreeRTOS для мигания светодиодом каждую секунду. Мы подключим светодиод к GPIO 2. Вместо этого вы можете пропустить светодиод и проверить результаты на встроенном светодиоде ESP32.

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

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

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

Подключите светодиод к GPIO 2, как показано на следующей схеме.

ESP32 со светодиодом, подключённым к GPIO2 через резистор 220 Ом

ESP32: Создание задач FreeRTOS – Код Arduino

Следующий код создаёт задачу, которая будет мигать светодиодом, подключённым к GPIO 2, каждую секунду.

/*
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-freertos-arduino-tasks/
  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 LED_PIN 2

// Declare task handle
TaskHandle_t BlinkTaskHandle = NULL;

void BlinkTask(void *parameter) {
  for (;;) { // Infinite loop
    digitalWrite(LED_PIN, HIGH);
    Serial.println("BlinkTask: LED ON");
    vTaskDelay(1000 / portTICK_PERIOD_MS); // 1000ms
    digitalWrite(LED_PIN, LOW);
    Serial.println("BlinkTask: LED OFF");
    vTaskDelay(1000 / portTICK_PERIOD_MS);
    Serial.print("BlinkTask running on core ");
    Serial.println(xPortGetCoreID());
  }
}

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

  pinMode(LED_PIN, OUTPUT);

  xTaskCreatePinnedToCore(
    BlinkTask,         // Task function
    "BlinkTask",       // Task name
    10000,             // Stack size (bytes)
    NULL,              // Parameters
    1,                 // Priority
    &BlinkTaskHandle,  // Task handle
    1                  // Core 1
  );
}

void loop() {
  // Empty because FreeRTOS scheduler runs the task
}

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

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

Начнём с определения GPIO, к которому будет подключён светодиод.

#define LED_PIN 2

Дескриптор задачи (Task Handle)

Затем объявляем дескриптор задачи. TaskHandle_t – это переменная, которая указывает на задачу FreeRTOS, позволяя управлять ею: возобновлять, останавливать или удалять. В этом примере нам не понадобится дескриптор задачи, но мы создаём его, чтобы показать, как это делается.

TaskHandle_t BlinkTaskHandle = NULL;

Функция задачи (Task Function)

Затем мы создаём задачу. Задача – это не что иное, как функция, выполняющая любые нужные вам команды. Вот функция BlinkTask, используемая в этом примере.

void BlinkTask(void *parameter) {
  for (;;) { // Infinite loop
    digitalWrite(LED_PIN, HIGH);
    Serial.println("BlinkTask: LED ON");
    vTaskDelay(1000 / portTICK_PERIOD_MS); // 1000ms
    digitalWrite(LED_PIN, LOW);
    Serial.println("BlinkTask: LED OFF");
    vTaskDelay(1000 / portTICK_PERIOD_MS);
    Serial.print("BlinkTask running on core ");
    Serial.println(xPortGetCoreID());
  }
}

Эта функция является задачей FreeRTOS – особым видом функции, которая выполняется независимо под управлением планировщика FreeRTOS, обеспечивая многозадачность на ESP32.

Задачи FreeRTOS должны возвращать void и принимать единственный аргумент, который можно использовать для передачи данных в функцию (в нашем случае не используется).

void BlinkTask(void *parameter) {

Конструкция for(;;) создаёт бесконечный цикл, чтобы задача выполнялась бесконечно, пока не будет явно остановлена. Это аналогично функции loop(), используемой в коде Arduino.

for (;;) { // Infinite loop

Затем мы используем функцию digitalWrite() для включения и выключения светодиода. Обратите внимание, что вместо типичной функции delay() мы используем vTaskDelay().

digitalWrite(LED_PIN, HIGH);
Serial.println("BlinkTask: LED ON");
vTaskDelay(1000 / portTICK_PERIOD_MS); // 1000ms
digitalWrite(LED_PIN, LOW);
Serial.println("BlinkTask: LED OFF");
vTaskDelay(1000 / portTICK_PERIOD_MS);

vTaskDelay()

vTaskDelay() – это функция FreeRTOS, которая приостанавливает задачу на указанное количество тиков, позволяя другим задачам выполняться в это время. Она не блокирует ваш код, как delay(). Функция vTaskDelay() принимает тики. На ESP32 каждый тик обычно равен 1 мс (определяется portTICK_PERIOD_MS), поэтому vTaskDelay(1000 / portTICK_PERIOD_MS) приостанавливает задачу на 1000 мс (1 секунду).

vTaskDelay(1000 / portTICK_PERIOD_MS);

Получение номера ядра

Для демонстрации мы также выводим, на каком ядре выполняется задача. Эту информацию можно получить, вызвав функцию xPortGetCoreID().

Serial.print("BlinkTask running on core ");
Serial.println(xPortGetCoreID());

setup()

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

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

  pinMode(LED_PIN, OUTPUT);

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

Теперь, чтобы фактически создать задачу FreeRTOS и назначить её определённому ядру, нам нужно использовать функцию xTaskCreatePinnedToCore(). Эта функция также указывает функцию задачи, имя, размер стека, параметры, приоритет и дескриптор задачи.

xTaskCreatePinnedToCore(
  BlinkTask,      // Task function
  "BlinkTask",   // Task name
  10000,           // Stack size (bytes)
  NULL,            // Parameters
  1,                   // Priority
  &BlinkTaskHandle,  // Task handle
  1                  // Core 1
);

Функция задачи – это BlinkTask, которую мы определили ранее. Мы также можем дать имя задаче. В данном случае – «BlinkTask».

BlinkTask,     // Task function
"BlinkTask",   // Task name

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

10000,    // Stack size (bytes)

В данном случае у нашей задачи нет параметров, поэтому мы устанавливаем этот параметр в NULL.

NULL,     // Parameters

Мы даём задаче приоритет 1. Чем выше число, тем выше приоритет. В данном случае это не имеет большого значения, потому что у нас только одна задача.

1,    // Priority

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

&BlinkTaskHandle,  // Task handle

И наконец, мы определяем, на каком ядре мы хотим запустить задачу. ESP32 имеет два ядра, обозначенных как ядро 0 и ядро 1. В этом примере мы используем ядро 1.

1   // Core 1

loop()

Функция loop() пуста, потому что планировщик FreeRTOS будет запускать задачу. Однако в loop() можно добавить код для выполнения любых других команд.

void loop() {
  // Empty because FreeRTOS scheduler runs the task
}

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

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

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

В то же время светодиод должен мигать каждую секунду.

ESP32 мигающий светодиод (выключен) ESP32 мигающий светодиод (включён)

2) Приостановка и возобновление задач

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

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

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

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

Добавьте кнопку к предыдущей схеме. Мы подключаем кнопку к GPIO 23, но вы можете использовать любой другой подходящий GPIO.

ESP32 со светодиодом на GPIO 2 и кнопкой на GPIO 23

Рекомендуемое чтение: Распиновка ESP32: Какие GPIO пины следует использовать?

ESP32: Приостановка и возобновление задач FreeRTOS – Код Arduino

Следующий код прослушивает нажатие кнопки для приостановки или возобновления задачи мигания FreeRTOS.

/*
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-freertos-arduino-tasks/
  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 LED1_PIN 2
#define BUTTON_PIN 23

// Task handle
TaskHandle_t BlinkTaskHandle = NULL;

// Volatile variables for ISR
volatile bool taskSuspended = false;
volatile uint32_t lastInterruptTime = 0;
const uint32_t debounceDelay = 100; // debounce period

void IRAM_ATTR buttonISR() {
  // Debounce
  uint32_t currentTime = millis();
  if (currentTime - lastInterruptTime < debounceDelay) {
    return;
  }
  lastInterruptTime = currentTime;

  // Toggle task state
  taskSuspended = !taskSuspended;
  if (taskSuspended) {
    vTaskSuspend(BlinkTaskHandle);
    Serial.println("BlinkTask Suspended");
  } else {
    vTaskResume(BlinkTaskHandle);
    Serial.println("BlinkTask Resumed");
  }
}

void BlinkTask(void *parameter) {
  for (;;) { // Infinite loop
    digitalWrite(LED1_PIN, HIGH);
    Serial.println("BlinkTask: LED ON");
    vTaskDelay(1000 / portTICK_PERIOD_MS); // 1000ms
    digitalWrite(LED1_PIN, LOW);
    Serial.println("BlinkTask: LED OFF");
    Serial.print("BlinkTask running on core ");
    Serial.println(xPortGetCoreID());
    vTaskDelay(1000 / portTICK_PERIOD_MS);
  }
}

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

  // Initialize pins
  pinMode(LED1_PIN, OUTPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP); // Internal pull-up resistor

  // Attach interrupt to button
  attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING);

  // Create task
  xTaskCreatePinnedToCore(
    BlinkTask,         // Task function
    "BlinkTask",       // Task name
    10000,             // Stack size (bytes)
    NULL,              // Parameters
    1,                 // Priority
    &BlinkTaskHandle,  // Task handle
    1                  // Core 1
  );
}

void loop() {
  // Empty because FreeRTOS scheduler runs the task
}

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

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

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

Мы определяем GPIO для светодиода и кнопки.

#define LED1_PIN 2
#define BUTTON_PIN 23

Мы создаём volatile-переменные, которые будут использоваться в ISR (подпрограмме обработки прерывания для кнопки). Переменная taskSuspended используется для определения того, приостановлена задача или нет, а lastInterruptTime и debounceDelay необходимы для устранения дребезга кнопки.

// Volatile variables for ISR
volatile bool taskSuspended = false;
volatile uint32_t lastInterruptTime = 0;
const uint32_t debounceDelay = 100; // debounce period

Для обнаружения нажатий кнопки мы используем прерывания. При использовании прерываний нам нужно определить подпрограмму обработки прерывания (функцию, которая выполняется в оперативной памяти ESP32). В данном случае мы создаём функцию buttonISR(). Мы должны добавить IRAM_ATTR к определению функции, чтобы она выполнялась в RAM.

void IRAM_ATTR buttonISR() {

Рекомендуемое чтение: ESP32 с датчиком движения PIR с использованием прерываний и таймеров.

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

// Debounce
uint32_t currentTime = millis();
if (currentTime - lastInterruptTime < debounceDelay) {
  return;
}
lastInterruptTime = currentTime;

// Toggle task state
taskSuspended = !taskSuspended;

Приостановка и возобновление задач

Затем, если переменная taskSuspended равна true, мы вызываем функцию vTaskSuspend() и передаём дескриптор задачи в качестве аргумента. Здесь вы можете увидеть одно из применений дескриптора задачи (Task Handle). Это способ ссылаться на задачу для управления ею.

if (taskSuspended) {
  vTaskSuspend(BlinkTaskHandle);

Если переменная taskSuspended равна false, мы вызываем функцию vTaskResume() для возобновления выполнения задачи.

} else {
  vTaskResume(BlinkTaskHandle);

setup()

В setup() мы должны объявить кнопку как прерывание следующим образом:

attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING);

Остальная часть кода аналогична предыдущему примеру.

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

Загрузите код на плату ESP32.

Светодиод начнёт мигать. Если вы нажмёте кнопку, светодиод перестанет мигать. Нажмите снова, и светодиод снова начнёт мигать.

ESP32 с мигающим светодиодом и кнопкой для приостановки и возобновления задачи

В то же время вы должны получить всю информацию в Serial Monitor.

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

3) Создание и запуск нескольких задач

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

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

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

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

Мы подключим два светодиода к ESP32. Мы будем использовать GPIO 2 и 4. Вы можете использовать любые другие подходящие GPIO, если измените код соответствующим образом.

Два светодиода подключены к ESP32 - один на GPIO2, другой на GPIO4

ESP32: Создание и запуск нескольких задач – Код Arduino

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

/*
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-freertos-arduino-tasks/
  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 LED1_PIN 2
#define LED2_PIN 4

TaskHandle_t Task1Handle = NULL;
TaskHandle_t Task2Handle = NULL;

void Task1(void *parameter) {
  pinMode(LED1_PIN, OUTPUT);
  for (;;) {
    digitalWrite(LED1_PIN, HIGH);
    Serial.println("Task1: LED1 ON");
    vTaskDelay(1000 / portTICK_PERIOD_MS);
    digitalWrite(LED1_PIN, LOW);
    Serial.println("Task1: LED1 OFF");
    vTaskDelay(1000 / portTICK_PERIOD_MS);
  }
}

void Task2(void *parameter) {
  pinMode(LED2_PIN, OUTPUT);
  for (;;) {
    digitalWrite(LED2_PIN, HIGH);
    Serial.println("Task2: LED2 ON");
    vTaskDelay(333 / portTICK_PERIOD_MS);
    digitalWrite(LED2_PIN, LOW);
    Serial.println("Task2: LED2 OFF");
    vTaskDelay(333 / portTICK_PERIOD_MS);
  }
}

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

  xTaskCreatePinnedToCore(
    Task1,             // Task function
    "Task1",           // Task name
    10000,             // Stack size (bytes)
    NULL,              // Parameters
    1,                 // Priority
    &Task1Handle,      // Task handle
    1                  // Core 1
  );

  xTaskCreatePinnedToCore(
    Task2,            // Task function
    "Task2",          // Task name
    10000,            // Stack size (bytes)
    NULL,             // Parameters
    1,                // Priority
    &Task2Handle,     // Task handle
    1                 // Core 1
  );
}

void loop() {
  // Empty because FreeRTOS scheduler runs the task
}

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

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

Создание и запуск нескольких задач так же просто, как создание и запуск одной задачи.

Сначала вам нужно определить ваши задачи. В нашем случае у нас есть две разные задачи для мигания двух разных светодиодов с разной частотой. Мы называем их Task1 и Task2.

void Task1(void *parameter) {
  pinMode(LED1_PIN, OUTPUT);
  for (;;) {
    digitalWrite(LED1_PIN, HIGH);
    Serial.println("Task1: LED1 ON");
    vTaskDelay(1000 / portTICK_PERIOD_MS);
    digitalWrite(LED1_PIN, LOW);
    Serial.println("Task1: LED1 OFF");
    vTaskDelay(1000 / portTICK_PERIOD_MS);
  }
}

void Task2(void *parameter) {
  pinMode(LED2_PIN, OUTPUT);
  for (;;) {
    digitalWrite(LED2_PIN, HIGH);
    Serial.println("Task2: LED2 ON");
    vTaskDelay(333 / portTICK_PERIOD_MS);
    digitalWrite(LED2_PIN, LOW);
    Serial.println("Task2: LED2 OFF");
    vTaskDelay(333 / portTICK_PERIOD_MS);
  }
}

Затем в setup() нам просто нужно создать задачи с помощью xTaskCreatePinnedToCore. В этом примере мы запускаем обе задачи на ядре 1 ESP32, и обе имеют одинаковый приоритет.

xTaskCreatePinnedToCore(
  Task1,             // Task function
  "Task1",           // Task name
  10000,             // Stack size (bytes)
  NULL,              // Parameters
  1,                 // Priority
  &Task1Handle,      // Task handle
  1                  // Core 1
);

xTaskCreatePinnedToCore(
  Task2,            // Task function
  "Task2",          // Task name
  10000,            // Stack size (bytes)
  NULL,             // Parameters
  1,                // Priority
  &Task2Handle,     // Task handle
  1                 // Core 1
);

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

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

ESP32 с двумя задачами FreeRTOS - мигание двух светодиодов с разной частотой

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


4) Создание и запуск нескольких задач на разных ядрах (ESP32 Dual-Core)

Большинство моделей ESP32 имеют два ядра, обозначенных как ядро 0 и ядро 1. По умолчанию, когда мы запускаем код в Arduino IDE, код выполняется на ядре 1. В этом разделе мы покажем, как запускать разные задачи на разных ядрах ESP32.

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

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

Задачи на нескольких ядрах

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

/*
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-freertos-arduino-tasks/
  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 LED1_PIN 2
#define LED2_PIN 4

TaskHandle_t Task1Handle = NULL;
TaskHandle_t Task2Handle = NULL;

void Task1(void *parameter) {
  pinMode(LED1_PIN, OUTPUT);
  for (;;) {
    digitalWrite(LED1_PIN, HIGH);
    Serial.println("Task1: LED1 ON");
    vTaskDelay(1000 / portTICK_PERIOD_MS);
    digitalWrite(LED1_PIN, LOW);
    Serial.println("Task1: LED1 OFF");
    vTaskDelay(1000 / portTICK_PERIOD_MS);
    Serial.print("Task 1 running on core ");
    Serial.println(xPortGetCoreID());
  }
}

void Task2(void *parameter) {
  pinMode(LED2_PIN, OUTPUT);
  for (;;) {
    digitalWrite(LED2_PIN, HIGH);
    Serial.println("Task2: LED2 ON");
    vTaskDelay(333 / portTICK_PERIOD_MS);
    digitalWrite(LED2_PIN, LOW);
    Serial.println("Task2: LED2 OFF");
    vTaskDelay(333 / portTICK_PERIOD_MS);
    Serial.print("Task 2 running on core ");
    Serial.println(xPortGetCoreID());
  }
}

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

  xTaskCreatePinnedToCore(
    Task1,             // Task function
    "Task1",           // Task name
    10000,             // Stack size (bytes)
    NULL,              // Parameters
    1,                 // Priority
    &Task1Handle,      // Task handle
    1                  // Core 1
  );

  xTaskCreatePinnedToCore(
    Task2,            // Task function
    "Task2",          // Task name
    10000,            // Stack size (bytes)
    NULL,             // Parameters
    1,                // Priority
    &Task2Handle,     // Task handle
    0                 // Core 0
  );
}

void loop() {
  // Empty because FreeRTOS scheduler runs the task
}

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

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

Единственное отличие в этом примере – мы определяем Task2 для выполнения на ядре 0, как показано ниже.

xTaskCreatePinnedToCore(
  Task2,            // Task function
  "Task2",          // Task name
  10000,            // Stack size (bytes)
  NULL,             // Parameters
  1,                // Priority
  &Task2Handle,     // Task handle
  0                 // Core 0
);

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

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

ESP32 с двумя задачами FreeRTOS - мигание двух светодиодов с разной частотой

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

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

5) Использование памяти задачами

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

Стек (Stack) и куча (Heap) – это два типа памяти, используемых задачами FreeRTOS на ESP32, каждый из которых выполняет свою уникальную роль в управлении памятью программы.

Что такое использование стека? Стек – это выделенная область памяти для каждой задачи FreeRTOS, используемая для хранения временных данных, таких как локальные переменные, информация о вызовах функций и состояние задачи во время выполнения. Каждая задача имеет свой собственный стек, выделяемый при создании задачи. Вы видели, что в предыдущих примерах мы выделяли стек размером 10000 байт для каждой задачи.

Существует функция, которую можно вызвать внутри задачи для определения использования стека: функция uxTaskGetStackHighWaterMark(). Эта функция определяет выделенный размер стека, который не используется.

Что такое использование кучи? Куча – это общий пул памяти в SRAM ESP32, используемый для динамического выделения памяти, включая стеки задач, буферы и другие данные времени выполнения, выделяемые FreeRTOS или ядром Arduino. Мы можем вызвать функцию xPortGetFreeHeapSize() в нашем коде для определения свободной кучи.

Размер стека задачи и свободная куча – Код

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

Загрузите следующий код на плату.

/*
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-freertos-arduino-tasks/
  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 LED1_PIN 2
#define LED2_PIN 4

TaskHandle_t Task1Handle = NULL;
TaskHandle_t Task2Handle = NULL;

void Task1(void *parameter) {
  pinMode(LED1_PIN, OUTPUT);
  for (;;) {
    digitalWrite(LED1_PIN, HIGH);
    Serial.println("Task1: LED1 ON");
    vTaskDelay(1000 / portTICK_PERIOD_MS);
    digitalWrite(LED1_PIN, LOW);
    Serial.println("Task1: LED1 OFF");
    vTaskDelay(1000 / portTICK_PERIOD_MS);
    Serial.printf("Task1 Stack Free: %u bytes\n", uxTaskGetStackHighWaterMark(NULL));
  }
}

void Task2(void *parameter) {
  pinMode(LED2_PIN, OUTPUT);
  for (;;) {
    digitalWrite(LED2_PIN, HIGH);
    Serial.println("Task2: LED2 ON");
    vTaskDelay(333 / portTICK_PERIOD_MS);
    digitalWrite(LED2_PIN, LOW);
    Serial.println("Task2: LED2 OFF");
    vTaskDelay(333 / portTICK_PERIOD_MS);
    Serial.printf("Task2 Stack Free: %u bytes\n", uxTaskGetStackHighWaterMark(NULL));
  }
}

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.printf("Starting FreeRTOS: Memory Usage\nInitial Free Heap: %u bytes\n", xPortGetFreeHeapSize());

  xTaskCreatePinnedToCore(
    Task1,
    "Task1",
    10000,
    NULL,
    1,
    &Task1Handle,
    1
  );

  xTaskCreatePinnedToCore(
    Task2,
    "Task2",
    10000,
    NULL,
    1,
    &Task2Handle,
    1
  );
}

void loop() {
  static uint32_t lastCheck = 0;
  if (millis() - lastCheck > 5000) {
    Serial.printf("Free Heap: %u bytes\n", xPortGetFreeHeapSize());
    lastCheck = millis();
  }
}

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

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

После загрузки кода на плату вы должны получить нечто подобное в Serial Monitor.

ESP32 с FreeRTOS определение высшей отметки стека задачи

Функция uxTaskGetStackHighWaterMark сообщает, что свободно 8556 байт для Task1 и 8552 байта для Task2, что означает, что каждая задача использует 1444–1448 байт из своего 10000-байтного стека на пике. Таким образом, мы можем значительно уменьшить выделенный размер стека для каждой задачи.

Хороший размер стека должен покрывать пиковое использование задачи (1444 байта) плюс запас безопасности 500–1000 байт для обработки неожиданных увеличений.

В случае свободной кучи функция xPortGetFreeHeapSize() сообщает 247616 свободных байт (не показано на скриншоте), что указывает на оставшуюся кучу после выделения стеков (20000 байт для двух задач) и других системных ресурсов.


Заключение

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

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

Другие руководства по FreeRTOS с ESP32:

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


Источник: :doc:`ESP32 with FreeRTOS (Arduino IDE) – Getting Started: Create Tasks <../esp32-freertos-arduino-tasks/index>` – Random Nerd Tutorials