Перенёс счётчик включений из урока про счётчик по байтам на ESP32 для узла теплицы с Wi-Fi: тот же EEPROM.read, тот же инкремент. Скомпилировалось без единого ругательства — и счётчик застыл на единице: сколько ни перезапускай, при старте снова один. Полчаса я подозревал модуль Wi-Fi в самовольных перезагрузках, пока не открыл документацию ядра: у ESP32 нет EEPROM. Вообще. Ни байта.
Тупик разрешился чтением, а не паяльником. EEPROM.h в ядре ESP32 существует, но это эмуляция: библиотека держит буфер в оперативной памяти и сносит его во флеш только по явной команде commit(). Мой скетч буфер обновлял, а команду сохранения не давал никогда — отсюда вечная единица после каждого включения питания.
Разберём, чем ESP32 хранит настройки на самом деле: раздел NVS во флеше и библиотека Preferences поверх него — хранилище ключ-значение с namespace. Перенесём счётчик и настройки термостата из прошлого урока, найдём ловушки миграции и посмотрим, что происходит физически, когда вы пишете ключ.
Preferences в симуляторе не эмулируется — модель хранилища это массив store в скетче, а журнал и commit изображают счётчик записей и печать в Serial. Логика ключей, дефолтов и read-only показывается честно: переключите read-only и посмотрите, как записи проходят мимо хранилища.
NVS: флеш вместо ячеек
Физика здесь другая целиком. У ATmega есть отдельная EEPROM-область на кристалле — 1024 ячейки с адресами от 0 до 1023. У ESP32 вместо неё один кусок флеш-памяти, где лежат и прошивка, и данные: под данные отведён раздел NVS — non-volatile storage. В стандартной таблице разделов ему отведено порядка двадцати килобайт — в двадцать раз больше, чем весь EEPROM у Uno, — и устроен он как журнал: каждая запись дописывается, устаревшие помечаются, страницы периодически уплотняются. Wear leveling встроен в сам механизм — флеш не протирается в одной точке, как это случилось бы при наивной записи по фиксированному адресу.
Preferences — официальная обёртка над NVS в ардуино-ядре ESP32. Хранилище не байтовое, а ключ-значение: вы открываете namespace — группу настроек с именем до пятнадцати символов — и кладёте в неё ключи с типизированными значениями: putUChar, putShort, putFloat, putString. Чтение зеркальное, и главная удобность в том, что get принимает дефолт прямо в аргументе: ключа нет — вернётся ваша заводская цифра, без проверок вокруг. Тот самый magic-байт из урока про первые настройки здесь встроен в API: отсутствующий ключ и есть признак первого запуска.
Ограничения стоит знать до кода. Имя namespace и имена ключей — до пятнадцати символов, более длинные молча не работают. Значения — от байта до структур через putBytes. И флеш остаётся флешем: сектора выдерживают порядка ста тысяч циклов стирания, поэтому принцип редкой записи из урока про update и износ переезжает на ESP32 без единой поправки. Одно отличие в пользу ESP32: журнал уплотняется, поэтому износ делится по всему разделу — а не по одной несчастной ячейке, которую вы забыл ротировать.
#include <Preferences.h> Preferences prefs; int zapuski = 0;int t10 = 50; // порог в десятых градусаbyte rezhim = 1; void setup() { Serial.begin(115200); prefs.begin("teplica", false); // namespace: читать и писать zapuski = prefs.getUChar("zapuski", 0) + 1; // ключа нет - вернётся 0 prefs.putUChar("zapuski", zapuski); t10 = prefs.getShort("porog", 50); // дефолт прямо в вызове rezhim = prefs.getUChar("rezhim", 1); Serial.print("zapuski="); Serial.println(zapuski); Serial.print("porog t10="); Serial.println(t10); // prefs.putShort("porog", 80); // сохранить новый порог // prefs.remove("porog"); // стереть один ключ // prefs.clear(); // стереть весь namespace prefs.end(); // commit происходит здесь} void loop() {}Две строки в этом скетче решают судьбу данных: begin и end. begin открывает namespace в режиме записи — второй аргумент false; end закрывает и выполняет commit, фактический перенос буфера в NVS. Если put был, а end не случился, данные живут в оперативной памяти до отключения: скетч выглядит рабочим ровно до первого выдёргивания вилки — самый коварный вид поломки. Форумы полны тем в духе «Preferences не сохраняет»: в девяти случаях из десяти человек забыл end() или открыл namespace только для чтения и удивляется, что put проходит мимо. Держите запись сконцентрированной: открыл — написал всё — закрыл. Второй аргумент begin умеет и наоборот: передадите true — получите namespace только для чтения, удобно для конфигурации, которую пишется один раз при прошивке.
Частоту commit держите на уровне редких событий, а не потока: настройки пишутся при нажатии кнопок и смене режима — это единицы записей в день, и раздел переживёт любой разумный срок службы. А вот лог каждые десять секунд в NVS отправлять не стоит: журнал каждый commit помечает и дописывает, и даже с встроенным wear leveling вы сами решаете, как быстро исчерпать ресурс секторов.
Чтение с дефолтом меняет стиль кода. На Uno я сначала читал ячейку, проверял на 255 и мусор, потом подставлял заводское — три шага. Здесь getUChar делает это в одном вызове: первый запуск даёт ноль, дальше — накопленное, и счётчик переехал одной строкой с сохранением всей логики инкремента.
Структуры настроек из урока про put и get структур переезжают через putBytes: передаёте указатель на структуру и её размер, читаете getBytes. Имена ключей у меня короткие: porog, rezhim, zapuski. Экономия букв тут не для красоты: длинный ключ молча не сохранится, и вы будете искать пропажу в совершенно другом месте. Один нюанс: структура — это сырые байты, поэтому при изменении состава полей меняйте и подпись внутри неё, иначе старая версия структуры прочитается в новую как чемодан без замка.
Перенос скетча с Uno на ESP32
Миграция в части памяти — это словарь замен. EEPROM.read(5) становится prefs.getUChar с потерей адреса и обретением имени: вместо магических цифр адресов — ключи. EEPROM.write становится putUChar, а EEPROM.update не нужен вовсе: журнал NVS и так дописывает записи и вычищает их при уплотнении. Зато правило писать редко остаётся железным: каждый commit — это стирание и запись секторов флеша, и commit на каждое нажатие кнопки изнашивает раздел быстрее, чем наивный код на Uno изнашивал ячейку. Заодно уходит класс грызни с картой адресов: у меня в проектах был листочек, где какой байт лежит, — теперь его заменяют имена ключей прямо в коде.
Отдельная тема — сон. Узел теплицы спит между измерениями, и счётчик циклов сна логично держать в том же NVS: проснулся — begin, чтение, инкремент, запись, end. Запись раз в цикл сна — это единицы commit в сутки, и раздел с такими темпами живёт дольше самого узла. Бюджеты сна и срок узла на батарейке мы считали в уроке про глубокий сон: память настроек вписывается туда без дополнительных схем. Если вы только выбираете плату под такой узел, я начинал бы с первого скетча на ESP32: периферии там больше, но привычной EEPROM-модели больше нет.
| Хранилище | Как адресуется | Когда физически пишется |
|---|---|---|
| EEPROM ATmega | адрес байта 0–1023 | сразу при write/update, около 3,3 мс на ячейку |
| Preferences на ESP32 | ключ в namespace до 15 символов | при end() — commit в раздел NVS |
| EEPROM.h на ESP32 | адрес, но это RAM-буфер | только по EEPROM.commit(), без него теряется |
| NVS напрямую | ключ в разделе | при commit, журналами с уплотнением страниц |
- 1. Сколько байт физической EEPROM у ESP32?
- 2. Скетч вызвал putUChar, но не end(). Что окажется во флеше после отключения питания?
- 3. Ключ длиной двадцать символов. Что произойдёт при putString?
- 4. Чем на ESP32 заменяется проверка на 255 и magic-байт из скетчей для Uno?
- 5. Куда попадает значение после EEPROM.write на ESP32 без последующего commit?
- У ESP32 нет EEPROM: данные живут в разделе NVS флеша, поверх него — библиотека Preferences с namespace и ключами
- Имена namespace и ключей — до 15 символов; get принимает дефолт в аргументе — встроенный заменитель magic-байта
- Запись попадает во флеш только при end() с commit; namespace в режиме чтения молча отклоняет put
- EEPROM.h на ESP32 — эмуляция: begin(size) даёт RAM-буфер, write правит буфер, во флеш он уезжает по commit()
- Редкая запись остаётся правилом: каждый commit стирает и пишет сектора флеша
Три последних урока закрыли горизонт от простой ячейки до журнальной флешки: настройки с дефолтами, ресурс и update, структуры через put и get, заводской сброс по удержанию, термостат, пережидающий сбой, и переезд на Preferences. Если упёрлись в объём — у внешних микросхем 24Cxx на шине I2C ёмкости начинаются там, где заканчивается раздел NVS; дверь туда открыта в уроке про два провода I2C. Держите настройки маленькими, записи редкими, а сброс — долгим: этим трём правилам я доверяю больше любой библиотеки, и они одинаково честно работают и на Uno, и на ESP32.
Частые вопросы по теме урока
- Сколько байт физической EEPROM у ESP32?
- EEPROM-области на кристалле нет вообще. Настройки живут в разделе NVS флеш-памяти, поверх которого работает Preferences с ключами и namespace. Ни одного: вместо EEPROM раздел NVS во флеше
- Скетч вызвал putUChar, но не end(). Что окажется во флеше после отключения питания?
- Изменения держатся в буфере и переносятся в NVS при end() с его commit. Без него запись теряется вместе с питанием — классическая тема форумов про несохраняющиеся настройки. Старое значение или ничего: commit не случился
- Ключ длиной двадцать символов. Что произойдёт при putString?
- Имена ключей и namespace ограничены пятнадцатью символами, превышение молча не работает. Имена делайте короткими, а поиск пропажи начинайте с длины ключей. Молча не сохранится: лимит ключа — пятнадцать символов
- Чем на ESP32 заменяется проверка на 255 и magic-байт из скетчей для Uno?
- Отсутствующий ключ возвращает дефолт из аргумента get: первый запуск и заводские значения решаются одним вызовом, без подписей и проверок на мусор. Дефолтом в аргументе get: getUChar(key, default)
- Куда попадает значение после EEPROM.write на ESP32 без последующего commit?
- EEPROM.h на ESP32 держит буфер в оперативной памяти; write и update правят его, а во флеш буфер уезжает только по EEPROM.commit(). Без commit это RAM, а не память. В RAM-буфер эмуляции EEPROM