0、前言

        在嵌入式物联网设备中,或多或少都会涉猎到数据掉电存储的需求。例如生活中常见的智能锁,如果不能实现数据掉电存储,智能锁的数据仅仅是存储于RAM内存中的话,一但掉电,此前用户所作的修改密码操作也会失效,仅仅有一个最初烧录到程序代码中的初始密码,如果这样的产品上市,也不会有用户愉快的买单。因此数据掉电存储在嵌入式物联网开发中是非常常见的,我们日常生活的许多电子产品也都有数据掉电存储的功能。

提醒:本文使用的是乐鑫官方的ESP-IDF开发工具对ESP32开发,并非Arduino、Micro-python等第三方工具开发。

ESP32乐鑫官方开发指南:ESP-IDF 编程指南 - ESP32-S3 - — ESP-IDF 编程指南 v5.1.2 文档 (espressif.com)icon-default.png?t=N7T8https://docs.espressif.com/projects/esp-idf/zh_CN/v5.1.2/esp32s3/index.html

1、NVS介绍

        NVS即Non-volatile storage,是一种非易失性存储技术,用于在嵌入式系统中保存持久化数据。它主要用于在flash存储器中存储键值格式的数据,提供了一种简单且有效的方法来保存和读取配置信息、状态数据、用户设置等应用程序数据。NVS在设备重新启动或断电后能够恢复状态,因此非常适合保存需要长期存储的数据。

        NVS库通常通过调用底层的flash API进行读、写、擦除操作,可以对主flash的部分空间进行管理。应用程序可以通过调用NVS的API来选择使用带有NVS标签的分区,或者使用指定名称的任意分区。NVS还支持BLOB(Binary Large OBjects)类型的数据存储,但有一定的大小限制,通常取决于分区的总大小和预留空间。

        通俗的来说,NVS存储 就是在内置的 flash 上分配的一块内存空间 ,提供给用户保存掉电不丢失的数据 。

        本文使用的乐鑫ESP32系列芯片中,提供了非易失性存储(NVS)库,这是一个专为ESP32系列设计的用于保存简单数据的库。如图所示为ESP-IDF开发工具安装路径下的nvs_flash组件库。

        NVS 最适合存储一些较小的数据,而非字符串或二进制大对象 (BLOB) 等较大的数据。如需存储较大的 BLOB 或者字符串,则需要考虑使用基于磨损均衡库的 FAT 文件系统。

        键值对:NVS 的操作对象为键值对,其中键是 ASCII 字符串,当前支持的最大键长为 15 个字符,键必须唯一

值可以为以下几种类型:

①、整数型:uint8_t、int8_t、uint16_t、int16_t、uint32_t、int32_t、uint64_t 和 int64_t;

②、以 0 结尾的字符串;

③、可变长度的二进制数据 (BLOB)

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

提醒:当前ESP32的NVS并不支持float和double类型的数据存储!!!

        命名空间:为了减少不同组件之间键名的潜在冲突,NVS 将每个键值对分配给一个命名空间。命名空间的命名规则遵循键名的命名规则。此外,单个 NVS 分区最多只能容纳 254 个不同的命名空间。        

2、常用的NVS API说明

        在ESP32官方参考指南中,选择好对应的芯片型号和开发工具版本后,找到API参考目录下的存储API,可以看到官方对非易失性存储库的介绍。同一个开发工具版本的不同芯片API参考库说明变动不大。如下所示为乐鑫官网的存储API部分内容。     

如下所述API所在的头文件路径:components/nvs_flash/include/nvs_flash.h

①、nvs_flash_init

esp_err_t nvs_flash_init(void);
//初始化默认的NVS分区,每次操作NVS分区都需要先初始化NVS
//此 API 初始化默认的 NVS 分区。默认的 NVS 分区是在分区表中标记为“nvs”的分区。
//在 menuconfig 中启用“NVS_ENCRYPTION”时,此 API 会为默认 NVS 分区启用 NVS 加密。
//初始化成功返回 ESP_OK。

②、nvs_flash_init_partition

esp_err_t nvs_flash_init_partition(const char *partition_label);
//初始化指定分区的 NVS 闪存。
//参数: partition_label – [in] 分区的标签。长度不得超过 16 个字符。
//初始化成功返回 ESP_OK。

③、nvs_flash_deinit

esp_err_t nvs_flash_deinit(void);
//取消初始化默认 NVS 分区的 NVS 存储。
//默认 NVS 分区是分区表中带有“nvs”标签的分区。
//成功时返回ESP_OK(存储已取消初始化),否则返回ESP_ERR_NVS_NOT_INITIALIZED在此调用之前是否未初始化存储

④、nvs_flash_deinit_partition

esp_err_t nvs_flash_deinit_partition(const char *partition_label);
//取消初始化给定 NVS 分区的 NVS 存储。
//参数: partition_label – [in] 分区的标签
//成功时返回ESP_OK,否则返回ESP_ERR_NVS_NOT_INITIALIZED 在此调用之前是否未初始化给定分区的存储

 ⑤、nvs_flash_erase

esp_err_t nvs_flash_erase(void);
//擦除默认的 NVS 分区。
//擦除默认 NVS 分区(标签为“nvs”的分区)的所有内容。
//如果分区已初始化,则此函数首先取消初始化它。之后,必须再次初始化分区才能使用。
//成功返回ESP_OK

⑥、nvs_flash_erase_partition

esp_err_t nvs_flash_erase_partition(const char *part_name);
//擦除指定的 NVS 分区。
//擦除指定 NVS 分区的所有内容
//如果分区已初始化,则此函数首先取消初始化它。之后,必须再次初始化分区才能使用。
//参数:part_name – [in] 应擦除的分区的名称(标签)
//成功返回ESP_OK

如下所述API所在的头文件路径:components/nvs_flash/include/nvs.h

①、nvs_open

esp_err_t nvs_open(const char *namespace_name, nvs_open_mode_t open_mode, nvs_handle_t *out_handle);
//使用默认NVS分区中的给定名称空间,打开非易失性存储。
//为了减少键名上可能发生的冲突,每个模块都可以使用自己的命名空间。默认的NVS分区是分区表中标记为“NVS”的分区。
//参数:
    namespace_name : 命名空间名称。最大长度为(NVS_KEY_NAME_MAX_SIZE-1)个字符。不应该是空的。
    open_mode :[in] NVS_READWRITE或NVS_READONLY。如果NVS_READONLY,将打开一个只读句柄。对于这个句柄,所有写请求都将被拒绝。
    out_handle :[out]如果成功(返回代码为0),则在此参数中返回句柄。
//成功返回ESP_OK

②、nvs_commit

esp_err_t nvs_commit(nvs_handle_t handle);
//将设置的值,写入更新到非易失性存储器中。
//在设置任何值之后,必须调用nvs_commit()以确保将更改写入非易失性存储。
//参数: handle--通过nvs_open获取的存储句柄。以只读方式打开的句柄不能使用。
//成功返回ESP_OK

③、nvs_close

void nvs_close(nvs_handle_t handle);
//关闭存储句柄并释放所有已分配的资源。
//关闭句柄可能不会自动将更改写入非易失性存储。必须使用nvs_commit函数显式地完成。

④、nvs_erase_all

esp_err_t nvs_erase_all(nvs_handle_t handle);
//擦除命名空间中的所有键值对。
//注意,在调用nvs_commit函数之前,实际存储可能不会更新。
//参数:handle--通过nvs_open获取的存储句柄。以只读方式打开的句柄不能使用。
//成功返回ESP_OK

⑤、nvs_set_i8

esp_err_t nvs_set_i8(nvs_handle_t handle、const char *key、int8_t value);
//为给定键设置int8_t值
//根据其名称设置键的值。请注意,在调用nvs_commit函数之前,不会更新实际存储。
//参数:handle – [in] 从nvs_open函数获取的句柄。不能使用以只读方式打开的句柄。
//      key – [in] 键名。最大长度为 (NVS_KEY_NAME_MAX_SIZE-1) 个字符。不应为空。
//      value – [in] 要设置的值。
//成功返回ESP_OK

⑥、nvs_set_str

esp_err_t nvs_set_str(nvs_handle_t handle, const char *key, const char *value);
//为给定键设置字符串类型的值
//根据其名称设置键的值。请注意,在调用nvs_commit函数之前,不会更新实际存储。
//参数:handle – [in] 从nvs_open函数获取的句柄。不能使用以只读方式打开的句柄。
//      key – [in] 键名。最大长度为 (NVS_KEY_NAME_MAX_SIZE-1) 个字符。不应为空。
//      value – [in] 要设置的值。对于字符串最大的长度为4000字节(包括NULL)
//成功返回ESP_OK

⑦、除了写入的数据类型不同,下面列出的这些函数的使用与上面的 nvs_set_i8 函数使用方法基本相同

esp_err_t nvs_set_u8(nvs_handle_t handle、const char *key、uint8_t value);
esp_err_t nvs_set_i16(nvs_handle_t handle, const char *key, int16_t value);
esp_err_t nvs_set_u16(nvs_handle_t handle, const char *key, uint16_t value);
esp_err_t nvs_set_i32(nvs_handle_t handle, const char *key, int32_t value);
esp_err_t nvs_set_u32(nvs_handle_t handle, const char *key, uint32_t value);
esp_err_t nvs_set_i64(nvs_handle_t handle, const char *key, int64_t value);
esp_err_t nvs_set_u64(nvs_handle_t handle, const char *key, uint64_t value);

⑧、nvs_get_i8

esp_err_t nvs_get_i8(nvs_handle_t handle, const char *key, int8_t *out_value);
//获取给定键的int8_t值
//这些函数根据键的名称检索键的值。如果key不存在,或者请求的变量类型与设置值时使用的类型不匹配,则返回错误。如果出现任何错误,则不修改out_value。
//参数:
    handle -从nvs_open函数中获取的句柄。
    key - [in]密钥名称。最大长度为(NVS_KEY_NAME_MAX_SIZE-1)个字符。不应该是空的。
    out_value -输出值指针。对于nvs_get_str和nvs_get_blob可能为NULL,在这种情况下,所需的长度将在长度参数中返回。
//成功返回ESP_OK

⑨、nvs_get_str

esp_err_t nvs_get_str(nvs_handle_t handle, const char *key, char *out_value, size_t *length);
//获取给定键的字符串值
//这些函数检索给定键的条目的数据。如果key不存在,或者请求的变量类型与设置值时使用的类型不匹配,则返回错误。
//参数:
    handle -从nvs_open函数中获取的句柄。
    key - [in]密钥名称。最大长度为(NVS_KEY_NAME_MAX_SIZE-1)个字符。不应该是空的。
    out_value - [out]输出值指针。对于nvs_get_str和nvs_get_blob可能为NULL,在这种情况下,所需的长度将在长度参数中返回。
    length - [inout]指向保存out_value长度的变量的非零指针。如果out_value为零,则设置为保存该值所需的长度。如果out_value不为零,将被设置为写入值的实际长度。对于nvs_get_str,这包括零终止符。
//成功返回ESP_OK

⑩、除了获取的数据类型不同,下面列出的这些函数的使用与上面的 nvs_get_i8 函数使用方法基本相同

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

3、NVS存储参考程序

#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "esp_flash.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "spi_flash_mmap.h"

/**
  * @brief  设置设备初始化状态标志位
  * @param  void
  * @retval 成功返回0,失败返回-1
  */
int set_device_init_flag(void)
{
    esp_err_t ret;
    nvs_handle_t nvs_handle;
    uint8_t flag = 0xac; //设备初始化标志
    //1、初始化NVS
    ret = nvs_flash_init();
    if(ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
    {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);
    //2、打开NVS
    ret = nvs_open("list", NVS_READWRITE, &nvs_handle);
    if(ret != ESP_OK)
    {
        ESP_LOGI("nvs", "nvs open failed");
        nvs_close(nvs_handle);
        return -1;
    }
    //3、写入数据
    ret = nvs_set_u8(nvs_handle, "FLAG", flag); //写一个uint8_t类型的数据
    if(ret != ESP_OK)
    {
        ESP_LOGI("nvs", "nvs write init flag failed");
        nvs_close(nvs_handle);
        return -1;
    }
    //4、写完提交数据
    ret = nvs_commit(nvs_handle);
    if(ret != ESP_OK)
    {
        ESP_LOGI("nvs", "nvs commit failed");
        nvs_close(nvs_handle);
        return -1;
    }
    //4、关闭NVS
    nvs_close(nvs_handle);
    return 0;
}

/**
  * @brief  获取设备初始化状态标志位
  * @param  void
  * @retval 成功返回:flag,失败返回:1
  */
uint8_t get_device_init_flag(void)
{
    esp_err_t ret;
    nvs_handle_t nvs_handle;
    uint8_t flag = 0;
    //1、初始化NVS
    ret = nvs_flash_init();
    if(ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
    {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);
    //2、打开NVS
    ret = nvs_open("list", NVS_READONLY, &nvs_handle);
    if(ret != ESP_OK)
    {
        printf("ret = %d\n", ret);
        ESP_LOGI("nvs", "nvs open failed");
        nvs_close(nvs_handle);
        return 1;
    }

    //3、读取数据
    ret = nvs_get_u8(nvs_handle, "FLAG", &flag);
    if(ret != ESP_OK)
    {
        ESP_LOGI("nvs", "nvs read init flag failed");
        nvs_close(nvs_handle);
        return 1;
    }
    //ESP_LOGI("nvs", "FLAG = %#X", flag);
    printf("FLAG = %#X\n", flag);
    //4、关闭NVS
    nvs_close(nvs_handle);
    return flag;
}

int app_main(void)
{
    uint8_t flag = 0x00;
    int8_t ret = 0;
    ret = set_device_init_flag();
    if(ret == -1)
    { 
        printf("set device init flag failed\n");
        return -1;
    }
    printf("set flag success!\n");
    flag = get_device_init_flag();
    if(flag == 1)
    {
        printf("get init flag failed!");
        return -1;
    }
    printf("get init flag success\nFlag = %0X\n", flag);
    while (1)
    {
        vTaskDelay(1000);
    }
    return 0;
}

 ESP32 NVS读写数据程序运行效果如下图所示:

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐