Как использовать двухъядерный ESP32 с Arduino IDE

ESP32 оснащён двумя 32-битными микропроцессорами Xtensa LX6: ядро 0 и ядро 1. Таким образом, он является двухъядерным. Когда мы запускаем код в Arduino IDE, по умолчанию он выполняется на ядре 1. В этом руководстве мы покажем вам, как запускать код на втором ядре ESP32, создавая задачи FreeRTOS. Вы сможете выполнять фрагменты кода одновременно на обоих ядрах, обеспечивая многозадачность вашего ESP32.

Руководство по использованию двухъядерного ESP32 с FreeRTOS в Arduino IDE

Обновлено 26 ноября 2025 года.

Примечание: вам не обязательно использовать оба ядра для реализации многозадачности.

Двухъядерный ESP32 – Введение

ESP32 оснащён двумя 32-битными микропроцессорами Xtensa LX6: ядро 0 и ядро 1.*

Блок-схема двухъядерного ESP32

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

  • На базе ESP32-S2:

    • ESP32-S2-Saola-1 (плата разработчика от Espressif)

    • ESP32-S2-Kaluga-1 (мультимедийный набор разработчика с поддержкой дисплея/камеры)

  • На базе ESP32-C3:

    • ESP32-C3-DevKitM-1 (мини-плата разработчика от Espressif)

    • ESP32-C3-WROOM-0

    • Seeed Studio XIAO ESP32-C3

    • Waveshare ESP32-C3-Zero

  • На базе ESP32-C6:

    • ESP32-C6-DevKitC-1 (плата разработчика от Espressif)

    • Seeed Studio XIAO ESP32-C6

  • На базе ESP32-H2:

    • ESP32-H2-DevKitM-1

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

Существует функция, которая возвращает номер ядра, на котором выполняется код:

xPortGetCoreID()

Если вы используете эту функцию в скетче Arduino, вы увидите, что и setup(), и loop() выполняются на ядре 1. Проверьте это сами, загрузив следующий скетч в ваш ESP32.

/*********
  Rui Santos
  Complete project details at https://randomnerdtutorials.com
*********/

void setup() {
  Serial.begin(115200);
  Serial.print("setup() running on core ");
  Serial.println(xPortGetCoreID());
}

void loop() {
  Serial.print("loop() running on core ");
  Serial.println(xPortGetCoreID());
}

Исходный код

Откройте Монитор порта на скорости 115200 бод и проверьте, на каком ядре выполняется скетч Arduino.

ESP32 определение ядра, на котором выполняется код -- Монитор порта

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

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

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

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

Чтобы назначить определённые части кода конкретному ядру, необходимо создать задачи. При создании задачи вы можете выбрать, на каком ядре она будет выполняться, а также её приоритет. Значения приоритета начинаются с 0, где 0 – самый низкий приоритет. Процессор сначала выполнит задачи с более высоким приоритетом. Для создания задач необходимо выполнить следующие шаги:

Для создания задач необходимо выполнить следующие шаги:

1. Создайте дескриптор задачи. Пример для Task1:

TaskHandle_t Task1;

2. В setup() создайте задачу, назначенную конкретному ядру, используя функцию xTaskCreatePinnedToCore. Эта функция принимает несколько аргументов, включая приоритет и ядро, на котором должна выполняться задача (последний параметр).

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

3. После создания задачи необходимо создать функцию, содержащую код для созданной задачи. В этом примере нужно создать функцию Task1code(). Вот как выглядит функция задачи:

Void Task1code( void * parameter) {
  for(;;) {
    Code for task 1 - infinite loop
    (...)
  }
}

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

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

vTaskDelete(Task1);

Для более подробного руководства по работе с задачами прочитайте это руководство по началу работы с FreeRTOS для ESP32.

Давайте посмотрим, как эти концепции работают на простом примере.

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

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

Вы можете использовать ссылки выше или перейти непосредственно на MakerAdvisor.com/tools, чтобы найти все компоненты для ваших проектов по лучшей цене!

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

Схема подключения двух светодиодов к ESP32 для примера работы с двумя ядрами

Мы создадим две задачи, выполняющиеся на разных ядрах:

  • Task1 выполняется на ядре 0;

  • Task2 выполняется на ядре 1;

ESP32 двухъядерный -- ядро 0 и ядро 1 выполняют задачи

Загрузите следующий скетч в ваш ESP32, чтобы мигать каждым светодиодом на отдельном ядре:

/*********
  Rui Santos
  Complete project details at https://randomnerdtutorials.com
*********/

TaskHandle_t Task1;
TaskHandle_t Task2;

// LED pins
const int led1 = 2;
const int led2 = 4;

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

  //create a task that will be executed in the Task1code() function, with priority 1 and executed on core 0
  xTaskCreatePinnedToCore(
                    Task1code,   /* Task function. */
                    "Task1",     /* name of task. */
                    10000,       /* Stack size of task */
                    NULL,        /* parameter of the task */
                    1,           /* priority of the task */
                    &Task1,      /* Task handle to keep track of created task */
                    0);          /* pin task to core 0 */
  delay(500);

  //create a task that will be executed in the Task2code() function, with priority 1 and executed on core 1
  xTaskCreatePinnedToCore(
                    Task2code,   /* Task function. */
                    "Task2",     /* name of task. */
                    10000,       /* Stack size of task */
                    NULL,        /* parameter of the task */
                    1,           /* priority of the task */
                    &Task2,      /* Task handle to keep track of created task */
                    1);          /* pin task to core 1 */
    delay(500);
}

//Task1code: blinks an LED every 1000 ms
void Task1code( void * pvParameters ){
  Serial.print("Task1 running on core ");
  Serial.println(xPortGetCoreID());

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

//Task2code: blinks an LED every 700 ms
void Task2code( void * pvParameters ){
  Serial.print("Task2 running on core ");
  Serial.println(xPortGetCoreID());

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

void loop() {

}

Исходный код

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

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

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

Код начинается с создания дескрипторов задач для Task1 и Task2, называемых Task1 и Task2.

TaskHandle_t Task1;
TaskHandle_t Task2;

Назначаем GPIO 2 и GPIO 4 для светодиодов:

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

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

Serial.begin(115200);

Объявляем светодиоды как выходы:

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

Затем создаём Task1 с помощью функции xTaskCreatePinnedToCore():

xTaskCreatePinnedToCore(
             Task1code, /* Task function. */
             "Task1",   /* name of task. */
             10000,     /* Stack size of task */
             NULL,      /* parameter of the task */
             1,         /* priority of the task */
             &Task1,    /* Task handle to keep track of created task */
             0);        /* pin task to core 0 */

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

Мы создаём Task2 тем же способом, но назначаем её ядру 1:

xTaskCreatePinnedToCore(
             Task2code,  /* Task function. */
             "Task2",    /* name of task. */
             10000,      /* Stack size of task */
             NULL,       /* parameter of the task */
             1,          /* priority of the task */
             &Task2,     /* Task handle to keep track of created task */
             1);         /* pin task to core 0 */

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

void Task1code( void * pvParameters ){
  Serial.print("Task1 running on core ");
  Serial.println(xPortGetCoreID());

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

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

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

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

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

void Task2code( void * pvParameters ){
  Serial.print("Task2 running on core ");
  Serial.println(xPortGetCoreID());

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

Наконец, функция loop() пуста:

void loop() { }

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

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

Загрузите код в ваш ESP32. Убедитесь, что выбрана правильная плата и COM-порт.

Откройте Монитор порта на скорости 115200 бод. Вы должны увидеть следующие сообщения:

ESP32 двухъядерный -- вывод в Монитор порта, на каком ядре выполняется каждая задача

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

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

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

Заключение

Подводя итог, в этом руководстве вы узнали, что:

  • Большинство моделей ESP32 являются двухъядерными.

  • Скетчи Arduino по умолчанию выполняются на ядре 1.

  • Для использования ядра 0 необходимо создавать задачи FreeRTOS.

  • Вы можете использовать функцию xTaskCreatePinnedToCore() для привязки конкретной задачи к конкретному ядру.

  • Используя этот метод, вы можете выполнять две разные задачи независимо и одновременно, используя два ядра.

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

Если вы хотите узнать больше о задачах и программировании FreeRTOS, вы можете ознакомиться со всеми нашими другими руководствами по FreeRTOS:

Узнайте больше об ESP32:

Если вы хотите узнать больше об ESP32, обязательно ознакомьтесь с курсом: Learn ESP32 with Arduino IDE.


Источник: How to use ESP32 Dual Core with Arduino IDE