ESP32 中的 NVS 介绍

NVS(Non-Volatile Storage,非易失性存储)是 ESP-IDF 提供的一种轻量级、高效的存储机制,用于在 ESP32 的闪存中保存数据。它允许开发者以键值对的形式存储配置信息或其他需要持久化的数据。

NVS 存储的数据类型

NVS 适合存储以下类型的小型数据:

  1. Wi-Fi 配置

    • SSID 和密码。
    • 其他网络相关的设置。
  2. 设备配置

    • 用户自定义的参数(如亮度、音量等)。
    • 系统运行时的配置(如模式选择、语言设置等)。
  3. 状态信息

    • 设备重启前的状态(如开关状态、计数器值等)。
    • 日志或调试信息。
  4. 用户数据

    • 小型的二进制数据块(如加密密钥、证书等)。

NVS 与直接操作 Flash 的区别

特性 NVS 直接操作 Flash
接口复杂度 提供简单的键值对 API,易于使用。 需要手动管理地址和数据结构,复杂度较高。
数据组织方式 键值对存储,支持多种数据类型(字符串、整数等)。 直接操作裸数据,需自行设计数据格式。
可靠性 内置磨损均衡和错误检测机制,提高数据可靠性。 需要自行实现磨损均衡和错误处理机制。
性能 使用 RAM 缓冲区优化写入性能,减少闪存访问次数。 每次操作都需要直接访问闪存,可能影响性能。
适用场景 适合存储小型、频繁更新的配置数据。 适合存储大块数据或不常更新的数据。

为什么使用 NVS?

  1. 简化开发

    • NVS 提供了简单易用的 API,开发者无需关心底层闪存的操作细节,可以专注于业务逻辑。
  2. 数据可靠性

    • NVS 内置了磨损均衡(wear leveling)机制,可以延长闪存的使用寿命。
    • 支持 CRC 校验,确保数据的完整性。
  3. 灵活性

    • 支持多种数据类型(如整数、字符串、二进制数据等),并以键值对的形式存储,方便管理和检索。
  4. 性能优化

    • 数据先缓存在 RAM 中,只有在调用 nvs_commit 时才会写入闪存,减少了对闪存的频繁访问,提高了性能。
  5. 节省资源

    • NVS 的设计目标是高效利用有限的闪存空间,适合存储小型数据。

NVS 的工作原理

  1. 分区管理

    • NVS 使用 ESP32 的分区表来分配存储空间。每个 NVS 分区是一个独立的命名空间,通常在 partition_table.csv 文件中定义。
  2. 数据存储格式

    • 数据以键值对的形式存储,键是字符串,值可以是整数、字符串或二进制数据。
    • NVS 在内部将数据分块存储,并通过索引机制快速查找。
  3. 磨损均衡

    • NVS 自动分散写入操作,避免某些闪存块被频繁擦写,从而延长闪存寿命。
  4. 事务支持

    • NVS 提供事务机制,确保多步操作的原子性(要么全部成功,要么全部失败)。

NVS读取实验

#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_err.h"
#include "nvs_flash.h"

#define NAME_SPACE_WIFI1    "wifi1"
#define NAME_SPACE_WIFI2    "wifi2"

#define NVS_SSID_KEY        "ssid"
#define NVS_PASSWORD_KEY    "password"

void nvs_blob_read(const char* namespace, const char* key, void* buffer, int maxlen)
{
    nvs_handle_t nvs_handle;
    size_t length = 0;
    nvs_open(namespace, NVS_READONLY, &nvs_handle);
    /* 倒数第二个参数应该给一个buf,传入NULL时只会返回长度length,这样我们就得到了数据的长度 */
    nvs_get_blob(nvs_handle, key, NULL, &length);
    if(length && length < maxlen)
    {
        nvs_get_blob(nvs_handle, key, buffer, &length);
    }
    nvs_close(nvs_handle);
}

void app_main(void)
{
    nvs_handle_t nvs_handle1;
    nvs_handle_t nvs_handle2;
    esp_err_t ret = nvs_flash_init();
    if(ret != ESP_OK)
    {
        nvs_flash_init();
        ESP_ERROR_CHECK(nvs_flash_init());
    }
    //命名空间wifi1 NVS的写入
    /* 	NVS_READONLY  只读    |    NVS_READWRITE  读写 */
    nvs_open(NAME_SPACE_WIFI1, NVS_READWRITE, &nvs_handle1);

    nvs_set_blob(nvs_handle1, NVS_SSID_KEY, "wifi_esp32", strlen("wifi_esp32"));
    nvs_set_blob(nvs_handle1, NVS_PASSWORD_KEY, "123456", strlen("123456"));

    /* NVS 是 ESP32 提供的一种非易失性存储机制,用于保存需要在设备重启后仍然保留的数据(如 Wi-Fi 配置、用户设置等)。
    NVS 使用闪存来存储数据,但为了提高性能和减少对闪存的频繁写入操作,数据通常会先缓存在 RAM 中,而不会立刻写入到闪存。*/
    nvs_commit(nvs_handle1);//将当前存储在 RAM 缓冲区中的更改立即写入到 NVS 分区的闪存中
    nvs_close(nvs_handle1);


    //命名空间wifi2 NVS的写入
    nvs_open(NAME_SPACE_WIFI2, NVS_READWRITE, &nvs_handle2);
    nvs_set_blob(nvs_handle2, NVS_SSID_KEY, "helloworld", strlen("helloworld"));
    nvs_set_blob(nvs_handle2, NVS_PASSWORD_KEY, "654321", strlen("654321"));
    nvs_commit(nvs_handle2);
    nvs_close(nvs_handle2);

    vTaskDelay(pdMS_TO_TICKS(1000));

    char read_buffer[64];
    //命名空间WIFE1 ssid的值
    memset(read_buffer, 0, sizeof(read_buffer));
    nvs_blob_read(NAME_SPACE_WIFI1, NVS_SSID_KEY, read_buffer, sizeof(read_buffer));
    ESP_LOGI("nvs", "namespace:%s, key:%s -> value:%s", NAME_SPACE_WIFI1, NVS_SSID_KEY, read_buffer);
    //命名空间WIFE1 password的值
    memset(read_buffer, 0, sizeof(read_buffer));
    nvs_blob_read(NAME_SPACE_WIFI1, NVS_PASSWORD_KEY, read_buffer, sizeof(read_buffer));
    ESP_LOGI("nvs", "namespace:%s, key:%s -> value:%s", NAME_SPACE_WIFI1, NVS_PASSWORD_KEY, read_buffer);

    //命名空间WIFE2 ssid的值
    memset(read_buffer, 0, sizeof(read_buffer));
    nvs_blob_read(NAME_SPACE_WIFI2, NVS_SSID_KEY, read_buffer, sizeof(read_buffer));
    ESP_LOGI("nvs", "namespace:%s, key:%s -> value:%s", NAME_SPACE_WIFI2, NVS_SSID_KEY, read_buffer);
    //命名空间WIFE2 password的值
    memset(read_buffer, 0, sizeof(read_buffer));
    nvs_blob_read(NAME_SPACE_WIFI2, NVS_PASSWORD_KEY, read_buffer, sizeof(read_buffer));
    ESP_LOGI("nvs", "namespace:%s, key:%s -> value:%s", NAME_SPACE_WIFI2, NVS_PASSWORD_KEY, read_buffer);
}

在这里插入图片描述
可以观察到,即使我使用了相同的键 ssid 和 password,但是在不同的命名空间中它们是互不干扰的

NVS总结

NVS 是 ESP32 中一种高效、可靠的非易失性存储解决方案,特别适合存储小型、频繁更新的配置数据。相比于直接操作闪存,NVS 提供了更高层次的抽象,简化了开发流程,同时内置了多种优化机制(如磨损均衡、CRC 校验等),确保数据的安全性和可靠性。因此,在大多数应用场景中,推荐使用 NVS 而不是直接操作闪存。

ESP32 中的 SPIFFS介绍

SPIFFS(Serial Peripheral Interface Flash File System)是 ESP32 提供的一种轻量级文件系统,专门用于在嵌入式设备的闪存中存储和管理文件。它适合存储小型文件,并提供了基本的文件操作功能(如读、写、删除等)。

SPIFFS 的特点

  1. 专为小文件设计

    • SPIFFS 适合存储小型文件(如配置文件、日志文件、图片、HTML 页面等),而不是大块数据。
    • 文件大小通常限制在几百 KB 或几 MB 范围内。
  2. 磨损均衡

    • SPIFFS 内置了磨损均衡机制,可以延长闪存的使用寿命。
  3. 断电保护

    • 支持一定程度的断电保护,确保在意外断电时数据不会损坏。
  4. 简单易用

    • 使用标准的 POSIX 文件 API(如 fopenfprintffgets 等),开发者无需关心底层细节。
  5. 独立分区

    • SPIFFS 数据存储在一个独立的分区中,与应用程序代码和其他数据(如 NVS)分开。

使用 SPIFFS 存储什么数据?

SPIFFS 适合存储以下类型的数据:

  1. 配置文件

    • 如 JSON、XML 或 INI 格式的配置文件。
  2. 静态资源

    • HTML、CSS 和 JavaScript 文件,用于嵌入式 Web 服务器。
    • 图片、图标或其他多媒体资源。
  3. 日志文件

    • 用于记录系统运行状态或调试信息。
  4. 用户数据

    • 用户上传的小型文件(如证书、密钥等)。
  5. 固件更新文件

    • 存储 OTA 更新包或其他临时文件。

SPIFFS 与 NVS 的区别

特性 SPIFFS NVS (Non-Volatile Storage)
存储方式 文件系统,支持文件和目录结构 键值对存储,不支持文件系统
数据类型 任意文件(文本、二进制等) 整数、字符串、二进制数据块
适用场景 存储小型文件(如配置文件、日志、资源文件等) 存储配置参数、设备状态等小型数据
复杂性 需要管理文件路径和文件操作 简单的键值对操作,易于使用
性能 文件操作可能较慢,适合非频繁访问 访问速度快,适合频繁更新的小型数据
可靠性 内置磨损均衡和断电保护 内置磨损均衡和 CRC 校验
存储容量 可以存储多个文件,总大小受分区限制 单个键值对大小有限,总容量较小

为什么使用 SPIFFS?

  1. 文件系统支持

    • 如果你的应用需要存储和管理多个文件(如 HTML 文件、日志文件等),SPIFFS 是一个更好的选择。
    • 它支持文件路径、目录结构以及标准的文件操作 API。
  2. 灵活性

    • SPIFFS 允许动态创建、修改和删除文件,而 NVS 的键值对存储不适合这种场景。
  3. 资源管理

    • 对于需要存储较大的静态资源(如网页、图片等)的应用,SPIFFS 更加高效。
  4. 兼容性

    • SPIFFS 支持标准的 POSIX 文件 API,方便与现有的文件处理代码集成。

SPIFFS读取实验

配置分区表,详见:ESP32入门-学习笔记 ESP32中的分区表
在这里插入图片描述

#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_err.h"
#include "esp_spiffs.h"
#include "esp_log.h"

void app_main(void)
{
    // 配置 SPIFFS 文件系统
    esp_vfs_spiffs_conf_t conf = {
        .base_path = "/spiffs",          // 文件系统的挂载点(路径前缀)
        .partition_label = "storage",   // 使用的分区标签(在分区表中定义)
        .max_files = 5,                 // 最大允许打开的文件数
        .format_if_mount_failed = true, // 如果挂载失败,则格式化文件系统
    };

    // 注册 SPIFFS 文件系统
    esp_err_t ret = esp_vfs_spiffs_register(&conf);
    if (ret != ESP_OK) {
        ESP_LOGI("spiffs", "SPIFFS mount failed!"); // 挂载失败时打印日志
        return;
    }

    // 检查 SPIFFS 文件系统的完整性
    ret = esp_spiffs_check(conf.partition_label);
    if (ret != ESP_OK) {
        ESP_LOGI("spiffs", "SPIFFS check failed!"); // 检查失败时打印日志
        return;
    }

    // 获取 SPIFFS 文件系统的总大小和已使用大小
    size_t total = 0, used = 0;
    ret = esp_spiffs_info(conf.partition_label, &total, &used);
    if (ret != ESP_OK) {
        ESP_LOGI("spiffs", "Failed to get SPIFFS info!"); // 获取信息失败时打印日志
        return;
    }

    // 打印文件系统的总大小和已使用大小
    ESP_LOGI("spiffs", "SPIFFS total size: %d bytes, used: %d bytes", total, used);

    // 检查是否已使用空间超过总空间(理论上不可能,但增加健壮性)
    if (used > total) {
        ret = esp_spiffs_check(conf.partition_label);
        if (ret != ESP_OK) {
            ESP_LOGI("spiffs", "Used > Total and SPIFFS check failed!"); // 再次检查文件系统
            return;
        }
    }

    // 创建一个文件并写入数据
    FILE *fp = fopen("/spiffs/helloworld.txt", "w"); // 打开文件(写模式)
    if (fp == NULL) {
        ESP_LOGI("spiffs", "Failed to open file for writing!"); // 打开失败时打印日志
        return;
    }

    fprintf(fp, "Helloworld\n"); // 向文件写入字符串 "Helloworld\n"
    fclose(fp);                  // 关闭文件

    vTaskDelay(pdMS_TO_TICKS(1000)); // 延迟 1 秒,模拟一些操作

    // 打开文件并读取数据
    fp = fopen("/spiffs/helloworld.txt", "r"); // 打开文件(读模式)
    if (fp == NULL) {
        ESP_LOGI("spiffs", "Failed to open file for reading!"); // 打开失败时打印日志
        return;
    }

    char line[64];                              // 定义缓冲区用于存储读取的内容
    fgets(line, sizeof(line), fp);             // 从文件中读取一行数据
    fclose(fp);                                // 关闭文件

    // 去掉换行符(如果存在)
    char *pos = strchr(line, '\n');            // 查找换行符的位置
    if (pos) {
        *pos = 0;                               // 将换行符替换为字符串结束符
    }

    // 打印读取到的字符串
    ESP_LOGI("spiffs", "Read string: %s", line);

    // 卸载 SPIFFS 文件系统
    esp_vfs_spiffs_unregister(conf.partition_label);
}

常见错误
在这里插入图片描述
出现这个错误是因为我的flash大小配置为2M,但是分区表中配置的总大小超过2M。如果你的板子flash大小是4M或者更大,可以使用idf.py menuconfig配置flash的大小。或者直接在分区表中更改你定义的spiffs的分区大小。
使用idf.py menuconfig进入配置界面,选择Serial flasher config
在这里插入图片描述
更改flash size为4M或者更大(根据你的板子flash大小)
在这里插入图片描述
在这里插入图片描述
SPIFFS文件系统挂载失败:首次运行程序,或者之前的 SPIFFS 分区数据被清除(例如通过重新烧录固件),文件系统可能尚未初始化,导致挂载失败。
在这里插入图片描述
可以看到成功读出写入的数据

SPIFFS总结

SPIFFS 是 ESP32 中一种轻量级的文件系统,适合存储和管理小型文件(如配置文件、日志文件、静态资源等)。根据具体需求选择合适的存储方式,可以更好地利用 ESP32 的闪存资源。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐