Back to Esp Idf

非易失性存储库

docs/zh_CN/api-reference/storage/nvs_flash.rst

6.1-dev26.8 KB
Original Source

非易失性存储库

:link_to_translation:en:[English]

简介

非易失性存储 (NVS) 库主要用于在 flash 中存储键值格式的数据。本文档将详细介绍 NVS 常用的一些概念。

底层存储 ^^^^^^^^^^^^^^^^^^

NVS 库通过调用 :ref:esp_partition <flash-partition-apis> API 使用主 flash 的部分空间,即类型为 data 且子类型为 nvs 的所有分区。应用程序可调用 :cpp:func:nvs_open API 选择使用带有 nvs 标签的分区,也可以通过调用 :cpp:func:nvs_open_from_partition API 选择使用指定名称的任意分区。

NVS 库后续版本可能会增加其他存储器后端,来将数据保存至其他 flash 芯片(SPI 或 I2C 接口)、RTC 或 FRAM 中。

.. note:: 如果 NVS 分区被截断(例如,更改分区表布局时),则应擦除分区内容。可以使用 ESP-IDF 构建系统中的 idf.py erase-flash 命令擦除 flash 上的所有内容。

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

.. note:: NVS 组件在设计上支持磨损均衡。进行设置操作时,新数据会添加至现存条目之后。即便要使旧值失效,也无需立即执行 flash 擦除操作。通过将数据存储在页面和条目中,该 NVS 空间组织方式大幅降低了 flash 擦除和写入的频率,实现了 126 倍的效率提升。

在 NVS 中存储大量数据 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

NVS 支持至多存储数万个键,且 NVS 分区的大小也可以达到几兆字节(但并不推荐按此上限进行存储)。

.. note:: NVS 组件会在堆上占用一定的 RAM 空间,具体占用量取决于 flash 上 NVS 分区的大小以及使用的键的数量。可以参考以下近似数值,估算 RAM 的使用情况:每 1 MB 的 NVS flash 分区会占用 22 KB 的 RAM,每 1000 个键会占用 5.5 KB 的 RAM。

.. note:: 使用 :cpp:func:nvs_flash_init 初始化 NVS 的时间与已有键的数量成正比。每有 1000 个键,NVS 的初始化时间则增加约 0.5 秒。

.. only:: SOC_SPIRAM_SUPPORTED

默认情况下,内部 NVS 会在设备的内部 RAM 中分配堆。当 NVS 分区较大或键的数量较多时,可能会因为使用内部 NVS 所需的内存开销过大,设备的内部 RAM 堆耗尽,导致应用程序遇到内存不足的问题。
如果应用程序使用了带有 SPI 连接的 PSRAM 模块,则可以通过启用 Kconfig 选项 :ref:`CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM` 来克服此限制。启用该选项后,RAM 分配会重定向至带有 SPI 连接的 PSRAM 上。
启用 SPIRAM 且将 :ref:`CONFIG_SPIRAM_USE` 设为 ``CONFIG_SPIRAM_USE_CAPS_ALLOC`` 后,即可在 menuconfig 菜单的 nvs_flash 组件中启用 :ref:`CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM` 选项。
.. note:: 使用带有 SPI 连接的 PSRAM 会导致 NVS API 的整数操作速度减慢约 2.5 倍。

.. _nvs_bootloader:

在引导加载程序代码中使用 NVS

运行中的应用程序可使用本指南中描述的标准 NVS API。也可以在自定义引导加载程序代码中从 NVS 读取数据。更多信息请参阅 :doc:nvs_bootloader 指南。

键值对 ^^^^^^^^^^^^^^^

NVS 的操作对象为键值对,其中键是 ASCII 字符串,当前支持的最大键长为 15 个字符。值可以为以下几种类型:

  • 整数型:uint8_tint8_tuint16_tint16_tuint32_tint32_tuint64_tint64_t
  • 以 0 结尾的字符串;
  • 可变长度的二进制数据 (BLOB)

.. note::

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

.. note::

在设置新的或更新现有的键值对之前,需要确保 NVS 页面具备可用的空闲条目。对于整数类型,确保至少有一个可用的空闲条目。对于字符串值,确保至少有一个 NVS 页面,页面中有足够的连续空闲条目,以便能够完整地存储整个字符串。对于 Blob 值,确保在 NVS 中有足够的空闲条目,以容纳新数据的大小。

后续可能会增加对 floatdouble 等其他类型数据的支持。

键必须唯一。为现有的键写入新值时,会将旧的值及数据类型更新为写入操作指定的值和数据类型。

读取值时会执行数据类型检查。如果读取操作预期的数据类型与对应键的数据类型不匹配,则返回错误。

命名空间 ^^^^^^^^

为缓解不同组件间可能出现的键名冲突问题,NVS 将每个键值对分配到某个命名空间。命名空间的命名规则与键名相同,即最大长度为 15 个字符。此外,单个 NVS 分区中,最多支持 254 个不同的命名空间。调用 :cpp:func:nvs_open 或 :cpp:type:nvs_open_from_partition 可以指定命名空间名称。

open mode 参数控制访问级别和安全行为:

  • NVS_READONLY:只读访问权限。所有写操作将被拒绝。
  • NVS_READWRITE:标准读写访问权限。被擦除的数据会被标记为已删除,但仍保留在 flash 中。
  • NVS_READWRITE_PURGE:安全的读写访问权限。已擦除的数据将从 flash 中物理移除。

此调用返回一个不透明句柄,用于后续对 nvs_get_*nvs_set_* 和 :cpp:func:nvs_commit 函数的调用。这样,句柄与某个命名空间关联,键名不会与其他命名空间中的同名键发生冲突。请注意,不同 NVS 分区中同名的命名空间被视为相互独立的命名空间。

.. _data_purging_security:

数据清除与安全 ^^^^^^^^^^^^^^

默认情况下,当 NVS 更新或擦除键值对时,旧数据被标记为已擦除,但实际仍存在于 flash 中。这种做法可以提升写入性能,并且有助于减轻 flash 芯片的磨损。但如果能够物理访问 flash 芯片,被擦除的数据仍可能被恢复。

对于需要更高安全性、必须将敏感数据从 flash 中物理删除的应用,NVS 提供了两种机制:

清除模式NVS_READWRITE_PURGE 模式打开命名空间时,所有写入和擦除操作在写入新值的同时会自动清除(物理擦除)先前存储的数据,确保旧数据无法从 flash 中恢复。

手动清除 调用 :cpp:func:nvs_purge_all 函数,应用程序可以随时显式清理某个命名空间内所有已擦除的条目。可用于以 NVS_READWRITENVS_READWRITE_PURGE 模式打开的句柄。

.. note::

相较于标准擦除操作,清除操作会增加 flash 擦写次数。应用程序在决定是否使用清除功能时,应在安全需求与 flash 磨损之间进行权衡。

NVS 迭代器 ^^^^^^^^^^^^^

迭代器允许根据指定的分区名称、命名空间和数据类型轮询 NVS 中存储的键值对。

使用以下函数,可执行相关操作:

  • nvs_entry_find:创建一个不透明句柄,用于后续调用 nvs_entry_nextnvs_entry_info 函数;
  • nvs_entry_next:让迭代器指向下一个键值对;
  • nvs_entry_info:返回每个键值对的信息。

总的来说,所有通过 :cpp:func:nvs_entry_find 获得的迭代器(包括 NULL 迭代器)都必须使用 :cpp:func:nvs_release_iterator 释放。

一般情况下,:cpp:func:nvs_entry_find 和 :cpp:func:nvs_entry_next 会将给定的迭代器设置为 NULL 或为一个有效的迭代器。但如果出现参数错误(如返回 ESP_ERR_NVS_NOT_FOUND),给定的迭代器不会被修改。因此,在调用 :cpp:func:nvs_entry_find 之前最好将迭代器初始化为 NULL,这样可以避免在释放迭代器之前进行复杂的错误检查。

安全性、篡改性及鲁棒性 ^^^^^^^^^^^^^^^^^^^^^^^^^^

.. only:: not SOC_HMAC_SUPPORTED

NVS 与 {IDF_TARGET_NAME} flash 加密系统不直接兼容。然而,如果 NVS 加密与 {IDF_TARGET_NAME} flash 加密一起使用,数据仍可以加密形式存储。详情请参考 :doc:`nvs_encryption`。

.. only:: SOC_HMAC_SUPPORTED

NVS 与 {IDF_TARGET_NAME} flash 加密系统不直接兼容。然而,如果 NVS 加密与 {IDF_TARGET_NAME} flash 加密或 HMAC 外设一起使用,数据仍可以加密形式存储。详情请参考 :doc:`nvs_encryption`。

如果未启用 NVS 加密,任何对 flash 芯片有物理访问权限的用户都可以修改、擦除或添加键值对。NVS 加密启用后,如果不知道相应的 NVS 加密密钥,则无法修改或添加键值对并将其识别为有效键值对。但是,针对擦除操作没有相应的防篡改功能。

防止数据恢复:默认情况下,当键值对被更新或擦除时,旧数据实际仍然存在于 flash 中,仅被标记为无效。对于禁止恢复旧数据的敏感应用,打开命名空间时应使用 NVS_READWRITE_PURGE 模式,或调用 :cpp:func:nvs_purge_all 以显式清除已擦除的数据。详情参见 :ref:数据清除与安全 <data_purging_security> 部分。

当 flash 处于不一致状态时,NVS 库会尝试恢复。在任何时间点关闭设备电源,然后重新打开电源,不会导致数据丢失;但如果关闭设备电源时正在写入新的键值对,这一键值对可能会丢失。该库还应该能够在 flash 中存在任何随机数据的情况下正常初始化。

电源不稳定状态

当 NVS 用于弱电源或不稳定电源系统(如太阳能或电池供电系统)时,flash 擦除操作可能偶尔无法彻底完成,而应用程序无法检测到这一问题。这会导致实际 flash 内容与预留页面的预期布局不一致。在极少数情况下(特别是在意外断电时),可能造成可用 NVS 页面耗尽,导致分区初始化失败并返回 ESP_ERR_NVS_NO_FREE_PAGES 错误。

为解决此问题,可通过 Kconfig 选项 :ref:CONFIG_NVS_FLASH_VERIFY_ERASE 启用 flash 擦除操作的验证机制,通过回读受影响页面进行检测。若在 flash_erase 操作后页面未完全擦除为 0xFF,系统将重试擦除操作直至页面被正确清空。包括首次尝试在内的擦除尝试总次数可通过 Kconfig 选项 :ref:CONFIG_NVS_FLASH_ERASE_ATTEMPTS 进行配置。

.. _nvs_encryption:

NVS 加密

详情请参考 :doc:nvs_encryption

NVS 分区生成程序

NVS 分区生成程序帮助生成 NVS 分区二进制文件,可使用烧录程序将二进制文件单独烧录至特定分区。烧录至分区上的键值对由 CSV 文件提供,详情请参考 :doc:nvs_partition_gen

可以直接使用函数 nvs_create_partition_image 通过 CMake 创建分区二进制文件,无需手动调用 nvs_partition_gen.py 工具::

nvs_create_partition_image(<partition> <csv> [FLASH_IN_PROJECT] [DEPENDS  dep dep dep ...])

位置参数:

.. list-table:: :header-rows: 1

* - 参数
  - 描述
* - ``partition``
  - NVS 分区名
* - ``csv``
  - 解析的 CSV 文件路径

可选参数:

.. list-table:: :header-rows: 1

    • 参数
    • 描述
    • FLASH_IN_PROJECT
    • NVS 分区名
    • DEPENDS
    • 指定命令依赖的文件

在没有指定 FLASH_IN_PROJECT 的情况下,也支持生成分区镜像,不过此时需要使用 idf.py <partition>-flash 手动进行烧录。举个例子,如果分区名为 nvs,则需使用的命令为 idf.py nvs-flash

目前,仅支持从组件中的 CMakeLists.txt 文件调用 nvs_create_partition_image,且此选项仅适用于非加密分区。

应用示例

ESP-IDF :example:storage/nvs 目录下提供了数个代码示例:

:example:storage/nvs/nvs_rw_value

演示如何读取及写入 NVS 单个整数值。

此示例中的值表示 {IDF_TARGET_NAME} 模组重启次数。NVS 中数据不会因为模组重启而丢失,因此只有将这一值存储于 NVS 中,才能起到重启次数计数器的作用。

该示例也演示了如何检测读取/写入操作是否成功,以及某个特定值是否在 NVS 中尚未初始化。诊断程序以纯文本形式提供,有助于追踪程序流程,及时发现问题。

:example:storage/nvs/nvs_rw_blob

演示如何读取及写入 NVS 单个整数值和 BLOB(二进制大对象),并在 NVS 中存储这一数值,即便 {IDF_TARGET_NAME} 模组重启也不会消失。

* value - 记录 {IDF_TARGET_NAME} 模组软重启次数和硬重启次数。
* blob - 内含记录模组运行次数的表格。此表格将被从 NVS 读取至动态分配的 RAM 上。每次手动软重启后,表格内运行次数即增加一次,新加的运行次数被写入 NVS。下拉 GPIO0 即可手动软重启。

该示例也演示了如何执行诊断程序以检测读取/写入操作是否成功。

:example:storage/nvs/nvs_rw_value_cxx

这个例子与 :example:storage/nvs/nvs_rw_value 完全一样,只是使用了 C++ 的 NVS 句柄类。

:example:storage/nvs/nvs_statistics

该示例演示了如何获取并解读 NVS 使用情况统计信息:包括指定 NVS 分区中的空闲、已用、可用、总条目数、以及命名空间数量。

默认的 NVS 分区会在运行本示例前被擦除,以确保干净的运行环境。随后,会写入模拟的字符串类型数据。

在写入数据前后分别获取使用情况统计信息,并将两者的差异与新占用条目的预期值进行比较。

:example:storage/nvs/nvs_iteration

该示例演示了如何遍历特定(或任意)NVS 数据类型的条目,以及如何获取这些条目的相关信息。

默认的 NVS 分区会在运行本示例前被擦除,以确保干净的运行环境。随后,会写入包含不同 NVS 整型数据类型的模拟数据。

之后,本示例会遍历各个数据类型以及通用的 NVS_TYPE_ANY 类型,并记录在每次遍历过程中获取到的信息。

内部实现

键值对日志 ^^^^^^^^^^^^^^^^^^^^^^

NVS 按顺序存储键值对,新的键值对添加在最后。因此,如需更新某一键值对,实际是在日志最后增加一对新的键值对,同时将旧的键值对标记为已擦除。

标准擦除行为:默认情况下,被擦除的条目实际仍存在于 flash 中,仅修改其状态位以表明不再有效。这样可以优化性能并减少 flash 磨损。

清除行为:使用 NVS_READWRITE_PURGE 模式或调用 :cpp:func:nvs_purge_all 时,NVS 库会对已擦除的条目内容进行物理覆盖,防止数据被恢复。此操作会增加 flash 擦写次数,但能为敏感数据提供更高的安全性。

页面和条目 ^^^^^^^^^^^^^^^^^

NVS 库在其操作中主要使用两个实体:页面和条目。页面是一个逻辑结构,用于存储部分的整体日志。逻辑页面对应 flash 的一个物理扇区,正在使用中的页面具有与之相关联的 序列号。序列号赋予了页面顺序,较高的序列号对应较晚创建的页面。页面有以下几种状态:

空或未初始化 页面对应的 flash 扇区为空白状态(所有字节均为 0xff)。此时,页面未存储任何数据且没有关联的序列号。

活跃状态 此时 flash 已完成初始化,页头部写入 flash,页面已具备有效序列号。页面中存在一些空条目,可写入数据。任意时刻,至多有一个页面处于活跃状态。

写满状态 flash 已写满键值对,状态不再改变。 用户无法向写满状态下的页面写入新键值对,但仍可将一些键值对标记为已擦除。

擦除状态 未擦除的键值对将移至其他页面,以便擦除当前页面。这一状态仅为暂时性状态,即 API 调用返回时,页面应脱离这一状态。如果设备突然断电,下次开机时,设备将继续把未擦除的键值对移至其他页面,并继续擦除当前页面。

损坏状态 页头部包含无效数据,无法进一步解析该页面中的数据,因此之前写入该页面的所有条目均无法访问。相应的 flash 扇区并不会被立即擦除,而是与其他处于未初始化状态的扇区一起等待后续使用。这一状态可能对调试有用。

flash 扇区映射至逻辑页面并没有特定的顺序,NVS 库会检查存储在 flash 扇区的页面序列号,并根据序列号组织页面。

::

+--------+     +--------+     +--------+     +--------+
| Page 1 |     | Page 2 |     | Page 3 |     | Page 4 |
| Full   +---> | Full   +---> | Active |     | Empty  |   <- 状态
| #11    |     | #12    |     | #14    |     |        |   <- 序列号
+---+----+     +----+---+     +----+---+     +---+----+
    |               |              |             |
    |               |              |             |
    |               |              |             |
+---v------+  +-----v----+  +------v---+  +------v---+
| Sector 3 |  | Sector 0 |  | Sector 2 |  | Sector 1 |    <- 物理扇区
+----------+  +----------+  +----------+  +----------+

页面结构 ^^^^^^^^^^^^^^^^^^^

当前,我们假设 flash 扇区大小为 4096 字节,并且 {IDF_TARGET_NAME} flash 加密硬件在 32 字节块上运行。未来有可能引入一些编译时可配置项(可通过 menuconfig 进行配置),以适配具有不同扇区大小的 flash 芯片。但目前尚不清楚 SPI flash 驱动和 SPI flash cache 之类的系统组件是否支持其他扇区大小。

页面由头部、条目状态位图和条目三部分组成。为了实现与 {IDF_TARGET_NAME} flash 加密功能兼容,条目大小设置为 32 字节。如果键值为整数型,条目则保存一个键值对;如果键值为字符串或 BLOB 类型,则条目仅保存一个键值对的部分内容(更多信息详见条目结构描述)。

页面结构如下图所示,括号内数字表示该部分的大小(以字节为单位)。

::

+-----------+--------------+-------------+-------------------------+
| State (4) | Seq. no. (4) | version (1) | Unused (19) | CRC32 (4) |   页头部 (32)
+-----------+--------------+-------------+-------------------------+
|                Entry state bitmap (32)                           |
+------------------------------------------------------------------+
|                       Entry 0 (32)                               |
+------------------------------------------------------------------+
|                       Entry 1 (32)                               |
+------------------------------------------------------------------+
/                                                                  /
/                                                                  /
+------------------------------------------------------------------+
|                       Entry 125 (32)                             |
+------------------------------------------------------------------+

头部和条目状态位图写入 flash 时不加密。如果启用了 {IDF_TARGET_NAME} flash 加密功能,则条目写入 flash 时将会加密。

通过将 0 写入某些位可以定义页面状态值,表示状态改变。因此,如果需要变更页面状态,并不一定要擦除页面,除非要将其变更为 擦除 状态。

头部中的 version 字段反映了所用的 NVS 格式版本。为实现向后兼容,版本升级从 0xff 开始依次递减(例如,version-1 为 0xff,version-2 为 0xfe,以此类推)。

头部中 CRC32 值是由不包含状态值的条目计算所得(4 到 28 字节)。当前未使用的条目用 0xff 字节填充。

条目结构和条目状态位图的详细信息见下文描述。

条目和条目状态位图 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

每个条目可处于以下三种状态之一,每个状态在条目状态位图中用两位表示。位图中的最后四位 (256 - 2 * 126) 未使用。

空 (2'b11) 条目还未写入任何内容,处于未初始化状态(全部字节为 0xff)。

写入(2'b10) 一个键值对(或跨多个条目的键值对的部分内容)已写入条目中。

擦除(2'b00) 条目中的键值对已丢弃,条目内容不再解析。

.. _structure_of_entry:

条目结构 ^^^^^^^^^^^^^^^^^^

如果键值类型为基础类型,即 1 - 8 个字节长度的整数型,条目将保存一个键值对;如果键值类型为字符串或 BLOB 类型,条目将保存整个键值对的部分内容。另外,如果键值为字符串类型且跨多个条目,则键值所跨的所有条目均保存在同一页面。BLOB 则可以切分为多个块,实现跨多个页面。BLOB 索引是一个附加的固定长度元数据条目,用于追踪 BLOB 块。目前条目仍支持早期 BLOB 格式(可读取可修改),但这些 BLOB 一经修改,即以新格式储存至条目。

::

+--------+----------+----------+----------------+-----------+---------------+----------+
| NS (1) | Type (1) | Span (1) | ChunkIndex (1) | CRC32 (4) |    Key (16)   | Data (8) |
+--------+----------+----------+----------------+-----------+---------------+----------+

                                         Primitive  +--------------------------------+
                                        +-------->  |     Data (8)                   |
                                        | Types     +--------------------------------+
                   +-> Fixed length --
                   |                    |           +---------+--------------+---------------+-------+
                   |                    +-------->  | Size(4) | ChunkCount(1)| ChunkStart(1) | Rsv(2)|
    Data format ---+                    BLOB Index  +---------+--------------+---------------+-------+
                   |
                   |                             +----------+---------+-----------+
                   +->   Variable length   -->   | Size (2) | Rsv (2) | CRC32 (4) |
                        (Strings, BLOB Data)     +----------+---------+-----------+

条目结构中各个字段含义如下:

命名空间 (NS, NameSpace) 该条目的命名空间索引,详细信息参见命名空间实现章节。

类型 (Type) 一个字节表示的值的数据类型,:component_file:nvs_flash/include/nvs_handle.hpp 下的 :cpp:type:ItemType 枚举了可能的类型。

跨度 (Span) 该键值对所用的条目数量。如果键值为整数型,条目数量即为 1。如果键值为字符串或 BLOB,则条目数量取决于值的长度。

块索引 (ChunkIndex) 用于存储 BLOB 类型数据块的索引。如果键值为其他数据类型,则此处索引应写入 0xff

CRC32 对条目下所有字节进行校验后,所得的校验和(CRC32 字段不计算在内)。

键 (Key) 即以零结尾的 ASCII 字符串,字符串最长为 15 字节,不包含最后一个字节的零终止符。

数据 (Data) 如果键值类型为整数型,则数据字段仅包含键值。如果键值小于八个字节,使用 0xff 填充未使用的部分(右侧)。

如果键值类型为 BLOB 索引条目,则该字段的八个字节将保存以下数据块信息:

- 块大小
    整个 BLOB 数据的大小(以字节为单位)。该字段仅用于 BLOB 索引类型条目。

- ChunkCount
    存储过程中 BLOB 分成的数据块总量。该字段仅用于 BLOB 索引类型条目。

- ChunkStart
    BLOB 第一个数据块的块索引,后续数据块索引依次递增,步长为 1。该字段仅用于 BLOB 索引类型条目。

如果键值类型为字符串或 BLOB 数据块,数据字段的这八个字节将保存该键值的一些附加信息,如下所示:

- 数据大小
    实际数据的大小(以字节为单位)。如果键值类型为字符串,此字段也应将零终止符包含在内。此字段仅用于字符串和 BLOB 类型条目。

- CRC32
    数据所有字节的校验和,该字段仅用于字符串和 BLOB 类型条目。

可变长度值(字符串和 BLOB)写入后续条目,每个条目 32 字节。第一个条目的 Span 字段将指明使用了多少条目。

命名空间 ^^^^^^^^^^

如上所述,每个键值对属于一个命名空间。命名空间标识符(字符串)也作为键值对的键,存储在索引为 0 的命名空间中。与这些键对应的值就是这些命名空间的索引。

::

+-------------------------------------------+
| NS=0 Type=uint8_t Key="wifi" Value=1      |   Entry describing namespace "wifi"
+-------------------------------------------+
| NS=1 Type=uint32_t Key="channel" Value=6  |   Key "channel" in namespace "wifi"
+-------------------------------------------+
| NS=0 Type=uint8_t Key="pwm" Value=2       |   Entry describing namespace "pwm"
+-------------------------------------------+
| NS=2 Type=uint16_t Key="channel" Value=20 |   Key "channel" in namespace "pwm"
+-------------------------------------------+

条目哈希列表 ^^^^^^^^^^^^^^

为了减少对 flash 执行的读操作次数,Page 类对象均设有一个列表,包含一对数据:条目索引和条目哈希值。该列表可大大提高检索速度,而无需迭代所有条目并逐个从 flash 中读取。Page::findItem 首先从哈希列表中检索条目哈希值,如果条目存在,则在页面内给出条目索引。由于哈希冲突,在哈希列表中检索条目哈希值可能会得到不同的条目,对 flash 中条目再次迭代可解决这一冲突。

哈希列表中每个节点均包含一个 24 位哈希值和 8 位条目索引。哈希值根据条目命名空间、键名和块索引由 CRC32 计算所得,计算结果保留 24 位。为减少将 32 位条目存储在链表中的开销,链表采用了数组的双向链表。每个数组占用 128 个字节,包含 29 个条目、两个链表指针和一个 32 位计数字段。因此,每页额外需要的 RAM 最少为 128 字节,最多为 640 字节。

.. _read-only-nvs:

只读 NVS ^^^^^^^^

NVS 正常运行所需的最小大小默认为 12kiB (0x3000),这意味着至少需要 3 个页面,其中一个页面必须处于 Empty 状态。但是,如果 NVS 分区在分区表 CSV 中标记为 readonly 并以只读 (read-only) 模式打开,则该分区大小最少只需 4kiB(0x1000),此时仅需一个 Active 状态的页面,无需 Empty 页面。因为在这种情况下,库无需向分区写入任何数据。此类型分区适用于存储不会更改的数据,如校准数据或出厂设置。大小为 0x1000 和 0x2000 的分区始终为只读分区。大小为 0x3000 及以上的分区始终支持读写 (read-write),但仍可以在代码中以只读模式打开。

API 参考

.. include-build-file:: inc/nvs_flash.inc

.. include-build-file:: inc/nvs.inc