Добавить в избранное

FreeRTOS на ESP32: задачи, ядра и параллельная работа по-настоящему

Чтение ≈ 4 мин

На ESP32 уже работает FreeRTOS — два ядра, задачи и настоящая параллельность. xTaskCreate, приоритеты, очереди и семафоры между задачами, привязка к ядру и почему loop тоже является задачей.

FreeRTOS уже внутри

В отличие от AVR, ESP32 поставляется с FreeRTOS — маленькой операционной системой реального времени. Каждый ваш скетч уже работает в ней: setup() и loop() — это задача loopTask на ядре 1. Wi-Fi, Bluetooth, TCP-стек — отдельные задачи на ядре 0.

Это значит: вы можете создавать свои задачи, которые работают параллельно — на разных ядрах, с приоритетами, с настоящим вытеснением. Долгая обработка не мешает опросу кнопок, веб-сервер не тормозит логику.

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

void sensorTask(void* param) {
    while (true) {
        readSensors();
        vTaskDelay(pdMS_TO_TICKS(1000));   // неблокирующий delay для задачи
    }
}

void setup() {
    xTaskCreate(
        sensorTask,       // функция задачи
        "sensors",        // имя (для отладки)
        4096,             // размер стека в байтах
        nullptr,          // параметры
        1,                // приоритет (больше — важнее)
        nullptr           // хэндл (если нужен)
    );
}

vTaskDelay — это не delay: он «отдаёт» процессор другим задачам на время сна. Задача, которая никогда не спит, забьёт ядро — всегда делайте vTaskDelay или ждите события.

Два ядра: параллельность по-настоящему

xTaskCreatePinnedToCore(
    heavyTask, "heavy", 8192, nullptr, 1, nullptr,
    0);   // ядро 0 — тяжёлая обработка

// loop и Wi-Fi остаются на ядре 1

Тяжёлую обработку (DSP, видео, вычисления) выносят на ядро 0 — Wi-Fi и системные задачи там уже крутятся, но планировщик разруливает. На ядре 1 — ваш loop и лёгкие задачи.

Очереди: данные между задачами

Глобальные переменные между задачами — race condition. Правильный способ — очереди FreeRTOS:

QueueHandle_t q;

void producerTask(void*) {
    int v = 0;
    while (true) {
        xQueueSend(q, &v, portMAX_DELAY);   // положить (ждать места бесконечно)
        v++;
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

void consumerTask(void*) {
    int v;
    while (true) {
        if (xQueueReceive(q, &v, portMAX_DELAY) == pdTRUE) {
            Serial.println(v);              // спит до появления данных
        }
    }
}

void setup() {
    q = xQueueCreate(10, sizeof(int));      // очередь на 10 int
    xTaskCreate(producerTask, "prod", 2048, nullptr, 1, nullptr);
    xTaskCreate(consumerTask, "cons", 2048, nullptr, 1, nullptr);
}

Очередь — безопасный канал: producer не ждёт чтения, consumer не крутит пустой цикл.

Мьютексы и семафоры

Для защиты общего ресурса (Serial, шина I2C, дисплей):

SemaphoreHandle_t i2cMutex;

void setup() {
    i2cMutex = xSemaphoreCreateMutex();
}

void i2cSafeRead() {
    if (xSemaphoreTake(i2cMutex, pdMS_TO_TICKS(100))) {
        // безопасно работаем с I2C
        xSemaphoreGive(i2cMutex);
    }
}

Семафоры также используются как «сигнал»: задача ждёт xSemaphoreTake — прерывание или другая задача даёт xSemaphoreGive — просыпается мгновенно.

Прерывания и задачи: ISR-to-task

Из прерывания нельзя делать долгую работу — но можно «пнуть» задачу:

SemaphoreHandle_t btnSem;

void IRAM_ATTR isr() {
    BaseType_t woken = pdFALSE;
    xSemaphoreGiveFromISR(btnSem, &woken);  // из прерывания
    if (woken) portYIELD_FROM_ISR();
}

void buttonTask(void*) {
    while (true) {
        xSemaphoreTake(btnSem, portMAX_DELAY);  // спит до прерывания
        handleButton();
    }
}

ISR делает минимум — отдаёт семафор; вся обработка в задаче с нормальным стеком и приоритетом.

Стек задачи и мониторинг

Каждая задача имеет свой стек — при переполнении плата перезагружается. Размер указывается в байтах (не в словах, как у классической FreeRTOS):

uxTaskGetStackHighWaterMark(NULL);   // минимум свободного стека вашей задачи
xTaskCreate(task, "t", 8192, ...);   // для задач с Serial/строками — не жадничайте

Задача с Serial.println, String и вложенными вызовами легко ест 4–8 КБ. Маленький стек — причина загадочных перезагрузок.

Типичные ошибки

  • delay в задаче — блокирует планировщик; vTaskDelay — правильно.
  • Маленький стек — переполнение и ребут без сообщения.
  • Гонки на глобальных переменных — используйте очереди/мьютексы или volatile + атомики.
  • Вызов не-ISR API из прерывания — только *FromISR версии.
  • while(1) без сна — монополизация ядра, watchdog или выдавливание задач.

Итог

FreeRTOS на ESP32 — полноценная ОС: задачи на двух ядрах, xTaskCreate и vTaskDelay вместо delay, очереди для данных, семафоры для событий и защиты ресурсов. Схема: setup создаёт задачи, каждая живёт своей жизнью — loop превращается в одну из задач, а не в единственный цикл.

Полный пример: датчик + дисплей + лог

QueueHandle_t tempQueue;

void sensorTask(void*) {
    while (true) {
        float t = dht.readTemperature();
        xQueueSend(tempQueue, &t, portMAX_DELAY);
        vTaskDelay(pdMS_TO_TICKS(2000));
    }
}

void displayTask(void*) {
    float t;
    while (true) {
        if (xQueueReceive(tempQueue, &t, portMAX_DELAY)) {
            display.setCursor(0, 0);
            display.printf("T=%.1f", t);
            display.display();
        }
    }
}

void logTask(void*) {
    while (true) {
        vTaskDelay(pdMS_TO_TICKS(60000));
        // log to SD / MQTT
    }
}

void setup() {
    tempQueue = xQueueCreate(5, sizeof(float));
    xTaskCreatePinnedToCore(sensorTask, "sens", 4096, nullptr, 1, nullptr, 0);
    xTaskCreatePinnedToCore(displayTask, "disp", 4096, nullptr, 1, nullptr, 1);
    xTaskCreatePinnedToCore(logTask, "log", 4096, nullptr, 1, nullptr, 0);
}

Event groups: флаги событий

Для «жди несколько условий» — EventGroup:

EventGroupHandle_t events;
const int BIT_WIFI = 1, BIT_MQTT = 2, BIT_NTP = 4;

// в задаче ждём всё:
xEventGroupWaitBits(events, BIT_WIFI | BIT_MQTT | BIT_NTP,
                    pdFALSE, pdTRUE, portMAX_DELAY);
// теперь все три готовы

Сколько стека дать задаче

Зависит от того, что она делает:

  • Лёгкий GPIO-полинг: 1024–2048 байт.
  • Serial/String/lcd: 4096–8192 байт.
  • JSON/HTTP: 8192–16384 байта.
  • TLS/SSL: 16384+ байт.

Проверка uxTaskGetStackHighWaterMark() показывает фактический минимум свободного стека — оптимизируйте под него.

Приоритеты

  • 0 — idle-задачи (фон, не важно).
  • 1 — обычные задачи (датчики, UI).
  • 5–10 — важные (логика, безопасность).
  • 20+ — real-time (PID-регулятор, аудио).

Wi-Fi и Bluetooth — высокий приоритет на ядре 0; ваши тяжёлые задачи не должны их вытеснять.

Частые ошибки

  • delay в задаче — vTaskDelay правильно.
  • Маленький стек — таинственные ребуты.
  • Гонки на глобалах — очереди/мьютексы или volatile.
  • ISR вызывает не-ISR API — только *FromISR версии.
  • while(1) без сна — монополизация ядра.
Поделиться
Понравился материал?

Комментарии

Добавить комментарий