Датчик уровня воды в дачной бочке просыпался по таймеру каждые десять минут и должен был копить статистику: сколько раз за сутки уровень падал ниже забора помпы. Счётчик я честно завёл — обычный int в loop-логике, — и к утру журнал выглядел как заикание: 1, 1, 1, 1. Каждое пробуждение обнуляло переменную, она жила между снами ровно ноль секунд, а я два вечера искал несуществующий глюк датчика. Второй вопрос был даже честнее первого: сколько раз за ночь плата вообще просыпалась, я сказать не мог — на такой счётчик я и не подумал завести память.
Ответ прятался в строке, которая уже мелькала в уроке про таймер сна и бюджет: RTC_DATA_ATTR int boots. В глубоком сне ESP32 отключает ядро и почти всю память, но маленький домен с восемью килобайтами продолжает питаться — туда и складывают то, что обязано пережить сон: счётчики, минимумы и максимумы, флаги «отправил или ещё нет». Этот урок — про то, какая память ESP32 что переживает, как писать счётчик снов, который растёт сам, и в какой момент вместо RTC-памяти всё-таки нужна EEPROM.
Три памяти ESP32: кто что переживает
В чипе три места для ваших данных, и живут они по разным правилам. Обычные переменные попадают в SRAM — там их 520 КБ, больше, чем у Uno в шестнадцать раз, и WiFi-буферы отъедают свою долю без спроса. Во глубоком сне питание с этой памяти снимается целиком: скетч после пробуждения стартует заново с setup(), каждая обычная переменная получает начальное значение, и millis() отсчитывает первый миллисекунд. Хранить между снами там нечего.
RTC-память — те же самые восемь килобайт RAM, просто подключённые к другому источнику: домену, который не гаснет во сне и ест свои микроамперы в составе тех 6-15 мкА, что мы считали в бюджете сна. Записей во флеш нет вообще — ни одной, поэтому износа не существует, а скорость доступа как у обычной RAM. Единственная смерть, которую эта память знает, — снятие питания: выключили вилку, сдох аккумулятор, сработал сброс питания — и восемь килобайт вернулись к начальным значениям.
Флеш — третий житель: EEPROM-библиотека ESP32 и её современная замена NVS пишут именно туда, поэтому переживают всё — сон, рестарт, отключение питания. Цена вопроса — ресурс ячейки, около 100 000 перезаписей. Считаем на цикле сна: пробуждение каждые десять минут — это 144 записи в сутки, 52 560 за год, и предел ячейки наступает меньше чем за два года. Тот же счётчик при цикле «раз в час» пишет 8 760 раз в год и проживёт одиннадцать лет. Арифметику износа я подробно разбирал в уроке про ресурс EEPROM — для ESP32 она своя, но формула та же.
| Память | Переживает сон | Переживает снятие питания | Цена |
|---|---|---|---|
| Обычная переменная в SRAM | нет: после сна setup() с нуля | нет | 520 КБ места, износа нет |
| RTC_DATA_ATTR, 8 КБ | да | нет: умрёт вместе с питанием | износа нет вообще |
| EEPROM и NVS во флеше | да | да | около 100 000 циклов на ячейку |
Механика RTC_DATA_ATTR обманчиво скучна. Помечаете глобальную переменную — линковщик кладёт её не в общую RAM, а в RTC-секцию, и на этом всё. Начальное значение из строки RTC_DATA_ATTR int boots = 0 присваивается один раз, при подаче питания; после пробуждения из сна setup() находит в переменной вчерашнее число и спокойно прибавляет единицу. Именно поэтому счётчик из урока про сон растёт сам, без единой строки сохранения.
Типы данных — только плоские: числа, массивы, структуры из чисел. Восемь килобайт — это 2048 int или 2048 float, для счётчиков и ночных минимумов хватит с запасом. String и любые объекты с конструкторами туда не кладут: RTC-секция — голые байты, конструкторы и деструкторы в ней не вызываются, и компилятор просто не пропустит такую переменную. Для сложного — структура с int и long, только без записи во флеш и без put-get.
Есть и ловушка, о которой молчат примеры: загрузчик RTC-секцию не очищает. После перепрошивки новый скетч может найти в счётчике не ноль, а мусор от прошлой жизни — у меня после экспериментов boots стартовал с 47. Лечится числом-сторожем, как в уроке про первые настройки EEPROM: рядом с данными живёт константа, которую скетч записывает сам, и если при старте она не совпала — память для этого скетча новая, сбрасываем всё к нулям.
Счётчик снов: полный скетч для платы
#include <WiFi.h>#include "esp_sleep.h" RTC_DATA_ATTR int boots = 0; // выживает сон и рестарт: живёт в 8 КБ RTC void setup() { Serial.begin(115200); delay(300); // UART после сброса оживает не мгновенно esp_sleep_wakeup_cause_t why = esp_sleep_get_wakeup_cause(); if (why == ESP_SLEEP_WAKEUP_UNDEFINED) { Serial.println("start: pitanie podano"); // не будильник, а вилка/reset boots = 0; // новое включение — счётчик с нуля } boots = boots + 1; Serial.print("boots="); Serial.println(boots); // ... здесь работа цикла: опрос датчика, отправка, всё что нужно ... WiFi.mode(WIFI_OFF); // глушим радио до сна btStop(); Serial.println("usnul na 60 s"); delay(100); // дать UART дописать строку esp_sleep_enable_timer_wakeup(60ULL * 1000000ULL); // 60 с в микросекундах esp_deep_sleep_start(); // не возвращается никогда} void loop() { } // сюда плата не доходитРазбор по строкам. Причину читаем первой же строкой setup(): возврат ESP_SLEEP_WAKEUP_UNDEFINED означает, что будильники не звонили — плата включилась от питания или reset; по этому случаю я обнуляю счётчик и здороваюсь, а все остальные причины означают плановое пробуждение. Полная таблица причин от нуля до четырёх разобрана в уроке про кнопку и причины пробуждения — здесь мне нужен только факт «будил ли кто-то вообще». Суффикс ULL в настройке таймера — тот же, что и в бюджете сна: без него час в микросекундах не влезает в 32 бита.
Тонкий момент, на котором спотыкаются: строка RTC_DATA_ATTR int boots = 0 выглядит как обычная инициализация, но выполняется она только при подаче питания. При пробуждении из сна загрузчик видит, что RTC-секция уже жива, и начальные значения не трогает — поэтому boots и растёт от сна к сну. Если нужны данные, которые переживут и вилку из розетки, — это уже EEPROM или NVS: сохраняйте перед сном в флеш и читайте при старте. Время в этот список тоже входит: часы после сна стартуют с нуля, и без NTP или внешнего RTC показывать нечего — я это разбирал в уроке про синхронизацию времени.
Считаем в симуляторе
- 1. Плата ушла в глубокий сон и проснулась. Какие данные найдутся на месте?
- 2. RTC_DATA_ATTR int boots = 0. Когда выполнится присваивание нуля?
- 3. Девайс месяц пролежал без питания, потом включился. Что покажет счётчик boots?
- 4. Почему в RTC-памяти не хранят String?
- 5. Цикл сна — десять минут, счётчик каждый раз дублируют в EEPROM с записью во флеш. На сколько хватит ячейки с ресурсом 100 000 записей?
- Во сне живёт только RTC-домен: 8 КБ под RTC_DATA_ATTR, остальное после пробуждения — с нуля
- Присваивание начального значения срабатывает один раз при подаче питания; сон и рестарт значение берегут
- RTC-память умирает при снятии питания: то, что должно пережить вилку, — в EEPROM или NVS
- Износ: флеш держит около 100 000 записей на ячейку; RTC-память не изнашивается вообще
- Только глобальные и static, только плоские типы; число-сторож отсекает мусор после перепрошивки
Моё правило выбора памяти в батарейных девайсах теперь короткое. Счётчики снов, ночные минимумы и максимумы, флаг «успел отправить» — RTC_DATA_ATTR: бесплатно, без износа, живёт ровно столько, сколько нужно циклу сна. Настройки пользователя и всё, что обязано пережить пересаживание батарейки, — EEPROM или NVS, и перед сном пишу только изменившееся, помня про 100 000 циклов ячейки. Если счётчик всё-таки пишется в флеш каждый цикл, сначала делю ресурс на частоту пробуждений: двухбайтовый приём из урока про счётчик включений экономит адреса, а арифметика износа — саму ячейку.
Время в эту схему не влезает: millis() после сна всегда ноль. Часы набирают заново — по NTP или от таблетки DS3231, которая переживает и сон, и вилку из розетки: я про неё рассказывал в уроке про первый запуск часов. И бюджет сна никто не отменял: восемь килобайт RTC-памяти съедают ноль микроамперов сверху, зато каждая лишняя секунда бодрствования стоит миллиамперов — формулы среднего тока в том же уроке про бюджет сна. Сначала память и сон, потом WiFi и отправка — в этом порядке батарейный девайс доживает до весны.
Частые вопросы по теме урока
- Плата ушла в глубокий сон и проснулась. Какие данные найдутся на месте?
- Во сне питается только RTC-домен с восемью килобайтами. RAM умирает, WiFi теряется, millis() стартует с нуля — выживает лишь помеченное RTC_DATA_ATTR. Только переменные с RTC_DATA_ATTR — 8 КБ RTC-памяти
- RTC_DATA_ATTR int boots = 0. Когда выполнится присваивание нуля?
- Загрузчик инициализирует RTC-секцию один раз, при подаче питания. При пробуждении из сна секция уже жива, и начальные значения не трогаются — поэтому счётчик растёт. Только при подаче питания; после сна переменная приходит как была
- Девайс месяц пролежал без питания, потом включился. Что покажет счётчик boots?
- RTC-память живёт, только пока есть питание. Снятая вилка — смерть восьми килобайт; данные, которым нужна вечность, дублируют в EEPROM или NVS. Начнёт с единицы: RTC-память умерла вместе с питанием
- Почему в RTC-памяти не хранят String?
- Помечать можно плоские типы: числа, массивы, структуры из чисел. Объекты с конструкторами компилятор в RTC-секцию не пропустит. RTC-секция — голые байты: конструкторы объектов в ней не вызываются
- Цикл сна — десять минут, счётчик каждый раз дублируют в EEPROM с записью во флеш. На сколько хватит ячейки с ресурсом 100 000 записей?
- Пробуждение раз в десять минут — 144 записи в сутки, 52 560 за год, предел за неполные два года. При цикле раз в час тот же счётчик пишет 8 760 раз в год и живёт одиннадцать лет. Меньше чем на два года: 144 записи в сутки дают 52 560 в год