ESP32: библиотека энергонезависимого хранилища данных Печать
Добавил(а) microsin   

Библиотека для энергонезависимого хранилища данных (Non-volatile storage, NVS) была разработана для хранения пар ключ-значение в памяти flash. В этой статье показаны некоторые концепции NVS (перевод документации [1]).

[Варианты хранилища нижнего уровня]

В настоящее время NVS использует часть основной памяти flash через вызовы esp_partition API. Библиотека использует все разделы (partitions) с типом data и субтипом nvs. Приложение может выбрать использование раздела с меткой nvs вызовом API-функции nvs_open(), или выбрать любой другой раздел путем указания его имени с использованием API-функции nvs_open_from_partition().

Будущие версии этой библиотеки смогут использовать другие системы хранения данных в различных типах микросхем (SPI или I2C), RTC, FRAM, и т. д.

Замечание: если раздел NVS урезается (например, когда меняется конфигурация таблицы разделов), то его содержимое должно быть очищено. Система сборки проекта в среде разработки ESP-IDF [2] предоставляет цель idf.py erase-flash для стирания всего содержимого чипа flash.

NVS лучше всего работает для очень маленьких значений, но не с большими массивами данных наподобие типов string и blob. Если нужно работать с большими порциями данных, то рассмотрите вариант хранилища на основе файловой системы FAT, реализованной поверх библиотеки выравнивания износа секторов FLASH (wear levelling library).

[Ключи и значения]

Как уже упоминалось, NVS работает с данными по принципу пар ключ-значение. Ключи это строки ASCII, т. е. имена значений. Максимальная длина ключа в настоящий момент составляет 15 символов. Значения могут быть одним из следующих типов (дополнительные типы, такие как float и double, могут быть добавлены позже):

• Целочисленные типы: uint8_t, int8_t, uint16_t, int16_t, uint32_t, int32_t, uint64_t, int64_t.
• Строки, заканчивающиеся нулем (zero-terminated string, или ASCIIZ).
• Двоичные данные переменной длины (blob).

В настоящее время значения строк, сохраняемых nvs_set_str, ограничены 4000 байтами, включая null-терминатор. Blob-значения ограничены 508000 байтами, или 97.6% от размера раздела - 4000 байт, в зависимости от того, что меньше.

Для функции nvs_set_str в ESP-IDF существует максимальная длина строки, но на практике это ограничение редко является единственной причиной ошибок. Основная информация сведена в таблицу ниже:

Аспект Описание Примечания
Теоретический предел 4000 байт (включая нулевой символ конца строки `\0`) Официальное ограничение библиотеки. Реальное значение может быть меньше.
Ключевое ограничение на практике Требуется непрерывное свободное место в одной NVS-странице. Строка не может быть разбита между страницами. Частая причина ошибки ESP_ERR_NVS_NOT_ENOUGH_SPACE, даже если общее свободное место достаточно.
Размер страницы 4096 байт (стандартный размер сектора флеш-памяти ESP32)  
Затраты на служебные данные Каждые 32 байта строки (включая `\0`) занимают одну запись (entry), также требуется одна дополнительная "служебная" запись. Например, строка длиной 100 байт потребует `ceil(100/32) + 1 = 4 или 5` записей.

[Что вызывает проблемы на практике]

Если при записи строки вы получаете ошибку ESP_ERR_NVS_NOT_ENOUGH_SPACE, то это чаще всего связано не с длиной строки, а с фрагментацией NVS-памяти.

Вот типичный сценарий:

1. Вы записываете, изменяете и удаляете различные данные (ключи) в NVS с течением времени.

2. В результате в памяти образуются "дыры" — свободные записи, разбросанные по разным страницам.

3. При попытке записать длинную строку, которой требуется несколько непрерывных записей в одной странице, система не может найти подходящий блок, даже если суммарно свободного места хватает.

[Решения и рекомендации]

Чтобы избежать проблем, следуйте этим рекомендациям:

● Оптимизируйте хранение данных: используйте NVS по его прямому назначению — для хранения множества небольших значений (пароли Wi-Fi, настройки, счетчики). Для действительно больших данных (логи, HTML-страницы, аудиофайлы) лучше подойдет файловая система FAT поверх wear levelling library.

● Управляйте пространством: регулярно вызывайте nvs_commit() и аккуратно работайте с ключами. Помните, что обновление строки не освобождает старое место мгновенно — оно помечается как удаленное и будет очищено в фоновом режиме.

● Обрабатывайте ошибки: всегда проверяйте коды возврата nvs_set_str. При ошибке ESP_ERR_NVS_NOT_ENOUGH_SPACE сначала попробуйте записать строку меньшей длины, чтобы убедиться, что проблема именно в фрагментации, а не в другом месте.

Ошибка ESP_ERR_NVS_NOT_ENOUGH_SPACE при записи строки часто возникает не из-за длины самой строки, а из-за фрагментации (fragmentation) области NVS во флеш-памяти.

[Что такое фрагментация NVS-памяти?]

Представьте NVS (Non-Volatile Storage) как склад, разделенный на ячейки фиксированного размера (страницы).

Когда вы постоянно записываете и удаляете данные разного размера (особенно часто обновляя значения одних и тех же ключей), на "складе" появляется много разрозненных освобожденных ячеек. Хотя общий объем свободного места может быть значительным, не найдется одного непрерывного участка нужного размера для записи новой большой порции данных (например, строки). Именно это и вызывает ошибку NOT_ENOUGH_SPACE.

Основные причины фрагментации:

● Частое обновление значений ключей.
● Запись данных разной длины в одни и те же ключи.
● Длительная работа устройства без обслуживания NVS.

[Как диагностировать проблему?]

Используйте функции из ESP-IDF для получения статистики NVS:

#include "nvs.h"
nvs_stats_t nvs_stats; nvs_get_stats(NULL, &nvs_stats); // "NULL" означает дефолтное пространство имен
printf("Всего записей: %d\n", nvs_stats.total_entries); printf("Использовано: %d\n", nvs_stats.used_entries); printf("Свободно: %d\n", nvs_stats.free_entries);

Ключевой показатель: если free_entries (свободных записей) много, но при записи возникает ошибка, это явный признак сильной фрагментации — свободные записи разбросаны, и нет непрерывного блока.

[Методы устранения и предотвращения фрагментации]

Метод Как реализовать Зачем это нужно и важные замечания
1. Оптимизация стратегии записи Объединяйте данные: несколько связанных параметров сохраняйте одним структурой.

Кешируйте изменения: для часто меняющихся данных (счетчики) накапливайте изменения в RAM и записывайте в NVS реже, одним блоком.
Самый эффективный способ. Снижает количество операций и риск фрагментации в корне.
2. Увеличение размера раздела NVS В файле partitions.csv вашего проекта найдите строку с nvs, data, nvs и увеличьте размер (например, с 0x6000 до 0x9000). Дает больше "буферного" пространства, откладывая момент критической фрагментации. Требует полной перезаписи прошивки.
3. Полная очистка (сброс) раздела NVS В коде: nvs_flash_erase() -> nvs_flash_init(). Или через idf.py erase-flash. Радикальное решение. Полностью очищает раздел, убирая всю фрагментацию. Удаляет все сохраненные настройки!
4. Использование отдельных пространств имен (namespaces) Разделяйте данные по типам: nvs_open("app_config", ...), nvs_open("runtime_log", ...). Изолирует данные с разным жизненным циклом, не давая их фрагментации влиять друг на друга.

Важные предупреждения перед действиями.

1. Резервное копирование: перед очисткой раздела NVS или изменением partitions.csv убедитесь, что критичные настройки можно восстановить или экспортировать.
2. Изменение раздела: после редактирования partitions.csv обычно требуется полная очистка флеш-памяти (idf.py erase-flash), чтобы новая таблица разделов была применена корректно.

[Практические советы для долгосрочной стабильности]

● Избегайте использования nvs_set_str для часто меняющихся данных. Вместо этого логируйте в оперативную память или файловую систему (SPIFFS/LittleFS).
● Пишите "умные" дефрагментаторы**: для критичных систем можно реализовать процедуру, которая при запуске считывает все ключи из NVS, очищает раздел и записывает их обратно. Делайте это редко и с учетом риска потери данных при сбое питания во время процесса.
● Мониторьте статистику: встраивайте сбор статистики NVS в диагностику вашего устройства, чтобы видеть проблему до того, как она вызовет сбой.

Итог: для борьбы с ошибкой ESP_ERR_NVS_NOT_ENOUGH_SPACE сосредоточьтесь на оптимизации логики записи и периодическом мониторинге состояния NVS через nvs_get_stats. Увеличение размера раздела — хорошая временная мера, но не устраняет первопричину.

Ключи должны быть уникальными. Назначение нового значения существующему ключу работает следующим образом:

• Если новое значение такого же типа, что и старое, то новое значение обновляется.
• Если у нового значения другой тип данных, не совпадающий со старым типом, то будет возвращена ошибка.

Проверка типа данных выполняется при чтении значения. Будет возвращена ошибка, если тип данных операции чтения не соответствует типу данных значения.

Пространства имен. Чтобы уменьшить потенциальные конфликты в именах ключей между различными компонентами, NVS назначает каждую пару ключ-значение одному из пространств имен (namespace). Имена пространств следуют тем же правилам, что и имена ключей, например их максимальная длина составляет 15 символов. Имя для namespace указывается в вызове nvs_open() или nvs_open_from_partition(). Этот вызов вернет непрозрачный дескриптор, который используется в последующих вызовах функций nvs_get_*, nvs_set_* и nvs_commit(). Таким методом дескриптор связывается с namespace, и имена ключей не конфликтуют с такими же именами в других namespace. Обратите внимание, что пространства имен с одинаковым именем в разных разделах NVS считаются разными namespace.

Итераторы NVS. Итераторы позволяют пролистать пары ключ-значение, сохраненные в NVS, основываясь на имени раздела, namespace, и типе данных.

Для этого доступны следующие функции:

nvs_entry_find() вернет непрозрачный дескриптор, который используется в последующих вызовах функций nvs_entry_next() и nvs_entry_info().
nvs_entry_next() вернет итератор для следующей пары ключ-значение.
nvs_entry_info() вернет информацию для каждой пары ключ-значение.

Если не найдена никакая другая пара ключ-значение по заданному критерию, то nvs_entry_find() и nvs_entry_next() вернут NULL. В таком случае итератор освобождать не нужно. Если итератор больше не нужен, его можно освободить вызовом функции nvs_release_iterator().

Значение параметра namespace для функции nvs_open() — это имя пространства имен (раздела), которое вы сами задали при создании данных в NVS. Хотя иногда это имя задаете не вы сами, а какие-либо компоненты ESP-IDF.

Откуда взять значение параметра namespace и как с ним работать (источники имени namespace):

Источник Описание Пример
Собственный код Имя, которое вы использовали при первом создании раздела с помощью nvs_open() или nvs_set_*(). nvs_open("my_app_config", NVS_READWRITE, &handle);
В этом примере namespace = "my_app_config"
Стандартные namespace Предопределённые системные пространства (обычно начинаются с "nvs" или "phy", "wifi" и т.д.). "nvs.net80211" (настройки Wi-Fi)
Документация/примеры Имя из примеров кода ESP-IDF или другой документации. "storage", "nvs"

[Как найти нужный namespace]

1. Поиск в своём исходном коде. Просмотрите свой проект по ключевым словам:

nvs_open

или выполните команду в корневом каталоге проекта:

$ grep -r "nvs_open" . --include="*.c" --include="*.cpp"

2. Поиск в библиотеках и примерах ESP-IDF (~/esp это каталог, где установлен SDK ESP-IDF):

$ grep -rnw "nvs_open" ~/esp

3. Просмотр всех Namespace в памяти устройства (программно). Можно написать небольшую функцию для вывода всех существующих namespace:

#include "nvs.h"
#include "nvs_flash.h"

void list_all_namespaces() {
nvs_iterator_t it = NULL;

esp_err_t res = nvs_entry_find(NVS_DEFAULT_PART_NAME, NULL, NVS_TYPE_ANY, &it);
while(res == ESP_OK) {
nvs_entry_info_t info;
nvs_entry_info(it, &info);
printf("Namespace: '%s', Key: '%s', Type: %d\n",
info.namespace_name, info.key, info.type);
res = nvs_entry_next(&it);
}
nvs_release_iterator(it); }

Пример результата вызова list_all_namespaces:

Namespace: 'dhcp_state', Key: 'ETH_SPI_0', Type: 4
Namespace: 'nvs.net80211', Key: 'ap.sndchan', Type: 1
Namespace: 'nvs.net80211', Key: 'ap.authmode', Type: 1
Namespace: 'nvs.net80211', Key: 'ap.sae_h2e', Type: 1
Namespace: 'nvs.net80211', Key: 'ap.chanisset', Type: 1
Namespace: 'nvs.net80211', Key: 'ap.chan', Type: 1
Namespace: 'nvs.net80211', Key: 'ap.ssid', Type: 66
Namespace: 'nvs.net80211', Key: 'ap.passwd', Type: 66
Namespace: 'nvs.net80211', Key: 'ap.hidden', Type: 1
Namespace: 'nvs.net80211', Key: 'ap.max.conn', Type: 1
Namespace: 'nvs.net80211', Key: 'bcn.interval', Type: 2
Namespace: 'nvs.net80211', Key: 'ap.p_cipher', Type: 1
Namespace: 'nvs.net80211', Key: 'ap.trans_d', Type: 1
Namespace: 'nvs.net80211', Key: 'ap.ftm_r', Type: 1
Namespace: 'nvs.net80211', Key: 'ap.pmf_e', Type: 1
Namespace: 'nvs.net80211', Key: 'ap.pmf_r', Type: 1
Namespace: 'nvs.net80211', Key: 'ap.pmk_info', Type: 66
Namespace: 'nvs.net80211', Key: 'ap.csa_count', Type: 1
Namespace: 'nvs.net80211', Key: 'ap.dtim_period', Type: 1
Namespace: 'phy', Key: 'cal_mac', Type: 66
Namespace: 'phy', Key: 'cal_version', Type: 4
Namespace: 'phy', Key: 'cal_data', Type: 66

Значения Type определены в перечислении nvs_type_t:

typedef enum {
NVS_TYPE_U8 = 0x01, /*!< 1: тип uint8_t */
NVS_TYPE_I8 = 0x11, /*!< 17: тип int8_t */
NVS_TYPE_U16 = 0x02, /*!< 2: тип uint16_t */
NVS_TYPE_I16 = 0x12, /*!< 18: тип int16_t */
NVS_TYPE_U32 = 0x04, /*!< 4: тип uint32_t */
NVS_TYPE_I32 = 0x14, /*!< 20: тип int32_t */
NVS_TYPE_U64 = 0x08, /*!< 8: тип uint64_t */
NVS_TYPE_I64 = 0x18, /*!< 24: тип int64_t */
NVS_TYPE_STR = 0x21, /*!< 33: тип string */
NVS_TYPE_BLOB = 0x42, /*!< 66: тип blob */
NVS_TYPE_ANY = 0xff /*!< маркер конца массива параметров */ } nvs_type_t;

4. Использование утилиты nvs_partition_gen.py (для встроенных разделов). Если нужно посмотреть структуру встроенного раздела:

$ python $IDF_PATH/components/nvs_flash/nvs_partition_gen.py/nvs_partition_gen.py \
        read partition.csv input.bin

[Практические примеры использования]

Пример 1: Чтение данных из своего namespace.

nvs_handle_t handle;
esp_err_t err = nvs_open("app_settings", NVS_READONLY, &handle);
if (err != ESP_OK) {
printf("Ошибка открытия namespace. Возможно, он не существует\n"); }
else {
// Чтение данных
int32_t value;
nvs_get_i32(handle, "brightness", &value);
nvs_close(handle); }

Пример 2: Запись данных в новый namespace.

// Если namespace не существует, он будет создан автоматически
nvs_handle_t handle;
esp_err_t err = nvs_open("device_config", NVS_READWRITE, &handle);
if (err == ESP_OK) {
nvs_set_i32(handle, "start_count", 1);
nvs_commit(handle);
nvs_close(handle); }

[Частые ошибки и решения]

Проблема Причина Решение
ESP_ERR_NVS_NOT_FOUND Namespace не существует Проверить написание, создать namespace записью данных
ESP_ERR_NVS_INVALID_NAME Недопустимые символы в имени Использовать только буквы, цифры, подчёркивание, дефис
Не видит свои данные Открыли другой namespace Проверить точное совпадение имени (регистр важен)

[Рекомендации по именованию]

- Используйте лаконичные, понятные имена: "wifi_config", "sensor_calib", "user_prefs".
- Избегайте специальных символов, кроме `_` и `-`.
- Регистр символах имен имеет значение: "Config" и "config" — разные namespace.
- Для разных модулей приложения используйте разные namespace.

Безопасность, взлом и надежность. NVS не совместима напрямую с системой шифрования flash (ESP32 flash encryption system). Однако данные все еще можно хранить в зашифрованном виде, если шифрование NVS используется вместе с ESP32 flash encryption (подробнее см. далее "Шифрование NVS").

Если шифрование NVS не используется, то любой взломщик, который получил физический доступ к микросхеме flash, может изменить, удалить или добавить пары ключ-значение. С разрешенным шифрованием NVS невозможно изменить или добавить пару ключ-значение, чтобы они потом были распознаны как достоверные, не зная соответствующих ключей шифрования NVS. Однако от операции стирания не существует защиты.

Библиотека сделает попытку восстановления из ситуаций, когда память flash находится в несогласованном состоянии. В частности, необходимо иметь возможность выключить питание устройства, и потом его снова включить. Это не должно привести к потере данных, за исключением новой пары ключ-значение, если она была записана в момент выключения питания. Библиотека должна быть способна инициализировать любые случайные данные, которые присутствуют в памяти flash.

[Шифрование NVS]

Данные, сохраненные в разделах (NVS partitions), могут быть зашифрованы с использованием AES-XTS способом, описанным в стандарте шифрования диска IEEE P1619. Для шифрования каждая запись рассматривается как один сектор и относительный адрес записи (w.r.t. partition-start), и в алгоритм шифрования подается как номер сектора. NVS Encryption можно разрешить опцией CONFIG_NVS_ENCRYPTION. Ключи, требуемые для шифрования, сохраняются в другом разделе, который защищен Flash Encryption. Таким образом, включение Flash Encryption является обязательным условием для работы NVS encryption.

По умолчанию NVS Encryption разрешается, когда разрешено Flash Encryption. Это осуществляется потому, что драйвер Wi-Fi сохраняет учетные данные (наподобие SSID и пароля) в разделе по умолчанию (default NVS partition). Важно зашифровать их по умолчанию, если на уровне платформы шифрование уже включено.

Для использования шифрования NVS таблица разделов должна содержать раздел ключа (NVS key partition). Для шифрования предоставляются две таблицы разделов, содержащие раздел ключа NVS (опция menuconfig -> Partition Table). Они могут быть выбраны через меню конфигурации проекта (командой idf.py menuconfig). Как конфигурировать и использовать функцию шифрования NVS, см. пример проекта security/flash_encryption (находится среди примеров каталога установки ESP-IDF [2]).

Раздел ключа NVS. Приложению, которому требуется поддержка шифрования NVS, должно быть скомпилировано с разделом ключа (key-partition) типа data и субтипом key. Этот раздел должен быть помечен как зашифрованный, и его размер должен быть минимальным для раздела (4KB), подробности см. в [3]. Две дополнительные таблицы разделов, которые содержат NVS key partition, предоставляются опцией menuconfig -> Partition Table. Они могут непосредственно использоваться для NVS Encryption, и их структура следующая:

+-----------+--------------+-------------+----+
|              XTS encryption key (32)        |
+---------------------------------------------+
|              XTS tweak key (32)             |
+---------------------------------------------+
|                  CRC32 (4)                  |
+---------------------------------------------+

Ключи шифрования (XTS encryption keys) в NVS key partition можно сгенерировать одним из двух способов.

1. Генерация ключей на чипе ESP.

Когда шифрование разрешено, API-функция nvs_flash_init() может использоваться для инициализации зашифрованного раздела NVS по умолчанию. Эта API-функция генерирует внутри себя ключи шифрования XTS на чипе ESP. API-функция находит первый раздел ключей NVS, за. Then the API functionем она автоматически генерирует и сохраняет ключи NVS в этом разделе с помощью API-функции nvs_flash_generate_keys(), предоставленной в nvs_flash/include/nvs_flash.h. Новые ключи генерируются и сохраняются только когда соответствующий раздел ключей пуст. Тот же самый раздел ключей может затем использоваться для чтения конфигураций безопасности, чтобы инициализировать пользовательский зашифрованный раздел NVS, с помощью nvs_flash_secure_init_partition().

API-функции nvs_flash_secure_init() и nvs_flash_secure_init_partition() не генерируют ключи внутри себя. Когда эти API-функции используются для инициализации зашифрованных разделов NVS, ключи могут быть сгенерированы после запуска системы (startup) с использованием API-функции nvs_flash_generate_keys(), предоставленной в nvs_flash.h. Эта API-функция затем запишет эти ключи на key-partition в зашифрованном виде.

2. Использование предварительно сгенерированного раздела ключей.

Этот способ потребуется, когда ключи в NVS key partition не генерируются приложением. NVS key partition, содержащий ключи хранить сгенерированный раздел ключей во flash с помощью следующих двух команд.

i) Сборка и прошивка таблицы разделов:

idf.py partition-table partition-table-flash

ii) Сохранение ключей в NVS key partition (на flash) с помощью скрипта parttool.py (см. секцию Partition Tool в [3] для получения дополнительной информации).

parttool.py --port /dev/ttyUSB0 --partition-table-offset "nvs_key partition offset" write_partition
 --partition-name="name of nvs_key partition" --input "nvs_key partition"

Поскольку раздел ключа помечен как зашифрованный, и разрешено Flash Encryption is enabled, загрузчик (bootloader) зашифрует этот раздел, используя flash encryption key при первой загрузке приложения (boot).

Приложение может использовать разные ключи для разных разделов NVS, и иметь для этого соответственно несколько разделов ключей (key-partitions). Однако в зоне ответственности приложения находится предоставление корректных key-partition/ключей для шифрования/дешифровки.

[Шифрованные чтение/запись]

Те же функции NVS API nvs_get_ * или nvs_set_ * могут также использоваться для чтения и записи в зашифрованный раздел NVS.

Шифрование раздела NVS по умолчанию: чтобы разрешить шифрование default NVS partition, не требуется предпринимать дополнительные шаги. Когда разрешена опция CONFIG_NVS_ENCRYPTION, API-функция nvs_flash_init() внутри себя выполнит некоторые дополнительные шаги, используя первый найденный раздел ключей (NVS key partition), чтобы разрешить шифрование для default NVS partition (подробнее см. документацию по API [1]). Альтернативно также может использоваться API-функция nvs_flash_secure_init() для разрешения шифрования default NVS partition.

Шифрование пользовательского раздела NVS: чтобы разрешить шифрование custom NVS partition, используется API-функция nvs_flash_secure_init_partition() вместо nvs_flash_init_partition().

Когда используются API-функции nvs_flash_secure_init() и nvs_flash_secure_init_partition(), от приложения ожидаются следующие шаги, чтобы выполнить операции чтения/записи NVS с разрешенным шифрованием:

1. Найдите раздел ключа (key partition) и раздел данных (data partition) NVS, используя API-функции esp_partition_find*.

2. Заполните структуру nvs_sec_cfg_t, используя API-функции nvs_flash_read_security_cfg() или nvs_flash_generate_keys().

3. Инициализируйте NVS flash partition, используя API-функции nvs_flash_secure_init() или nvs_flash_secure_init_partition().

4. Откройте namespace, используя API-функции nvs_open() или nvs_open_from_partition().

5. Выполните операции чтения/записи NVS, используя nvs_get_* или nvs_set_*.

6. Отмените инициализацию раздела NVS, используя nvs_flash_deinit().

[Утилита NVS Partition Generator]

Эта утилита помогает генерировать двоичные файлы раздела NVS, которые можно отдельно прошить в выделенный раздел partition binary с помощью скрипта программирования. Пары ключ-значение для прошивки в раздел могут быть предоставлены посредством файла CSV. Для подробностей см. NVS Partition Generator Utility [4].

[Пример приложения]

Примеры кода можно найти в каталоге storage, среди прочих примеров ESP-IDF [2].

storage/nvs_rw_value

Этот пример демонстрирует, как прочитать одно целое значение из NVS, и как его записывать в NVS.

Значение, проверяемое в этом примере, хранит количество перезагрузок модуля ESP32. Функция значения в качестве счетчика возможна только благодаря его хранению в NVS.

Пример также показывает, что проверить, была ли успешной операция чтения / записи, и было ли инициализировано значение в NVS. Процедура диагностики предоставляется в виде обычного текста, чтобы помочь разобраться в ходе выполнения программы, и легче исправлять возникающие проблемы.

storage/nvs_rw_blob

Пример демонстрирует, как читать одиночное целое значение и объект blob (binary large object), и как их записывать в NVS, чтобы сохранять значения между перезапусками модуля ESP32.

value - отслеживает количество программных (soft) и аппаратных (hard) перезагрузок модуля ESP32.

blob - содержит информацию о реальном времени выполнения. Таблица считывается из NVS в динамически выделенную область RAM. Новая run time информация добавляется в таблицу при каждом запущенном вручную программном рестарте, и затем эта информация записывается в NVS. Этот процесс запускается притягиванием к лог. 0 ножки GPIO0.

Этот пример также показывает, как реализовать процедуру диагностики, чтобы проверить, была ли успешной операция чтения / записи.

storage/nvs_rw_value_cxx

Этот пример делает то же самое, что и storage/nvs_rw_value, отличие только в том, что используется дескриптор класса C++ NVS.

[Внутренняя организация NVS]

Журнал пар ключ-значение. NVS сохраняет пары key-value последовательно, добавляя новые пары в конец заполненной области. Когда обновляется значение любого указанного ключа, в конец журнала добавляется новая пара key-value, и старая пара key-value помечается как стертая (erased).

Страницы (pages) и записи (entries). Библиотека NVS в своей работе использует 2 основных объекта: страницы и записи. Страница это логическая структура, которая хранит порцию общего журнала. Логическая страница соответствует одному физическому сектору памяти flash. С используемыми страницами связан порядковый номер. Этот номер позволяет упорядочить страницы. Чем больше порядковый номер, тем позже была создана страница. Каждая страница может быть в одном из следующих состояний:

Empty/uninitialized
Носитель данных flash для этой страницы пустой (все байты в значении 0xff). Эта страница не используется в настоящий момент для хранения каких-либо данных, и у неё нет порядкового номера.

Active
Хранилище flash инициализировано, заголовок страницы записан во flash, странице присвоен правильный порядковый номер. На странице есть несколько пустых записей, куда можно записать данные. В любой момент времени в этом состоянии может быть не более одной страницы.

Full
Хранилище flash находится в согласованном состоянии, хранилище заполнено парами key-value. Запись новых пар key-value на эту страницу невозможно. Некоторые пары key-value все еще можно пометить как erased.

Erasing
Не стертые пары key-value перемещаются на другую страницу, так что текущая страница может быть стерта. Это переходное состояние, т. е. страница никогда не остается в этом состоянии, когда происходит выход из любой API-функции. В случае внезапного отключения питания процесс перемещения и стирания будет завершен при следующем включении питания.

Corrupted
Заголовок страницы содержит недопустимые данные, и дальнейший парсинг данных страницы был отменен. Любые элементы данных, которые ранее были записаны на эту страницу, больше не будут доступны. Соответствующий сектор flash не стирается немедленно, и сохраняется вместе с секторами в uninitialized-состоянии для последующего использования. Это может быть полезно для отладки.

Сопоставление секторов flash-памяти с логическими страницами не имеет определенного порядка. Библиотека будет проверять порядковые номера страниц, найденных в каждом секторе памяти flash, и упорядочивать страницы в списке на основе этих номеров.

+--------+     +--------+     +--------+     +--------+
| Page 1 |     | Page 2 |     | Page 3 |     | Page 4 |
| Full   +---> | Full   +---> | Active |     | Empty  |   < - состояния
| #11    |     | #12    |     | #14    |     |        |   < - последовательные номера
+---+----+     +----+---+     +----+---+     +---+----+
    |               |              |             |
    |               |              |             |
    |               |              |             |
+---v------+  +-----v----+  +------v---+  +------v---+
| Sector 3 |  | Sector 0 |  | Sector 2 |  | Sector 1 |    < - физические секторы flash
+----------+  +----------+  +----------+  +----------+

Структура страницы. На данный момент мы предполагаем, что размер сектора памяти flash составляет 4096 байт, и что оборудование ESP32 шифрования flash-памяти работает на 32-байтовых блоках. Можно ввести некоторые настройки, конфигурируемые во время компиляции (например, через menuconfig) для установки микросхем flash с различными размерами секторов (хотя неясно, могут ли другие компоненты в системе, например драйвер flash-памяти SPI и кэш flash-памяти SPI, поддерживать эти другие размеры).

Страница состоит из 3 частей: заголовок (header), биты состояния записи (entry state bitmap), и сами записи (entries). Для поддержки совместимости шифрования ESP32 flash, размер записи равен 32 байтам. Для целочисленных типов одна запись хранит одну пару ключ-значение (key-value). Для строк (string) и двоичных данных (blob) запись хранит часть пары key-value (подробнее об этом в описании структуры записи).

На следующей диаграмме показана структура страницы. Числа в круглых скобках показывают размер в байтах для каждой части.

+-----------+---------------+-------------+-------------------------+
| State (4) | Порядк. № (4) | version (1) | Unused (19) | CRC32 (4) |   Header (32)
+-----------+---------------+-------------+-------------------------+
|                 Entry state bitmap (32)                           |
+-------------------------------------------------------------------+
|                        Entry 0 (32)                               |
+-------------------------------------------------------------------+
|                        Entry 1 (32)                               |
+-------------------------------------------------------------------+
/                                                                   /
/                                                                   /
+-------------------------------------------------------------------+
|                        Entry 125 (32)                             |
+-------------------------------------------------------------------+

Заголовок страницы (header) и entry state bitmap всегда записываются во flash не зашифрованными. Записи (Entry n) шифруются, если используются шифрование flash ESP32.

Значения для поля state определены таким образом, что изменение состояния фиксируется записью 0 в некоторые биты. Поэтому нет необходимости стирать страницу, чтобы изменить её состояние, если это не является изменением для перехода в состояние erased.

Поле version в заголовке отражает используемую версию формата NVS. Для обеспечения обратной совместимости при каждом обновлении поле версии декрементируется, начиная с 0xff (например, 0xff для version-1, 0xfe для version-2, и так далее).

Значение контрольной суммы CRC32 в заголовке вычисляется от части страницы, которая не включает поле state и поле самой контрольной суммы (байты от 4 до 28). Не используемая часть сейчас заполняется байтами 0xff.

Следующие секции описывают структуру поля состояния записи (entry state bitmap) и саму запись (entry).

[Entry и entry state bitmap]

Каждая запись (entry) может быть в одном из следующих состояний, которые представлены двумя битами в entry state bitmap. Последние 4 бита bitmap (256 - 2 * 126) не используются.

Empty (11)
Пока в эту запись ничего не записаноt. Это соответствует не инициализированному состоянию (все байты в значении 0xff).

Written (10)
Пара key-value (или часть от пары key-value, которая распространяется на несколько записей) была записана в эту запись.

Erased (00)
Пара key-value в этой записи отброшена (считается стертой). Содержимое этой записи больше не будет анализироваться.

Структура записи. Для значений примитивных типов (в настоящий момент это целые числа размером от 1 до 8 байт), запись хранит одну пару key-value. Для строк и типов blob запись хранит часть от всей пары key-value. Для строк, когда пара key-value распространяется на несколько записей, все эти записи хранятся на одной и той же странице. Данным blob разрешается распространятся между несколькими страницами, путем разделения их на отдельные куски. Для отслеживания этих кусков сохраняется дополнительная запись метаданных фиксированного размера, которая называется "blob index". Ранние форматы blob все еще поддерживаются (могут быть прочитаны и изменены). Однако после того, как blob был изменен, он будет сохранен уже в новом формате.

+--------+----------+----------+----------------+-----------+---------------+----------+
| NS (1) | Type (1) | Span (1) | ChunkIndex (1) | CRC32 (4) |    Key (16)   | Data (8) |
+--------+----------+----------+----------------+-----------+---------------+----------+

                                       Примитивные  +--------------------------------+
                                        +-------->  |     Data (8)                   |
                                        | типы      +--------------------------------+
                   +-> Фикс. длина ----
                   |                    |           +---------+--------------+---------------+-------+
                   |                    +-------->  | Size(4) | ChunkCount(1)| ChunkStart(1) | Rsv(2)|
  Формат данных ---+                    Blob Index  +---------+--------------+---------------+-------+
                   |
                   |                             +----------+---------+-----------+
                   +-> Переменная длина    -->   | Size (2) | Rsv (2) | CRC32 (4) |
                        (string, Blob Data)      +----------+---------+-----------+

Отдельные поля в структуре записи имеют следующее назначение:

NS
Индекс namespace для этой записи.

Type
Один байт, показывающий тип данных value. Возможные значения см. в перечислении заголовочного файла nvs_flash/include/nvs_handle.hpp for possible values.

Span
Количество записей, которые использует эта пара key-value. Для целых типов это поле равно 1. Для строк и blob количество зависит от длины value.

ChunkIndex
Используется для хранения индекса куска данных blob. Для других типов это поле должно быть равно 0xff.

CRC32
Контрольная сумма, вычисленная от всех байт этой записи, кроме байт самого поля CRC32.

Key
Строка, заканчивающаяся нулем (ASCII), где находится имя key. Максимальная длина строки имени составляет 15 байт, не считая завершающего нуля.

Data
Для целых типов это поле содержит само value. Если value короче 8, то оно добавляется справа не используемыми байтами, равными 0xff.

Для записи "blob index" эти 8 байт хранят следующую информацию о кусках данных:

• Size
(только для blob index) размер в байтах всех данных blob.

• ChunkCount
(только для blob index) Общее количество кусков данных blob, на которые blob был поделен для хранения.

• ChunkStart
(только для blob index) ChunkIndex первого куска данных этого blob. Последующие куски получают инкрементное распределение индекса (с шагом 1).

Для кусков строк и кусков данных blob эти 8 байт хранят дополнительные описательные данные для value:

• Size
(только для строк и blob) размер реальных данных в байтах. Для строк это значение включает завершающий 0.

• CRC32
(только для строк и blob) контрольная сумма, вычисленная от всех байт данных.

Value с переменной длиной (строки и blob) сохраняются в последующих записях, по 32 байта на запись. Поле Span последней записи показывает, сколько записей используется.

[Пространства имен (namespace)]

Как упоминалось выше, каждая пара key-value принадлежит одному из пространств имен (namespace). Идентификаторы пространства имен (строки) сохраняются как ключи пар key-value в namespace с индексом 0. Значения, соответствующие этим ключам, будут индексами для этих namespace.

+-------------------------------------------+
| NS=0 Type=uint8_t Key="wifi" Value=1      |   Запись, описывающая namespace "wifi"
+-------------------------------------------+
| NS=1 Type=uint32_t Key="channel" Value=6  |   Ключ "channel" в namespace "wifi"
+-------------------------------------------+
| NS=0 Type=uint8_t Key="pwm" Value=2       |   Запись, описывающая namespace "pwm"
+-------------------------------------------+
| NS=2 Type=uint16_t Key="channel" Value=20 |   Ключ "channel" в namespace "pwm"
+-------------------------------------------+

Hash-список элементов. Для уменьшения количества чтений из памяти flash, каждый элемент класса Page поддерживает список пар: индекс элемента (item index); хэш элемента (item hash). Этот список значительно ускоряет поиск. Вместо того, чтобы выполнять итерацию по всем записям, считывая записи из flash по одной, функция Page::findItem сначала осуществляет поиск хэша элемента в hash-списке. Это дает индекс элемента на странице, если такой элемент существует. Из-за хеш-коллизии есть вероятность, что будет найден не тот элемент. Эта ситуация разрешается путем отката на итерацию по элементам из flash-памяти.

Каждый узел в hash-списке содержит 24-битный хэш и 8-битный индекс элемента. Хэш вычисляется от namespace элемента, имени ключа и ChunkIndex. Для вычисления используется CRC32, результат обрезается до 24 бит. Для снижения чрезмерных расходов на сохранение 32-битных записей в связанном списке, этот список реализован как двойной список (double-linked list) массивов. Каждый массив содержит 29 записей, общий размер которых составляет 128 байт, вместе со связанным списком указателей и 32-битным полем счетчика. Минимальный объем дополнительного использования RAM на страницу составляет 128 байт, максимальный 640 байт.

[NVS API]

Заголовочные файлы:

components/nvs_flash/include/nvs_flash.h
components/nvs_flash/include/nvs.h

В таблице ниже приведено общее описание API-функций NVS, полное описание см. в документации [1].

Функция Описание
nvs_flash_init Инициализация раздела NVS по умолчанию.
nvs_flash_init_partition Инициализация хранилища flash NVS для указанного раздела.
nvs_flash_init_partition_ptr Инициализация хранилища flash NVS для раздела, указанного указателем.
nvs_flash_deinit Отмена инициализации хранилища NVS для раздела NVS по умолчанию.
nvs_flash_deinit_partition Отмена инициализации хранилища NVS для указанного раздела NVS.
nvs_flash_erase Стирает раздел NVS по умолчанию.
nvs_flash_erase_partition Стирает указанный раздел NVS.
nvs_flash_erase_partition_ptr Стирает пользовательский раздел.
nvs_flash_secure_init Инициализирует раздел NVS по умолчанию.
nvs_flash_secure_init_partition Инициализирует хранилище NVS flash для указанного раздела.
nvs_flash_generate_keys Генерация и сохранение ключей NVS в предоставленном разделе.
nvs_flash_read_security_cfg Чтение конфигурации безопасности NVS из раздела.
nvs_set_i8 Установит значение int8_t для указанного ключа.
nvs_set_u8 Установит значение uint8_t для указанного ключа.
nvs_set_i16 Установит значение int16_t для указанного ключа.
nvs_set_u16 Установит значение uint16_t для указанного ключа.
nvs_set_i32 Установит значение int32_t для указанного ключа.
nvs_set_u32 Установит значение uint32_t для указанного ключа.
nvs_set_i64 Установит значение int64_t для указанного ключа.
nvs_set_u64 Установит значение uint64_t для указанного ключа.
nvs_set_str Установит строку для указанного ключа.
nvs_get_i8 Получит значение int8_t для указанного ключа.
nvs_get_u8 Получит значение uint8_t для указанного ключа.
nvs_get_i16 Получит значение int16_t для указанного ключа.
nvs_get_u16 Получит значение uint16_t для указанного ключа.
nvs_get_i32 Получит значение int32_t для указанного ключа.
nvs_get_u32 Получит значение uint32_t для указанного ключа.
nvs_get_i64 Получит значение int64_t для указанного ключа.
nvs_get_u64 Получит значение uint64_t для указанного ключа.
nvs_get_str Получит значение строки для указанного ключа.
nvs_set_blob Установит значение данных переменной длины для указанного ключа.
nvs_get_blob Получит значение данных переменной длины для указанного ключа.
nvs_open Откроет энергонезависимое хранилище с указанным namespace из раздела NVS по умолчанию.
nvs_open_from_partition Откроет энергонезависимое хранилище с указанным namespace из указанного раздела NVS.
nvs_erase_key Сотрет пару key-value с указанным именем ключа.
nvs_erase_all Сотрет все пары key-value в указанном namespace.
nvs_commit Запишет любые ожидающие сохранения изменения в энергонезависимое хранилище.
nvs_close Закроет дескриптор хранилища и освободит любые выделенные ресурсы.
nvs_get_stats Заполнит структуру nvs_stats_t, которая предоставит информацию об используемой разделом памяти.
nvs_get_used_entry_count Подсчитает все записи в namespace.
nvs_entry_find Создает итератор, чтобы просмотреть записи NVS, основываясь на одном или нескольких параметров.
nvs_entry_next Возвратит следующий элемент, который подходит под критерий итератора, или NULL, если таких элементов нет.
nvs_entry_info Заполнит структуру nvs_entry_info_t информацией о записи, на которую указывает итератор.
nvs_release_iterator Освободит итератор.

[Особенность использования nvs_get_str и nvs_get_blob]

esp_err_t nvs_get_str(nvs_handle_t handle,
                      const char *key,
                      char *out_value,
                      size_t *length);

esp_err_t nvs_get_blob(nvs_handle_t handle, const char *key, void *out_value, size_t *length);

Функции nvs_get_str и nvs_get_blob обе возвратят запрошенные данные в буфер out_value по указанному ключу key. Если ключ key не существует, или тип запрошенной переменной не соответствует типу, который использовался при установке значения этой переменной, то будет возвращена ошибка.

В случае любой ошибки буфер out_value не будет изменен.

Обе функции ожидают, что out_value это указатель на уже выделенное место в памяти для переменной, соответствующее указанному типу переменной.

Функции nvs_get_str и nvs_get_blob поддерживают WinAPI-стиль запроса длины переменной, когда изначально неизвестна длина переменной при необходимости её чтения. Чтобы получить размер памяти, необходимый для сохранения переменной, вызовите nvs_get_str или nvs_get_blob с NULL в out_value и ненулевым указателем в length. Переменная, на которую указывает length, будет тогда установлена в необходимую длину переменной. Для nvs_get_str эта длина включает нулевой терминатор строки. Когда вызов nvs_get_str и nvs_get_blob сделан с ненулевым указателем out_value, параметр length должен быть ненулевым, и должен указывать на размер памяти в байтах, доступный в буфере out_value. Рекомендуется, чтобы nvs_get_str/nvs_set_str использовали нуль-терминированные C-строки, а nvs_get_blob/nvs_set_blob использовали произвольные структуры данных.

Ниже показан пример (для упрощения без проверки на ошибки) использования nvs_get_str для получения строки с сохранением её в динамический массив:

size_t required_size;
nvs_get_str(my_handle, "server_name", NULL, &required_size);
char* server_name = malloc(required_size); nvs_get_str(my_handle, "server_name", server_name, &required_size);

Пример (для упрощения без проверки на ошибки) использования nvs_get_blob для получения двоичных данных и сохранения их в статический массив:

uint8_t mac_addr[6];
size_t size = sizeof(mac_addr); nvs_get_blob(my_handle, "dst_mac_addr", mac_addr, &size);

Параметры функций nvs_get_str и nvs_get_blob:

handle [in] дескриптор, полученный вызовом функции nvs_open.

key [in] имя ключа. Максимальная длина ключа составляет (NVS_KEY_NAME_MAX_SIZE-1) символов. Ключ не должен быть пустой строкой.

out_value [out] Указатель на выходное значение (буфер в памяти). Может содержать NULL, и в этом случае требуемая длина буфера будет сохранена в переменную, на которую указывает аргумент length.

length [inout] Ненулевой указатель на переменную, где хранится размер буфера в байтах, на который указывает аргумент out_value. В случае, когда out_value == NULL, переменная *length будет установлена в необходимый размер памяти для переменной (в этом случае параметр length выходной, [out]). В случае, когда out_value не NULL, значение *length должно содержать реальный размер буфера out_value в байтах. Для nvs_get_str этот размер включает нуль-терминатор.

Возвращаемые значения:

ESP_OK значение было успешно получено и сохранено в буфер out_value.

ESP_FAIL внутренняя ошибка; скорее всего это случай поврежденного раздела NVS (только если были запрещены проверки NVS assertion).

ESP_ERR_NVS_NOT_FOUND если запрошенный ключ не существует в разделе NVS.

ESP_ERR_NVS_INVALID_HANDLE если дескриптор handle был закрыт или NULL.

ESP_ERR_NVS_INVALID_NAME если имя ключа не удовлетворяет ограничениям.

ESP_ERR_NVS_INVALID_LENGTH если размер length недостаточен для сохранения данных.

[Ссылки]

1. ESP32 Non-volatile storage library site:docs.espressif.com.
2. Установка среды разработки ESP-IDF для ESP32.
3. ESP32: таблицы разделов.
4. ESP32: утилита генерации раздела NVS.