非易失性存储器(NVS)库

简介

非易失性存储器(Non-volatile storage,NVS)库被设计用于在 flash 中存储键值对。本节介绍一些在 NVS 中所使用的概念。

Underlying storage

当前,NVS 通过 spi_flash_{read|write|erase} API 使用了一部分主 flash 存储器。该库使用第一个分区 —— 其类型是 data,子类型是 nvs

该库将来可能会添加其它的存储器后端,让数据可以保存在另一个 flash(I2C 或者 SPI)芯片、RTC、FRAM 等中。

注解

如果 NVS 分区被截断了(例如分区表布局文件被修改),它上面的内容必修被擦除。ESP-IDF 构建系统提供了一个 make erase_flash 目标来擦除 flash 芯片上的所有内容。

键和值

NVS 操作的对象是键值对。键是 ASCII 字符串,当前的最大键长度是 15 个字符。值可以是下面某一种类型:

  • 整数值 uint8_t, int8_t, uint16_t, int16_t, uint32_t, int32_t, uint64_t, int64_t
  • 以零结尾的字符串
  • 长度可变的二进制数据 (blob)

在今后也可能支持其它类型,例如 floatdouble

键必须是唯一的。向一个已存在的键写值时的行为如下:

  • 如果新值与就指类型相同,则将值更新
  • 如果新值与就指类型相同,则返回一个错误

读取值时会执行能够数据类型检查。如果读操作的数据类型与值的数据类型不匹配,则返回一个错误。

命名空间

为了减小不同组件间命令冲突的可能性,NVS 将每个键值对分配到一个命名空间中。命名空间的名字与键名的规则相同,即最长 15 个字符。命名空间的名字在执行 nvs_open 调用时指定。这个调用会返回一个不透明的句柄,这个句柄在随后调用函数 nvs_read_*`nvs_write_*nvs_commit 时使用。这样,句柄与命名空间相关联,键名就不会与其它组件具有相同名字的键冲突。

安全、篡改和鲁棒性

NVS 库没有实现篡改预防策略。任何可以在物理上访问芯片的代码都可以更改、擦除或添加新的键值对。

NVS 与 ESP32 的 flash 加密系统是兼容的,它可以以加密形式存储键值对。一些元数据,例如页状态、私立入口(individual entries)的写/擦除标志,不能被加密,因为因为它们表示为有效访问和操作 flash 存储器的比特。Flash 加密可以阻止某些形式的修改:

  • 使用任意值替换键或值
  • 改变值的数据类型

下列形式的修改即使在使用了 flash 加密依然可以:

  • 完整地擦除页,移除该页中擦除的所有键值对
  • 破坏(corrupting)页中的数据,发生这种情况将会造成页被自动擦除
  • 回滚 flash 存储器的状态到某个早期快照
  • 合并 flash 存储器的两个快照,回滚一些键值对到某个早起状态(即使当前的设计还不支持 — TODO)

当 flash 存储器处于非一致性状态时,库会尝试取从这些条件中恢复。特别的,你可以在任何时间、任何点将设备断电再上电,除了新键值对(即该键值对正在写时断电了),这一般不会造成数据丢失。库能够能够使用 flash 存储器中的任何随机数进行恰当地初始化。

内部

键值对的记录

NVS 按顺序存储键值对,新的键值对被添加到末尾。当任意所给键的值被更新时,新的键值对别添加到记录(log)的默认,旧的键值对被标记未已擦除。

页和条目

NVS 库在操作时主要使用了两种实体:页(page)和条目(entry)。页是一个存储整个记录中一部分内容的逻辑结构。逻辑页对应一个 flash 存储器的扇区。正在使用的页有一个与之绑定咋一起的 序列号(sequence number)。序列号反映了也的顺序。序列号越大表示页创建的时间越晚。每个页可以处于下列的某种状态:

空/未初始化(Empty/uninitialized)
页的 flash 存储器是空的,即所有的字节都是``0xff``。在这种状态时,页不能用于存储任何数据,也没有序列号。
有效(Active)
Flash 存储器被初始化了,页的头部被写到 flash 中,且页有一个序列号。页有一些可以被写入的空的条目和数据。大多数时候,页都处于这种状态。
满(Full)
Flash 存储器处于一个一致性转台,且被写满了键值对。向该页新写的键值对将失败。此时仍然可以将某些键值对标记为已擦除。
擦除中(Erasing)
未被标记未已擦除的键值对正在被移动到另一页,然后该也就可以被擦除。这是一个临时状态,即当任何 API 调用返回时页都不会处于该状态。如果遇到突然断电,移动-擦除操作将在下一次上电后继续。
被破坏(Corrupted)
页的头部包含无效的数据,对页数据的解析将会取消。之前写入该页的任何数据都不可访问。相应的 flash 扇区不会立即擦除,将会保持为 未初始化 状态,以供今后使用。这有助于进行调试。

从 flash 扇区到逻辑页的映射没有任何特殊的顺序。库将会检查每个 flash 扇区中的页序列号,然后基于这些数字将页按照形成一个链表。

+--------+     +--------+     +--------+     +--------+
| Page 1 |     | Page 2 |     | Page 3 |     | Page 4 |
| Full   +---> | Full   +---> | Active |     | Empty  |   <- states
| #11    |     | #12    |     | #14    |     |        |   <- sequence numbers
+---+----+     +----+---+     +----+---+     +---+----+
    |               |              |             |
    |               |              |             |
    |               |              |             |
+---v------+  +-----v----+  +------v---+  +------v---+
| Sector 3 |  | Sector 0 |  | Sector 2 |  | Sector 1 |    <- physical sectors
+----------+  +----------+  +----------+  +----------+

页的结构

现在我们假设 flash 扇区的大小是 4096 字节,且 ESP32 的 flash 加密硬件是以 32 字节块为单位进行操作的。为了适应扇区大小不相同的 flash 芯片,可以在编译时(例如通过配置菜单)引入一些可配置的设置(尽管不清楚系统其它组件,例如 SPI flash 驱动和 SPI flash cache,是否可以支持其它的大小)。

页由三部分组成:头部、条目状态位映射(bitmap)和条目自身。为了与 ESP32 的 flash 加密兼容,条目的大小是 32 字节。对于整数类型,条目拥有一个键值对。对于字符串和块(blob),条目拥有部分键值对(更多的在条目的结构体描述符中)。

下列框图描述了页的结构。原括号中的数字表示每部分的大小(以字节为单位)。

+-----------+--------------+-------------+-----------+
| State (4) | Seq. no. (4) | Unused (20) | CRC32 (4) | Header (32)
+-----------+--------------+-------------+-----------+
|                Entry state bitmap (32)             |
+----------------------------------------------------+
|                       Entry 0 (32)                 |
+----------------------------------------------------+
|                       Entry 1 (32)                 |
+----------------------------------------------------+
/                                                    /
/                                                    /
+----------------------------------------------------+
|                       Entry 125 (32)               |
+----------------------------------------------------+

页的头部和条目状态位映射通常被写到 flash 的未加密部分。如果使用了 ESP32 的 flash 加密功能,条目会被加密。

页的状态是这样定义的:向某些比特写 0 可以改变状态。因此,一般没有必要通过擦除页来改变页的状态,除非要改变的状态是 已擦除 状态。

头部中计算的 CRC32 值不包括状态值(底 4 ~ 28 字节)。未使用部分当前使用 0xff 填充。今后的库可能会在这里存储格式化版本。

下面的章节描述了条目状态位映射和条目自身的结构。

条目和条目状态位映射

每个条目可以处于一下三个状态之一。每个状态都由条目状态位映射中的两个比特表示。位映射中的最后四个比特(256 - 2 * 126)未被使用。

空 Empty (2’b11)
所指定的条目还没有写入任何东西。这是一个未初始化状态(所有的字节都是 0xff)。
已写入 Written (2’b10)
一个键值对(或者跨越多个条目的键值对的一部分)已被写入到条目。
已擦除 Erased (2’b00)
该条目中的键值对被丢弃。该条目中的内容将不会被解析。

条目的结构

For values of primitive types (currently integers from 1 to 8 bytes long), entry holds one key-value pair. For string and blob types, entry holds part of the whole key-value pair. In case when a key-value pair spans multiple entries, all entries are stored in the same page.

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

                                               +--------------------------------+
                         +->    Fixed length:  | Data (8)                       |
                         |                     +--------------------------------+
          Data format ---+
                         |                     +----------+---------+-----------+
                         +-> Variable length:  | Size (2) | Rsv (2) | CRC32 (4) |
                                               +----------+---------+-----------+

Individual fields in entry structure have the following meanings:

NS
Namespace index for this entry. See section on namespaces implementation for explanation of this value.
Type
One byte indicating data type of value. See ItemType enumeration in nvs_types.h for possible values.
Span
Number of entries used by this key-value pair. For integer types, this is equal to 1. For strings and blobs this depends on value length.
Rsv
Unused field, should be 0xff.
CRC32
Checksum calculated over all the bytes in this entry, except for the CRC32 field itself.
Key
Zero-terminated ASCII string containing key name. Maximum string length is 15 bytes, excluding zero terminator.
Data
For integer types, this field contains the value itself. If the value itself is shorter than 8 bytes it is padded to the right, with unused bytes filled with 0xff. For string and blob values, these 8 bytes hold additional data about the value, described next:
Size
(Only for strings and blobs.) Size, in bytes, of actual data. For strings, this includes zero terminator.
CRC32
(Only for strings and blobs.) Checksum calculated over all bytes of data.

Variable length values (strings and blobs) are written into subsequent entries, 32 bytes per entry. Span field of the first entry indicates how many entries are used.

命名空间

As mentioned above, each key-value pair belongs to one of the namespaces. Namespaces identifiers (strings) are stored as keys of key-value pairs in namespace with index 0. Values corresponding to these keys are indexes of these namespaces.

+-------------------------------------------+
| NS=0 Type=uint8_t Key="wifi" Value=1      |   Entry describing namespace "wifi"
+-------------------------------------------+
| NS=1 Type=uint32_t Key="channel" Value=6  |   Key "channel" in namespace "wifi"
+-------------------------------------------+
| NS=0 Type=uint8_t Key="pwm" Value=2       |   Entry describing namespace "pwm"
+-------------------------------------------+
| NS=2 Type=uint16_t Key="channel" Value=20 |   Key "channel" in namespace "pwm"
+-------------------------------------------+

Item 哈希链表

To reduce the number of reads performed from flash memory, each member of Page class maintains a list of pairs: (item index; item hash). This list makes searches much quicker. Instead of iterating over all entries, reading them from flash one at a time, Page::findItem first performs search for item hash in the hash list. This gives the item index within the page, if such an item exists. Due to a hash collision it is possible that a different item will be found. This is handled by falling back to iteration over items in flash.

Each node in hash list contains a 24-bit hash and 8-bit item index. Hash is calculated based on item namespace and key name. CRC32 is used for calculation, result is truncated to 24 bits. To reduce overhead of storing 32-bit entries in a linked list, list is implemented as a doubly-linked list of arrays. Each array holds 29 entries, for the total size of 128 bytes, together with linked list pointers and 32-bit count field. Minimal amount of extra RAM useage per page is therefore 128 bytes, maximum is 640 bytes.

应用程序示例

下面这两个示例是在 ESP-IDF 示例的 storage 目录提供的:

storage/nvs_rw_value

演示了如何使用 NVS 读写一个整数值。

该值记录了 ESP32 模块重启的次数。由于它是写到 NVS 中的,因此重启后该值还保留着。

示例还演示了如何检查读/写操作是否成功,或者某个值在 NVS 中是否被初始化了。 提供的诊断消息可用于帮助跟踪程序流、采集问题。

storage/nvs_rw_blob

演示了如何使用 NVS 读/写一个整数值和一块数据(二进制大对象,blob),让它们在 ESP32 模块重启后依然保存着。

  • 值 - 跟踪 ESP32 模块软件/硬件重启的次数.
  • 块(blob) - 包含一个记录模块运行时间的表格。程序会将表格由 NVS 读取到动态分配的 RAM。每次手工触发软件复位时,新的运行时间被添加到表格中,并被写回 NVS。触发是通过下拉 GPIO0 完成的。

示例还演示了如何检查读/写操作是否成功。

API 参考手册

ESP_ERR_NVS_BASE

Starting number of error codes

ESP_ERR_NVS_NOT_INITIALIZED

The storage driver is not initialized

ESP_ERR_NVS_NOT_FOUND

Id namespace doesn’t exist yet and mode is NVS_READONLY

ESP_ERR_NVS_TYPE_MISMATCH

The type of set or get operation doesn’t match the type of value stored in NVS

ESP_ERR_NVS_READ_ONLY

Storage handle was opened as read only

ESP_ERR_NVS_NOT_ENOUGH_SPACE

There is not enough space in the underlying storage to save the value

ESP_ERR_NVS_INVALID_NAME

Namespace name doesn’t satisfy constraints

ESP_ERR_NVS_INVALID_HANDLE

Handle has been closed or is NULL

ESP_ERR_NVS_REMOVE_FAILED

The value wasn’t updated because flash write operation has failed. The value was written however, and update will be finished after re-initialization of nvs, provided that flash operation doesn’t fail again.

ESP_ERR_NVS_KEY_TOO_LONG

Key name is too long

ESP_ERR_NVS_PAGE_FULL

Internal error; never returned by nvs_ API functions

ESP_ERR_NVS_INVALID_STATE

NVS is in an inconsistent state due to a previous error. Call nvs_flash_init and nvs_open again, then retry.

ESP_ERR_NVS_INVALID_LENGTH

String or blob length is not sufficient to store data

ESP_ERR_NVS_NO_FREE_PAGES

NVS partition doesn’t contain any empty pages. This may happen if NVS partition was truncated. Erase the whole partition and call nvs_flash_init again.

类型定义

typedef uint32_t nvs_handle

Opaque pointer type representing non-volatile storage handle

枚举

enum nvs_open_mode

Mode of opening the non-volatile storage.

Values:

NVS_READONLY

Read only

NVS_READWRITE

Read and write

函数

esp_err_t nvs_flash_init(void)

Initialize NVS flash storage with layout given in the partition table.

Return
  • ESP_OK if storage was successfully initialized.
  • ESP_ERR_NVS_NO_FREE_PAGES if the NVS storage contains no empty pages (which may happen if NVS partition was truncated)
  • one of the error codes from the underlying flash storage driver

esp_err_t nvs_open(const char *name, nvs_open_mode open_mode, nvs_handle *out_handle)

Open non-volatile storage with a given namespace.

Multiple internal ESP-IDF and third party application modules can store their key-value pairs in the NVS module. In order to reduce possible conflicts on key names, each module can use its own namespace.

Return
  • ESP_OK if storage handle was opened successfully
  • ESP_ERR_NVS_NOT_INITIALIZED if the storage driver is not initialized
  • ESP_ERR_NVS_NOT_FOUND id namespace doesn’t exist yet and mode is NVS_READONLY
  • ESP_ERR_NVS_INVALID_NAME if namespace name doesn’t satisfy constraints
  • other error codes from the underlying storage driver
Parameters
  • name: Namespace name. Maximal length is determined by the underlying implementation, but is guaranteed to be at least 16 characters. Shouldn’t be empty.
  • open_mode: NVS_READWRITE or NVS_READONLY. If NVS_READONLY, will open a handle for reading only. All write requests will be rejected for this handle.
  • out_handle: If successful (return code is zero), handle will be returned in this argument.

esp_err_t nvs_set_i8(nvs_handle handle, const char *key, int8_t value)

set value for given key

This family of functions set value for the key, given its name. Note that actual storage will not be updated until nvs_commit function is called.

Return
  • ESP_OK if value was set successfully
  • ESP_ERR_NVS_INVALID_HANDLE if handle has been closed or is NULL
  • ESP_ERR_NVS_READ_ONLY if storage handle was opened as read only
  • ESP_ERR_NVS_INVALID_NAME if key name doesn’t satisfy constraints
  • ESP_ERR_NVS_NOT_ENOUGH_SPACE if there is not enough space in the underlying storage to save the value
  • ESP_ERR_NVS_REMOVE_FAILED if the value wasn’t updated because flash write operation has failed. The value was written however, and update will be finished after re-initialization of nvs, provided that flash operation doesn’t fail again.
Parameters
  • handle: Handle obtained from nvs_open function. Handles that were opened read only cannot be used.
  • key: Key name. Maximal length is determined by the underlying implementation, but is guaranteed to be at least 16 characters. Shouldn’t be empty.
  • value: The value to set.

esp_err_t nvs_set_u8(nvs_handle handle, const char *key, uint8_t value)
esp_err_t nvs_set_i16(nvs_handle handle, const char *key, int16_t value)
esp_err_t nvs_set_u16(nvs_handle handle, const char *key, uint16_t value)
esp_err_t nvs_set_i32(nvs_handle handle, const char *key, int32_t value)
esp_err_t nvs_set_u32(nvs_handle handle, const char *key, uint32_t value)
esp_err_t nvs_set_i64(nvs_handle handle, const char *key, int64_t value)
esp_err_t nvs_set_u64(nvs_handle handle, const char *key, uint64_t value)
esp_err_t nvs_set_str(nvs_handle handle, const char *key, const char *value)
esp_err_t nvs_set_blob(nvs_handle handle, const char *key, const void *value, size_t length)

set variable length binary value for given key

This family of functions set value for the key, given its name. Note that actual storage will not be updated until nvs_commit function is called.

Return
  • ESP_OK if value was set successfully
  • ESP_ERR_NVS_INVALID_HANDLE if handle has been closed or is NULL
  • ESP_ERR_NVS_READ_ONLY if storage handle was opened as read only
  • ESP_ERR_NVS_INVALID_NAME if key name doesn’t satisfy constraints
  • ESP_ERR_NVS_NOT_ENOUGH_SPACE if there is not enough space in the underlying storage to save the value
  • ESP_ERR_NVS_REMOVE_FAILED if the value wasn’t updated because flash write operation has failed. The value was written however, and update will be finished after re-initialization of nvs, provided that flash operation doesn’t fail again.
Parameters
  • handle: Handle obtained from nvs_open function. Handles that were opened read only cannot be used.
  • key: Key name. Maximal length is determined by the underlying implementation, but is guaranteed to be at least 16 characters. Shouldn’t be empty.
  • value: The value to set.
  • length: length of binary value to set, in bytes.

esp_err_t nvs_get_i8(nvs_handle handle, const char *key, int8_t *out_value)

get value for given key

These functions retrieve value for the key, given its name. If key does not exist, or the requested variable type doesn’t match the type which was used when setting a value, an error is returned.

In case of any error, out_value is not modified.

All functions expect out_value to be a pointer to an already allocated variable of the given type.

// Example of using nvs_get_i32:
int32_t max_buffer_size = 4096; // default value
esp_err_t err = nvs_get_i32(my_handle, "max_buffer_size", &max_buffer_size);
assert(err == ESP_OK || err == ESP_ERR_NVS_NOT_FOUND);
// if ESP_ERR_NVS_NOT_FOUND was returned, max_buffer_size will still
// have its default value.

Return
  • ESP_OK if the value was retrieved successfully
  • ESP_ERR_NVS_NOT_FOUND if the requested key doesn’t exist
  • ESP_ERR_NVS_INVALID_HANDLE if handle has been closed or is NULL
  • ESP_ERR_NVS_INVALID_NAME if key name doesn’t satisfy constraints
  • ESP_ERR_NVS_INVALID_LENGTH if length is not sufficient to store data
Parameters
  • handle: Handle obtained from nvs_open function.
  • key: Key name. Maximal length is determined by the underlying implementation, but is guaranteed to be at least 16 characters. Shouldn’t be empty.
  • out_value: Pointer to the output value. May be NULL for nvs_get_str and nvs_get_blob, in this case required length will be returned in length argument.

esp_err_t nvs_get_u8(nvs_handle handle, const char *key, uint8_t *out_value)
esp_err_t nvs_get_i16(nvs_handle handle, const char *key, int16_t *out_value)
esp_err_t nvs_get_u16(nvs_handle handle, const char *key, uint16_t *out_value)
esp_err_t nvs_get_i32(nvs_handle handle, const char *key, int32_t *out_value)
esp_err_t nvs_get_u32(nvs_handle handle, const char *key, uint32_t *out_value)
esp_err_t nvs_get_i64(nvs_handle handle, const char *key, int64_t *out_value)
esp_err_t nvs_get_u64(nvs_handle handle, const char *key, uint64_t *out_value)
esp_err_t nvs_get_str(nvs_handle handle, const char *key, char *out_value, size_t *length)

get value for given key

These functions retrieve value for the key, given its name. If key does not exist, or the requested variable type doesn’t match the type which was used when setting a value, an error is returned.

In case of any error, out_value is not modified.

All functions expect out_value to be a pointer to an already allocated variable of the given type.

nvs_get_str and nvs_get_blob functions support WinAPI-style length queries. To get the size necessary to store the value, call nvs_get_str or nvs_get_blob with zero out_value and non-zero pointer to length. Variable pointed to by length argument will be set to the required length. For nvs_get_str, this length includes the zero terminator. When calling nvs_get_str and nvs_get_blob with non-zero out_value, length has to be non-zero and has to point to the length available in out_value. It is suggested that nvs_get/set_str is used for zero-terminated C strings, and nvs_get/set_blob used for arbitrary data structures.

// Example (without error checking) of using nvs_get_str to get a string into dynamic array:
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);

// Example (without error checking) of using nvs_get_blob to get a binary data
into a static array:
uint8_t mac_addr[6];
size_t size = sizeof(mac_addr);
nvs_get_blob(my_handle, "dst_mac_addr", mac_addr, &size);

Return
  • ESP_OK if the value was retrieved successfully
  • ESP_ERR_NVS_NOT_FOUND if the requested key doesn’t exist
  • ESP_ERR_NVS_INVALID_HANDLE if handle has been closed or is NULL
  • ESP_ERR_NVS_INVALID_NAME if key name doesn’t satisfy constraints
  • ESP_ERR_NVS_INVALID_LENGTH if length is not sufficient to store data
Parameters
  • handle: Handle obtained from nvs_open function.
  • key: Key name. Maximal length is determined by the underlying implementation, but is guaranteed to be at least 16 characters. Shouldn’t be empty.
  • out_value: Pointer to the output value. May be NULL for nvs_get_str and nvs_get_blob, in this case required length will be returned in length argument.
  • length: A non-zero pointer to the variable holding the length of out_value. In case out_value a zero, will be set to the length required to hold the value. In case out_value is not zero, will be set to the actual length of the value written. For nvs_get_str this includes zero terminator.

esp_err_t nvs_get_blob(nvs_handle handle, const char *key, void *out_value, size_t *length)
esp_err_t nvs_erase_key(nvs_handle handle, const char *key)

Erase key-value pair with given key name.

Note that actual storage may not be updated until nvs_commit function is called.

Return
  • ESP_OK if erase operation was successful
  • ESP_ERR_NVS_INVALID_HANDLE if handle has been closed or is NULL
  • ESP_ERR_NVS_READ_ONLY if handle was opened as read only
  • ESP_ERR_NVS_NOT_FOUND if the requested key doesn’t exist
  • other error codes from the underlying storage driver
Parameters
  • handle: Storage handle obtained with nvs_open. Handles that were opened read only cannot be used.
  • key: Key name. Maximal length is determined by the underlying implementation, but is guaranteed to be at least 16 characters. Shouldn’t be empty.

esp_err_t nvs_erase_all(nvs_handle handle)

Erase all key-value pairs in a namespace.

Note that actual storage may not be updated until nvs_commit function is called.

Return
  • ESP_OK if erase operation was successful
  • ESP_ERR_NVS_INVALID_HANDLE if handle has been closed or is NULL
  • ESP_ERR_NVS_READ_ONLY if handle was opened as read only
  • other error codes from the underlying storage driver
Parameters
  • handle: Storage handle obtained with nvs_open. Handles that were opened read only cannot be used.

esp_err_t nvs_commit(nvs_handle handle)

Write any pending changes to non-volatile storage.

After setting any values, nvs_commit() must be called to ensure changes are written to non-volatile storage. Individual implementations may write to storage at other times, but this is not guaranteed.

Return
  • ESP_OK if the changes have been written successfully
  • ESP_ERR_NVS_INVALID_HANDLE if handle has been closed or is NULL
  • other error codes from the underlying storage driver
Parameters
  • handle: Storage handle obtained with nvs_open. Handles that were opened read only cannot be used.

void nvs_close(nvs_handle handle)

Close the storage handle and free any allocated resources.

This function should be called for each handle opened with nvs_open once the handle is not in use any more. Closing the handle may not automatically write the changes to nonvolatile storage. This has to be done explicitly using nvs_commit function. Once this function is called on a handle, the handle should no longer be used.

Parameters
  • handle: Storage handle to close