ESP32-IDF 非易失存储 NVS

@toc


零、前言

NVS(Non-volatile storage,非易失存储),意思是掉电后能依然能持久化保存数据。在我们应用 NVS 时,一般用于存储一些配置数据、状态数据等,一般不会用来存储存放大量的数据量。

在嵌入式系统中,NVS 主要是在 Flash 进行==键值对==的存储。举个例子,假设我们要把东西存到 Flash 中,按照底层的操作习惯,我们要先指定一个地址,然后对这个地址执行擦除操作,然后才能写入;读取的时候也需要根据这个地址,然后指定读取长度。如果我们要存的项比较多,又在代码中比较分散,我们对 Flash 的地址就很难管理。因为我们很难知道要存的内容与其他地址有没冲突,会不会误擦除。存在诸多问题。所以需要一个机制,方便帮我们把这些检测判断活都干了,不需要我们指定地址操作。文件系统就是这样的,但 NVS 操作更加轻量级。

在 NVS 中,我们要存一个值,我们不需要指定地址,但需要指定一个“键” key,然后我们在这个“键”索引下存我们的值 value。假设我们要存 WIFI 的 SSID 和 pasword,我们可以在 NVS 中这样定义:

1
2
key = ssid,value = testwifi
key = password,value = 12345678
  • 在键名 ssid 下,我们存的值是 testwifi
  • 在键名 password 下,我们存的值是 12345678。

因此键名 key 一般不会修改,经常修改的是 value。

在 ESP32 中对于 NVS 的操作,还需要指定一个命名空间,是因为还考虑了一种情况,在各个不同的功能模块中,键名是有可能取到一样的,比如对于 WIFI 模块,存在一个 password 键名,对于管理员模块,可能也存在一个 password 键名,这样有可能就造成了重复,程序无法按我们的意思进行。如果我们增加了一个命名空间进行隔离,那么键名有重复也不怕,比如说在 WIFI 模块中,我们指定一个命名空间”wifi”,在此命名空间下有 ssid 和 password 键名;在管理员模块,我们指定一个命名空间”manager”,在此命名空间下有 password 键名,这两组命名空间互不干扰。

一、基本介绍

1、配置结构体

1.1 nvs_entry_info_t

存储从 nvs_entry_info() 中获取到的条目信息:

1
2
3
4
5
typedef struct {
char namespace_name[NVS_NS_NAME_MAX_SIZE]; /*!< Namespace to which key-value belong */
char key[NVS_KEY_NAME_MAX_SIZE]; /*!< Key of stored key-value pair */
nvs_type_t type; /*!< Type of stored key-value pair */
} nvs_entry_info_t;

2、常用 API

2.1 nvs_flash_init

1
esp_err_t nvs_flash_init(void)
  • 作用
    • 初始化默认 NVS 分区。默认 NVS 分区是在分区表中标记为 “nvs” 的分区。
  • 返回值
    • ESP_OK:成功
    • ESP_ERR_NVS_NO_FREE_PAGES NVS:存储是否不包含空页(如果 NVS 分区被截断,则可能会发生这种情况)
    • ESP_ERR_NOT_FOUND:如果在分区表中找不到标签为 “nvs” 的分区
    • ESP_ERR_NO_MEM:无法为内部结构分配内存

2.2 nvs_flash_init_partition

1
esp_err_t nvs_flash_init_partition(const char *partition_label)
  • 参数
    • partition_label:分区的标签。不得超过 16 个字符。
  • 作用
    • 初始化指定分区的 NVS 闪存。
  • 返回值
    • ESP_OK:成功
    • ESP_ERR_NVS_NO_FREE_PAGES NVS:存储是否不包含空页(如果 NVS 分区被截断,则可能会发生这种情况)
    • ESP_ERR_NO_MEM:无法为内部结构分配内存

2.3 nvs_flash_init_partition_ptr

1
esp_err_t nvs_flash_init_partition_ptr(const esp_partition_t *partition)
  • 参数
    • partition:指向 ESP 分区 API 获取的分区的指针。
  • 作用
    • 初始化分区指针指定的分区的 NVS 闪存存储。
  • 返回值
    • ESP_OK:成功
    • ESP_ERR_NVS_NO_FREE_PAGES NVS:存储是否不包含空页(如果 NVS 分区被截断,则可能会发生这种情况)
    • ESP_ERR_NO_MEM:无法为内部结构分配内存

2.4 nvs_flash_erase

1
esp_err_t nvs_flash_init_partition_ptr(const esp_partition_t *partition)
  • 参数
    • partition:指向 ESP 分区 API 获取的分区的指针。
  • 作用
    • 擦除默认 NVS 分区(带有标签 “nvs” 的分区)的所有内容。
  • 返回值
    • ESP_OK:成功
    • ESP_ERR_NVS_NO_FREE_PAGES NVS:存储是否不包含空页(如果 NVS 分区被截断,则可能会发生这种情况)

如果分区已初始化,则此函数首先 deinit 它。之后,必须再次初始化分区才能使用。

2.5 nvs_flash_erase_partition

1
esp_err_t nvs_flash_erase_partition(const char *part_name)
  • 参数
    • part_name:应擦除的分区的名称 (标签)
  • 作用
    • 擦除指定的 NVS 分区。
  • 返回值
    • ESP_OK:成功
    • ESP_ERR_NVS_NO_FREE_PAGES NVS:存储是否不包含空页(如果 NVS 分区被截断,则可能会发生这种情况)

如果分区已初始化,则此函数首先 deinit 它。之后,必须再次初始化分区才能使用。

2.6 nvs_flash_erase_partition_ptr

1
esp_err_t nvs_flash_erase_partition_ptr(const esp_partition_t *partition)
  • 参数
    • partition:指向 ESP 分区 API 获取的分区的指针。
  • 作用
    • 擦除指定自定义分区的所有内容。
  • 返回值
    • ESP_OK:成功
    • ESP_ERR_NVS_NO_FREE_PAGES NVS:存储是否不包含空页(如果 NVS 分区被截断,则可能会发生这种情况)
    • ESP_ERR_INVALID_ARG:分区为 NULL

如果分区已初始化,则此函数首先 deinit 它。之后,必须再次初始化分区才能使用。

2.7 nvs_flash_generate_keys

1
esp_err_t nvs_flash_generate_keys(const esp_partition_t *partition, nvs_sec_cfg_t *cfg)
  • 参数
    • partition:向使用 esp_partition_find_firstesp_partition_get 获取的分区结构的指针。必须为非 NULL。
    • cfg:指向 nvs 安全配置结构的指针。指针必须为非 NULL。生成的键将填充到此结构中。
  • 作用
    • 擦除指定自定义分区的所有内容。
  • 返回值
    • ESP_OK:成功
    • ESP_ERR_INVALID_ARG:partition 或 cfg 为 NULL

2.8 nvs_set_i8

1
esp_err_t nvs_set_i8(nvs_handle_t handle, const char *key, int8_t value)
  • 参数
    • handle:向使用 esp_partition_find_firstesp_partition_get 获取的分区结构的指针。必须为非 NULL。
    • key:键名称。最大长度为 (NVS_KEY_NAME_MAX_SIZE-1) 个字符。不应为空。
    • value:要设置的值。
  • 作用
    • 为给定键设置 int8_t
  • 返回值
    • ESP_OK:成功
    • ESP_FAIL:如果存在内部错误;很可能是由于 NVS 分区损坏(仅当禁用 NVS 断言检查时)
    • ESP_ERR_NVS_INVALID_HANDLE:句柄是否已关闭或为 NULL
    • ESP_ERR_NVS_READ_ONLY:如果存储句柄以只读方式打开
    • ESP_ERR_NVS_INVALID_NAME:key name 不满足 constraints
    • ESP_ERR_NVS_NOT_ENOUGH_SPACE:如果底层存储中没有足够的空间来保存值
    • ESP_ERR_NVS_REMOVE_FAILED:值是否因闪存写入操作失败而未更新。但是,该值已写入,并且 update 将在重新初始化 nvs 后完成,前提是 flash 操作不会再次失败。

nvs_set_*:类似的还有 u8、i16、u16、i32、u32、i64、u64、str

2.8.1 nvs_set_blob

1
esp_err_t nvs_set_blob(nvs_handle_t handle, const char *key, const void *value, size_t length)

相比其它几个 get 函数,要注意一下:

  • value:要设置的值。
  • length: 要设置的二进制值的长度,以字节为单位;最大长度为 508000 字节或(分区大小的 97.6% - 4000)字节,以较低者为准。

2.9 nvs_get_u8

1
esp_err_t nvs_get_u8(nvs_handle_t handle, const char *key, uint8_t *out_value)
  • 参数
    • handle:向使用 esp_partition_find_firstesp_partition_get 获取的分区结构的指针。必须为非 NULL。
    • key:键名称。最大长度为 (NVS_KEY_NAME_MAX_SIZE-1) 个字符。不应为空。
    • value:存储要获取的值。
  • 作用
    • 获取给定键 uint8_t
  • 返回值
    • ESP_OK:成功
    • ESP_FAIL:如果存在内部错误;很可能是由于 NVS 分区损坏(仅当禁用 NVS 断言检查时)
    • ESP_ERR_NVS_INVALID_HANDLE:句柄是否已关闭或为 NULL
    • ESP_ERR_NVS_READ_ONLY:如果存储句柄以只读方式打开
    • ESP_ERR_NVS_INVALID_NAME:key name 不满足 constraints
    • ESP_ERR_NVS_NOT_ENOUGH_SPACE:如果底层存储中没有足够的空间来保存值
    • ESP_ERR_NVS_REMOVE_FAILED:值是否因闪存写入操作失败而未更新。但是,该值已写入,并且 update 将在重新初始化 nvs 后完成,前提是 flash 操作不会再次失败。

nvs_get_*:类似的还有 u8、i16、u16、i32、u32、i64、u64、str、blob

2.9.1 nvs_get_blob/nvs_get_str

1
2
esp_err_t nvs_get_blob(nvs_handle_t handle, const char *key, void *out_value, size_t *length)
esp_err_t nvs_get_str(nvs_handle_t handle, const char *key, char *out_value, size_t *length)

相比其它几个 get 函数,要注意一下:

  • out_value:指向输出值的指针。对于 nvs_get_strnvs_get_blob 可能为 NULL,在这种情况下,所需长度将在 length 参数中返回。
  • length: 一个非零指针,指向保存 out_value 长度的变量。
    • 如果 out_value 为 NULL,则将设置为保存该值所需的长度。
    • 如果 out_value 不为零,则将设置为写入值的实际长度。对于 nvs_get_str 来说,nvs_get_blob 还包括零终止符。

2.10 nvs_open

1
esp_err_t nvs_open(const char *namespace_name, nvs_open_mode_t open_mode, nvs_handle_t *out_handle)
  • 参数
    • namespace_name:Namespace 名称。最大长度为 (NVS_KEY_NAME_MAX_SIZE-1) 个字符。不应为空。
    • open_modeNVS_READWRITENVS_READONLY。如果 NVS_READONLY,将打开仅用于读取的句柄。此 handle 的所有写入请求都将被拒绝。
    • out_handle:如果成功(返回代码为零),则在此参数中返回 handle。
  • 作用
    • 从默认 NVS 分区中打开具有给定命名空间的非易失性存储。
  • 返回值
    • ESP_OK:成功
    • ESP_FAIL:如果存在内部错误;很可能是由于 NVS 分区损坏(仅当禁用 NVS 断言检查时)
    • ESP_ERR_NVS_INVALID_HANDLE:句柄是否已关闭或为 NULL
    • ESP_ERR_NVS_READ_ONLY:如果存储句柄以只读方式打开
    • ESP_ERR_NVS_INVALID_NAME:key name 不满足 constraints
    • ESP_ERR_NVS_NOT_ENOUGH_SPACE:如果底层存储中没有足够的空间来保存值
    • ESP_ERR_NVS_REMOVE_FAILED:值是否因闪存写入操作失败而未更新。但是,该值已写入,并且 update 将在重新初始化 nvs 后完成,前提是 flash 操作不会再次失败。

esp_err_t nvs_open_from_partition(const char *part_name, const char *namespace_name, nvs_open_mode_t open_mode, nvs_handle_t *out_handle) 类似,不过是从指定分区打开具有给定命名空间的非易失性存储器。

2.11 nvs_find_key

1
esp_err_t nvs_find_key(nvs_handle_t handle, const char *key, nvs_type_t *out_type)
  • 参数
    • namespace_name:通过 nvs_open 获得的存储句柄。
    • key:键名称。最大长度为 (NVS_KEY_NAME_MAX_SIZE-1) 个字符。不应为空。
    • out_type:指向输出变量的指针,该变量填充了 NVS 条目的数据类型,以防找到 key 的情况。可能为 NULL,则不提供相应的数据类型。
  • 作用
    • 查找具有给定键名称的键值对。
  • 返回值
    • ESP_OK:成功
    • ESP_FAIL:如果存在内部错误;很可能是由于 NVS 分区损坏(仅当禁用 NVS 断言检查时)
    • ESP_ERR_NVS_INVALID_HANDLE:句柄是否已关闭或为 NULL
    • ESP_ERR_NVS_READ_ONLY:如果存储句柄以只读方式打开
    • ESP_ERR_NVS_INVALID_NAME:key name 不满足 constraints
    • ESP_ERR_NVS_NOT_ENOUGH_SPACE:如果底层存储中没有足够的空间来保存值
    • ESP_ERR_NVS_REMOVE_FAILED:值是否因闪存写入操作失败而未更新。但是,该值已写入,并且 update 将在重新初始化 nvs 后完成,前提是 flash 操作不会再次失败。

请注意,如果找到密钥,函数可能同时指示密钥的存在以及 NVS 条目的数据类型。

nvs_erase_key 是擦除具有给定键名称的键值对。

1
esp_err_t nvs_erase_key(nvs_handle_t handle, const char *key)

nvs_erase_all 是擦除命名空间中的所有键值对。

1
esp_err_t nvs_erase_all(nvs_handle_t handle)

2.12 nvs_commit

1
esp_err_t nvs_commit(nvs_handle_t handle)
  • 参数
    • handle:通过 nvs_open 获得的存储句柄。不能使用以只读方式打开的句柄。
  • 作用
    • 将任何待处理的更改写入非易失性存储。
  • 返回值
    • ESP_OK:成功
    • ESP_ERR_NVS_INVALID_HANDLE:句柄是否已关闭或为 NULL

设置任何值后,必须调用 nvs_commit() 以确保将更改写入非易失性存储。单个 implementations 可能会在其他时间写入 storage,但不能保证这一点。

2.13 nvs_close

1
void nvs_close(nvs_handle_t handle)
  • 参数
    • handle:要关闭的存储 handle
  • 作用
    • 关闭存储 handle 并释放所有已分配的资源。

2.14 nvs_entry_find_in_handle

1
esp_err_t nvs_entry_find_in_handle(nvs_handle_t handle, nvs_type_t type, nvs_iterator_t *output_iterator)
  • 参数
    • handle:通过 nvs_open 获得的存储句柄。不能使用以只读方式打开的句柄。
    • typenvs_type_t 值之一。
    • output_iterator:设置为有效的迭代器以枚举找到的所有条目。
      • 如果未找到指定条件的条目,则设置为 NULL。
      • 如果发生除 ESP_ERR_INVALID_ARG 以外的任何其他错误,output_iterator 也为 NULL。
      • 如果发生 ESP_ERR_INVALID_ARG,则不会更改 output_iterator
      • 如果通过此函数获得有效的迭代器,则必须在不再使用时使用 nvs_release_iterator 释放它,除非返回 ESP_ERR_INVALID_ARG
  • 作用
    • 创建迭代器以根据句柄和类型枚举 NVS 条目。
  • 返回值
    • ESP_OK:成功
    • ESP_ERR_NVS_INVALID_HANDLE:句柄是否已关闭或为 NULL
    • ESP_ERR_NVS_NOT_FOUND:未找到指定条件的元素
    • ESP_ERR_NO_MEM:在分配内部结构期间内存是否已耗尽。
    • ESP_ERR_INVALID_ARG:如果 output_iterator 参数为 NULL。注意:如果 ESP_ERR_INVALID_ARG 已被退回,请不要释放 output_iterator

2.15 nvs_entry_next

1
esp_err_t nvs_entry_next(nvs_iterator_t *iterator)
  • 参数
    • iterator :从 nvs_entry_findnvs_entry_find_in_handle 函数获得的迭代器。必须为非 NULL。
      • 如果发生除 ESP_ERR_INVALID_ARG 之外的任何错误,则 iterator 设置为 NULL。
      • 如果出现 ESP_ERR_INVALID_ARG,则不会更改 iterator。
  • 作用
    • 将迭代器前进到与迭代器条件匹配的下一个项目。
  • 返回值
    • ESP_OK:成功
    • ESP_ERR_NVS_NOT_FOUND:未找到指定条件的元素
    • ESP_ERR_INVALID_ARG:如果有参数为 NULL。注意:如果 ESP_ERR_INVALID_ARG 已被退回,请不要释放 output_iterator

请注意,在此调用后,迭代器的任何副本都将无效。

2.16 nvs_entry_info

1
esp_err_t nvs_entry_info(const nvs_iterator_t iterator, nvs_entry_info_t *out_info)
  • 参数
    • iterator: 从 nvs_entry_findnvs_entry_find_in_handle 函数获取的 Iterator。必须为非 NULL。
    • out_info:将条目信息复制到的结构体。
  • 作用
    • 将任何待处理的更改写入非易失性存储。
  • 返回值
    • ESP_OK:成功
    • ESP_ERR_INVALID_ARG:如果有参数为 NULL。注意:如果 ESP_ERR_INVALID_ARG 已被退回,请不要释放 output_iterator

2.17 nvs_release_iterator

1
void nvs_release_iterator(nvs_iterator_t iterator)
  • 参数
    • iterator:从 nvs_entry_find 或 nvs_entry_find_in_handle 或 nvs_entry_next 函数获得的 Release 迭代器。允许 NULL 参数。
  • 作用
    • 释放迭代器。

3、枚举类型

3.1 nvs_open_mode_t

打开类型:

1
2
3
4
typedef enum {
NVS_READONLY, /*!< Read only */
NVS_READWRITE /*!< Read and write */
} nvs_open_mode_t;

3.2 nvs_type_t

数据类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef enum {
NVS_TYPE_U8 = 0x01, /*!< Type uint8_t */
NVS_TYPE_I8 = 0x11, /*!< Type int8_t */
NVS_TYPE_U16 = 0x02, /*!< Type uint16_t */
NVS_TYPE_I16 = 0x12, /*!< Type int16_t */
NVS_TYPE_U32 = 0x04, /*!< Type uint32_t */
NVS_TYPE_I32 = 0x14, /*!< Type int32_t */
NVS_TYPE_U64 = 0x08, /*!< Type uint64_t */
NVS_TYPE_I64 = 0x18, /*!< Type int64_t */
NVS_TYPE_STR = 0x21, /*!< Type string */
NVS_TYPE_BLOB = 0x42, /*!< Type blob */
NVS_TYPE_ANY = 0xff /*!< Must be last */
} nvs_type_t;

二、NVS

1、底层存储

NVS 库通过调用 esp_partition() 使用主 flash 的部分空间,即类型为 data 且子类型为 nvs 的所有分区。应用程序可调用 nvs_open() 选择使用带有 nvs 标签的分区,也可以通过调用 nvs_open_from_partition() 选择使用指定名称的任意分区。

如果 NVS 分区被截断(例如,更改分区表布局时),则应擦除分区内容。可以使用 ESP-IDF 构建系统中的 idf.py erase-flash 命令擦除 flash 上的所有内容。

2、键值对

NVS 的操作对象为键值对,其中键是 ASCII 字符串,当前支持的最大键长为 15 个字符。值可以为以下几种类型:

  • 整数型:uint8_tint8_tuint16_tint16_tuint32_tint32_tuint64_tint64_t
  • 以 0 结尾的字符串;
  • 可变长度的二进制数据 (BLOB)

注意,字符串值当前上限为 4000 字节,其中包括空终止符。BLOB 值上限为 508,000 字节或分区大小的 97.6% 减去 4000 字节,以较低值为准。

键必须唯一。为现有的键写入新值时,会将旧的值及数据类型更新为写入操作指定的值和数据类型。

读取值时会执行数据类型检查。如果读取操作预期的数据类型与对应键的数据类型不匹配,则返回错误。

3、命名空间

为了减少不同组件之间键名的潜在冲突,NVS 将每个键值对分配给一个命名空间。命名空间的命名规则遵循键名的命名规则:

  • 键名最多可占 15 个字符
  • 单个 NVS 分区最多只能容纳 254 个不同的命名空间

命名空间的名称在调用 nvs_open()nvs_open_from_partition 中指定,调用后将返回一个不透明句柄,用于后续调用 nvs_get_*nvs_set_*nvs_commit 函数。这样,一个句柄关联一个命名空间,键名便不会与其他命名空间中相同键名冲突。请注意,不同 NVS 分区中具有相同名称的命名空间将被视为不同的命名空间。

4、NVS 迭代器

迭代器允许根据指定的分区名称、命名空间和数据类型轮询 NVS 中存储的键值对。

使用以下函数,可执行相关操作:

  • nvs_entry_find:创建一个不透明句柄,用于后续调用 nvs_entry_nextnvs_entry_info 函数;
  • nvs_entry_next:让迭代器指向下一个键值对;
  • nvs_entry_info:返回每个键值对的信息。

总的来说,所有通过 nvs_entry_find() 获得的迭代器(包括 NULL 迭代器)都必须使用 nvs_release_iterator() 释放。

一般情况下,nvs_entry_find()nvs_entry_next() 会将给定的迭代器设置为 NULL 或为一个有效的迭代器。但如果出现参数错误(如返回 ESP_ERR_NVS_NOT_FOUND),给定的迭代器不会被修改。因此,在调用 nvs_entry_find() 之前最好将迭代器初始化为 NULL,这样可以避免在释放迭代器之前进行复杂的错误检查。

5、NVS 分区生成程序

NVS 分区生成程序帮助生成 NVS 分区二进制文件,可使用烧录程序将二进制文件单独烧录至特定分区。烧录至分区上的键值对由 CSV 文件提供。

可以直接使用函数 nvs_create_partition_image 通过 CMake 创建分区二进制文件,无需手动调用 nvs_partition_gen.py 工具:

1
nvs_create_partition_image(<partition> <csv> [FLASH_IN_PROJECT] [DEPENDS  dep dep dep ...])
  • 位置参数
    • partition:NVS 分区名
    • csv:解析的 CSV 文件路径
  • 可选参数
    • FLASH_IN_PROJECT:NVS 分区名
    • DEPENDS:指定命令依赖的文件

在没有指定 FLASH_IN_PROJECT 的情况下,也支持生成分区镜像,不过此时需要使用 idf.py <partition>-flash 手动进行烧录。举个例子,如果分区名为 nvs,则需使用的命令为 idf.py nvs-flash

目前,仅支持从组件中的 CMakeLists.txt 文件调用 nvs_create_partition_image,且此选项仅适用于非加密分区。

三、实例操作

1、例一——读写操作实验

本例中向 NVS 分区中的 storage 命名空间写入数据,并读出数据,并验证了 NVS 分区的掉电不丢失的特性:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
void app_main(void)
{
// 初始化 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();
}
ESP_ERROR_CHECK( err );

printf("\n");
printf("Opening Non-Volatile Storage (NVS) handle... ");

nvs_handle handle;
err = nvs_open("storage", NVS_READWRITE, &handle); // 打开 NVS 分区
if (err != ESP_OK) {
printf("Error (%s) opening NVS handle!\n", esp_err_to_name(err));
} else {
printf("Done\n");

printf("Reading restart counter from NVS ... ");
int32_t restart_counter = 0;
err = nvs_get_i32(handle, "restart_counter", &restart_counter); // 读取数据
switch (err) {
case ESP_OK:
printf("Done\n");
printf("Restart counter = %" PRIu32 "\n", restart_counter);
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("Updating restart counter in NVS ... ");
restart_counter++;
err = nvs_set_i32(handle, "restart_counter", restart_counter); // 写入数据
printf((err != ESP_OK) ? "Failed!\n" : "Done\n");

printf("Committing updates to NVS ... ");
err = nvs_commit(handle); // 更新数据
printf((err != ESP_OK) ? "Failed!\n" : "Done\n");

nvs_close(handle);
}

printf("\n");

for (int i = 10; i >= 0; --i) {
printf("Restarting in %d seconds... \n", i);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
printf("Restarting now. \n");
fflush(stdout);
esp_restart();
}

2、例二——blob 的写入和读取

blob 的读取(以及 str 的读取)有一点小技巧,因为事先读取的时候,我们并不知道要读取的 blob 数据的长度,所以我们可以利用 nvs_get_blob 的功能,先将第三个参数设为 NULL,读取出数据的长度,再次调用 nvs_get_blob,就可以获取到 blob 数据了:

1
2
3
size_t required_size = 0;  
err = nvs_get_blob(my_handle, "run_time", NULL, &required_size); // required_size 保存了 blob 数据的长度
err = nvs_get_blob(my_handle, "run_time", run_time, &required_size);
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#define NVS_NAMESPACE "storage"
#define NVS_KEY "password"

const char *TAG = "NVS_TEST";

static esp_err_t write_nvs_blob(const char* namespace, const char* key,uint8_t* value, size_t len)
{
nvs_handle_t nvs_handle;
esp_err_t ret;

ESP_ERROR_CHECK(nvs_open(namespace, NVS_READWRITE, &nvs_handle));
ESP_LOGI(TAG, "Write NVS: %s", value);
ret = nvs_set_blob(nvs_handle, key, value, len);

ESP_ERROR_CHECK(nvs_commit(nvs_handle));
nvs_close(nvs_handle);

return ret;
}

static void read_nvs_blob(const char* namespace,const char* key,uint8_t *value,int maxlen)
{
nvs_handle_t nvs_handle;
esp_err_t ret_val = ESP_FAIL;
size_t required_size = 0;

ESP_ERROR_CHECK(nvs_open(namespace, NVS_READONLY, &nvs_handle));
ret_val = nvs_get_blob(nvs_handle, key, NULL, &required_size);
if (ret_val == ESP_OK && required_size <= maxlen)
{
ESP_ERROR_CHECK(nvs_get_blob(nvs_handle, key, value, &required_size));
ESP_LOGI(TAG,"Read NVS: %s", value);
}
else
ESP_LOGI(TAG, "Read fail");

nvs_close(nvs_handle);
}

void app_main(void)
{
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();
}
ESP_ERROR_CHECK( err );

uint8_t blob_buf[20] = "abcd1234";
// 写入
write_nvs_blob(NVS_NAMESPACE,NVS_KEY, blob_buf, 8);

// 读取
read_nvs_blob(NVS_NAMESPACE, NVS_KEY, blob_buf, sizeof(blob_buf));

while (1) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
}

str 的读取类似,不再重复演示。


ESP32-IDF 非易失存储 NVS
http://example.com/2024/10/21/ESP32-IDF-NVS/
作者
Yu xin
发布于
2024年10月21日
许可协议