Изучаем NVS - энергонезависимое хранилище параметров [2024-10-25]

Практически любой проект автоматики требует применения настраиваемых во время работы программы параметров – ну например желаемая температура для термостата или пароль для подключения к сети WiFi. Получить эти данные с сервера или с панели управления не особо сложно, но сразу же возникает следующий вопрос – а что делать после перезагрузки или выключения и включения устройства? Нужно где-то хранить последнее установленное значение непосредственно на ESP.

Первоисточник: NVS - энергонезависимая библиотека хранения параметров

Разработчики ESP32 и ESP-IDF позаботились об этом, и предусмотрели специальный раздел для хранения данных в виде пар “ключ-значение” – Non-volatile Storage Library или кратко NVS. Этот механизм очень напоминает текстовые INI-файлы Windows и другие конфигурационные файлы.


Ключевые особеннoсти NVS API

Как устрoена NVS

Рабoта с NVS API

Примеры чтения и записи в энергoнезависимое хранилище

Arduinо NVS Library

Библиoграфия


Ключевые особенности NVS API

Хранилище NVS - это отдельно выделенный раздел во флэш-памяти (SPIRAM) контроллера, который в своей работе использует две основные сущности: страницы и записи. Страница — это логическая структура, в которой хранится часть общего набора данных. Логическая страница соответствует одному физическому сектору флэш-памяти или 4096 байт. Страница состоит из трех частей: заголовка, карты записей и самих записей.

Структура страницы
Структура страницы

NVS работает с парами ключ-значение. Одна пара ключ-значение называется записью. Размер записи фиксирован и равен 32 байта. Но под непосредственно данные из них остается всего 8 байт, остальное место занимают служебные данные. А что если требуется хранить данные длиннее 8 байт? Тогда используется сразу несколько записей – см. иллюстрацию ниже.

Схема хранения данных в записи
Схема хранения данных в записи

Собственно данные могут иметь один из следующих типов:

Все остальные типы данных (например float) должны быть приведены к одному из вышеперечисленных типов.

Как видно из схемы, длина ключа не должна превышать 15 символов (15 + завершающий ноль). Ключи должны быть уникальными. Говоря простыми словами ключ – это уникальное имя записи, или идентификатор, по которому API находит запрашиваемую запись.

Все ключи должны быть объединены в одном из пространств имен (группе). Длина имени группы также не должна превышать 15 символов.

Можно использовать шифрование данных в разделах NVS, дополнительно защищая чувствительные данные.

в начало

Как устроена NVS

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

При изменении в конце страницы добавляется новая пара ключ-значение, а старая запись помечается как удаленная. Как только вся страница будет заполнена, система открывает следующую страницу, и т.д.

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

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

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

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

в начало

Работа с NVS API

Прежде чем начинать пользоваться разделом NVS, его нужно инициализировать, например, как в следующем скетче:

esp_err_t err = nvs_flash_init();
if ((err == ESP_ERR_NVS_NO_FREE_PAGES) || (err == ESP_ERR_NVS_NEW_VERSION_FOUND)) 
{
  ESP_LOGW("NVS", "Erasing NVS partition...");
  nvs_flash_erase();
  err = nvs_flash_init();
};
if (err == ESP_OK) 
{
  ESP_LOGI("NVS", "NVS partition initilized");
} 
else 
{
  ESP_LOGE("NVS", "NVS partition initialization error: %d (%s)", err, esp_err_to_name(err));
};

В приведенном коде пытаемся открыть NVS раздел с помощью nvs_flash_init(). Если функция возвращает состояние “нет свободных страниц” или “найдена новая версия”, то стираем раздел и пытаемся инициализировать его повторно.

Затем можно уже начинать открывать пространство имен и работать с ним. Открыть пространство имен можно в режиме “только чтение” или “чтение и запись”:

bool nvsOpen(const char* name_group, nvs_open_mode_t open_mode, nvs_handle_t *nvs_handle)
{
  esp_err_t err = nvs_open(name_group, open_mode, nvs_handle); 
  if (err != ESP_OK) 
  {
    if (!((err == ESP_ERR_NVS_NOT_FOUND) && (open_mode == NVS_READONLY))) 
    {
      ESP_LOGE("NVS", "Error opening NVS namespace \"%s\": %d (%s)!", name_group, err, esp_err_to_name(err));
    };
    return false;
  };
  return true;
}

Ну и, наконец, уже можно читать и даже писать в NVS раздел, например так:

nvs_handle_t nvs_handle;
if (nvsOpen("counters", NVS_READWRITE, &nvs_handle)) 
{
  nvs_set_u32(nvs_handle, "total", _counters.cntTotal);
  nvs_set_u32(nvs_handle, "today", _counters.cntToday);
  nvs_set_u32(nvs_handle, "yesterday", _counters.cntYesterday);
  nvs_close(nvs_handle);
};
в начало

Примеры чтения и записи в энергонезависимое хранилище

В первом примере сохраняемое значение указывает на количество перезапусков модуля ESP32, который увеличивается при каждом запуске. Поскольку оно записывается в NVS, значение сохраняется между перезапусками.

Скетч1: Сохранить в NVS количество перезапусков модуля ESP32

/** Arduino-Esp32-CAM                                  *** nvs-rw-value.ino ***
 * 
 *                Сохранить в NVS количество перезапусков модуля ESP32, которое
 *     увеличивается при каждом запуске (поскольку значение записывается в NVS, 
 *                                        оно сохраняется между перезапусками).
 *                 Также проверить, была ли операция чтения/записи успешной или 
 *            определённое значение не было инициализировано в NVS. Диагностику 
 *         представить в виде обычного текста, чтобы можно было отслеживать ход 
 *                               выполнения программы и выявлять любые проблемы
 *                                        (на контроллере AI-Thinker ESP32-CAM)
 * 
 * v1.1, 21.10.2024                                   Автор:      Труфанов В.Е.
 * Copyright © 2024 tve                               Дата создания: 21.10.2024
 * 
**/


#include <Arduino.h>
#include "nvs_flash.h"
#include "nvs.h"

char buffer[60];

void setup() 
{
   Serial.begin(115200);
   // --------------------------------------------------- Инициализация NVS ---
   esp_err_t err = nvs_flash_init();
   // Если раздел NVS не содержит пустых страниц или он содержит данные в 
   // незнакомом формате, который не распознаётся текущей версией кода,
   // то стираем весь раздел и снова вызываем инициализацию
   if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) 
   {
      ESP_ERROR_CHECK(nvs_flash_erase());
      err = nvs_flash_init();
   }
   ESP_ERROR_CHECK(err);
   // Открываем хранилище параметров
   Serial.print("\n");
   Serial.print("Открываем энергонезависимое хранилище (NVS) ... ");
   nvs_handle_t my_handle;
   err = nvs_open("storage", NVS_READWRITE, &my_handle);
   if (err != ESP_OK) 
   {
      sprintf(buffer,"Ошибка (%s) открытия хранилища!\n", esp_err_to_name(err));
      Serial.print(buffer);
   } 
   else 
   {
      Serial.println("Сделано!");
      // ----------------------------------------------------------- Чтение ---
      Serial.print("Считываем значение счетчика перезапусков из NVS ... ");
      // Присваиваем начальное значение счетчику = 0, 
      // на случай, если значение счетчика еще не было записано в NVS
      int32_t restart_counter = 0; 
      err = nvs_get_i32(my_handle, "restart_counter", &restart_counter);
      switch (err) 
      {
         case ESP_OK:
            Serial.println("Сделано!");
            sprintf(buffer,"Значение счётчика перезапусков = %" PRIu32 "\n", restart_counter);
            Serial.print(buffer);
         break;
         case ESP_ERR_NVS_NOT_FOUND:
            Serial.print("Значение счётчика еще не инициализировано!\n");
            break;
         default :
            sprintf(buffer,"Ошибка чтения (%s)!\n", esp_err_to_name(err));
            Serial.print(buffer);
      }
      // ----------------------------------------------------------- Запись ---
      Serial.print("Обновляем счётчик перезапусков в NVS ... ");
      restart_counter++;
      err = nvs_set_i32(my_handle, "restart_counter", restart_counter);
      Serial.print((err != ESP_OK) ? "Не получилось!\n" : "Сделано!\n");
      // Фиксируем записанные значения (после установки любых значений необходимо
      // вызывать функцию nvs_commit(), чтобы обеспечить запись изменений во 
      // флэш-память)
      Serial.print("Фиксируем обновления в NVS ... ");
      err = nvs_commit(my_handle);
      Serial.print((err != ESP_OK) ? "Не получилось!\n" : "Сделано!\n");
      // ----------------------------------------------------- Закрытие NVS ---
      nvs_close(my_handle);
   }
   // Предупреждаем о перезапусе контроллера и перезапускаем его
   for (int i = 10; i >= 0; i--) 
   {
      sprintf(buffer,"Перезапуск через %d секунд ...\n", i);
      Serial.print(buffer);
      vTaskDelay(1000 / portTICK_PERIOD_MS);
   }
   Serial.print("Перезапускаем контроллер.\n\n");
   esp_restart();
}

void loop() {}

// ******************************************************* nvs-rw-value.ino ***

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

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

Открываем энергонезависимое хранилище (NVS) ... Сделано!
Считываем значение счетчика перезапусков из NVS ... Значение счётчика еще не инициализировано!
Обновляем счётчик перезапусков в NVS ... Сделано!
Фиксируем обновления в NVS ... Сделано!
Перезапуск через 10 секунд ...
Перезапуск через 9 секунд ...
Перезапуск через 8 секунд ...
Перезапуск через 7 секунд ...
Перезапуск через 6 секунд ...
Перезапуск через 5 секунд ...
Перезапуск через 4 секунд ...
Перезапуск через 3 секунд ...
Перезапуск через 2 секунд ...
Перезапуск через 1 секунд ...
Перезапуск через 0 секунд ...
Перезапускаем контроллер.

Последующие запуски:

Открываем энергонезависимое хранилище (NVS) ... Сделано!
Считываем значение счетчика перезапусков из NVS ... Сделано!
Значение счётчика перезапусков = 1
Обновляем счётчик перезапусков в NVS ... Сделано!
Фиксируем обновления в NVS ... Сделано!
Перезапуск через 10 секунд ...
Перезапуск через 9 секунд ...
Перезапуск через 8 секунд ...
Перезапуск через 7 секунд ...
Перезапуск через 6 секунд ...
Перезапуск через 5 секунд ...
Перезапуск через 4 секунд ...
Перезапуск через 3 секунд ...
Перезапуск через 2 секунд ...
Перезапуск через 1 секунд ...
Перезапуск через 0 секунд ...
Перезапускаем контроллер.

Чтобы сбросить счётчик, сотрите содержимое флэш-памяти, затем снова загрузите программу. Почистить флэш-память можно, например, при загрузке скетча из IDE Arduino с установленным в состояние “Enabled” параметром компиляции “Erase All Flash Before Scetcn Upload” в закладке “Инструменты”.

Во втором примере показано, как считывать и записывать целочисленное значение и большой двоичный объект (blob) с помощью NVS для их сохранения между перезапусками модуля ESP.

Целое сохраняемое значение отслеживает количество программных и жестких перезапусков.

Большой двоичный объект содержит таблицу (массив) времён выполнения модуля между перезапусками контроллера.

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

Перезагрузки контроллера запускаются замыканием пина для перезагрузки на GND (GPIO0 на ESP32 и ESP32S2, GPIO9 на ESP32C3).

Скетч2: Сохранить в NVS количество перезапусков и таблицу времён выполнения между перезапусками

/** Arduino-Esp32-CAM                                   *** nvs-rw-blob.ino ***
 * 
 *       Показать, как считывать и записывать целое значение и большой двоичный 
 *          объект (binary large object) с использованием NVS для их сохранения 
 *                                   между перезапусками контроллеров ESP, где:
 *  целое значение - отслеживает количество программных и жестких перезапусков,
 *  большой двоичный объект - содержит таблицу(в смыле - массив) с указанием 
 *  времени выполнения модуля между перезапусками. 
 *  
 *  Таблица считывается из NVS в динамически выделяемую оперативную память. При 
 *  каждом программном перезапуске, запускаемом вручную, в таблицу добавляется 
 *  новое время выполнения и записывается обратно в NVS. Перезагрузки контроллера 
 *  запускаются замыканием пинов для перезагрузки на GND (IO0 на ESP32 и ESP32S2, 
 *  IO9 на ESP32C3).
 * 
 * v1.1, 24.10.2024                                   Автор:      Труфанов В.Е.
 * Copyright © 2024 tve                               Дата создания: 23.10.2024
 * 
**/


#include <Arduino.h>
#include "nvs_flash.h"
#include "nvs.h"

#define STORAGE_NAMESPACE "storage"

// Переопределяем пин для запуска перезагрузок контроллера
#if CONFIG_IDF_TARGET_ESP32C3
   #define BOOT_MODE_PIN GPIO_NUM_9
#else
   #define BOOT_MODE_PIN GPIO_NUM_0
#endif

// ****************************************************************************
// *    Сохранить количество перезапусков модуля в NVS, сначала прочитав, а   *
// *           затем увеличив число, которое было сохранено ранее.            *
// *         Вернуть сообщение об ошибке, если что-то пойдет не так           *
// *                         во время этого процесса.                         *
// ****************************************************************************
esp_err_t save_restart_counter(void)
{
   nvs_handle_t my_handle;
   esp_err_t err;
   // Открываем хранилище
   err = nvs_open(STORAGE_NAMESPACE, NVS_READWRITE, &my_handle);
   if (err != ESP_OK) return err;
   // Читаем, меняем значение и записываем счетчик перезагрузок
   int32_t restart_counter = 0; 
   err = nvs_get_i32(my_handle, "restart_conter", &restart_counter);
   if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) return err;
   restart_counter++;
   err = nvs_set_i32(my_handle, "restart_conter", restart_counter);
   if (err != ESP_OK) return err;
   // Фиксируем записанные значения (после установки любых значений необходимо 
   // вызывать функцию nvs_commit(), чтобы обеспечить запись изменений во 
   // флэш-память.
   err = nvs_commit(my_handle);
   if (err != ESP_OK) return err;
   // Закрываем хранилище
   nvs_close(my_handle);
   return ESP_OK;
}
// ****************************************************************************
// *  Сохранить новое значение времени выполнения между перезапусками в NVS,  *
// *     сначала прочитав таблицу с ранее сохраненными значениями, а затем    *
// *                 добавив новое значение в конец таблицы.                  *
// *         Вернуть сообщение об ошибке, если что-то пойдет не так           *
// *                        во время этого процесса.                          *
// ****************************************************************************
esp_err_t save_run_time(void)
{
   nvs_handle_t my_handle;
   esp_err_t err;
   // Открываем хранилище
   err = nvs_open(STORAGE_NAMESPACE, NVS_READWRITE, &my_handle);
   if (err != ESP_OK) return err;
   // Определяем размер памяти, которая требуется для двоичного объекта (таблицы)
   size_t required_size = 0;  
   err = nvs_get_blob(my_handle, "run_time", NULL, &required_size);
   if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) return err;
   // Если это возможно, то считываем ранее сохранённую таблицу
   uint32_t* run_time = malloc(required_size + sizeof(uint32_t));
   if (required_size > 0) 
   {
      err = nvs_get_blob(my_handle, "run_time", run_time, &required_size);
      if (err != ESP_OK) 
      {
         free(run_time);
         return err;
      }
   }
   // Записываем новое значение, включая ранее сохраненные значения 
   required_size += sizeof(uint32_t);
   run_time[required_size / sizeof(uint32_t) - 1] = xTaskGetTickCount() * portTICK_PERIOD_MS;
   err = nvs_set_blob(my_handle, "run_time", run_time, required_size);
   free(run_time);
   if (err != ESP_OK) return err;
   // Фиксируем новые занесённые значения
   err = nvs_commit(my_handle);
   if (err != ESP_OK) return err;
   // Закрываем хранилище
   nvs_close(my_handle);
   return ESP_OK;
}
// ****************************************************************************
// *    Выбрать из NVS и показать счетчик перезапусков и таблицу с указанием  *
// * времён выполнения между перезапусками. Вернуть сообщение об ошибке, если *
// *               что-то пойдет не так во время этого процесса.              *
// ****************************************************************************
esp_err_t print_what_saved(void)
{
   // Открываем хранилище параметров
   nvs_handle_t my_handle;
   esp_err_t err;
   err = nvs_open(STORAGE_NAMESPACE, NVS_READWRITE, &my_handle);
   if (err != ESP_OK) return err;
   printf("Раздел NVS открыт!\n");
   // Считываем значение счетчика перезапусков из NVS
   int32_t restart_counter = 0;
   err = nvs_get_i32(my_handle, "restart_conter", &restart_counter);
   if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) return err;
   printf("Счетчик перезапусков = %" PRIu32 "\n", restart_counter);
   // Считываем таблицу времен выполнения между перезапусками
   size_t required_size = 0;  
   // Инициируем выборку размера blob-элемента
   err = nvs_get_blob(my_handle, "run_time", NULL, &required_size);
   if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) return err;
   printf("Таблица времён между перезапусками:\n");
   // В соответствии с определенным размером таблицы, выделяем память
   // и считываем таблицу
   if (required_size == 0) 
   {
      printf("еще данные не сохранялись!\n");
   } 
   else 
   {
      uint32_t* run_time = malloc(required_size);
      err = nvs_get_blob(my_handle, "run_time", run_time, &required_size);
      if (err != ESP_OK) 
      {
         free(run_time);
         return err;
      }
      for (int i = 0; i < required_size / sizeof(uint32_t); i++) 
      {
         printf("%d: %" PRIu32 "\n", i + 1, run_time[i]);
      }
      free(run_time);
   }
   // Закрываем хранилище
   nvs_close(my_handle);
   printf("Раздел NVS закрыт!\n");
   return ESP_OK;
}

void setup() 
{
   Serial.begin(115200);
   
   // Инициализируем NVS
   esp_err_t err = nvs_flash_init();
   printf("Проинициализировали NVS сразу ...\n");
   // Если раздел NVS не содержит пустых страниц или он содержит данные в 
   // незнакомом формате, который не распознаётся текущей версией кода,
   // то стираем весь раздел и снова вызываем инициализацию
   if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) 
   {
      ESP_ERROR_CHECK(nvs_flash_erase());
      err = nvs_flash_init();
      printf("Затерли весь раздел NVS и проинициализировали ...\n");
   }
   ESP_ERROR_CHECK(err);
   // Выбираем из NVS и показываем счетчик перезапусков и таблицу с указанием
   // времён выполнения между перезапусками
   err = print_what_saved();
   if (err != ESP_OK) printf("Ошибка (%s) открытия хранилища!\n", esp_err_to_name(err));
   // Сохраняем количество перезапусков модуля в NVS
   err = save_restart_counter();
   if (err != ESP_OK) printf("Ошибка (%s) сохранения счетчика перезапусков в NVS!\n", esp_err_to_name(err));

   printf("\nЗамкните GPIO0 на GND для того, чтобы продолжить работу скетча! ...\n");
   
   // По обязательному правилу после сброса контроллера заданный контакт 
   // устанавливаем в режим ввода-вывода
   gpio_reset_pin(BOOT_MODE_PIN);
   // Определяем пин как GPIO_MODE_INPUT - работающий только на вход
   gpio_set_direction(BOOT_MODE_PIN, GPIO_MODE_INPUT);
   
   // Считываем состояние GPIO0. Если GPIO0 на нижнем уровне, то есть
   // замкнут на GND, то через 1000 ms (1000 тактов),
   // сохраняем время выполнения и перезапускаем контроллер
   while (1) 
   {
      // Если IO0 замкнут на GND, запускаем фрагмент кода
      if (gpio_get_level(BOOT_MODE_PIN) == 0) 
      {
         vTaskDelay(1000 / portTICK_PERIOD_MS);
         if(gpio_get_level(BOOT_MODE_PIN) == 0) 
         {
            err = save_run_time();
            if (err != ESP_OK) printf("Ошибка (%s) сохранения таблицы времен выполнения в NVS!\n", esp_err_to_name(err));
            printf("Перезапуск...\n");
            // Выводим остаток последней незавершённой строки (без "\n")
            fflush(stdout);
            esp_restart();
         }
      }
      vTaskDelay(2200 / portTICK_PERIOD_MS);
   }
}

void loop() {}

// ******************************************************** nvs-rw-blob.ino ***

В примере также выводятся диагностические сообщения при ошибках в работе с NVS - выделенном энергонезависимом хранилище данных.

в начало

Arduino NVS Library

Arduino NVS Library — это библиотека упрощенной работы с хранилищем в энергонезависимой памяти (разделом NVS во флэш-памяти) для ESP32 на платформе Arduino. Он объединяет основные функции NVS в класс C++ в стиле Arduino. Эта библиотека является дальнейшим развитием работы TridentTD_ESP32NVS.

NVS lib (обычно называемая «флэш-библиотекой») — это библиотека, используемая для хранения значений данных во флэш-памяти ESP32. Так как данные таким образом хранятся в энергонезависимой памяти, поэтому они неизменно остаются в памяти после отключения питания или перезагрузки ESP32.

ESP32 NVS хранит данные в виде пар «ключ-значение». Ключи представляют собой строки ASCII длиной до 15 символов. Значения могут иметь один из следующих типов:

Скетч3: Пример работы с библиотекой ArduinoNvs

/** Arduino-Esp32-CAM                                *** simpleFlashNVS.ino ***
 * 
 *               Пример работы с библиотекой ArduinoNvs:
 *               
                 * Authors:
                 * dRKr, Sinai RnD (info@sinai.io)
                 * (original version) TridentTD (https://github.com/TridentTD/)
 * 
 * v1.0, 24.10.2024                                   Автор:      Труфанов В.Е.
 * Copyright © 2024 tve                               Дата создания: 24.10.2024
 * 
**/


#include <Arduino.h>
#include "ArduinoNvs.h"

bool res;

void setup() 
{   
   Serial.begin(115200);
   // Инициируем NVS
   NVS.begin();
   // Записываем в NVS (flash) и читаем целое число
   const uint32_t ui32_set = 4294967295;
   res = NVS.setInt("unsigned-long", ui32_set);
   uint32_t uint32_max = NVS.getInt("unsigned-long"); 
   printf("МАХ целое без знака: в десятичном виде = %u, в шестнадцатеричном = %#x"  "\n", uint32_max, uint32_max);
   // Записываем в NVS и читаем строку
   const String st_set = "Это простая незамысловатая строка для записи в NVS";
   res = NVS.setString("str", st_set);
   String str = NVS.getString("str");
   Serial.println(str);
   // Записываем и читаем двоичные данные переменной длины (blob)
   uint8_t blolb_set[8] = {1,2,3,99,100,0xEE,0xFE,0xEE};
   res = NVS.setBlob("blob", blolb_set, sizeof(blolb_set));
   size_t blobLength = NVS.getBlobSize("blob"); 
   uint8_t blob[blobLength];
   res = NVS.getBlob("blob", blob, sizeof(blob));
   if (res) 
   {
      for (uint8_t i = 0; i < blobLength; i++) 
      {
         Serial.printf("blob[%u] = %u; ", i, blob[i]);
      }
   }
   else
   {
      Serial.println("Не получилось извлечь BLOB из NVS");
   }
}

void loop(){}

// ***************************************************** simpleFlashNVS.ino ***

Библиография

Non-Volatile Storage Library

Пример создания образа раздела NVS из содержимого CSV-файла

в начало