ESP32 с FreeRTOS (Arduino IDE) – Начало работы: Создание задач
В этом руководстве мы познакомим вас с основными концепциями FreeRTOS и покажем, как использовать его с ESP32 и Arduino IDE. Вы научитесь создавать одиночные и множественные задачи, приостанавливать и возобновлять задачи, запускать код на двух ядрах ESP32 и вычислять подходящий размер стека (памяти) для каждой задачи.
FreeRTOS – это операционная система реального времени, которая позволяет ESP32 управлять несколькими задачами и запускать их одновременно плавно и эффективно. Она встроена в ESP32 и полностью интегрирована как с ядром Arduino, так и с Espressif IoT Development Framework (IDF).
В этом руководстве мы рассмотрим следующие темы:
Предварительные требования
Это руководство посвящено программированию ESP32 с использованием ядра Arduino. Прежде чем продолжить, у вас должно быть установлено ядро ESP32 Arduino в вашей Arduino IDE. Следуйте следующему руководству для установки ESP32 в Arduino IDE, если вы ещё этого не сделали.
Что такое FreeRTOS?
FreeRTOS – это легковесная операционная система реального времени (RTOS) с открытым исходным кодом. Она предоставляет фреймворк для одновременного выполнения нескольких задач, каждая со своим собственным приоритетом и расписанием выполнения. Вместо выполнения кода строка за строкой, FreeRTOS позволяет создавать независимые задачи, между которыми ESP32 может быстро переключаться на основе приоритета задачи.
Она также предоставляет инструменты, такие как очереди и семафоры, чтобы задачи могли беспрепятственно обмениваться данными друг с другом.
Это делает ваш код более организованным и отзывчивым, особенно когда 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.
Необходимые компоненты
Для этого примера вам понадобятся следующие компоненты:
Плата ESP32 – читайте Лучшие платы разработки ESP32
Резистор 220 Ом (или аналогичные значения)
Схема подключения
Подключите светодиод к GPIO 2, как показано на следующей схеме.
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. Вы должны получить аналогичный результат.
В то же время светодиод должен мигать каждую секунду.
2) Приостановка и возобновление задач
В этом примере вы узнаете, как приостановить и возобновить задачу FreeRTOS на ESP32. Мы создадим задачу, которая мигает светодиодом, и используем кнопку для управления ею. При нажатии кнопки задача будет приостановлена и мигание прекратится, а повторное нажатие возобновит задачу.
Необходимые компоненты
Для этого примера вам понадобятся следующие компоненты:
Плата ESP32 – читайте Лучшие платы разработки ESP32
Резистор 220 Ом (или аналогичные значения)
Схема подключения
Добавьте кнопку к предыдущей схеме. Мы подключаем кнопку к GPIO 23, но вы можете использовать любой другой подходящий GPIO.
Рекомендуемое чтение: Распиновка 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.
Светодиод начнёт мигать. Если вы нажмёте кнопку, светодиод перестанет мигать. Нажмите снова, и светодиод снова начнёт мигать.
В то же время вы должны получить всю информацию в Serial Monitor.
3) Создание и запуск нескольких задач
В этом разделе вы узнаете, как создавать и запускать несколько задач FreeRTOS одновременно. В качестве примера мы создадим две задачи для мигания двух разных светодиодов.
Необходимые компоненты
Для этого примера вам понадобятся следующие компоненты:
Плата ESP32 – читайте Лучшие платы разработки ESP32
2x Светодиод
2x Резистор 220 Ом (или аналогичные значения)
Схема подключения
Мы подключим два светодиода к ESP32. Мы будем использовать GPIO 2 и 4. Вы можете использовать любые другие подходящие GPIO, если измените код соответствующим образом.
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, чтобы он начал выполнять код. У вас будут два разных светодиода, мигающих с разной частотой.
Как видите, с помощью задач 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, результат будет таким же, как в предыдущем примере. Два светодиода мигают с разной частотой.
В 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.
Функция uxTaskGetStackHighWaterMark сообщает, что свободно 8556 байт для Task1 и 8552 байта для Task2, что означает, что каждая задача использует 1444–1448 байт из своего 10000-байтного стека на пике. Таким образом, мы можем значительно уменьшить выделенный размер стека для каждой задачи.
Хороший размер стека должен покрывать пиковое использование задачи (1444 байта) плюс запас безопасности 500–1000 байт для обработки неожиданных увеличений.
В случае свободной кучи функция xPortGetFreeHeapSize() сообщает 247616 свободных байт (не показано на скриншоте), что указывает на оставшуюся кучу после выделения стеков (20000 байт для двух задач) и других системных ресурсов.
Заключение
Это руководство было подробным введением в FreeRTOS с ESP32. Вы узнали, как создавать одиночные и множественные задачи, назначать ядро для каждой задачи, приостанавливать и возобновлять задачи и даже вычислять стек задачи.
Использование FreeRTOS с ESP32 – отличный выбор, потому что оно позволяет выполнять несколько задач одновременно простым способом, с приоритетами для выполнения наиболее критических задач в первую очередь.
Другие руководства по FreeRTOS с ESP32:
ESP32 с FreeRTOS: Очереди (Queues) – Межзадачная коммуникация (Arduino IDE)
ESP32 с FreeRTOS: Начало работы с семафорами (Semaphores) (Arduino IDE)
ESP32 с FreeRTOS: Программные таймеры/Прерывания таймеров (Arduino IDE)
Узнайте больше о ESP32 с нашими ресурсами:
Источник: :doc:`ESP32 with FreeRTOS (Arduino IDE) – Getting Started: Create Tasks <../esp32-freertos-arduino-tasks/index>` – Random Nerd Tutorials