ESP32:Примеры/Создание задач для использования обоих ядер ESP32
Черновик |
Создание задач для использования обоих ядер ESP32
Плата ESP32 оснащена двумя 32-битными микропроцессорами Xtensa LX6 – ядром 0 и ядром 1. То есть, это 2-ядерная плата. По умолчанию код IDE Arduino запускается на ядре 1. В этом руководстве мы расскажем, как запустить код на втором ядре ESP32 при помощи создания заданий. Благодаря этому вы сможете запускать код одновременно на обоих ядрах, что сделает ESP32 многозадачной.
Примечание: Для того, чтобы добиться многозадачности, необязательно запускать оба ядра.
Необходимые компоненты
- Плата ESP32 – 1 шт.;
- 5-миллиметровый светодиод – 2 шт.;
- Резистор на 330 Ом – 2 шт.;
- Контактная макетная плата – 1 шт.;
- Провода-перемычки;
Введение
Плата 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();
- С помощью этой функции можно запустить две разные задачи на двух разных ядрах одновременно и независимо друг от друга;
В этом руководстве мы продемонстрировали создание задач на скетче-примере с двумя светодиодами. Но вы можете использовать этот метод и для более продвинутых проектов. К примеру, одно ядро можно использовать для чтения данных с датчиков, а другое – для публикации этих данных в веб-сервер, подключенный к системе домашней автоматизации.