Перейти к содержимому

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

Метки:

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

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

В прошлой статье я рассказывал, как выделить место на flash-памяти под этот раздел, а в данной статье поговорим о том, как собственно работать с этим разделом.

 


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

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

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

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

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

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

  • целые (int8_t – uint64_t)
  • строка не длиннее 4000 байт
  • двоичные данные (blob)

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

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

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

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

 


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

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

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

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

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

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

В природе и технике не бывает идеальных сущностей, поэтому стоит упомянуть о недостатках этой модели:

  • даже на самые “мелкие” данные типа int8 тратится целая запись размером 32 байта. Но с учетом солидного размера flash-памяти на ESP32, с этим можно смириться.
  • на текущий момент нет возможности хранить данные типов double, extended и time_t, например. Но этот недостаток легко обходится с помощью blob или “приведения” к целочисленному типу того же физического размерчика.
  • одного уровня группировки пространств имен лично мне не достаточно. Но обойти тоже можно, о чем я и расскажу в следующих статьях.
  • для индексации записей используется оперативная память из кучи. Поэтому, при каждой новой записи количество свободных байт в куче будет немного уменьшаться. До тех пор, пока очередная страница не будет удалена полностью – тогда свободный остаток кучи скачкообразно увеличивается. С этим фактом приходится мирится.

 


Работа с 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;
}

Открыть пространство имен можно в режиме “только чтение” или “чтение и запись”. Если мы открываем пространство имен в режиме записи, то ошибка “namespace не найден” нам не интересна (он будет создан при записи автоматически), а вот в режиме “только чтение” – это уже может быть проблемой.

Ну и, наконец, уже можно читать и даже писать в 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);
};

 

Так что же с float, double, time_t и прочими?

Разработчики почему-то не стали добавлять поддержку “по умолчанию” для данных типов данных, хотя и оговорились, что могут сделать это в будущем. Но если гора не идет к Магомету, то Магомет идет к горе. Сделаем это сами:

esp_err_t nvs_set_float(nvs_handle_t c_handle, const char* key, float in_value)
{
  uint32_t buf = 0;
  memcpy(&buf, &in_value, sizeof(float));
  return nvs_set_u32(c_handle, key, buf);
}

esp_err_t nvs_get_float(nvs_handle_t c_handle, const char* key, float* out_value)
{
  uint32_t buf = 0;
  esp_err_t err = nvs_get_u32(c_handle, key, &buf);
  if (err == ESP_OK) {
    memcpy(out_value, &buf, sizeof(float));
  } else {
    size_t _old_mode_size = sizeof(float);
    err = nvs_get_blob(c_handle, key, out_value, &_old_mode_size);
  };
  return err;
}

esp_err_t nvs_set_double(nvs_handle_t c_handle, const char* key, double in_value)
{
  uint64_t buf = 0;
  memcpy(&buf, &in_value, sizeof(double));
  return nvs_set_u64(c_handle, key, buf);
}

esp_err_t nvs_get_double(nvs_handle_t c_handle, const char* key, double* out_value)
{
  uint64_t buf = 0;
  esp_err_t err = nvs_get_u64(c_handle, key, &buf);
  if (err == ESP_OK) {
    memcpy(out_value, &buf, sizeof(double));
  } else {
    size_t _old_mode_size = sizeof(double);
    err = nvs_get_blob(c_handle, key, out_value, &_old_mode_size);
  };
  return err;
}

esp_err_t nvs_set_time(nvs_handle_t c_handle, const char* key, time_t in_value)
{
  uint64_t buf = 0;
  memcpy(&buf, &in_value, sizeof(time_t));
  return nvs_set_u64(c_handle, key, buf);
}

esp_err_t nvs_get_time(nvs_handle_t c_handle, const char* key, time_t* out_value)
{
  uint64_t buf = 0;
  esp_err_t err = nvs_get_u64(c_handle, key, &buf);
  if (err == ESP_OK) {
    memcpy(out_value, &buf, sizeof(time_t));
  } else {
    size_t _old_mode_size = sizeof(time_t);
    err = nvs_get_blob(c_handle, key, out_value, &_old_mode_size);
  };
  return err;
}

Раньше я использовал для этих целей nvs_get_blob(), но потом просто стал приводить к целочисленным типам того же физического размера. Для обеспечения возможности корректного чтения при переходе на новую версию, пришлось усложнять функцию чтения. Вам же может пригодится, если вы захотите использовать nvs_get_blob() всегда. Ну а я, как всегда, написал для своих проектов свою библиотечку-обертку, которую вы можете найти здесь: https://github.com/kotyara12/reNvs

Это все хорошо работает и достаточно просто. Но любой параметр внутри прошивки нужно еще как-то получить “извне”, например через подписку на топик mqtt, обработать и передать в логику проекта. А вот хорошо бы сделать так, чтобы один раз создал переменную, как-то её “оформил” в какой-то библиотеке, и она (эта библиотека) взяла бы на свои хрупкие плечи всю заботу об данной переменной в дальнейшем. И такую библиотеку я тоже создал: https://github.com/kotyara12/reParams, но об этом я расскажу как-нибудь в следующий раз.

Ну вот в общем-то и всё, о чем я планировал рассказать в данной статье. Как видите, работа с NVS не представляет особой сложности. Конечно, NVS API немного сложнее, чем описано в статье, но Вы всегда можете прочитать официальные доки самостоятельно, если у Вас возникнет такая необходимость.

 


На этом пока всё, до встречи на сайте и на telegram-канале!

Статьи на данную тему:

💠 Настройка таблицы разделов flash-памяти для ESP32

💠 Полный архив статей вы найдете здесь

 


Пожалуйста, оцените статью:
[ 0 из 5, всего 0 оценок ]

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *