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

Материал из Онлайн справочника
Версия от 23:10, 7 марта 2019; Myagkij (обсуждение | вклад) (→‎Итого)
(разн.) ← Предыдущая версия | Текущая версия (разн.) | Следующая версия → (разн.)
Перейти к навигацииПерейти к поиску

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


Черновик


Создание задач для использования обоих ядер 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();
  • С помощью этой функции можно запустить две разные задачи на двух разных ядрах одновременно и независимо друг от друга;

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

См.также

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