基于ESP32的桌面小屏幕实战[11]:非易失性存储(NVS)

项目位置:~/esp/esp-idf/examples/storage/nvs_rw_blob

我把它复制到了 ~/esp/demo4

1. 基本概念

非易失性存储(Non-Volatile Storage,NVS)是 ESP-IDF 中提供的一种轻量级存储解决方案,专门用于在设备重启或断电后保留数据。NVS 是通过使用 Flash 存储实现的,主要用于存储小型的键值对(key-value pairs),例如 Wi-Fi 配置、用户设置、计数器等不需要频繁更新但需要持久保存的数据。

KeyMap 存储是指将键值对映射存储在某种数据结构中,以便快速查找和管理键值对数据。通常,键(Key)表示一个唯一标识符,值(Value)表示与该键关联的数据。

2. 源码

包含头文件

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "driver/gpio.h"

宏定义

#define STORAGE_NAMESPACE "storage" //定义一个宏 STORAGE_NAMESPACE,值为字符串 "storage"

/* 根据目标平台条件配置 BOOT_MODE_PIN 的值 
 * 这种条件编译方法允许在不同的硬件平台上使用相同代码 */
#if CONFIG_IDF_TARGET_ESP32C3   //如果编译选项 CONFIG_IDF_TARGET_ESP32C3 为 true(表明当前平台为 ESP32-C3)
#define BOOT_MODE_PIN GPIO_NUM_9
#else
#define BOOT_MODE_PIN GPIO_NUM_0
#endif //CONFIG_IDF_TARGET_ESP32C3

定义一个名为 save_restart_counter 的函数,用于保存模块重启次数到非易失性存储(NVS)。具体步骤包括读取已存储的重启计数、将计数加1、写回并提交。

esp_err_t save_restart_counter(void)    //函数返回值类型是 esp_err_t,表示操作成功或错误代码
{
    nvs_handle_t my_handle; // my_handle 是 NVS 句柄,用于访问存储数据的命名空间
    esp_err_t err;  // err 用于接收并检查每一步的操作状态

    // Open
    err = nvs_open(STORAGE_NAMESPACE, NVS_READWRITE, &my_handle);//用 nvs_open 打开名为 STORAGE_NAMESPACE 的 NVS 命名空间,权限为读写
    if (err != ESP_OK) return err;//无法打开(例如命名空间不存在),则返回错误

    // Read
    int32_t restart_counter = 0; // value will default to 0, if not set yet in NVS
    /* 使用 nvs_get_i32 函数从 NVS 中读取 restart_counter 值,初始值为 0。
     * 如果密钥 "restart_conter" 未找到,则 restart_counter 保持默认值 0。*/
    err = nvs_get_i32(my_handle, "restart_conter", &restart_counter);
    if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) return err;

    // Write
    restart_counter++;
    err = nvs_set_i32(my_handle, "restart_conter", restart_counter);//使用 nvs_set_i32 将新的计数器值写入 NVS
    if (err != ESP_OK) return err;//如果写入失败,函数返回错误

    // Commit written value.
    // After setting any values, nvs_commit() must be called to ensure changes are written
    // to flash storage. Implementations may write to storage at other times,
    // but this is not guaranteed.
    err = nvs_commit(my_handle);//调用 nvs_commit 将更改提交到闪存中,确保数据已写入
    if (err != ESP_OK) return err;//如果提交失败,返回错误

    // Close
    nvs_close(my_handle);//调用 nvs_close 关闭 NVS 句柄,释放资源
    return ESP_OK;
}
  • 在编程中,句柄(Handle)是一种特殊的变量或指针,用来间接引用系统资源。句柄本质上是一个唯一标识符,指向特定资源,使程序可以通过它来访问或管理该资源,而不需要直接操作资源的底层细节。句柄在许多操作系统和库中广泛使用,用于管理文件、内存、设备、进程、网络连接等各种资源。
  • 句柄的工作流程通常如下:
    • 初始化或打开资源:调用相应的函数,生成句柄。例如 nvs_open 函数将会打开 NVS 分区,并返回一个句柄。
    • 通过句柄操作资源:程序使用句柄来执行资源的各种操作,如读取、写入等。句柄充当资源的“钥匙”。
    • 释放资源:操作结束后,通过句柄释放或关闭资源。例如,在代码中 nvs_close(my_handle) 用来释放 NVS 句柄。
  • 句柄的优点是让程序以一种更安全的方式管理系统资源,同时隐藏底层实现细节,提高代码的可维护性和平台兼容性。

定义一个名为 save_run_time 的函数,用于将新运行时间的值保存到 NVS(非易失性存储)中。实现的方式是读取之前保存的时间数据,将新数据添加到表的末尾,并将整个更新后的数据表重新写入 NVS。

esp_err_t save_run_time(void)   //返回值类型是 esp_err_t,用于表示操作是否成功或返回错误代码
{
    /* 定义 NVS 句柄和错误变量 */
    nvs_handle_t my_handle;
    esp_err_t err;

    // Open
    err = nvs_open(STORAGE_NAMESPACE, NVS_READWRITE, &my_handle);//调用nvs_open打开NVS命名空间STORAGE_NAMESPACE,权限为读写
    if (err != ESP_OK) return err;//如果无法打开,则返回err

    // Read the size of memory space required for blob
    size_t required_size = 0;  // value will default to 0, if not set yet in NVS
    /* 使用 nvs_get_blob 函数获取键 "run_time" 关联的 blob 数据的大小。
     * 参数 NULL 指示仅读取大小而不实际读取内容,结果存储在 required_size 中。
     * 如果该键不存在,则 required_size 将保持为 0。*/
    err = nvs_get_blob(my_handle, "run_time", NULL, &required_size);
    if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) return err;

    // Read previously saved blob if available
    /* 声明一个 uint32_t 类型的指针变量 run_time 指向 malloc 分配的内存块 */
    uint32_t* run_time = malloc(required_size + sizeof(uint32_t));
    if (required_size > 0) {//如果键 "run_time" 关联的 blob 数据存在
        err = nvs_get_blob(my_handle, "run_time", run_time, &required_size);//从 NVS 中读取 "run_time" 关联的 blob 数据
        if (err != ESP_OK) {
            free(run_time);//如果读取失败,释放内存并返回错误
            return err;
        }
    }

    // Write value including previously saved blob if available
    required_size += sizeof(uint32_t);//通过增加required_size使得分配的数组空间容纳新数据
    /* 使用 xTaskGetTickCount() * portTICK_PERIOD_MS 获取当前运行时间,将其作为数组的最后一个元素。*/
    run_time[required_size / sizeof(uint32_t) - 1] = xTaskGetTickCount() * portTICK_PERIOD_MS;
    err = nvs_set_blob(my_handle, "run_time", run_time, required_size);//nvs_set_blob 将更新后的数组写入 NVS
    free(run_time);//完成后,释放分配的内存

    if (err != ESP_OK) return err;

    // Commit
    err = nvs_commit(my_handle);//使用 nvs_commit 提交更改,确保新数据写入闪存
    if (err != ESP_OK) return err;

    // Close
    nvs_close(my_handle);//使用 nvs_close 关闭 NVS 句柄以释放资源
    return ESP_OK;
}
  • Blob 是 Binary Large Object 的缩写,表示二进制大对象。在编程和数据库领域中,Blob 通常用于存储大量的二进制数据,比如图像、音频、视频、文件、序列化数据等,数据内容可以是任何格式,而不仅仅是文本。由于 Blob 数据的内容是二进制的,它不受特定数据格式的限制,因此可以灵活地存储各种类型的数据。
  • Blob 的特点
    • 格式灵活:Blob 可以存储多种类型的数据,不要求数据是结构化的。
    • 大容量:Blob 通常用于存储大体积的数据,尤其是超过传统文本字段限制的数据。
    • 操作接口:存储和读取 Blob 数据时,通常通过特定的接口来操作二进制数据,以保证数据的一致性和完整性。

从 NVS 中读取之前存储的 "restart_conter""run_time" 数据

esp_err_t print_what_saved(void)
{
    nvs_handle_t my_handle;
    esp_err_t err;

    // Open
    err = nvs_open(STORAGE_NAMESPACE, NVS_READWRITE, &my_handle);//用nvs_open打开NVS中名为STORAGE_NAMESPACE的存储空间,权限为读写
    if (err != ESP_OK) return err;

    // Read restart counter
    int32_t restart_counter = 0; // value will default to 0, if not set yet in NVS
    err = nvs_get_i32(my_handle, "restart_conter", &restart_counter);//从NVS中读取"restart_conter"的值,并将其存储在restart_counter中
    if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) return err;
    printf("Restart counter = %d\n", restart_counter);

    // Read run time blob
    size_t required_size = 0;  // value will default to 0, if not set yet in NVS
    // obtain required memory space to store blob being read from NVS
    // 从 NVS 中读取一个 blob 类型的数据
    // "run_time" 是存储的数据的键名,NULL 表示我们不需要读取数据内容,只是想知道所需的内存大小。
    err = nvs_get_blob(my_handle, "run_time", NULL, &required_size);
    if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) return err;//如果 nvs_get_blob 出现错误,函数会立即返回错误
    printf("Run time:\n");
    if (required_size == 0) {
        printf("Nothing saved yet!\n");
    } else {
        uint32_t* run_time = malloc(required_size);//分配足够的内存以存储数据
        err = nvs_get_blob(my_handle, "run_time", run_time, &required_size);//从"run_time"中读取数据并存储到run_time数组中
        if (err != ESP_OK) {//如果读取失败,释放分配的内存并返回错误
            free(run_time);
            return err;
        }
        /* 如果读取成功,使用一个 for 循环遍历 run_time 数组并打印每个值。
        数组的大小是通过 required_size / sizeof(uint32_t) 计算得到的,
        因为每个元素是一个 uint32_t 类型(通常为 4 字节)*/
        for (int i = 0; i < required_size / sizeof(uint32_t); i++) {
            printf("%d: %d\n", i + 1, run_time[i]);
        }
        free(run_time);
    }

    // Close
    nvs_close(my_handle);//关闭 NVS 句柄,释放资源
    return ESP_OK;
}

主函数用来管理设备的启动计数、运行时间记录和重启逻辑。

void app_main(void)
{
    /* 初始化 NVS 存储 */
    esp_err_t err = nvs_flash_init();//初始化 NVS
    if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        // 如果初始化时返回 ESP_ERR_NVS_NO_FREE_PAGES 或 ESP_ERR_NVS_NEW_VERSION_FOUND,
        // 这意味着 NVS 分区被截断或版本不兼容,可能需要重新格式化。
        // NVS partition was truncated and needs to be erased
        // Retry nvs_flash_init
        ESP_ERROR_CHECK(nvs_flash_erase());//清空 NVS 存储
        err = nvs_flash_init();//重新初始化
    }
    ESP_ERROR_CHECK( err );

    /* 读取并打印已保存的重启计数器和运行时间表 */
    err = print_what_saved();// 从 NVS 中读取并打印保存的重启计数器和运行时间表
    if (err != ESP_OK) printf("Error (%s) reading data from NVS!\n", esp_err_to_name(err));

    /* 增加重启计数器并保存 */
    err = save_restart_counter();// 在 NVS 中将重启计数器值增加并保存
    if (err != ESP_OK) printf("Error (%s) saving restart counter to NVS!\n", esp_err_to_name(err));

    /* 初始化 GPIO 引脚 */
    gpio_reset_pin(BOOT_MODE_PIN);// 将引脚 BOOT_MODE_PIN 重置到默认状态
    gpio_set_direction(BOOT_MODE_PIN, GPIO_MODE_INPUT);// 设置引脚方向为输入,以便读取其电平状态。

    /* Read the status of GPIO0. If GPIO0 is LOW for longer than 1000 ms,
       then save module's run time and restart it
       监测 GPIO 引脚状态并判断是否重启
     */
    while (1) {
        if (gpio_get_level(BOOT_MODE_PIN) == 0) {   // 检测该引脚是否为低电平(按下状态)
            vTaskDelay(1000 / portTICK_PERIOD_MS);
            if(gpio_get_level(BOOT_MODE_PIN) == 0) {
                err = save_run_time();  // 保存当前的运行时间到 NVS。如果保存出错,打印错误信息。
                if (err != ESP_OK) printf("Error (%s) saving run time blob to NVS!\n", esp_err_to_name(err));
                printf("Restarting...\n");
                fflush(stdout);
                esp_restart();  // 重启系统
            }
        }
        vTaskDelay(200 / portTICK_PERIOD_MS);// 如果 BOOT_MODE_PIN 引脚未被按下,延迟 200 毫秒后重新检测
    }
}
  • GPIO 是 General-Purpose Input/Output 的缩写,表示通用输入/输出,是一种常用于微控制器(如 ESP32)和单板计算机(如 Raspberry Pi)上的可编程引脚,用来和外部硬件(如传感器、LED、电机等)进行通信。

3. 编译工程

在终端中输入

idf.py fullclean
idf.py clean
idf.py build

如果出现idf.py: command not found需要重新安装编译链,然后设置环境

cd ~/esp/esp-idf
export IDF_GITHUB_ASSETS="dl.espressif.com/github_assets"
./install.sh
. /home/xzh/esp/esp-idf/export.sh

回到项目路径

cd ~/esp/demo4

再次在终端中输入

idf.py fullclean
idf.py clean
idf.py build

出现下图表示编译成功
img

4. 下载运行

将板子接入电脑,输入 ls /dev/ttyUSB* 查看设备号
img

下载程序时按住IO0,轻按RST,2个按键同时放开,在终端中输入命令:

idf.py -p /dev/ttyUSB0 flash monitor

当出现下图时,再按一下RST键,程序就能开始运行
img

程序运行成功会显示下图这样
img

按一下RST,会显示
img
再按一下RST,会显示
img

说明程序已经成功将重启计数器 restart_counter 的值从 0 增加到 1,并在重启后打印了该值。不过,Run time 部分仍然显示 "Nothing saved yet!",这表明 run_time 的数据尚未保存。

长按IO0,首先会显示重启
img
然后可以看到
img
再次按下RST,可以看到
img
再次按下IO0,可以看到
img

img

1:2: 后面的 1596303830 代表了保存的运行时间(以毫秒为单位)。这些时间值是在你长按 BOOT_MODE_PIN 触发 save_run_time 函数时记录的,表示从系统启动到按下按钮的经过时间。

posted @ 2025-11-19 09:44  茴香豆的茴  阅读(132)  评论(0)    收藏  举报