NVS 非易失键值存储

本页依据 文档基线与后续更新 中记录的 Zephyr main 快照更新。

NVS 以 16 位 ID 保存可变长度二进制数据,适合小型配置和状态。 它提供挂载、读、写、删除与历史记录等接口,不具有目录、文件路径等通用文件系统语义。 ID 0xffff 用于内部标记,应用应选择其他 ID。

头文件与配置

当前头文件是 <zephyr/kvss/nvs.h>。旧 <zephyr/fs/nvs.h> 仍是兼容入口, 但已经发出弃用提示;早期 <fs/nvs.h> 写法更不能继续使用。 当前官方示例位于 samples/subsys/kvss/nvs。

示例需要的 prj.conf:

CONFIG_FLASH=y
CONFIG_FLASH_PAGE_LAYOUT=y
CONFIG_FLASH_MAP=y
CONFIG_NVS=y

还需目标 Flash 驱动、Devicetree 中的专用存储分区和满足该硬件要求的 Flash 写权限。 CONFIG_MPU_ALLOW_FLASH_WRITE 只在相应平台需要时配置,不应给所有目标无条件添加。

扇区与挂载

NVS 至少需要两个 sector,保留空闲 sector 供数据复制与回收。 按官方指南选择 2 的幂的 sector 大小,并使其为 Flash 擦除页的整数倍。 可存放记录的最大长度还受 sector 容量、元数据和写块对齐限制,不能从 16 位长度字段 推断“每条数据可直接写 64 KiB”。

下面示例假定 storage_partition 的擦除布局均匀,使用两个擦除页作为 NVS 区域:

#include <zephyr/device.h>
#include <zephyr/drivers/flash.h>
#include <zephyr/storage/flash_map.h>
#include <zephyr/kvss/nvs.h>
#include <errno.h>

static struct nvs_fs settings_nvs;

int open_nvs(void)
{
    const struct flash_area *area;
    struct flash_pages_info page_info;
    int rc = flash_area_open(PARTITION_ID(storage_partition), &area);

    if (rc != 0) {
        return rc;
    }
    if (!device_is_ready(area->fa_dev)) {
        flash_area_close(area);
        return -ENODEV;
    }
    rc = flash_get_page_info_by_offs(area->fa_dev, area->fa_off, &page_info);
    if (rc != 0) {
        flash_area_close(area);
        return rc;
    }
    if (page_info.size == 0 || (page_info.size & (page_info.size - 1)) != 0 ||
        page_info.start_offset != area->fa_off ||
        page_info.size > area->fa_size / 2) {
        flash_area_close(area);
        return -EINVAL;
    }
    settings_nvs.flash_device = area->fa_dev;
    settings_nvs.offset = area->fa_off;
    settings_nvs.sector_size = page_info.size;
    settings_nvs.sector_count = 2;
    rc = nvs_mount(&settings_nvs);
    flash_area_close(area);
    return rc;
}

挂载失败必须交给调用者处理。不要在任意失败时自动 nvs_clear(), 这会删除现有数据。该分区不能同时作为其他存储后端的独立原始区域使用。

读写与删除

int save_counter(uint32_t value)
{
    ssize_t rc = nvs_write(&settings_nvs, 1, &value, sizeof(value));

    if (rc < 0) {
        return (int)rc;
    }
    /* 数据未变化时 rc == 0,也表示成功 */
    return (rc == 0 || rc == sizeof(value)) ? 0 : -EIO;
}

int load_counter(uint32_t *value)
{
    ssize_t rc = nvs_read(&settings_nvs, 1, value, sizeof(*value));

    if (rc < 0) {
        return (int)rc;
    }
    return rc == sizeof(*value) ? 0 : -EINVAL;
}

这里使用原生 uint32_t 示意;需要跨固件版本或不同字节序迁移时,应定义存储格式。

nvs_read() 返回记录长度信息;返回值大于缓冲区大小说明只取到了部分数据。 不存在的记录返回负错误码。nvs_write() 成功返回写入长度, 相同数据不重复写 Flash 并返回 0。nvs_delete() 删除 ID,写入长度 0 也代表删除。 nvs_read_hist() 可以读取仍保留的历史版本,但历史可能被回收,不是永久审计日志。

CRC 与迁移

元数据自带 CRC;CONFIG_NVS_DATA_CRC=y 增加数据 CRC-32。 数据 CRC 只在完整读取记录时检查,部分读取不能据此断言完整记录通过校验。 已有无数据 CRC 的存储启用此配置后,原记录将被视为无效, 需要设计数据迁移或明确的重新初始化流程。

NVS 在空间不足时可返回 -ENOSPC。应为有效数据集、更新开销与回收预留容量, 降低写入频率,并在升级前确认分区布局、写块和记录格式兼容。

上游依据