【双MCU项目复盘与优化】05 - 使用并封装NVS库


一、NVS 简介

1.1 什么是NVS

  • 非易失性存储(Non-Volatile Storage,NVS)库是 ESP-IDF 提供的一种轻量级键值存储方案
  • 主要用于在 Flash 中存储键值格式的数据,如设备配置参数、校准数据、用户偏好等需要持久化保存的小型数据

1.2 核心概念

1.2.1 键值对

  • NVS 的操作对象为键值对
  • 其中键是 ASCII 字符串,当前支持的最大键长为 15 个字符
  • 而值支持以下类型:
    • 整数型:uint8_tint8_tuint16_tint16_tuint32_tint32_tuint64_tint64_t
    • \0 结尾的字符串
    • 可变长度的二进制数据(BLOB)
  • 设计约束:字符串值目前限制为 4000 字节(含终止符),blob 值的限制为 508000 字节
    • 实际可用大小受限于 NVS 分区大小,需根据分区表配置确认
  • 使用场景:NVS 最适合存储大量的小型数据值,而非少量的大型字符串或二进制大对象

1.2.2 命名空间

  • 为减少不同组件之间键名的潜在冲突,NVS 将每个键值对分配到一个命名空间
  • 命名空间的命名规则遵循键名的命名规则,最多可占 15 个字符,单个 NVS 分区最多只能容纳 254 个不同的命名空间
  • 调用 nvs_open() 时指定命名空间名称,返回一个不透明句柄,用于后续的读写操作

1.2.3 分区

  • NVS 使用分区表中类型为 data、子类型为 nvs 的分区
  • 通过 nvs_flash_init() 初始化默认 NVS 分区,或通过 nvs_flash_init_partition() 初始化特定分区

1.3 打开模式

  • nvs_open() 的第二个参数控制访问级别:
模式 说明
NVS_READONLY 只读访问,写操作将被拒绝
NVS_READWRITE 标准读写,擦除的数据标记为已删除,但仍保留在 Flash 中
NVS_READWRITE_PURGE 安全读写,擦除的数据会从 Flash 中物理移除

1.4 提交机制

  • NVS 采用延迟写入机制:nvs_set_* 系列函数将数据写入内存缓存,必须调用 nvs_commit() 才能真正写入 Flash
  • 这种设计提升了写入性能,但也意味着若在 commit 前发生复位或断电,已设置但未提交的数据将不会持久化到 Flash 中

1.5 磨损均衡

  • Flash 存储器的物理特性决定了每个扇区的擦除次数是有限的,如果频繁擦写同一个物理地址,该扇区会提前达到寿命上限,影响整个存储器的使用寿命
  • 而 NVS 组件在设计上原生支持磨损均衡,内部实现机制包含追加写入、多扇区磨损分摊等

二、具体封装思路及其相关代码

2.1 封装目标

  • bsp_nvs.c 对 ESP-IDF NVS 底层 API 进行了封装,主要目标包括:
    1. 简化初始化流程:自动处理分区擦除等异常情况
    2. 简化读取逻辑:读取时若键不存在,自动填充默认值,上层无需处理 ESP_ERR_NVS_NOT_FOUND
    3. 提供批量操作接口:支持数组类型配置的批量保存与加载
    4. 统一错误处理:将所有 NVS 错误码转换为项目统一的 Ret_t 枚举,上层无需感知 ESP-IDF 特有错误码

2.2 私有全局变量设计

static const char *_name_space = "user_cfg";   // 命名空间
static nvs_handle_t _nvs_handle = 0;           // NVS操作句柄
static bool _is_open = false;                  // 命名空间是否已打开
  • 设计思路
    • 将命名空间硬编码为 "user_cfg",统一管理所有用户配置,避免各模块各自打开命名空间造成碎片
    • 使用静态全局变量封装句柄和状态,上层调用者无需关心 NVS 句柄的传递
    • _is_open 标志位配合 _is_ready() 辅助函数,确保所有操作在 NVS 就绪后才能执行

2.3 初始化封装:bsp_nvs_init()

Ret_t bsp_nvs_init(void)
{
    // 1. 初始化NVS底层分区
    err = nvs_flash_init();
    if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        nvs_flash_erase();          // 擦除旧数据
        err = nvs_flash_init();     // 重新初始化
    }
    // 2. 打开命名空间
    err = nvs_open(_name_space, NVS_READWRITE, &_nvs_handle);
    ...
}
  • 封装要点
步骤 说明
分区初始化 调用 nvs_flash_init() 初始化默认 NVS 分区
异常处理 当返回 ESP_ERR_NVS_NO_FREE_PAGESESP_ERR_NVS_NEW_VERSION_FOUND 时,说明分区表变更或版本升级,需要擦除旧数据后重新初始化
打开命名空间 NVS_READWRITE 模式打开 "user_cfg" 命名空间
状态管理 成功后设置 _is_open = true,保存句柄到 _nvs_handle

2.4 单值读写封装

2.4.1 保存:bsp_nvs_save_u8()

Ret_t bsp_nvs_save_u8(const char *key, uint8_t value)
{
    if (!_is_ready() || key == NULL) return RET_ERR;
    
    err = nvs_set_u8(_nvs_handle, key, value);
    if (err == ESP_OK) {
        err = nvs_commit(_nvs_handle);   // 必须提交才能写入Flash
    }
    ...
}
  • 封装要点
    • 前置检查:验证 NVS 就绪状态和参数合法性
    • 写入 + 提交:先调用 nvs_set_u8() 写入缓存,再调用 nvs_commit() 持久化到 Flash

2.4.2 读取:bsp_nvs_load_u8()

Ret_t bsp_nvs_load_u8(const char *key, uint8_t *value, uint8_t default_value)
{
    err = nvs_get_u8(_nvs_handle, key, value);
    if (ESP_ERR_NVS_NOT_FOUND == err) {
        *value = default_value;   // 键不存在时静默填充默认值
        return RET_OK;
    }
    ...
}
  • 封装要点
    • 静默默认值处理:当键不存在时,不向上层返回错误,而是直接填充 default_value 并返回成功
    • 上层调用者无需处理 ESP_ERR_NVS_NOT_FOUND,简化了业务逻辑
    • 其他错误(如数据类型不匹配)仍返回 RET_FAIL

2.5 批量操作封装

2.5.1 批量保存:bsp_nvs_save_u8arr()

Ret_t bsp_nvs_save_u8arr(const char *base_key, const uint8_t *values, size_t count)
{
    for (size_t i = 0; i < count; i++) {
        char key[32];
        snprintf(key, sizeof(key), "%s_%zu", base_key, i);
        err = nvs_set_u8(_nvs_handle, key, values[i]);
    }
    err = nvs_commit(_nvs_handle);
    ...
}
  • 封装要点
    • 每个数组元素使用独立的 key,格式为 "baseKey_0""baseKey_1"、……
    • 一次性 commit 所有数据,减少 Flash 写入次数
    • 适用于均衡器参数、颜色表、校准数组等场景

2.5.2 批量读取:bsp_nvs_load_u8arr()

Ret_t bsp_nvs_load_u8arr(const char *base_key, uint8_t *out_values, size_t count, uint8_t default_value)
{
    for (size_t i = 0; i < count; i++) {
        char key[32];
        snprintf(key, sizeof(key), "%s_%zu", base_key, i);
        err = nvs_get_u8(_nvs_handle, key, &out_values[i]);
        if (ESP_ERR_NVS_NOT_FOUND == err) {
            out_values[i] = default_value;   // 单个缺失填充默认值
        }
    }
    ...
}
  • 封装要点
    • 与单值读取保持一致的默认值处理策略
    • 每个元素独立读取,某个元素缺失不影响其他元素的读取
    • 返回 RET_OK 表示所有元素均读取成功(缺失的已填充默认值)

2.6 辅助函数

static bool _is_ready(void)
{
    return (_is_open && _nvs_handle != 0);
}
  • 所有公有函数在操作前均调用此函数进行状态检查,确保 NVS 已正确初始化且命名空间已打开,避免了因未初始化导致的崩溃或未定义行为。

2.7 封装层次对比

层级 接口 特点
ESP-IDF 原生 API nvs_set_u8(), nvs_get_u8(), nvs_commit() 功能完整,但调用繁琐,需自行处理错误码和默认值
本封装(BSP层) bsp_nvs_save_u8(), bsp_nvs_load_u8() 统一错误返回、自动默认值、批量操作支持
posted @ 2026-06-21 22:59  临祁  阅读(8)  评论(0)    收藏  举报