ESP32:Примеры/Создание задач для использования обоих ядер ESP32

Материал из Онлайн справочника
Перейти к навигацииПерейти к поиску

Перевод: Максим Кузьмин
Проверка/Оформление/Редактирование: Мякишев Е.А.


Создание задач для использования обоих ядер ESP32

Плата ESP32 оснащена двумя 32-битными микропроцессорами Xtensa LX6 – ядром 0 и ядром 1. То есть, это 2-ядерная плата. По умолчанию код IDE Arduino запускается на ядре 1. В этом руководстве мы расскажем, как запустить код на втором ядре ESP32 при помощи создания заданий. Благодаря этому вы сможете запускать код одновременно на обоих ядрах, что сделает ESP32 многозадачной.

Примечание

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

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

Введение

Плата ESP32 оснащена двумя 32-битными микропроцессорами Xtensa LX6. То есть это 2-ядерная плата:

  • Ядро 0;
  • Ядро 1;

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

Когда мы загружаем код на ESP32 через IDE Arduino, он просто запускается и все – нам не нужно беспокоиться о том, на каком ядре выполняется этот код.

Но есть функция, с помощью которой можно узнать, на каком именно ядре запущен код:

xPortGetCoreID()

Воспользовавшись этой функцией в скетче IDE Arduino, вы увидите, что оба главных блока скетча – setup() и loop() – выполняются на ядре 1. Можете проверить это, загрузив на ESP32 код ниже.

/*********
  Руи Сантос
  Более подробно о проекте на: http://randomnerdtutorials.com  
*********/

void setup() {
  Serial.begin(115200);
  Serial.print("setup() running on core ");
           //  "Блок setup() выполняется на ядре "
  Serial.println(xPortGetCoreID());
}

void loop() {
  Serial.print("loop() running on core ");
           //  "Блок loop() выполняется на ядре "
  Serial.println(xPortGetCoreID());
}

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

IDE Arduino поддерживает использование для ESP32 операционной системы FreeRTOS. Она позволяет паралельно выполнять на микропроцессорах платы несколько независимых друг от друга задач.

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

И мы можем создавать задачи, привязывая определенные фрагменты кода к определенным ядрам. Создавая задачу, вы можете выбрать, на каком ядре его нужно запустить и с каким приоритетом. Самый слабый приоритет – это «0». Процессор в первую очередь выполняет задачи со высоким приоритетом.

Итак, чтобы создать задачу, нужно сделать следующее:

  • Создаем идентификатор задачи. Например, «Task1»:
TaskHandle_t Task1;
  • В блоке setup() при помощи функции xTaskCreatePinnedToCore() создаем задачу и привязываем ее к ядру 0 (последний параметр). Кроме того, в параметрах этой функции задаются код самой задачи, приоритет и т.д. Выглядит она вот так:
xTaskCreatePinnedToCore(
 Task1code, /* Функция, содержащая код задачи */
 "Task1", /* Название задачи */
 10000, /* Размер стека в словах */
 NULL, /* Параметр создаваемой задачи */
 0, /* Приоритет задачи */
 &Task1, /* Идентификатор задачи */
 0); /* Ядро, на котором будет выполняться задача */
  • После создания задачи нам нужно создать функцию, содержащую код для созданной задачи. В нашем случае нужно создать функцию Task1code(). Выглядит она вот так:
Void Task1code( void * parameter) {
  for(;;){
    Код для задачи «Task1»  бесконечный цикл
    (...)
  }
}

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

Если вы во время выполнения кода хотите удалить созданную задачу, это можно сделать при помощи функции vTaskDelete(), указав в ее параметре идентификатор задачи (Task1):

vTaskDelete(Task1);

Теперь давайте посмотрим, как все это работает на практике.

Создание задач для разных ядер – пример

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

Примечание

На этой схеме изображена 36-контактная версия платы ESP32 DEVKIT DOIT V1. Если у вас какая-то другая модель, обязательно сверьтесь с ее распиновкой.

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

  • Задача Task1 – на ядре 0
  • Задача Task2 – на ядре 1

Загрузите скетч ниже на ESP32.

/*********
  Руи Сантос
  Более подробно о проекте на: http://randomnerdtutorials.com  
*********/

TaskHandle_t Task1;
TaskHandle_t Task2;

// Контакты для светодиодов:
const int led1 = 2;
const int led2 = 4;

void setup() {
  Serial.begin(115200); 
  pinMode(led1, OUTPUT);
  pinMode(led2, OUTPUT);

  // Создаем задачу с кодом из функции Task1code(),
  // с приоритетом 1 и выполняемую на ядре 0:
  xTaskCreatePinnedToCore(
                    Task1code,   /* Функция задачи */
                    "Task1",     /* Название задачи */
                    10000,       /* Размер стека задачи */
                    NULL,        /* Параметр задачи */
                    1,           /* Приоритет задачи */
                    &Task1,      /* Идентификатор задачи,
                                    чтобы ее можно было отслеживать */
                    0);          /* Ядро для выполнения задачи (0) */                  
  delay(500); 

  // Создаем задачу с кодом из функции Task2code(),
  // с приоритетом 1 и выполняемую на ядре 1:
  xTaskCreatePinnedToCore(
                    Task2code,   /* Функция задачи */
                    "Task2",     /* Название задачи */
                    10000,       /* Размер стека задачи */
                    NULL,        /* Параметр задачи */
                    1,           /* Приоритет задачи */
                    &Task2,      /* Идентификатор задачи,
                                    чтобы ее можно было отслеживать */
                    1);          /* Ядро для выполнения задачи (1) */
    delay(500); 
}

// Функция Task1code: мигает светодиодом каждые 1000 мс:
void Task1code( void * pvParameters ){
  Serial.print("Task1 running on core ");
           //  "Задача Task1 выполняется на ядре "
  Serial.println(xPortGetCoreID());

  for(;;){
    digitalWrite(led1, HIGH);
    delay(1000);
    digitalWrite(led1, LOW);
    delay(1000);
  } 
}

// Функция Task2code: мигает светодиодом каждые 700 мс:
void Task2code( void * pvParameters ){
  Serial.print("Task2 running on core ");
           //  "Задача Task2 выполняется на ядре "
  Serial.println(xPortGetCoreID());

  for(;;){
    digitalWrite(led2, HIGH);
    delay(700);
    digitalWrite(led2, LOW);
    delay(700);
  }
}

void loop() {
  
}

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

Примечание

В этом коде создается две задачи. Выполнение одной из них привязывается к ядру 0, а другое – к ядру 1. По умолчанию Arduino-скетчи выполняются на ядре 1, поэтому код для задачи Task2 можно просто написать в блок loop(), не создавая для него отдельной задачи. Но в нашем случае мы создадим обе задачи – в обучающих целях. Впрочем, такой подход – организация кода по задачам – порой более практичен, но это зависит от особенностей проекта.

Код ниже начинается с создания идентификаторов для обеих задач – Task1 и Task2.

TaskHandle_t Task1;
TaskHandle_t Task2;

Задаем контакты GPIO2 и GPIO4 как контакты для подключения светодиодов.

const int led1 = 2;
const int led2 = 4;

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

Serial.begin(115200);

Переключаем контакты светодиодов в режим «OUTPUT» (т.е. в режим вывода данных).

pinMode(led1, OUTPUT);
pinMode(led2, OUTPUT);

Затем создаем задачу Task1 при помощи функции xTaskCreatePinnedToCore().

xTaskCreatePinnedToCore(
                    Task1code,   /* Функция задачи */
                    "Task1",     /* Название задачи */
                    10000,       /* Размер стека задачи */
                    NULL,        /* Параметр задачи */
                    1,           /* Приоритет задачи */
                    &Task1,      /* Идентификатор задачи,
                                    чтобы ее можно было отслеживать */
                    0);          /* Ядро для выполнения задачи (0) */

Эта задача будет выполнять код из функции Task1code(). Значит далее в коде нам нужно будет создать эту функцию. Мы даем ей приоритет 1 и привязываем к ядру 0.

Далее аналогичным образом создаем задачу Task2.

xTaskCreatePinnedToCore(
                    Task2code,   /* Функция задачи */
                    "Task2",     /* Название задачи */
                    10000,       /* Размер стека задачи */
                    NULL,        /* Параметр задачи */
                    1,           /* Приоритет задачи */
                    &Task2,      /* Идентификатор задачи,
                                    чтобы ее можно было отслеживать */
                    1);          /* Ядро для выполнения задачи (1) */

Создав задачи, создаем функции с кодом для этих задач.

// Функция Task1code: мигает светодиодом каждые 1000 мс:
void Task1code( void * pvParameters ){
  Serial.print("Task1 running on core ");
           //  "Задача Task1 выполняется на ядре "
  Serial.println(xPortGetCoreID());

  for(;;){
    digitalWrite(led1, HIGH);
    delay(1000);
    digitalWrite(led1, LOW);
    delay(1000);
  } 
}

Функция в задаче Task1 называется Task1code(), но вы можете назвать ее как заблагорассудится. В целях отладки сначала она напечатает в мониторе порта ядро, на котором выполняется задача:

Serial.print("Task1 running on core ");
         //  "Задача Task1 выполняется на ядре "
Serial.println(xPortGetCoreID());

Затем создаем бесконечный цикл, который работает аналогично loop() в скетче IDE Arduino. В этом цикле мигаем светодиодом «LED1» каждую секунду.

То же самое происходит и в функции для задачи Task2, но время мигания светодиода – другое.

// Функция Task2code: мигает светодиодом каждые 700 мс:
void Task2code( void * pvParameters ){
  Serial.print("Task2 running on core ");
           //  "Задача Task2 выполняется на ядре "
  Serial.println(xPortGetCoreID());

  for(;;){
    digitalWrite(led2, HIGH);
    delay(700);
    digitalWrite(led2, LOW);
    delay(700);
  }
}

Наконец, в блоке loop() не пишем ничего.

void loop() { 

}
Примечание

Ранее уже говорилось, но напомним – код в блоке loop() выполняется на ядре 1. Поэтому вместо создания задачи для выполнения на ядре 1 вы можете просто вписать этот код в блок loop().

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

Загрузите этот код на ESP32, но перед этим убедитесь, что выбрали в IDE Arduino правильные плату и COM-порт.

Откройте монитор порта на скорости 115200 бод. Там должна появиться примерно такая информация:

Как и ожидалось, задача Task1 выполняется на ядре 0, а Task2 – на ядре 1.

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

Итого

Итак, в этой статье мы узнали следующее:

  • Плата ESP32 имеет два ядра;
  • Скетчи IDE Arduino по умолчанию запускаются на ядре 1;
  • Чтобы использовать ядро 0, необходимо создать задачи;
  • Привязка задач к ядрам осуществляется с помощью функции xTaskCreatePinnedToCore();
  • С помощью этой функции можно запустить две разные задачи на двух разных ядрах одновременно и независимо друг от друга;

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

См.также

Внешние ссылки