Гайд · FreeRTOS · Два ядра
Вынеси работу на второе ядро ESP32 через FreeRTOS
У классического ESP32 два ядра, но твой Arduino loop() всегда крутится лишь
на одном. Как только в loop() попадает медленное чтение датчика или разбор данных — веб-страница
подтормаживает, а Wi-Fi икает. Решение уже на чипе: FreeRTOS. Создай задачу, закрепи её за
другим ядром — и они пойдут бок о бок.
Это шаг после 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()). Правило: передавай копии через очередь, не дели
глобалы.
Грабли, которые перезагружают плату
- Слишком маленький стек. 1–2 КБ хватит для мигалки; всё, что трогает Wi-Fi, JSON или
String, хочет 4–8 КБ. Переполнение стека мгновенно перезагружает. delay()внутри задачи блокирует слот планировщика этого ядра — используйvTaskDelay(pdMS_TO_TICKS(ms)).- Watchdog ядра 0. Жадная задача на ядре 0, которая не отдаёт управление, роняет
task-watchdog в панику. Всегда делай
vTaskDelay()хотя бы раз за цикл. Serialиз двух задач может перемешаться; для нормального лога гони печать через одну задачу (снова очередь).- Приоритеты относительны. Не делай всё высоким — начинай большинство задач с 1.
⚠ Какие ESP реально двухъядерные
- Два ядра: классический ESP32 и ESP32-S3. Тут закрепление за ядром 0 даёт реальный параллелизм.
- Одно ядро: ESP32-C3, C6, S2, H2. Задачи FreeRTOS всё равно работают —
планировщик нарезает им время — но второго ядра для закрепления нет.
xTaskCreatePinnedToCore(…, 1)просто выполнится на ядре 0. Отлично для структуры, но не для разделения нагрузки. - Без FreeRTOS (Arduino-ядро): ESP8266 крутит один кооперативный цикл.
Никакого
xTaskCreate— опирайся на millis() и прерывания. Не уверен, что у тебя? Смотри гайд по чипам.
Приём, который окупается почти в каждом проекте: оставь loop() сети и UI, а
блокирующую работу — датчик, файл, разбор — вынеси на закреплённую задачу, которая отвечает через
очередь. Плата перестаёт заикаться, а дашборд остаётся плавным, пока датчики делают своё дело.
Не уверен, есть ли у платы второе ядро — или какие пины свободны под задачу-датчик?