ESP32: Brownout detector was triggered и Guru Meditation — как исправить
ESP32 пишет «Brownout detector was triggered» и бесконечно перезагружается? Исправляем питание, кабель и конденсатор, читаем коды rst, Guru Meditation, Task WDT и backtrace.
Сообщение ESP32 brownout detector was triggered означает, что напряжение питания платы на мгновение упало ниже безопасного предела и микросхема перезапустилась, чтобы не работать со сбоями. Проблема почти никогда не в коде, а в питании: слабый USB-порт, тонкий и длинный кабель, слабый стабилизатор или сервоприводы и светодиодные ленты, которые питаются от той же линии, что и ESP32. Вторая крупная причина, по которой ESP32 постоянно перезагружается, — программный сбой; в этом случае в мониторе порта появляются строки вроде Guru Meditation Error, task_wdt или Stack canary.
Хорошая новость: ESP32 сама сообщает, почему перезапустилась, нужно только уметь читать её сообщения. В этом руководстве сначала разберём коды rst в строках загрузки и сообщения об ошибках, потом навсегда избавимся от просадок питания с помощью правильного источника питания, кабеля и конденсатора. Затем перейдём к ошибкам Guru Meditation вроде LoadProhibited и StoreProhibited, переполнению стека, нулевым указателям и таймаутам сторожевого таймера, а с помощью ESP Exception Decoder отследим сбой до конкретной строки кода. В конце посмотрим на пины конфигурации при старте (strapping: GPIO0, 2, 12 и 15), которые могут сорвать загрузку.

Почему ESP32 постоянно перезагружается? Сначала прочитай сообщение
Прежде чем искать неисправность, открой монитор порта на скорости 115200 бод и нажми кнопку EN на плате. При каждой загрузке встроенный в ПЗУ загрузчик ESP32 сначала печатает строку с датой вроде «ets Jun 8 2016 00:22:57», а затем строку вида rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT). Часть rst говорит, почему плата перезапустилась, а часть boot — в каком режиме она стартовала. Если плата застряла в цикле перезагрузок, прямо над этими строками будет сообщение об ошибке, последние слова предыдущего запуска. В таблице ниже собраны сообщения, которые встречаются чаще всего, и то, с чего начинать:
| Что видно в мониторе порта | Значение | Куда смотреть |
|---|---|---|
| Brownout detector was triggered (сработал детектор просадки питания) | Напряжение питания упало ниже порога | Источник питания, кабель, конденсатор |
| Guru Meditation Error: Core 1 panic'ed (LoadProhibited) | Код попытался прочитать данные по недопустимому адресу | Нулевой указатель, выход за границы массива |
| Guru Meditation Error: Core 1 panic'ed (StoreProhibited) | Код попытался записать данные по недопустимому адресу | Нулевой указатель, переполнение буфера |
| Stack canary watchpoint triggered (loopTask): стек переполнен | Стек задачи заполнился | Большие локальные массивы, рекурсия |
| task_wdt: Task watchdog got triggered (сработал сторожевой таймер задач) | Задача не отдавала процессор дольше 5 секунд | Циклы без пауз, зависшие задачи |
| Interrupt wdt timeout on CPU0 / CPU1 | Обработчик прерывания или участок с отключёнными прерываниями выполнялся дольше 300 мс | Долгий ISR, Serial внутри ISR |
| boot:0x3 (DOWNLOAD_BOOT...) waiting for download (ожидание загрузки кода) | Плата стартовала в режиме программирования | GPIO0 в LOW при старте |
| invalid header или flash read err | Не удалось прочитать флеш-память | GPIO12 в HIGH при старте, прерванная загрузка кода |
Что такое «Brownout detector was triggered» и почему это происходит?
Brownout — это кратковременная просадка напряжения без полного пропадания питания. В ESP32 есть детектор просадки питания (brownout detector), который постоянно следит за линией питания, и в ядре Arduino его порог выставлен на самый низкий уровень, около 2,43 V. Как только напряжение падает ниже этого значения, микросхема защищает радиомодуль и флеш-память, печатает в монитор порта «Brownout detector was triggered» и перезапускается. Причина простая: при низком напряжении флеш-память может читаться с ошибками, сохранённые данные могут испортиться, а процессор может выполнять случайные инструкции. Детектор заставляет плату чисто перезагрузиться, вместо того чтобы вести себя непредсказуемо. Модуль ESP32-WROOM-32 рассчитан на работу от 3,0 до 3,6 V, так что когда напряжение на линии доходит до 2,43 V, оно уже давно за пределами безопасной зоны.
После сообщения о просадке в строке загрузки, в зависимости от версии ядра Arduino, будет rst:0xc (SW_CPU_RESET) или rst:0x3 (SW_RESET), потому что прерывание детектора печатает сообщение, а затем перезапускает микросхему программно. Пусть это не сбивает с толку: тот же код записывает в память микросхемы, что причиной была просадка питания, и esp_reset_reason() возвращает ESP_RST_BROWNOUT. Если же срабатывает собственный аппаратный сброс по просадке, в строке будет rst:0xf (RTCWDT_BROWN_OUT_RESET). В ядрах 3.x на основе ESP-IDF 5 эта строка стала информационным сообщением журнала (I BOD: Brownout detector was triggered). Поскольку предварительно скомпилированные библиотеки Arduino выводят только сообщения уровня ошибки, это сообщение обычно вообще не появляется, и плата перезапускается молча; если при старте виден необъяснимый rst:0x3 или rst:0xc, посмотри причину сброса через диагностический скетч ниже. Чтобы понять, почему проседает напряжение, посмотри, какой ток потребляет ESP32:
| Режим работы | Типичный ток (микросхема ESP32) |
|---|---|
| Глубокий сон (таймер RTC включён) | Около 10 µA |
| Лёгкий сон (light sleep) | Около 0,8 mA |
| Wi-Fi выключен, процессор на 240 MHz | 30–68 mA |
| Приём Wi-Fi (RX) | 95–100 mA |
| Передача Wi-Fi, 802.11b, 19,5 dBm | Около 240 mA |
| Калибровка радио при старте и момент подключения | В пиках длительностью в микросекунды может превышать 400 mA |
Разрыв в этой таблице и есть суть проблемы: пока код не включил Wi-Fi, плата спокойно работает примерно на 50 mA, но на строке WiFi.begin() ток за несколько микросекунд вырастает в пять-шесть раз. Если источник питания, кабель и стабилизатор не успевают за таким резким скачком, напряжение проваливается. Поэтому Espressif рекомендует для ESP32 источник питания, способный отдавать не меньше 500 mA. Симптом очень знакомый: плата загружается, печатает в монитор порта несколько строк, сбрасывается в момент запуска Wi-Fi или Bluetooth, и всё превращается в бесконечный цикл. То же самое может случиться, когда двигается сервопривод, щёлкает реле или светодиодная лента загорается белым.
Самые частые причины просадки питания
- Тонкий или длинный USB-кабель: тонкие силовые жилы в дешёвых кабелях могут давать в сумме до 0,5 Ω туда и обратно. По закону Ома пик в 400 mA теряет на таком кабеле 0,2 V.
- Слабый USB-порт или хаб: порты на клавиатурах и мониторах, хабы без внешнего питания и передние порты некоторых ноутбуков дают напряжению просесть при пиковом токе.
- Падение напряжения на стабилизаторе (dropout): AMS1117, который стоит на большинстве плат, требует разницы 1,0–1,3 V между входом и выходом. Если напряжение на пине 5V падает примерно ниже 4,3 V, выход уже не может удержать 3,3 V.
- Батарея напрямую: если подключить Li-ion аккумулятор на 3,7 V к пину 5V, из-за падения напряжения на стабилизаторе выход опустится ниже 3 V; две батарейки AA под нагрузкой быстро проседают ниже минимума ESP32.
- Пин 3.3V на Arduino Uno: с этого пина рекомендуется брать всего около 50 mA; для ESP32 с включённым Wi-Fi этого совсем не достаточно.
- Нагрузки на той же линии: если сервоприводы, реле, зуммеры, DC-моторы или лента WS2812B питаются от тех же 5 V, что и ESP32, каждое их движение тянет напряжение вниз.
- Макетные платы и перемычки: разболтанные контакты и тонкие перемычки добавляют немного сопротивления в каждом соединении; если вести питание через макетную плату, становится только хуже.
- ESP32-CAM: когда одновременно работают камера, Wi-Fi и вспышка, ток становится очень большим; этой плате нужны 5 V и не меньше 2 A. Подробности — в руководстве по ESP32-CAM.
Решение 1: улучши питание и кабель
Первый и самый действенный шаг — более мощное питание. Подключи плату коротким толстым кабелем с передачей данных, в котором нет сомнений, напрямую к USB-порту на задней панели компьютера или к адаптеру 5 V, рассчитанному минимум на 1 A. Порты USB 2.0 рассчитаны на 500 mA, а USB 3.0 — на 900 mA; рекомендованные Espressif 500 mA — это ровно предел порта USB 2.0, поэтому, как только добавляются потери в кабеле, начинаются проблемы. Если одна только замена кабеля убирает ошибку, виновато, скорее всего, было его сопротивление. Сколько напряжения теряет кабель, можно посчитать по формуле V = I × R в калькуляторе закона Ома.

| Схема питания | Риск просадки | Примечание |
|---|---|---|
| Короткий толстый кабель и USB-порт на задней панели | Низкий | Самое простое решение на время разработки |
| Длинный тонкий кабель, передний порт или хаб | Высокий | Сопротивление кабеля тянет напряжение вниз при пиковом токе |
| Адаптер 5 V минимум на 1 A, на пин 5V или VIN | Низкий | Рекомендуется для постоянных проектов |
| Готовый источник 3,3 V прямо на пин 3V3 | Зависит от источника | Должен отдавать не меньше 500 mA и никогда не превышать 3,6 V |
| Li-ion аккумулятор на 3,7 V через AMS1117 | Высокий | Когда на аккумуляторе меньше примерно 4 V, выход падает ниже 3 V |
| Li-ion аккумулятор и LDO-стабилизатор на 3,3 V с малым падением напряжения или понижающе-повышающий преобразователь | Низкий | Даёт стабильные 3,3 V, даже когда аккумулятор разряжается |
| Две батарейки AA прямо на пин 3V3 | Очень высокий | Под нагрузкой напряжение быстро падает ниже минимума ESP32 |
Проекты на батарейках требуют особого внимания. Li-ion аккумулятор вроде 18650 полностью заряженным даёт 4,2 V, а разряженным — около 3,0 V. Такой диапазон не подходит ни для прямого подключения к пину 3V3 (4,2 V больше предела 3,6 V), ни для пина 5V с его AMS1117. Правильное решение — либо подавать питание от аккумулятора через повышающий модуль на 5 V на пин 5V, либо питать линию 3V3 напрямую через LDO-стабилизатор с малым падением напряжения, рассчитанный минимум на 500 mA (например, ME6211 или HT7833), плюс конденсатор. Сколько проект проработает от батареи, можно оценить в калькуляторе времени работы от батареи, а про схему зарядки рассказано в руководстве по модулю зарядки TP4056.
Решение 2: добавь конденсатор между 3V3 и GND
Конденсатор работает как маленький резервуар: во время пика тока он сам отдаёт энергию, пока стабилизатор и кабель не догонят. Поставь электролитический конденсатор на 100–470 µF между пинами 3V3 и GND платы, как можно ближе к ней, а рядом — керамический конденсатор на 100 nF. Электролитический справляется с провалами длительностью в миллисекунды, керамический — с очень короткими высокочастотными пульсациями. Эффект можно грубо оценить по формуле ΔV = I × Δt / C: даже если конденсатор на 470 µF в одиночку отдаёт дополнительные 200 mA в течение 200 µs, напряжение падает всего на 0,085 V. Маркировка 104 на керамическом конденсаторе означает 100 nF; другие коды можно расшифровать в утилите кодов конденсаторов.

Решение 3: питай сервоприводы, моторы и светодиодные ленты отдельно
Даже если сама ESP32 питается правильно, нагрузки на той же линии могут тянуть напряжение вниз. Даже маленький сервопривод вроде SG90 в момент начала движения потребляет несколько сотен mA; светодиод WS2812B на полном белом берёт около 60 mA, так что лента на 30 светодиодов может дойти до 1,8 A. Питай такие нагрузки не от пина 5V или 3V3 ESP32, а от отдельного источника 5 V; например, адаптер на 12 V можно понизить до 5 V понижающим модулем LM2596. Самое важное правило — соединить GND обоих источников; без общей земли у сигнальной линии нет опорного уровня, и сервопривод или лента ведут себя случайным образом. Подробно об этом — в статьях Arduino постоянно перезагружается? и GND и общая земля.

Отключить детектор просадки — это решение?
Самое популярное «решение» в интернете — отключить детектор одной строкой. Код ниже отключает детектор, обнуляя регистр brownout в ESP32, и сообщение об ошибке пропадает. Но провалы напряжения никуда не деваются; просто никто больше о них не предупреждает. Используй этот код только для диагностики: если с отключённым детектором плата работает без проблем, это подтверждает, что дело точно в питании. После этого удали строку и исправь питание.
1#include "soc/soc.h"
2#include "soc/rtc_cntl_reg.h"
3
4void setup() {
5 // YALNIZ TEŞHİS İÇİN: brownout dedektörünü kapat / DIAGNOSIS ONLY: disable the brownout detector
6 WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
7
8 Serial.begin(115200);
9 Serial.println("Brownout dedektoru kapali / brownout detector disabled");
10}
11
12void loop() {
13}Коды причины rst: ESP32 сообщает, почему перезапустилась
Значение rst:, которое ПЗУ печатает при каждой загрузке, показывает, чем закончился предыдущий запуск. При нормальной загрузке значение boot равно 0x13 (SPI_FAST_FLASH_BOOT): плата запускает программу из флеш-памяти. В таблице ниже — коды, которые встречаются чаще всего. Одних кодов ПЗУ не всегда достаточно: и ESP.restart(), и перезапуск после Guru Meditation выглядят как 0xc, а просадка питания в зависимости от версии ядра появляется как 0xc или 0x3. Чтобы их различить, используй скетч с esp_reset_reason() из следующего раздела.
| Код rst | Название | Что означает |
|---|---|---|
| 0x1 | POWERON_RESET | Питание подано впервые или нажата кнопка EN |
| 0x3 | SW_RESET | Программа сбросила всю цифровую систему; в новых версиях ядра появляется и после просадки питания |
| 0x5 | DEEPSLEEP_RESET | Плата проснулась после глубокого сна (это нормально) |
| 0x7 / 0x8 | TG0WDT_SYS_RESET / TG1WDT_SYS_RESET | Сторожевой таймер группы таймеров аппаратно сбросил систему; прерывания слишком долго были отключены или не смог отработать даже обработчик паники |
| 0x9 | RTCWDT_SYS_RESET | Сторожевой таймер RTC сбросил цифровое ядро |
| 0xc | SW_CPU_RESET | Программный перезапуск: ESP.restart(), после Guru Meditation или после просадки питания в ядре Arduino |
| 0xf | RTCWDT_BROWN_OUT_RESET | Аппаратный сброс по просадке питания: нестабильное напряжение питания |
| 0x10 | RTCWDT_RTC_RESET | Сторожевой таймер RTC сбросил всю микросхему вместе с RTC; чаще всего это бывает на платах, которые зависают во время загрузки |
Диагностический скетч: вывести причину сброса, память и стек
Загрузи код ниже отдельным скетчем или добавь его строки в setup() и loop() проблемного проекта. При каждом запуске плата выводит в монитор порта причину сброса понятным текстом, номер загрузки по счёту и состояние памяти. Счётчик, помеченный RTC_DATA_ATTR, хранится в памяти RTC, поэтому переживает программные сбросы и глубокий сон и обнуляется только при полном отключении питания. Если счётчик растёт каждые несколько секунд, сразу ясно: плата в цикле перезагрузок.
1#include <esp_system.h>
2
3RTC_DATA_ATTR uint32_t bootCount = 0; // yazılım resetinde korunur / survives software resets
4
5// Reset nedenini okunabilir yazıya çevir / turn the reset reason into readable text
6const char* reasonText(esp_reset_reason_t r) {
7 switch (r) {
8 case ESP_RST_POWERON: return "POWERON: guc acilisi ya da EN / power-on or EN button";
9 case ESP_RST_SW: return "SW: ESP.restart()";
10 case ESP_RST_PANIC: return "PANIC: Guru Meditation";
11 case ESP_RST_INT_WDT: return "INT_WDT: kesme watchdog'u / interrupt watchdog";
12 case ESP_RST_TASK_WDT: return "TASK_WDT: gorev watchdog'u / task watchdog";
13 case ESP_RST_WDT: return "WDT: diger watchdog / other watchdog";
14 case ESP_RST_DEEPSLEEP: return "DEEPSLEEP: derin uykudan uyanma / deep sleep wake-up";
15 case ESP_RST_BROWNOUT: return "BROWNOUT: gerilim dusmesi / supply voltage dropped";
16 default: return "UNKNOWN: bilinmiyor / unknown";
17 }
18}
19
20void setup() {
21 Serial.begin(115200);
22 delay(1000); // Seri Monitör'ün açılmasını bekle / wait for the Serial Monitor
23 bootCount++;
24 Serial.printf("Acilis sayisi / boot count: %u\n", (unsigned)bootCount);
25 Serial.printf("Reset nedeni / reset reason: %s\n", reasonText(esp_reset_reason()));
26}
27
28void loop() {
29 // Bellek durumu / memory status
30 Serial.printf("Bos heap / free heap: %u B, en dusuk / minimum: %u B, en buyuk blok / largest block: %u B\n",
31 (unsigned)ESP.getFreeHeap(), (unsigned)ESP.getMinFreeHeap(), (unsigned)ESP.getMaxAllocHeap());
32 // loopTask yığınında hiç kullanılmamış en küçük alan / smallest unused space on the loopTask stack
33 Serial.printf("loopTask bos yigin / free stack: %u B\n", (unsigned)uxTaskGetStackHighWaterMark(NULL));
34 delay(5000);
35}Значения, которые печатаются в цикле, выдают и скрытые проблемы. Если свободная куча (free heap) немного уменьшается при каждом проходе, в программе утечка памяти, и плата рано или поздно упадёт. Если самый большой блок намного меньше свободной кучи, память фрагментирована; обычно виноваты объекты String, которые постоянно растут и уменьшаются внутри цикла. Если свободный стек loopTask опускается ниже нескольких сотен байт, переполнение стека совсем близко. Если причина сброса — BROWNOUT, сосредоточься на разделах про питание; если PANIC, TASK_WDT или INT_WDT — на программных разделах ниже.
Что такое Guru Meditation Error? Читаем отчёт о сбое строка за строкой
Guru Meditation Error — это отчёт о сбое, который печатает обработчик паники, когда процессор ESP32 сталкивается с ошибкой, после которой не может продолжить работу; название пришло с экрана ошибок старых компьютеров Amiga. Сначала отчёт выглядит пугающе, но важны в нём всего несколько строк. Core 1 показывает, на каком ядре произошла ошибка: в Arduino setup() и loop() работают на ядре 1, а стек Wi-Fi — на ядре 0. Имя в скобках — тип исключения. Когда отчёт заканчивается, плата печатает «Rebooting...» и перезапускается; в строке загрузки будет rst:0xc.
| Строка | Значение |
|---|---|
| Guru Meditation Error: Core 1 panic'ed (LoadProhibited). Exception was unhandled. (исключение не обработано) | Код на ядре 1 попытался прочитать данные по недопустимому адресу, и ошибка не была обработана |
| PC : 0x400d1a2c | Адрес инструкции, на которой произошёл сбой; если он начинается с 0x400d, из флеш-памяти выполнялся твой код или библиотека |
| EXCCAUSE: 0x0000001c | Код исключения; 0x1c, то есть 28, означает LoadProhibited |
| EXCVADDR: 0x00000000 | Адрес, к которому шло обращение; 0 или очень маленькое число указывает на нулевой указатель |
| Backtrace: 0x400d1a29:0x3ffb2270 0x400d0f6e:0x3ffb2290 ... | Цепочка вызовов; каждая пара имеет вид PC:SP, и декодер превращает её в строку кода |
| ELF file SHA256: ... | Отпечаток программы, в которой произошёл сбой; декодеру нужна точно та же сборка |
| Rebooting... | Обработчик паники программно перезапускает микросхему |
LoadProhibited, StoreProhibited и другие типы исключений
| Исключение | EXCCAUSE | Что произошло? | Частая причина |
|---|---|---|---|
| LoadProhibited | 28 (0x1c) | Чтение по недопустимому адресу | Нулевой или неинициализированный указатель, удалённый объект |
| StoreProhibited | 29 (0x1d) | Запись по недопустимому адресу | Запись через нулевой указатель, выход буфера за границы массива |
| InstrFetchProhibited | 20 (0x14) | Выборка инструкции по недопустимому адресу | Неназначенный указатель на функцию или обратный вызов (callback) |
| IllegalInstruction | 0 (0x0) | Недопустимая инструкция | Повреждённый стек, пропущенный return в функции, которая должна возвращать значение |
| LoadStoreAlignment | 9 (0x9) | Невыровненный доступ к памяти | Приведение массива байтов к типу вроде uint32_t* |
| IntegerDivideByZero | 6 (0x6) | Целочисленное деление на ноль | Деление на счётчик, который может быть равен нулю |
| Unhandled debug exception | - | Отладочное исключение | Stack canary watchpoint triggered: переполнение стека |
| Interrupt wdt timeout on CPU0/CPU1 | - | Истёк сторожевой таймер прерываний | Долгий ISR, Serial или delay внутри ISR |
| Cache disabled but cached memory region accessed (обращение к кэшируемой памяти при отключённом кэше) | - | Обращение к коду во флеш-памяти, пока флеш занята | Обработчик прерывания без IRAM_ATTR |
Самая частая из этих ошибок — использование нулевого указателя (null pointer). NULL — это адрес 0, а в ESP32 вокруг адреса 0 нет читаемой памяти, поэтому чтение оттуда, куда указывает пустой указатель, даёт LoadProhibited, а запись туда — StoreProhibited. EXCVADDR в этом случае — отличная подсказка: 0x00000000 означает обращение прямо по нулевому указателю, а маленькие значения вроде 0x00000004 или 0x00000010 — к полю внутри объекта по пустому указателю (NULL плюс смещение поля). В проектах Arduino чаще всего функция возвращает NULL в значении «не найдено», а это значение используется без проверки. В примере ниже strstr() возвращает NULL, если не находит текст; без проверки плата падает с EXCVADDR 0x00000005.
1const char* response = "temp=23.5;hum=40"; // sunucudan gelen yanıt / reply from a server
2
3void setup() {
4 Serial.begin(115200);
5 delay(500);
6
7 // YANLIŞ: "pres=" yoksa strstr NULL döner ve kart çöker (EXCVADDR 0x00000005)
8 // WRONG: if "pres=" is missing strstr returns NULL and the board crashes (EXCVADDR 0x00000005)
9 // float pressure = atof(strstr(response, "pres=") + 5);
10
11 // DOĞRU: işaretçiyi kullanmadan önce NULL kontrolü yap / RIGHT: check for NULL before using the pointer
12 const char* found = strstr(response, "pres=");
13 if (found != nullptr) {
14 float pressure = atof(found + 5);
15 Serial.println(pressure);
16 } else {
17 Serial.println("pres alani bulunamadi / pres field not found");
18 }
19}
20
21void loop() {
22}Переполнение стека: ошибка «Stack canary watchpoint triggered»
В ESP32 у каждой задачи (task) есть стек фиксированного размера, где хранятся локальные переменные, вызовы функций и адреса возврата. В Arduino у задачи loopTask, которая выполняет setup() и loop(), стек размером 8192 байта. Локальный массив на 10 000 байт внутри loop(), функция, которая вызывает сама себя (рекурсия), или библиотечная функция с большим числом локальных переменных выйдут за этот предел. В конце стека есть защитная область (canary); когда стек до неё доходит, плата печатает Guru Meditation Error: Core 1 panic'ed (Unhandled debug exception) и сразу под этим Debug exception reason: Stack canary watchpoint triggered (loopTask). Имя в скобках — задача, у которой переполнился стек. Иногда та же проблема выглядит как сообщение ***ERROR*** A stack overflow in task loopTask has been detected (обнаружено переполнение стека в задаче loopTask). Вот как это исправить:
- Убери большие массивы из стека: объяви массив глобально или добавь перед ним static; тогда он будет жить в постоянной RAM, а не в стеке.
- Увеличь стек loopTask: добавь в самый верх скетча SET_LOOP_TASK_STACK_SIZE(16 * 1024);, чтобы увеличить стек loop() до 16 КБ.
- Дай своим задачам достаточно места: в ESP32 размер стека, который передаётся в xTaskCreate и xTaskCreatePinnedToCore, указывается в байтах, а не в словах, как во FreeRTOS на других платформах. 1024 байта мало даже для задачи, которая использует Serial.printf; начни с 4096.
- Измеряй: uxTaskGetStackHighWaterMark(NULL) возвращает в байтах наименьший объём стека, который задача до сих пор ни разу не использовала; если он опускается ниже нескольких сотен байт, увеличь стек.
- Избегай рекурсии: преврати функции, которые вызывают сами себя, в циклы или ограничь глубину вызовов.
Сторожевой таймер задач: ошибка «task_wdt: Task watchdog got triggered»
Сторожевой таймер (watchdog) — это таймер, который замечает зависшую программу и спасает плату. Сторожевой таймер задач (Task WDT) в ESP32 ожидает, что зарегистрированные в нём задачи будут через равные промежутки подавать сигнал «я жив». В ядре Arduino по умолчанию он следит за задачей простоя (IDLE) ядра 0, а его таймаут — 5 секунд. Если задача, работающая на ядре 0, 5 секунд не отдаёт процессор, задача IDLE не может выполниться, и в мониторе порта появляется сообщение, начинающееся с E (10257) task_wdt: Task watchdog got triggered. За ним идут список задач, которые не сбросили сторожевой таймер вовремя (обычно - IDLE (CPU 0)), и под заголовком Tasks currently running: задачи, которые выполнялись в этот момент. В ядре Arduino это вызывает панику со строкой task_wdt: Aborting., и плата перезапускается.
Самые частые причины: цикл, закреплённый за ядром 0 через xTaskCreatePinnedToCore, в котором ни разу не вызывается vTaskDelay(); цикл while, который ждёт ответа датчика без таймаута; или delay() и долгие операции внутри обратных вызовов таких библиотек, как AsyncWebServer. В последнем случае в списке вместо IDLE будет задача async_tcp. Решение — разбивать долгие операции на маленькие части и добавлять в циклы короткие паузы вроде vTaskDelay(1). Сама функция loop() по умолчанию не отслеживается; при желании вызови в setup() enableLoopWDT(), и плата будет перезапускаться также, если loop() зависнет дольше чем на 5 секунд. Отключение сторожевого таймера через disableCore0WDT() прячет ошибку, но не исправляет зависший код. Подробнее о задачах и ядрах — в статье FreeRTOS и два ядра ESP32. Код ниже показывает и правильную структуру задачи, и меры против переполнения стека:
1SET_LOOP_TASK_STACK_SIZE(16 * 1024); // loop() yığını 8 KB yerine 16 KB / loop() stack 16 KB instead of 8 KB
2
3volatile uint32_t sampleCount = 0;
4
5// 0. çekirdekte çalışan görev / task running on core 0
6void sensorTask(void* parameter) {
7 for (;;) {
8 sampleCount = sampleCount + 1; // sensörünü burada oku / read your sensor here
9 vTaskDelay(pdMS_TO_TICKS(10)); // IDLE görevine sıra ver, task_wdt tetiklenmez / let IDLE run, no task_wdt
10 }
11}
12
13void setup() {
14 Serial.begin(115200);
15 // Yığın boyutu bayt cinsindendir: 4096 bayt / stack size is in bytes: 4096 bytes
16 xTaskCreatePinnedToCore(sensorTask, "sensorTask", 4096, NULL, 1, NULL, 0);
17}
18
19void loop() {
20 static uint8_t buffer[10000]; // static: yığında değil kalıcı RAM'de / static: in RAM, not on the stack
21 memset(buffer, 0, sizeof(buffer));
22
23 Serial.printf("Ornek / samples: %u, loopTask bos yigin / free stack: %u B\n",
24 (unsigned)sampleCount, (unsigned)uxTaskGetStackHighWaterMark(NULL));
25 delay(1000);
26}Interrupt wdt timeout и IRAM_ATTR: сбои в прерываниях
Сторожевой таймер прерываний (Interrupt WDT) куда менее терпелив: если обработчик прерывания (ISR) или участок кода с отключёнными прерываниями выполняется дольше 300 мс, плата падает с Guru Meditation Error: Core 1 panic'ed (Interrupt wdt timeout on CPU1). Чаще всего причина в Serial.print, delay(), I2C или Wi-Fi внутри ISR. Ещё одна ловушка — флеш-память: пока Preferences, EEPROM или LittleFS пишут во флеш, кэш флеш-памяти на мгновение отключается. Если в этот момент приходит прерывание, а его обработчик лежит во флеш-памяти, плата сообщает Cache disabled but cached memory region accessed. Решение — пометить ISR атрибутом IRAM_ATTR, чтобы они жили в RAM, и внутри только поднимать флаг; настоящую работу выполняет loop(). Переменные, общие для ISR и loop(), должны быть volatile.
1const int BUTTON_PIN = 27; // buton GPIO 27 ile GND arasında / button between GPIO 27 and GND
2volatile bool buttonPressed = false; // ISR ile loop arasında paylaşılan bayrak / flag shared by the ISR and loop
3uint32_t pressCount = 0;
4
5void IRAM_ATTR onButton() { // IRAM_ATTR: fonksiyon flaşta değil RAM'de durur / the function lives in RAM, not flash
6 buttonPressed = true; // yalnızca bayrak kaldır, Serial yok / only set a flag, no Serial
7}
8
9void setup() {
10 Serial.begin(115200);
11 pinMode(BUTTON_PIN, INPUT_PULLUP);
12 attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onButton, FALLING);
13}
14
15void loop() {
16 if (buttonPressed) {
17 buttonPressed = false;
18 pressCount++;
19 Serial.printf("Butona basildi / button pressed: %u\n", (unsigned)pressCount); // yavaş iş burada / slow work here
20 }
21}Превращаем Backtrace в строку кода с ESP Exception Decoder
Адреса в строке Backtrace сами по себе ничего не говорят; инструмент, который превращает их в файл, функцию и номер строки исходного кода, называется ESP Exception Decoder. В старой Arduino IDE 1.8 это был плагин в меню «Инструменты». В Arduino IDE 2 используется одноимённое расширение, созданное сообществом:
- Скачай файл .vsix из раздела Releases проекта dankeboy36/esp-exception-decoder на GitHub. Для Arduino IDE 2 выбирай релиз 1.x; новые релизы 2.x сделаны для VS Code.
- Закрой Arduino IDE и скопируй файл в папку %UserProfile%\.arduinoIDE\plugins на Windows или ~/.arduinoIDE/plugins на macOS и Linux; если папки нет, создай её.
- Запусти IDE, открой скетч, который падает, скомпилируй его кнопкой Проверить (Verify) и загрузи в плату.
- Открой монитор порта и дождись, пока сбой повторится.
- Открой палитру команд сочетанием Ctrl+Shift+P (Cmd+Shift+P на macOS) и выполни ESP Exception Decoder: Show Decoder Terminal.
- Скопируй из монитора порта блок Guru Meditation вместе со строкой Backtrace и вставь его в открывшийся терминал; инструмент покажет для каждого адреса имя файла, функцию и номер строки.
Если расширение установить не получается, то же самое можно сделать вручную. Меню Sketch › Export Compiled Binary («Скетч › Экспорт скомпилированного бинарного файла») кладёт файл с расширением .elf в папку build внутри папки скетча. Команда xtensa-esp32-elf-addr2line -pfiaC -e sketch.ino.elf 0x400d1a29 0x400d0f6e из набора инструментов ESP32 превращает эти адреса в имена функций и номера строк. Если ты работаешь в PlatformIO, достаточно добавить строку monitor_filters = esp32_exception_decoder в platformio.ini; тогда вывод сбоя расшифровывается в мониторе порта автоматически.
Пины конфигурации при старте: как GPIO0, GPIO2, GPIO12 и GPIO15 могут сорвать загрузку
При запуске ESP32 смотрит на уровень некоторых пинов, чтобы решить, как загружаться. Это пины конфигурации при старте (strapping pins), и важен только их уровень в момент сброса; после загрузки их можно использовать как обычные GPIO. Проблема возникает, когда подключённый к одному из них датчик, кнопка или модуль тянет пин к неправильному уровню во время старта. Тогда плата вообще не запускает программу, входит в режим программирования или не может прочитать флеш-память; со стороны это тоже выглядит как «ESP32 не загружается» или «постоянно перезагружается». Полную карту пинов своей платы можно посмотреть в утилите распиновки.
| Пин | Встроенный резистор | Роль при старте | Что будет при неправильном уровне? |
|---|---|---|---|
| GPIO0 | Подтяжка (pull-up) | LOW — режим программирования (download), HIGH — обычная загрузка | boot:0x3 (DOWNLOAD_BOOT) и waiting for download; программа не запускается |
| GPIO2 | Стягивающий (pull-down) | Для входа в режим программирования должен быть в LOW или не подключён; для обычной загрузки не важен | Схема, которая тянет его к HIGH, мешает загрузке кода |
| GPIO12 (MTDI) | Стягивающий (pull-down) | Выбирает напряжение питания флеш-памяти: LOW — 3,3 V, HIGH — 1,8 V | При HIGH флеш-память на 3,3 V не получает нормального питания; плата не загружается или уходит в цикл перезагрузок |
| GPIO15 (MTDO) | Подтяжка (pull-up) | LOW отключает сообщения загрузки из ПЗУ | Строки загрузки не появляются в мониторе порта, диагностика усложняется |
| GPIO5 | Подтяжка (pull-up) | Задаёт тайминги SDIO | На отладочных платах проблемы бывают редко |
У новых плат пины конфигурации при старте другие: у ESP32-S3 это GPIO0, GPIO3, GPIO45 и GPIO46, а у ESP32-C3 на загрузку влияют GPIO2, GPIO8 и GPIO9. ESP32-S3 использует те же имена исключений Xtensa, что и классическая ESP32. А вот на ESP32-C3 на базе RISC-V вместо LoadProhibited будет Load access fault, вместо StoreProhibited — Store access fault, а роль EXCVADDR берёт на себя регистр MTVAL. Сообщение о просадке питания одинаково на всех моделях.
ESP32 и Raspberry Pi Pico в MicroPython
Если ESP32 программируется на MicroPython, просадки питания происходят точно так же, потому что проблема в железе, а не в программе. А вот ошибки в коде Python выглядят не как Guru Meditation, а как понятный Traceback, и плата падает редко. Причину сброса можно прочитать через machine.reset_cause(), но учти: порт MicroPython для ESP32 не возвращает просадку питания отдельным значением, а сообщает о ней как о PWRON_RESET, так же как о включении питания. Если плата, которую никто не трогает, постоянно стартует с PWRON_RESET, снова подозревай питание. Код ниже — для ESP32; на Raspberry Pi Pico модуль machine знает меньше типов сброса. У Pico понижающе-повышающий стабилизатор на входе VSYS работает с любым напряжением от 1,8 до 5,5 V, поэтому проблемы с просадкой там встречаются гораздо реже; чтобы познакомиться с платой, загляни в статью Что такое Raspberry Pi Pico?
1import machine, gc, time
2
3# Reset nedenleri (ESP32 için) / reset causes (for ESP32)
4causes = {
5 machine.PWRON_RESET: "PWRON: guc acilisi ya da brownout / power-on or brownout",
6 machine.HARD_RESET: "HARD: cokme ya da machine.reset() / crash or machine.reset()",
7 machine.WDT_RESET: "WDT: watchdog",
8 machine.DEEPSLEEP_RESET: "DEEPSLEEP: derin uykudan uyanma / deep sleep wake-up",
9 machine.SOFT_RESET: "SOFT: Ctrl+D ile yazilim reseti / soft reset with Ctrl+D",
10}
11print("Reset nedeni / reset cause:", causes.get(machine.reset_cause(), "bilinmiyor / unknown"))
12
13while True:
14 gc.collect() # çöp toplayıcıyı çalıştır / run the garbage collector
15 print("Bos RAM / free RAM:", gc.mem_free(), "bayt / bytes")
16 time.sleep(5)Частые ошибки и их решения
| Симптом | Вероятная причина | Решение |
|---|---|---|
| Brownout detector was triggered при запуске Wi-Fi | Кабель, порт или стабилизатор не успевают за пиковым током | Короткий толстый кабель, адаптер на 1 A, 470 µF + 100 nF между 3V3 и GND |
| Сброс, когда поворачивается сервопривод или щёлкает реле | Нагрузка сидит на одной линии питания с ESP32 | Питай нагрузку от отдельного источника 5 V и соедини GND |
| Сбросы начинаются, когда батарея немного разрядится | Падение напряжения на AMS1117 (dropout) | Используй LDO-стабилизатор или понижающе-повышающий преобразователь |
| Guru Meditation, EXCVADDR 0x00000000 | Нулевой указатель (null pointer) | Проверяй указатель перед использованием |
| Stack canary watchpoint triggered (loopTask): стек переполнен | Стек loop() заполнен | Сделай большие массивы static или используй SET_LOOP_TASK_STACK_SIZE |
| task_wdt: - IDLE (CPU 0) | Цикл без пауз на ядре 0 | Добавь vTaskDelay в цикл |
| Interrupt wdt timeout on CPU1 | ISR выполняется слишком долго | В ISR только поднимай флаг, используй IRAM_ATTR |
| Плата молчит, boot:0x3 и waiting for download | GPIO0 в LOW при старте | Убери схему с GPIO0, нажми EN |
| Плата вообще не загружается, ошибка чтения флеш-памяти | GPIO12 в HIGH при старте | Перенеси подтяжку или модуль с GPIO12 на другой пин |
| Failed to connect to ESP32 при загрузке кода | GPIO2 в HIGH, или плата не может войти в режим программирования | Освободи GPIO2, удерживай BOOT в момент начала загрузки |
Плата работает стабильно: что дальше?
- Устройство, бережное к батарее: с глубоким сном ESP32 плата спит между измерениями и потребляет ток на уровне микроампер.
- Обновления по воздуху: загружай код без USB-кабеля с помощью ESP32 OTA.
- Панель в реальном времени: смотри данные датчиков в браузере вживую через ESP32 WebSocket.
- Уведомления: с Telegram-ботом на ESP32 причина сброса будет приходить на телефон при каждом перезапуске платы.
- Световое шоу: безопасно управляй лентой WS2812B NeoPixel с отдельным питанием.
Большинство проблем с перезагрузкой ESP32 — это либо питание, либо несколько строк кода с ошибкой; если правильно прочитать сообщение, и то и другое исправляется быстро. Чтобы освежить основы, загляни в статью Что такое ESP32? В приложении BlueGrays есть проекты на ESP32 со светодиодами, сервоприводами, лентами NeoPixel, подключением к Wi-Fi, веб-сервером, OTA-обновлениями и глубоким сном, у каждого пошаговая схема и код, так что приёмы питания и диагностики из этой статьи можно сразу опробовать в настоящих проектах.
Частые вопросы
Как исправить ESP32 brownout detector was triggered?
Эта ошибка значит, что напряжение питания ESP32 на мгновение упало ниже примерно 2,43 V. Сначала возьми короткий толстый USB-кабель и источник 5 V минимум на 1 A, затем добавь между 3V3 и GND электролитический конденсатор на 100–470 µF и керамический на 100 nF. Нагрузки вроде сервоприводов, реле и светодиодных лент питай от отдельного источника и соедини GND.
Почему ESP32 постоянно перезагружается?
Самые частые причины — недостаточное питание (просадка, brownout), программные сбои (Guru Meditation), таймауты сторожевого таймера и пины конфигурации при старте, застрявшие на неправильном уровне. Открой монитор порта на скорости 115200 бод и нажми EN; последнее сообщение перед сбросом и код rst при загрузке покажут причину. Функция esp_reset_reason() позволяет прочитать её и прямо из кода.
Что значит Guru Meditation Error LoadProhibited?
Это значит, что код на ESP32 попытался прочитать данные по недопустимому адресу памяти. Если EXCVADDR равен 0 или очень маленькому числу, почти наверняка был использован нулевой указатель. Вставь строку Backtrace в ESP Exception Decoder, чтобы найти файл и строку, где произошла ошибка.
Можно ли отключить детектор просадки питания ESP32?
Да, его отключает строка WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0), но она только глушит предупреждение. Провалы напряжения продолжаются, поэтому данные во флеш-памяти могут повредиться, а плата — падать с другими ошибками. Используй это лишь ненадолго, чтобы убедиться, что проблема в питании.
Как исправить ESP32 Task watchdog got triggered?
Это сообщение значит, что отслеживаемая задача 5 секунд не отдавала процессор; в Arduino в списке обычно указана задача IDLE ядра 0. Добавь vTaskDelay() в циклы своих задач, используй таймауты при ожидании датчиков и избегай delay() и долгих операций в обратных вызовах веб-сервера. Отключение сторожевого таймера прячет ошибку, но не устраняет причину.
Какое питание нужно ESP32?
Espressif рекомендует для ESP32 источник питания, который может отдавать не меньше 500 mA; на практике адаптер 5 V на 1 A даёт безопасный запас. Если используется ESP32-CAM или добавлены нагрузки вроде сервоприводов и светодиодных лент, выбирай источник на 2 A и больше и по возможности питай эти нагрузки от отдельной линии.
Что собрать после прочтения
Все проекты
Мигающий светодиод на ESP32
Учит базовой работе с выходами GPIO на ESP32. Эффект мигания получается включением и выключением светодиода с цифрового пина. Это ESP32-версия классического проекта мигания на Arduino UNO; базовые команды мигания в среде Arduino те же, но расположение пинов ESP32 и уровни GPIO 3,3 V отличаются от Arduino UNO.

Подключение ESP32 к Wi-Fi в режиме STA (с экраном LCD)
В этом проекте ESP32 надёжно и устойчиво к сбоям подключается к домашней сети Wi-Fi в режиме станции (STA). SSID и пароль хранятся в отдельном файле secrets.h, чтобы не утекли в Git; настроено автоматическое переподключение, а уровень сигнала RSSI и IP-адрес выводятся на I2C LCD-экран. Встроенный светодиод служит индикатором состояния. Это менеджер подключения уровня реальных устройств.

Сервопривод на ESP32 (управление потенциометром)
В этом проекте сервопривод SG90 управляется потенциометром. Угол сервопривода задаётся сопоставлением аналогового значения потенциометра с диапазоном 0–180 градусов. Библиотека ESP32Servo формирует сигналы сервопривода на подходящих выходных пинах GPIO у ESP32; этот проект использует GPIO 18. Это базовый способ управления для роборук, подвесов камер и механизмов автоматических дверей.

Управление лентой WS2812B NeoPixel на ESP32
Адресуемые светодиоды WS2812B (NeoPixel) — умные RGB-светодиоды, которыми управляют цепочкой через один пин данных. В этом проекте лента WS2812B из 8 светодиодов управляется библиотекой Adafruit_NeoPixel и показывает плавную радужную анимацию. Логика 3,3V у ESP32 работает с большинством WS2812B, но на случай нестабильности показано решение с последовательным резистором 470Ω и конденсатором 1000µF. Анимация получается сдвигом значения оттенка.

Асинхронный веб-сервер на ESP32 (с выводом IP на LCD)
В этом проекте ESP32 превращается в асинхронный веб-сервер, которым можно управлять с телефона или компьютера в той же сети Wi-Fi. Благодаря библиотеке ESPAsyncWebServer сервер может обрабатывать несколько запросов одновременно (в отличие от классического WebServer.h, который блокируется на одном запросе). Открыв IP-адрес ESP32 в браузере, ты нажимаешь кнопки ON/OFF (маршруты /on и /off) и включаешь и выключаешь светодиод на GPIO 5.

Глубокий сон ESP32: экономия батареи (OLED + счётчик в RTC RAM)
Режим глубокого сна (Deep Sleep) у ESP32 снижает потребление до ~10 µA, выключая процессор и большую часть периферии, — это важно для IoT-устройств на батарейках. В этом проекте используются три источника пробуждения: таймер (15 секунд), внешний пин (EXT0 — кнопка GPIO 33) и касание (T0/GPIO 4). Переменные с RTC_DATA_ATTR сохраняются во время глубокого сна — счётчики загрузок, пробуждений по таймеру и по кнопке ведутся непрерывно.

Обновление ESP32 по воздуху (OTA): беспроводная установка прошивки (с экраном LCD)
В этом проекте ты узнаешь, как загружать новую прошивку в ESP32 по Wi-Fi без USB-кабеля. С библиотекой ArduinoOTA ESP32 становится видна в сети и автоматически появляется как bluegrays-ota на вкладке «Сетевой порт» в Arduino IDE. Первая загрузка выполняется по USB, а все следующие обновления — без проводов: это важно для устройств в эксплуатации. I2C LCD показывает состояние Wi-Fi, IP-адрес, имя хоста, процент загрузки OTA и сообщения об ошибках.

Сигнализация движения на ESP32 (PIR + зуммер + светодиод + Telegram)
Умная охранная сигнализация, которая при обнаружении движения PIR-датчиком включает зуммер и светодиод и одновременно сразу отправляет владельцу уведомление в Telegram. В ней есть 30-секундная пауза, ограничивающая частоту повторных тревог; этот механизм не проверяет срабатывания PIR и не устраняет ложные тревоги. Подходит для дома, гаража, склада или мастерской. С ESP32-CAM можно добавить уведомления с фото, а с обычной ESP32 DevKit V1 приходят текстовые.
Другие статьи
Приложение BlueGrays
Собери эту схему шаг за шагом в телефоне
Схема подключения, код и список деталей на одном экране. 200+ проектов на Arduino, ESP32 и Pico готовы к сборке.
Наведи камеру телефона
Наведи камеру на код — откроется магазин приложений на телефоне.
или открыть магазин

