Датчик протечки на даче я собрал с логикой «проснулся — проверил шлейф — ушёл в сон» и сразу сделал два промаха подряд. Первый: настройки WiFi и калибровку я держал в обычных переменных — и каждый час будильник будил плату, которая вежливо спрашивала роутер заново и просила калибровку у нулей; настройки жили ровно до первого сна. Второй: кнопку «проверить сейчас» я повесил на GPIO 35 — и она не будила плату вовсе, хотя digitalRead в бодрствовании читал её без всяких проблем.
Оба промаха выросли из одного непонимания: глубокий сон — не пауза в loop(), а полная перезагрузка со своими правилами. Пробуждение начинается не там, где я уснул, а в setup(), как при подаче питания. Выживает только RTC-память, будить умеют только RTC-пины, и у перезагрузки есть шильдик — причина, которую скетч обязан прочитать и по которой решает, что делать дальше. Этот урок — про шильдик и про кнопку, которая будит плату из мёртвого сна.
Пробуждение — это перезагрузка с шильдиком
Начнём с шильдика. esp_sleep_get_wakeup_cause() возвращает число — причину последнего пробуждения: 0 — UNDEFINED, плата проснулась так, как просыпается от подачи питания или кнопки reset; 1 — EXT0, сработал уровень на одном конкретном пине; 2 — EXT1, сработала группа пинов по маске; 3 — TIMER, звонил будильник RTC-таймера; 4 — TOUCHPAD, кто-то коснулся тач-пина. Вызывать надо сразу в начале setup(), до задержек и подключений WiFi: значение считывается однократно, и растерять его между двумя строками кода — реально.
У причины есть коварная особенность: она читается однократно, и второй вызов в этом же включении вернёт 0 — будто будильников не было вовсе. Я на этом собрал две ночные отладки: печать причины из двух мест показывала ноль во втором, и два вечера я лечил несуществующую поломку. С тех пор порядок строк в setup() у меня жёсткий: причина читается первой строкой и сразу кладётся в переменную — дальше скетч работает с копией, а не с железом.
Зачем читать причину — на живом примере. Причина 3 (таймер) — плановое пробуждение: измерил, отправил, спи дальше, никого не будить. Причина 1 (кнопка) — человек стоит рядом и чего-то хочет: поднять экран, включить точку доступа из урока про SoftAP для настройки WiFi. Причина 0 — плата включилась впервые: приветствие, сброс счётчиков, проверка прошивки. Один switch — и устройство ведёт себя по-разному в зависимости от того, кто его разбудил; без него все пробуждения одинаковые, и кнопка на корпусе ничем не отличается от будильника.
С памятью та же история, что и в уроке про таймер сна: обычные переменные умирают, RTC_DATA_ATTR — нет. Мой счётчик пробуждений, RTC_DATA_ATTR int boots, живёт месяцами и прибавляет единицу на каждом setup(). Для настроек, которые должны переживать не только сон, но и перепрошивку, я дублирую их в EEPROM — у неё лимит 100 000 записей на ячейку, зато она выживает и сброс, и обновление по воздуху.
#include <WiFi.h>#include "esp_sleep.h" RTC_DATA_ATTR int boots = 0; void setup() { Serial.begin(115200); delay(300); // UART оживает после сброса не мгновенно boots = boots + 1; Serial.print("boots="); Serial.println(boots); esp_sleep_wakeup_cause_t why = esp_sleep_get_wakeup_cause(); switch (why) { case ESP_SLEEP_WAKEUP_TIMER: Serial.println("wake=TIMER"); break; case ESP_SLEEP_WAKEUP_EXT0: Serial.println("wake=EXT0: knopka"); break; case ESP_SLEEP_WAKEUP_EXT1: Serial.println("wake=EXT1: gruppa pinov"); break; case ESP_SLEEP_WAKEUP_TOUCHPAD: Serial.println("wake=TOUCH"); break; default: Serial.println("wake=0: pitanie/reset"); break; } // будильники на этот цикл: кнопка GPIO 33 -> GND и таймер 60 с esp_sleep_enable_ext0_wakeup(GPIO_NUM_33, 0); // 0 — будить низким уровнем esp_sleep_enable_timer_wakeup(60ULL * 1000000ULL); // ... работа зависит от why: TIMER — тихо измерить; EXT0 — поднять экран ... WiFi.mode(WIFI_OFF); esp_deep_sleep_start();} void loop() { } // недостижимТеперь кнопка. ext0 — старший из будильников по пину: esp_sleep_enable_ext0_wakeup(GPIO_NUM_33, 0) будит плату, когда GPIO 33 падает в 0. Кнопка между пином и GND плюс подтяжка 10 кОм к 3,3 В — классическая схема; во сне внутренние подтяжки Arduino не работают, поэтому резистор внешний. Второй аргумент — уровень: 0 будит низким, 1 высоким. Будильник смотрит на уровень, а не на фронт, поэтому дребезг кнопки ему безразличен — он уже сработал; а вот читая ту же кнопку после пробуждения, антидребезг из урока про debounce я возвращаю сразу же. Резистор беру 10 кОм: 47 кОм тоже тянет, но фронт подтяжки медленнее, и на длинных проводах до кнопки плата начинает просыпаться от наводок.
Кстати, бесплатная wake-кнопка уже стоит на вашей плате: BOOT-кнопка физически тянет GPIO 0 к земле, и esp_sleep_enable_ext0_wakeup(GPIO_NUM_0, 0) превращает её в будильник без единого провода — я так будил прототипы до врезки нормальной кнопки в корпус. Одна оговорка: тот же GPIO 0 при старте снова strapping-пин, по которому выбирается режим прошивки, — пока плату не трогаешь, всё работает, но зажатая на включении BOOT отправляет плату в ожидание загрузчика. Для готового устройства я всё равно переношу кнопку на GPIO 33, а BOOT оставляю для прошивки.
ext1 — расширенный вариант: до 5 пинов разом в битовой маске и выбор логики — ANY_LOW, ANY_HIGH, ALL_LOW, ALL_HIGH. Он живёт на RTC-контроллере, умеет включать собственные RTC-подтяжки и потому не требует внешних резисторов. Мне он пригодился на калитке: магнитный датчик на GPIO 34 и кнопка на GPIO 33 — любое из двух событий будит плату, а причина EXT1 в setup() говорит, что проснулись от группы пинов, и дальше можно смотреть, какой именно.
И главное ограничение: будить умеют только RTC-пины — 0, 2, 4, 12-15, 25-27, 32-39. GPIO 35, на котором у меня висела та первая кнопка, формально RTC, но вход-only без внутренней подтяжки: кнопке на нём нужен внешний резистор, иначе пин висит в воздухе и плату будит случайная помеха — или не будит ничего. Прерывания из урока про attachInterrupt во сне не работают вовсе: ядро, которое их ловит, спит — событие ловит отдельный RTC-контроллер, и потому список будящих пинов жёсткий, а не «любой цифровой».
| Код | Константа | Кто разбудил | Что делает мой скетч |
|---|---|---|---|
| 0 | ESP_SLEEP_WAKEUP_UNDEFINED | Питание или reset | Приветствие, сброс счётчиков |
| 1 | ESP_SLEEP_WAKEUP_EXT0 | Уровень на одном пине | Режим настройки: экран, SoftAP |
| 2 | ESP_SLEEP_WAKEUP_EXT1 | Группа пинов по маске | Лог двери: смотреть, какой пин |
| 3 | ESP_SLEEP_WAKEUP_TIMER | Будильник RTC-таймера | Плановая работа: измерил и спать |
| 4 | ESP_SLEEP_WAKEUP_TOUCHPAD | Касание тач-пина | Подсветить табло |
Цикл в симуляторе
- 1. Плата проснулась после глубокого сна. Где продолжится выполнение скетча?
- 2. esp_sleep_get_wakeup_cause() вернул 0 (UNDEFINED). Что случилось?
- 3. Кнопка между GPIO 33 и GND, внешняя подтяжка 10 кОм к 3,3 В. Какой вызов будит плату при нажатии?
- 4. Чем ext1 отличается от ext0?
- 5. Почему кнопка пробуждения на GPIO 12 — плохая идея?
- Пробуждение из глубокого сна — перезагрузка: setup() с нуля, millis() с нуля, RAM пуста
- esp_sleep_get_wakeup_cause() — первым делом в setup(): 0 питание/reset, 1 ext0, 2 ext1, 3 таймер, 4 касание
- ext0 — один пин и уровень, подтяжка внешняя; ext1 — до 5 пинов маской с RTC-подтяжками
- Будят только RTC-пины 0, 2, 4, 12-15, 25-27, 32-39; attachInterrupt во сне не работает
- GPIO 12 — strapping: не вешать на него кнопку, если не хотите плату, которая не стартует
Что дальше — две дороги. Первая: тач-пины. На ESP32 их до десяти, любой проводок или контакт на корпусе становится кнопкой-прикосновением, esp_sleep_enable_touchpad_wakeup будит от касания, и для скрытой электроники за декоративной панелью это кнопка без единого отверстия в корпусе. Вторая дорога: точный график. Будильник таймера всегда отсчитывает «через час после того, как уснул», а «в 8:00 утра» умеет внешний RTC DS3231 — его будильники из урока про часы выводят сигнал на RTC-пин, и плата просыпается по настенному времени, как настоящий будильник.
Бюджет при этом не отменяется: способ пробуждения на ток сна не влияет — 12 мкА останутся 12 мкА и с таймером, и с кнопкой. Экономит только честная доля сна в цикле, которую мы считали формулой в уроке про таймер сна. Мой главный вывод после трёх батарейных девайсов прост: сначала формулы и тестер в разрыве питания, потом паяльник, и только в конце — красивый корпус. В обратном порядке вы перепаиваете батарейный отсек каждую неделю и вините чип, который ни в чём не виноват. Кнопка пробуждения при этом — последний штрих, а не первый: она полезна ровно настолько, насколько в устройстве есть кому нажимать её руками.
Частые вопросы по теме урока
- Плата проснулась после глубокого сна. Где продолжится выполнение скетча?
- Пробуждение из глубокого сна — это перезагрузка: setup() с нуля, millis() с нуля, RAM пуста. Продолжить с места сна умеет только light sleep. В setup(), как при подаче питания
- esp_sleep_get_wakeup_cause() вернул 0 (UNDEFINED). Что случилось?
- Ноль — причина «не по будильнику»: первое включение, кнопка reset или сброс загрузчика. По нему удобно сбрасывать счётчики и здороваться. Плата проснулась от подачи питания или reset — будильники не звонили
- Кнопка между GPIO 33 и GND, внешняя подтяжка 10 кОм к 3,3 В. Какой вызов будит плату при нажатии?
- Нажатие тянет пин к земле — будим низким уровнем, второй аргумент 0. Прерывания attachInterrupt во сне не работают: ядро, которое их ловит, спит. esp_sleep_enable_ext0_wakeup(GPIO_NUM_33, 0)
- Чем ext1 отличается от ext0?
- ext0 — один пин и жёсткий уровень. ext1 — маска до 5 RTC-пинов с режимами ANY_LOW/ANY_HIGH/ALL_LOW/ALL_HIGH и собственными RTC-подтяжками. Берёт до 5 пинов битовой маской и умеет логику ANY/ALL
- Почему кнопка пробуждения на GPIO 12 — плохая идея?
- GPIO 12 (MTDI) выбирает напряжение флеш-памяти при подаче питания. Подтянутый высоким пин валит загрузчик в цикл rst:0x10 — плата не стартует вовсе. Это strapping-пин MTDI: высокий уровень на старте ломает загрузку флеша