EEPROM, NVS и Preferences: что переживает выключение питания
Где Arduino и ESP32 хранят настройки и рекорды, как не прочитать мусор при первом запуске и не износить память
Переменная живёт, пока есть питание. Разбираем, куда Arduino и ESP32 складывают то, что должно пережить выключение, почему первый запуск читает мусор и сколько записей выдержит память.
Термометр на балконе показывает минимум за ночь. Утром выбило пробки, питание вернулось через минуту — и минимум снова равен текущей температуре. Программа не ошиблась: все её переменные лежали в оперативной памяти, а она без питания не хранит ничего. Чтобы число пережило выключение, его надо положить в энергонезависимую память, и у Arduino и ESP32 она устроена по-разному.
Ниже — где эта память находится, как с ней работать через EEPROM и Preferences, почему первый запуск читает мусор и сколько записей она выдерживает до износа.
Что стирается, а что остаётся
У микроконтроллера несколько видов памяти, и переживают они разное.
| Где лежит значение | Сброс кнопкой | Глубокий сон ESP32 | Снятие питания | Предел записей |
|---|---|---|---|---|
| Обычная переменная (ОЗУ) | нет | нет | нет | нет предела |
RTC_DATA_ATTR на ESP32 | нет | да | нет | нет предела |
| EEPROM ATmega328P | да | — | да | около 100 000 на ячейку |
| NVS во флеше ESP32 | да | да | да | около 100 000 стираний сектора |
Строка про RTC_DATA_ATTR — частая ловушка. Такая переменная лежит в памяти часов реального времени, которая остаётся под питанием во время глубокого сна. Поэтому в примере глубокого сна счётчик wakeups растёт от пробуждения к пробуждению. Но это не хранилище: после снятия питания или программной перезагрузки она снова получает начальное значение.
Энергонезависимых мест два: у ATmega328P — отдельная EEPROM на 1024 байта, у ESP32 — раздел NVS во внешней флеш-памяти. Принцип у них один, а подробности различаются настолько, что их стоит разобрать по отдельности.
EEPROM на Arduino: килобайт побайтно
EEPROM в ATmega328P — это 1024 ячейки по байту с собственными адресами. Библиотека EEPROM.h даёт к ним пять функций: read и write для одного байта, update — запись, которая сначала сравнивает и не трогает ячейку, если там уже то же значение, и get/put для структур целиком. Запись одного байта занимает около 3,3 мс, и всё это время процессор ждёт.
Важная деталь: EEPROM.put внутри вызывает update для каждого байта. Если из десяти байт структуры изменились два, записаны будут только два. Это бесплатная экономия ресурса, и ей стоит пользоваться.
Вторая деталь — что лежит в EEPROM у новой платы. Стёртая ячейка содержит 0xFF. Если прочитать в float четыре байта 0xFF, получится NaN, а в int — минус единица. Программа, которая при первом запуске доверяет прочитанному, начнёт с мусора. Поэтому в термометре на Nano рядом с данными хранится признак:
const uint16_t MAGIC = 0xC0DE; // признак «в EEPROM наши данные, а не мусор»
struct Records { uint16_t magic; float lo, hi; } rec;
void setup() {
EEPROM.get(ADDR, rec);
if (rec.magic != MAGIC) resetRecords(); // первый запуск: пишем начальные
}
Структура занимает 10 байт: два на признак и по четыре на каждый float. При первом запуске magic читается как 0xFFFF, не совпадает с 0xC0DE, и resetRecords() записывает начальные значения: минимум 1000, максимум −1000. Такие числа выбраны нарочно — первое же измерение побьёт оба рекорда.
Признак спасает и при смене формата. Если в новой версии прошивки в структуру добавится поле, старые данные прочитаются со сдвигом. Поменяйте MAGIC вместе со структурой, и новая прошивка честно начнёт с чистого листа, вместо того чтобы трактовать старые байты по-новому.
Arduino Nano: термометр, который помнит минимум и максимум после выключения
Поставьте датчику температуру 35 °C, дождитесь строки «записано в EEPROM», остановите симуляцию и запустите снова: первая строка покажет «из EEPROM: мин …, макс 35.0»
Открыть термометрNVS и Preferences на ESP32
У ESP32 отдельной EEPROM нет. В стандартной таблице разделов Arduino для флеша на 4 МБ под NVS отведено 20 КБ по адресу 0x9000. NVS — это хранилище «ключ — значение»: у каждой записи есть пространство имён, имя и тип. Работать с ним удобнее всего через библиотеку Preferences, которая входит в ядро arduino-esp32.
Preferences prefs;
prefs.begin("device", true); // true — только чтение
int bright = prefs.getInt("bright", 50); // 50 — если ключа ещё нет
float thr = prefs.getFloat("thr", 25.5);
String name = prefs.getString("name", "прибор");
prefs.end();
Это фрагмент из примера настроек прибора. Второй аргумент каждого get решает ту же задачу, что MAGIC на Arduino: если ключа нет, возвращается значение по умолчанию, а не мусор. На первом запуске пространства device ещё не существует, begin в режиме чтения вернёт false, и все get отдадут значения по умолчанию — ровно то, что нужно.
Имя пространства и имя ключа — не длиннее 15 знаков. Пространства разделяют ключи: ваш count в пространстве boot не столкнётся с count чужой библиотеки.
Устроена NVS не так, как EEPROM. Флеш нельзя переписать по байту: стереть можно только сектор в 4 КБ целиком. Поэтому NVS пишет журналом: новое значение ключа дописывается в свободное место страницы, а старое помечается как недействительное. Когда страница заполняется, живые записи переносятся на чистую, а старая стирается. Записи расходятся по всему разделу, и износ распределяется сам. Бонус такой схемы — устойчивость к пропаже питания посреди записи: запись либо завершена, либо её нет. У EEPROM структура из десяти байт пишется около 33 мс побайтно, и выключение в этот момент оставит половину старых данных и половину новых.
Библиотека EEPROM на ESP32 тоже есть, но это эмуляция поверх NVS: EEPROM.begin(размер) читает образ в ОЗУ, а на флеш он попадает только после EEPROM.commit(). Забытый commit() — частая причина жалоб «на ESP32 EEPROM не сохраняет».
Сколько записей выдержит память
Обе памяти рассчитаны примерно на 100 000 циклов: EEPROM — на ячейку, флеш — на стирание сектора. Много это или мало, зависит от того, как часто писать.
Для EEPROM всё считается прямо. Если писать одну и ту же ячейку каждые 2 секунды, как измеряет термометр из примера: 100 000 × 2 с = 200 000 с, это около 55 часов. Раз в минуту — 100 000 минут, около 69 суток. Запись только при новом рекорде случается несколько раз в день, и ячейки хватит на десятилетия.
Для NVS нужно учесть журнал. Каждое целое число занимает запись в 32 байта, на странице их помещается 126, страниц в разделе пять, одна держится свободной для переноса. Одна страница стирается примерно раз в 4 × 126 ≈ 500 записей, и 100 000 стираний дают порядка 50 миллионов записей. Оценка грубая, но порядок верный.
| Как пишем | EEPROM, одна ячейка | NVS, один ключ-число |
|---|---|---|
в loop() без задержки | минуты | часы |
| раз в секунду | около 28 часов | порядка полутора лет |
| раз в минуту | около 69 суток | срок службы прибора |
| только при изменении | годы | срок службы прибора |
putInt() в loop() без задержки выполняет сотни и тысячи записей в секунду, и 50 миллионов кончаются за часы. Об этом предупреждает комментарий в примере настроек: писать настройки в loop() значит убить NVS за сутки. Строки занимают по записи на каждые 32 байта текста и расходуют ресурс быстрее чисел.
Правило одно: писать тогда, когда значение изменилось, и не чаще, чем нужно. В рекордах NVS к этому добавлен запас:
int raw = analogRead(POT);
if (raw > best + 20) { // запас, чтобы шум не гонял запись
best = raw;
prefs.putInt("max", best);
}
АЦП шумит на несколько единиц всегда, и без запаса каждый всплеск шума на единицу выше рекорда стоил бы записи. Для настроек, которые меняют ручкой или кнопками, работает отложенное сохранение: запомнить момент последнего изменения и писать, когда ручку не трогали, скажем, 10 секунд.
Типичные ошибки
- Нет значения по умолчанию или признака. Первый запуск читает
0xFFили пустоту и работает с NaN, нулевой яркостью, порогом −1. - Запись в каждом проходе
loop(). Прибор работает неделю и начинает терять настройки. Искать причину в коде бесполезно: код правильный, память изношена. begin(..., true)и попытка записи. В режиме только чтенияputне пишет, а возвращает ноль. Если результат не проверяют, ошибка молчаливая.- Длинное имя. Пространство
thermometer_settingsдлиннее 15 знаков, и в зависимости от версии библиотеки имя либо обрежется, либоbegin()вернёт ошибку. Держите имена короткими и проверяйте результатbegin(). - Смена структуры без смены признака. Новая прошивка читает старые байты по новой раскладке.
RTC_DATA_ATTRвместо хранилища. Переживает сон, но не выключение.
Что проверить в симуляторе
С версии 1.8.14 EEPROM Arduino в PinPort сохраняется между запусками той же платы, пока страница открыта: остановили симуляцию, запустили снова — рекорды термометра на месте. Сценарий примера это и проверяет: ставит датчику 35 °C и ждёт в мониторе порта строку «макс 35.0». Кнопка на D2 сбрасывает рекорды, и после сброса признак MAGIC записывается заново вместе с начальными значениями.
У ESP32 то же самое с NVS и файловой системой SPIFFS: с версии 1.8.15 разделы данных флеш-памяти переживают «Стоп» и новый «Запустить» той же платы, пока страница открыта. Счётчик запусков показывает это буквально: каждый запуск печатает номер на единицу больше. Внутри одного прогона NVS переживает и ESP.restart(), и сторожевой таймер, и глубокий сон — модель перезапускается, а флеш остаётся прежним. Данные возвращаются только в раздел с тем же именем и адресом: сменили схему разделов — старые данные не лягут, как не легли бы и на плате. Тот же опыт с настройками прибора: яркость при каждом старте прибавляет 10 и сохраняется, после 100 возвращается к нулю.
Итог
Оперативная память без питания не хранит ничего, RTC_DATA_ATTR переживает только сон. Настоящие хранилища — EEPROM на Arduino и NVS на ESP32, и у обоих два правила: при первом чтении не доверять пустой памяти (признак или значение по умолчанию) и писать только то, что изменилось. Первое правило защищает от мусора, второе — от износа, который проявится через неделю работы и который в коде не найти.
Попробуйте в симуляторе
NVS: счётчик запусков
Запустите, нажмите «Стоп» и запустите снова: номер запуска вырастет на единицу — NVS пережила выключение. Потом добавьте в loop() delay(3000) и ESP.restart(): номер будет расти и при каждой перезагрузке
Запустить в симулятореNVS: сохранение рекордов
Уберите запас +20 из условия и покрутите потенциометр медленно: строк «сохранён» станет в разы больше — каждая из них стоила бы записи во флеш
Рекорды в NVSГлубокий сон по таймеру
Уберите RTC_DATA_ATTR у переменной wakeups: после каждого пробуждения номер станет равен единице, потому что обычная память сон не переживает
Глубокий сонКонфиг в JSON и в памяти
Добавьте в чтение новое поле mode со значением по умолчанию 1 и выведите его в порт: в старой сохранённой строке поля нет, но прошивка получит единицу, а не ноль
Конфиг в JSON