Хранение данных на ESP32: Preferences, LittleFS и схемы разделов
Переменная в скетче живёт до первой перезагрузки. Чтобы устройство помнило имя сети, калибровку или счётчик запусков, данные нужно класть во flash. На ESP32 для этого два разных инструмента, и путать их не стоит: Preferences — для настроек, LittleFS — для файлов. Разбираем оба, плюс схемы разделов, без которых ни то ни другое не заработает.
Всё проверено на пакете плат esp32 by Espressif Systems 3.x.
Что выбрать
| Preferences | LittleFS | |
|---|---|---|
| Что хранит | пары «ключ — значение» | файлы и папки |
| Типичный объём | байты и короткие строки | килобайты и мегабайты |
| Для чего | настройки, калибровки, счётчики | веб-страницы, логи, конфиги |
| Нужен раздел | nvs, есть всегда | spiffs, есть в большинстве схем |
| Сложность | одна строка на значение | открыть, прочитать, закрыть |
Правило простое: если данные помещаются в «ключ равно значение» — берите Preferences. Если это файл, который хочется положить целиком, — LittleFS.
Preferences: настройки
Библиотека встроена в пакет плат, ставить ничего не нужно. Данные лежат в разделе
nvs, который есть в любой схеме разделов.
#include <Preferences.h>
Preferences preferences;
void setup() {
Serial.begin(115200);
preferences.begin("robot", false); // имя раздела, false = чтение и запись
uint32_t bootCount = preferences.getUInt("bootCount", 0);
bootCount++;
preferences.putUInt("bootCount", bootCount);
Serial.printf("Запуск номер %u\n", bootCount);
preferences.end();
}
void loop() {
}
Первый аргумент begin() — пространство имён: логическая группа настроек. Разные
части программы могут использовать свои, не мешая друг другу. Второй аргумент — режим
только для чтения.
У каждого типа своя пара методов: putUInt и getUInt, putFloat и getFloat,
putString и getString, putBool и getBool и так далее. Второй аргумент
геттера — значение по умолчанию, когда ключа ещё нет. Это удобно: не нужно отдельно
проверять первый запуск.
Полезные вспомогательные методы:
preferences.isKey("wifiSsid"); // есть ли такой ключ
preferences.remove("wifiSsid"); // удалить один ключ
preferences.clear(); // очистить всё пространство имён
preferences.freeEntries(); // сколько записей ещё влезет
Имя пространства имён и имя ключа — не длиннее 15 символов. Это ограничение
самого NVS, а не библиотеки. Ключ temperatureCalibration (22 символа) молча не
сохранится.
Ещё одна вещь, о которой не пишут в туториалах: методы putX() возвращают количество
записанных байтов, и ноль означает ошибку. Если раздел nvs переполнен, запись
просто не произойдёт — без исключения и без сообщения, если не включён отладочный
вывод. В ответственных местах результат стоит проверять:
if (preferences.putString("wifiSsid", ssid) == 0) {
Serial.println("Не удалось сохранить: NVS переполнен?");
}
LittleFS: файлы
Файловая система нужна, когда хранить надо не значения, а содержимое: HTML-страницу для веб-интерфейса, файл конфигурации, журнал измерений.
#include <LittleFS.h>
void setup() {
Serial.begin(115200);
// ВНИМАНИЕ: true — «отформатировать при любой неудаче монтирования».
// Удобно при первом запуске, опасно потом: сбой чтения уничтожит все файлы.
if (!LittleFS.begin(true)) {
Serial.println("Не удалось смонтировать LittleFS");
return;
}
Serial.printf("Всего %u байт, занято %u\n",
LittleFS.totalBytes(), LittleFS.usedBytes());
File file = LittleFS.open("/config.json", FILE_WRITE);
if (file) {
file.print("{\"speed\":120}");
file.close();
}
file = LittleFS.open("/config.json", FILE_READ);
while (file.available()) {
Serial.write(file.read());
}
file.close();
}
void loop() {
}
Режимы открытия: FILE_READ, FILE_WRITE (перезаписывает с нуля) и FILE_APPEND
(дописывает в конец). Файл обязательно закрывать — иначе данные могут не попасть во
flash.
В отличие от старой SPIFFS, LittleFS понимает настоящие папки:
LittleFS.mkdir("/logs");
File root = LittleFS.open("/");
for (File entry = root.openNextFile(); entry; entry = root.openNextFile()) {
Serial.printf("%s — %u байт, папка: %d\n",
entry.name(), entry.size(), entry.isDirectory());
}
Приятная неожиданность: LittleFS по умолчанию монтирует раздел с меткой spiffs —
тот самый, что есть в стандартных схемах. Собственная схема разделов для неё не
нужна, хотя многие руководства утверждают обратное. Достаточно выбрать любую схему,
где раздел файловой системы вообще есть.
LittleFS или SPIFFS
Про SPIFFS в интернете часто пишут, что Espressif объявила её устаревшей. Это не
так: ни в документации ESP-IDF, ни в исходниках нет ни отметки deprecated, ни
предупреждения, а сам компонент по-прежнему поддерживается и остаётся в официальных
примерах.
Тем не менее для нового кода стоит брать LittleFS, и по техническим причинам:
- Папки. У SPIFFS их нет — только плоский список файлов, где слэш в имени ничего не значит.
- Устойчивость к потере питания. LittleFS проектировалась с расчётом на внезапное отключение, SPIFFS при этом может повредиться.
- Скорость при заполнении. Сборщик мусора SPIFFS по мере заполнения раздела начинает занимать всё больше времени — отдельная запись может уйти в секунды.
- Полезный объём. SPIFFS надёжно использует около 75% раздела, дальше начинаются сбои.
Формулировка «SPIFFS — legacy, для нового берите LittleFS» точна. «Объявлена устаревшей» — нет.
Образы SPIFFS и LittleFS двоично несовместимы. Вызов LittleFS.begin(true) на
разделе, где лежала SPIFFS, отформатирует его и уничтожит данные. При переходе
со старого проекта данные надо выгрузить заранее.
EEPROM: не используйте
А вот библиотека EEPROM действительно объявлена устаревшей — это единственная из
трёх, где есть прямая формулировка Espressif. В её README сказано, что для новых
приложений на ESP32 следует использовать Preferences, а EEPROM оставлена только для
совместимости со старыми скетчами Arduino.
Причина техническая: настоящей EEPROM у ESP32 нет, и библиотека эмулирует её одним
большим двоичным объектом внутри того же NVS. Получается контейнер в контейнере —
медленно и без каких-либо преимуществ. Preferences работает с NVS напрямую.
Схемы разделов
Flash-память ESP32 разбита на разделы: загрузчик, приложение, NVS, файловая система. Разметка выбирается в Tools → Partition Scheme, и от неё зависит, сколько места достанется прошивке и файлам.
| Схема | Приложение | Файловая система | OTA |
|---|---|---|---|
| Default 4MB with spiffs | 2 × 1,25 МБ | 1,375 МБ | Да |
| Minimal SPIFFS (1.9MB APP) | 2 × 1,875 МБ | 128 КБ | Да |
| No OTA (2MB APP/2MB SPIFFS) | 2 МБ | 1,875 МБ | Нет |
| Huge APP (3MB No OTA/1MB SPIFFS) | 3 МБ | 896 КБ | Нет |
| No FS 4MB (2MB APP x2) | 2 × 2 МБ | Нет | Да |
Схема по умолчанию выглядит так:
# Name, Type, SubType, Offset, Size
nvs, data, nvs, 0x9000, 0x5000
otadata, data, ota, 0xe000, 0x2000
app0, app, ota_0, 0x10000, 0x140000
app1, app, ota_1, 0x150000, 0x140000
spiffs, data, spiffs, 0x290000, 0x160000
coredump, data, coredump,0x3F0000, 0x10000
Два раздела app0 и app1 нужны для обновления по воздуху: новая прошивка пишется в
неактивный. Отсюда важное следствие — доступный размер скетча равен размеру одного
раздела, а не их сумме. Схемы с пометкой «No OTA» отдают всё место одному разделу и
тем самым лишают устройство возможности обновляться по сети.
Практический выбор для платы с 4 МБ: если прошивка не влезает в 1,25 МБ, берите Minimal SPIFFS — 1,875 МБ на приложение с сохранением OTA, ценой урезанной до 128 КБ файловой системы. Это последний вариант, где обновление по воздуху ещё работает. Подробнее — в статье про OTA-обновление.
Как залить файлы в LittleFS
Файлы веб-интерфейса удобно готовить на компьютере и заливать целиком. Положите их в
папку data рядом со скетчем.
Через плагин. Для Arduino IDE 2.x подходит arduino-littlefs-upload: он
устанавливается как расширение и добавляет команду загрузки в палитру
Ctrl+Shift+P. Старый плагин ESP32FS работает только с Arduino IDE 1.x.
Вручную. Оба нужных инструмента уже лежат в каталоге пакета плат, скачивать ничего не надо. Сначала собирается образ, потом заливается по смещению раздела:
mklittlefs -c data -b 4096 -p 256 -s 0x160000 littlefs.bin
esptool --chip esp32 --port /dev/cu.usbserial-0001 write_flash 0x290000 littlefs.bin
Смещение 0x290000 и размер 0x160000 берутся из таблицы разделов выбранной схемы —
для схемы по умолчанию они как раз такие. Установка esptool разобрана в
отдельной статье.
Износ flash
Flash-память выдерживает ограниченное число циклов перезаписи — обычно порядка ста
тысяч на сектор. Для настроек, которые меняются раз в день, это вечность. Для
счётчика, который пишется в каждом проходе loop(), — несколько часов.
Практические правила:
- Не пишите в цикле. Сохраняйте только при реальном изменении значения.
- Сравнивайте перед записью.
Preferencesне проверяет, изменилось ли значение, и честно пишет то же самое заново. - Копите в ОЗУ. Журнал измерений разумно накапливать в памяти и сбрасывать в файл раз в минуту, а не после каждого замера.
Что дальше
- Обновление прошивки по воздуху — OTA-обновление ESP32.
- Веб-интерфейс, который отдаёт файлы из LittleFS — Веб-сервер на ESP32.
- Оперативная память и её экономия — Память Arduino и ESP32.