RisalDash RisalDash

Гайд · FreeRTOS · Два ядра

Вынеси работу на второе ядро ESP32 через FreeRTOS

У классического ESP32 два ядра, но твой Arduino loop() всегда крутится лишь на одном. Как только в loop() попадает медленное чтение датчика или разбор данных — веб-страница подтормаживает, а Wi-Fi икает. Решение уже на чипе: FreeRTOS. Создай задачу, закрепи её за другим ядром — и они пойдут бок о бок.

FreeRTOSДва ядраxTaskCreateESP32Многозадачность
Схема двух ядер ESP32: WiFi-стек на ядре 0, ваша задача на ядре 1, между ними очередь
Ядро 0 занято радио; ядро 1 целиком ваше — общайтесь через очередь.

Это шаг после millis() и прерываний: те не дают одному циклу блокироваться; FreeRTOS позволяет крутить несколько циклов сразу — на реальных аппаратных ядрах.

Ты уже им пользуешься

Arduino-ядро ESP32 работает поверх FreeRTOS. Твои setup()/loop() — это сами по себе задача (loopTask), закреплённая за ядром 1. Стек Wi-Fi/Bluetooth крутит свои задачи, в основном на ядре 0. Так что «добавить задачу» — не экзотика: ты просто создаёшь ещё одну из того, что система уже делает.

Своя задача

Задача — это функция с бесконечным циклом for(;;), которая никогда не возвращается. Ты создаёшь её один раз, и планировщик крутит её вечно, нарезая процессорное время между ней и всем остальным. Правило, на котором все спотыкаются: внутри задачи используй vTaskDelay(), а не delay()vTaskDelay отдаёт процессор другим задачам, пока ждёт.

Закрепи за вторым ядром

Используй xTaskCreatePinnedToCore() и дай ей ядро 0 — так тяжёлая работа уходит с ядра 1, где живут loop() и веб-сервер:

void sensorTask(void *pv) {
  for (;;) {
    float t = readTemp();              // your slow work
    Serial.printf("temp: %.1f\n", t);
    vTaskDelay(pdMS_TO_TICKS(1000));   // yield — never delay() in a task
  }
}

void setup() {
  Serial.begin(115200);
  xTaskCreatePinnedToCore(
    sensorTask,   // function to run
    "sensor",     // name (for debugging)
    4096,         // stack in bytes — too small = crash/reboot
    NULL,         // parameter
    1,            // priority (higher = more urgent)
    NULL,         // task handle (optional)
    0);           // core 0 — leave core 1 for loop() + Wi-Fi
}

void loop() {
  // core 1: keep the web server / UI / Wi-Fi responsive
}

Семь аргументов: функция, имя, размер стека, параметр, приоритет, необязательный хэндл и ядро (0 или 1). Двое из них кусают новичков: слишком маленький стек — и задача падает, едва тронув память; а приоритет относителен — высокоприоритетная задача, которая не отдаёт управление, заморит всё, что ниже.

Передача данных между ядрами — очередь

Теперь ловушка — общие данные. Обычный глобал, записанный на ядре 0 и прочитанный на ядре 1, может «порваться» или прочитаться устаревшим — одного volatile мало для чего-либо крупнее одного выровненного слова. Чистый путь — очередь: одна задача кладёт значения, другая их забирает, а блокировки берёт на себя FreeRTOS:

QueueHandle_t q;

void producer(void *pv) {
  for (;;) {
    float v = readTemp();
    xQueueSend(q, &v, 0);              // hand the value across — no shared globals
    vTaskDelay(pdMS_TO_TICKS(1000));
  }
}

void setup() {
  q = xQueueCreate(8, sizeof(float));  // room for 8 floats
  xTaskCreatePinnedToCore(producer, "prod", 4096, NULL, 1, NULL, 0);
}

void loop() {
  float v;
  while (xQueueReceive(q, &v, 0) == pdTRUE)   // core 1 drains it safely
    Serial.println(v);
}

Очереди идеальны для схемы «производитель/потребитель» — задача-датчик измеряет, задача-UI показывает. Для общей структуры, к которой лезешь с обеих сторон, защищай её мьютексом (xSemaphoreCreateMutex()). Правило: передавай копии через очередь, не дели глобалы.

Грабли, которые перезагружают плату

⚠ Какие ESP реально двухъядерные

Приём, который окупается почти в каждом проекте: оставь loop() сети и UI, а блокирующую работу — датчик, файл, разбор — вынеси на закреплённую задачу, которая отвечает через очередь. Плата перестаёт заикаться, а дашборд остаётся плавным, пока датчики делают своё дело.

Не уверен, есть ли у платы второе ядро — или какие пины свободны под задачу-датчик?