ESP32. Урок 33. Энергонезависимое хранилище данных NVS
Помимо файловой системы SPIFFS в контроллере ESP32 для энергонезависимого хранения данных используется также энергонезависимое хранилище данных NVS.
Энергонезависимое хранилища данных (Non-volatile storage, NVS) работает на уровне комплекта IDF и разработано оно для хранения пар ключ-значение в памяти FLASH.
Данный способ хранения порой упрощает работу с хранением структурированных данных, так как хранить их в файлах не совсем рентабельно. Так как файл надо полностью открыть, закрыть, перезаписать весь при незначительном изменении и т.д.
Комплект IDF предоставляет очень немалый функционал по работе с NVS, только полностью мы с ним со всем не будем в рамках данного занятия знакомиться, а познакомимся мы лишь с некоторой частью данного функционала. Это позволит нам не нагромождать теорию в мозгах и легче усвоить материал. То есть мы будем знакомиться лишь с той частью, которая, как я считаю нам пригодится в недалёком будущем, а если нам потребуется знать что-то ещё, мы всегда успеем это сделать.
Схема наша будет состоять из платы с контроллером ESP32, подключенной к порту USB компьютера
А проект мы возьмём из давнего урока 4, в котором мы работали с кнопкой, с именем BUTTON01 и на его основе создадим проект с именем NVS01.
Откроем наш проект в ESPRESSIF IDE и из файла Kconfig.projbuild удалим пункт меню по работе с ножкой, к которой была подключена кнопка (BUTTON_GPIO). Также в данном файле ножку порта по умолчанию для светодиода выберем вторую
default 48 if IDF_TARGET_ESP32S3
default 2
В функции app_main файла main.c оставим только вот эти строки
|
1 2 3 4 5 6 7 |
//------------------------------------------------------------- void app_main(void) { gpio_reset_pin(CONFIG_BLINK_GPIO); gpio_set_direction(CONFIG_BLINK_GPIO, GPIO_MODE_OUTPUT); } //------------------------------------------------------------- |
Не забываем также в конфигураторе выбрать 4 мегабайта объёма памяти FLASH

Попробуем собрать наш проект. Если всё нормально собирается, то ещё создадим заголовочный файл main.h, в который соберём все подключенные заголовочные файлы из main.c, а заодно и подключим заголовочные файлы для работы с хранилищем NVS
|
1 2 3 4 5 6 7 8 9 10 11 12 |
#ifndef MAIN_MAIN_H_ #define MAIN_MAIN_H_ //------------------------------------------------------------- #include <stdio.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "driver/gpio.h" #include "nvs_flash.h" #include "nvs.h" #include "sdkconfig.h" //------------------------------------------------------------- #endif /* MAIN_MAIN_H_ */ |
Вернёмся в main.c и удалим подключение всех заголовочных файлов, а подключим только один
|
1 |
#include "main.h" |
В принципе, мы немного уже затрагивали тему хранилища NVS. По крайней мере, в проектах с использованием сети WiFi мы её инициализировали. Добавим такой же код инициализации в функции app_main и сейчас
|
1 2 3 4 5 6 7 |
gpio_set_direction(CONFIG_BLINK_GPIO, GPIO_MODE_OUTPUT); // Initialize NVS esp_err_t err = nvs_flash_init(); 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(); } |
Попробуем собрать проект и прошить его.
Запустим терминал и посмотрим, где у нас находится раздел хранилища nvs и каков его объём
С помощью esptool для чистоты эксперимента мы полностью очистим данный участок памяти
Вернёмся в наш проект в функцию app_main файла main.c и попробуем вывести терминал об уже существующих записях в хранилище
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
err = nvs_flash_init(); } //listing all the key-value pairs of any type under specified partition and namespace nvs_iterator_t it = NULL; it = nvs_entry_find("nvs", NULL, NVS_TYPE_ANY); while (it != NULL) { nvs_entry_info_t info; nvs_entry_info(it, &info); it = nvs_entry_next(it); printf("key '%s', type '%02x' \n", info.key, info.type); }; nvs_release_iterator(it); |
Краткое описание используемых функций в данном блоке кода:
nvs_entry_find возвращает итератор, который используется в последующих вызовах и который позволяет пролистать пары ключ-значение, сохраненные в NVS, основываясь на имени раздела, namespace, и типе данных;
nvs_entry_info возвращает информацию для каждой пары ключ-значение;
nvs_entry_next возвращает итератор для следующей пары ключ-значение.
Также следует отметить, что хранилище nvs позволяет работать с очень широким списком типов данных, а именно int8_t, uint8_t, int16_t, uint16_t, int32_t, uint32_t, int64_t, uint64_t, строками, а также данными переменной длины (blob).
Соберём код, прошьём контроллер и посмотрим результат работы кода в терминале
Наш код нам ничего не вернул.
Это результат того, что мы очистили раздел. Если бы перед этим был бы прошит проект прошлого урока, то у нас бы были данные, так как мы работали с WiFi.
Поэтому давайте попробуем что-нибудь записать в хранилище.
Для этого мы сначала должны объявить хендл и открыть хранилище, передав ему пространство имён, способ открытия и адрес хендла. Затем мы пока закроем наше хранилище при удачном его открытии
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
nvs_release_iterator(it); nvs_handle_t my_handle; // Open printf("\n"); printf("Opening Non-Volatile Storage (NVS) handle... "); err = nvs_open("storage", NVS_READWRITE, &my_handle); if (err != ESP_OK) { printf("Error (%s) opening NVS handle!\n", esp_err_to_name(err)); } else { printf("Done\n"); // Close nvs_close(my_handle); } printf("\n"); |
А дальнейший код мы уже будем, соответственно, писать перед закрытием хранилища nvs.
Попробуем собрать код и прошить контроллер.
В терминале мы видим, что хранилище удачно открылось

Попробуем что-нибудь записать в наше хранилище. Для этого сначала объявим инициализируем несколько переменных различных целочисленных типов
|
1 2 3 4 5 6 7 8 9 10 11 |
printf("Done\n"); // Write int8_t val01 = -108; uint8_t val02 = 198; int16_t val03 = -10135; uint16_t val04 = 61145; int32_t val05 = -101459; uint32_t val06 = 3352582294; int64_t val07 = -4047512760478312334; uint64_t val08 = 7214561357951254753; |
Затем попробуем записать данные значения в хранилище под различными именами ключей
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
uint64_t val08 = 7214561357951254753; printf("Updating data in NVS ... "); err = nvs_set_i8(my_handle, "dt01", val01); printf((err != ESP_OK) ? "Failed!\n" : "Done\n"); err = nvs_set_u8(my_handle, "dt02", val02); printf((err != ESP_OK) ? "Failed!\n" : "Done\n"); err = nvs_set_i16(my_handle, "dt03", val03); printf((err != ESP_OK) ? "Failed!\n" : "Done\n"); err = nvs_set_u16(my_handle, "dt04", val04); printf((err != ESP_OK) ? "Failed!\n" : "Done\n"); err = nvs_set_i32(my_handle, "dt05", val05); printf((err != ESP_OK) ? "Failed!\n" : "Done\n"); err = nvs_set_u32(my_handle, "dt06", val06); printf((err != ESP_OK) ? "Failed!\n" : "Done\n"); err = nvs_set_i64(my_handle, "dt07", val07); printf((err != ESP_OK) ? "Failed!\n" : "Done\n"); err = nvs_set_u64(my_handle, "dt08", val08); printf((err != ESP_OK) ? "Failed!\n" : "Done\n"); |
Соберём код, прошьём контроллер и посмотрим результат

Если мы перезагрузим контроллер после этого, то предыдущий код по перебору записей нам также даст результаты

Мы можем не бояться, перезагружая контроллер, того, что у нас будет повторяться процесс записи и тем самым вызовет задвоение данных. Здесь ситуация такая. Если данные пишутся под уже существующим именем ключа, то они не создают новую пару ключ-значение в том случае, если не изменился тип данных, они просто модифицируются, если есть изменение значения.
Мы также видим в терминале только типы данных, сами данные мы не видим.
Давайте запишем ещё и строки
|
1 2 3 4 5 6 7 8 9 10 11 |
err = nvs_set_u64(my_handle, "dt08", val08); printf((err != ESP_OK) ? "Failed!\n" : "Done\n"); err = nvs_set_str(my_handle, "MySSID", "MyLogin"); printf((err != ESP_OK) ? "Failed!\n" : "Done\n"); err = nvs_set_str(my_handle, "MyPASS", "123abc7890"); printf((err != ESP_OK) ? "Failed!\n" : "Done\n"); printf("Committing updates in NVS ... "); err = nvs_commit(my_handle); printf((err != ESP_OK) ? "Failed!\n" : "Done\n"); |
По окончанию записи мы вызываем функцию nvs_commit для применения изменений.
А чтобы видеть значения данных, для этого мы напишем код следующий.
Для начала в целях чистоты эксперимента мы объявим новые переменные, так как в существующих у нас уже есть значения
|
1 2 3 4 5 6 7 8 9 10 11 |
printf((err != ESP_OK) ? "Failed!\n" : "Done\n"); // Read int8_t res01 = 0; uint8_t res02 = 0; int16_t res03 = 0; uint16_t res04 = 0; int32_t res05 = 0; uint32_t res06 = 0; int64_t res07 = 0; uint64_t res08 = 0; |
Затем прочитаем пока целочисленные ключи и отобразим значения в терминале
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
uint64_t res08 = 0; nvs_get_i8(my_handle, "dt01", &res01); printf("dt01: %d\n", res01); nvs_get_u8(my_handle, "dt02", &res02); printf("dt02: %d\n", res02); nvs_get_i16(my_handle, "dt03", &res03); printf("dt03: %d\n", res03); nvs_get_u16(my_handle, "dt04", &res04); printf("dt04: %d\n", res04); nvs_get_i32(my_handle, "dt05", &res05); printf("dt05: %d\n", res05); nvs_get_u32(my_handle, "dt06", &res06); printf("dt06: %u\n", res06); nvs_get_i64(my_handle, "dt07", &res07); printf("dt07: %lld\n", res07); nvs_get_u64(my_handle, "dt08", &res08); printf("dt08: %llu\n", res08); |
Далее прочитаем ключи строкового (символьного) типа. Здесь всё несколько замороченно
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
printf("dt08: %llu\n", res08); printf("Reading MySSID from NVS ... "); size_t nvs_required_size; err = nvs_get_str(my_handle, "MySSID", NULL, &nvs_required_size); switch (err) { case ESP_OK: printf("Done\n"); char *nvs_ret_data; nvs_ret_data = malloc(nvs_required_size); err = nvs_get_str(my_handle, "MySSID", nvs_ret_data, &nvs_required_size ); printf("MySSID: %s\n", nvs_ret_data); free(nvs_ret_data); break; case ESP_ERR_NVS_NOT_FOUND: printf("The value is not initialized yet!\n"); break; default : printf("Error (%s) reading!\n", esp_err_to_name(err)); } printf("Reading MyPASS from NVS ... "); err = nvs_get_str(my_handle, "MyPASS", NULL, &nvs_required_size); switch (err) { case ESP_OK: printf("Done\n"); char *nvs_ret_data; nvs_ret_data = malloc(nvs_required_size); err = nvs_get_str(my_handle, "MyPASS", nvs_ret_data, &nvs_required_size ); printf("MyPASS: %s\n", nvs_ret_data); free(nvs_ret_data); break; case ESP_ERR_NVS_NOT_FOUND: printf("The value is not initialized yet!\n"); break; default : printf("Error (%s) reading!\n", esp_err_to_name(err)); } |
Соберём код, прошьём контроллер и посмотрим результат в терминале

Всё отлично прочиталось!
Итак, на данном уроке мы познакомились с энергонезависимым хранилищем NVS, а также поупражнялись с записью в данное хранилище и чтением из него различных типов данных.
Всем спасибо за внимание!
Недорогие отладочные платы ESP32 можно купить здесь Недорогие отладочные платы ESP32
Недорогие отладочные платы ESP32/ESP32-C3/ESP32-S3 можно купить здесь Недорогие отладочные платы ESP32
Логический анализатор 16 каналов можно приобрести здесь
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в Дзен (нажмите на картинку)






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