esp-idf/docs/zh_CN/api-reference/storage/nvs_flash.rst
radek.tandler ffaf1d2968 feat(nvs_flash): Added Kconfig option contolling NVS heap allocation source
NVS configuration is extended with Kconfig option controlling RAM area for
NVS heap allocation. Either Internal RAM or SPIRAM can be chosen.
Tests were extended to check memory consumption from Internal and SPIRAM pool with respect
to the Kconfig option chosen.
Documentation was extended with notes related to NVS behavior in various situations.
2024-06-06 08:25:51 +02:00

371 lines
22 KiB
ReStructuredText
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

非易失性存储库
============================
: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 的操作对象为键值对,其中键是 ASCII 字符串,当前支持的最大键长为 15 个字符。值可以为以下几种类型:
- 整数型:``uint8_t````int8_t````uint16_t````int16_t````uint32_t````int32_t````uint64_t````int64_t``
- 以 0 结尾的字符串;
- 可变长度的二进制数据 (BLOB)
.. note::
字符串值当前上限为 4000 字节其中包括空终止符。BLOB 值上限为 508,000 字节或分区大小的 97.6% 减去 4000 字节,以较低值为准。
.. note::
在设置新的或更新现有的键值对之前,需要确保 NVS 页面具备可用的空闲条目。对于整数类型,确保至少有一个可用的空闲条目。对于字符串值,确保至少有一个 NVS 页面,页面中有足够的连续空闲条目,以便能够完整地存储整个字符串。对于 Blob 值,确保在 NVS 中有足够的空闲条目,以容纳新数据的大小。
后续可能会增加对 ``float````double`` 等其他类型数据的支持。
键必须唯一。为现有的键写入新值时,会将旧的值及数据类型更新为写入操作指定的值和数据类型。
读取值时会执行数据类型检查。如果读取操作预期的数据类型与对应键的数据类型不匹配,则返回错误。
命名空间
^^^^^^^^^^
为了减少不同组件之间键名的潜在冲突NVS 将每个键值对分配给一个命名空间。命名空间的命名规则遵循键名的命名规则,例如,最多可占 15 个字符。此外,单个 NVS 分区最多只能容纳 254 个不同的命名空间。命名空间的名称在调用 :cpp:func:`nvs_open`:cpp:type:`nvs_open_from_partition` 中指定,调用后将返回一个不透明句柄,用于后续调用 ``nvs_get_*````nvs_set_*````nvs_commit`` 函数。这样,一个句柄关联一个命名空间,键名便不会与其他命名空间中相同键名冲突。请注意,不同 NVS 分区中具有相同名称的命名空间将被视为不同的命名空间。
NVS 迭代器
^^^^^^^^^^^^^
迭代器允许根据指定的分区名称、命名空间和数据类型轮询 NVS 中存储的键值对。
使用以下函数,可执行相关操作:
- ``nvs_entry_find``:创建一个不透明句柄,用于后续调用 ``nvs_entry_next````nvs_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 库会尝试恢复。在任何时间点关闭设备电源,然后重新打开电源,不会导致数据丢失;但如果关闭设备电源时正在写入新的键值对,这一键值对可能会丢失。该库还应该能够在 flash 中存在任何随机数据的情况下正常初始化。
.. _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` 目录下提供了数个代码示例:
:example:`storage/nvs_rw_value`
演示如何读取及写入 NVS 单个整数值。
此示例中的值表示 {IDF_TARGET_NAME} 模组重启次数。NVS 中数据不会因为模组重启而丢失,因此只有将这一值存储于 NVS 中,才能起到重启次数计数器的作用。
该示例也演示了如何检测读取/写入操作是否成功,以及某个特定值是否在 NVS 中尚未初始化。诊断程序以纯文本形式提供,有助于追踪程序流程,及时发现问题。
:example:`storage/nvs_rw_blob`
演示如何读取及写入 NVS 单个整数值和 BLOB二进制大对象并在 NVS 中存储这一数值,即便 {IDF_TARGET_NAME} 模组重启也不会消失。
* value - 记录 {IDF_TARGET_NAME} 模组软重启次数和硬重启次数。
* blob - 内含记录模组运行次数的表格。此表格将被从 NVS 读取至动态分配的 RAM 上。每次手动软重启后,表格内运行次数即增加一次,新加的运行次数被写入 NVS。下拉 GPIO0 即可手动软重启。
该示例也演示了如何执行诊断程序以检测读取/写入操作是否成功。
:example:`storage/nvs_rw_value_cxx`
这个例子与 :example:`storage/nvs_rw_value` 完全一样,只是使用了 C++ 的 NVS 句柄类。
内部实现
---------
键值对日志
^^^^^^^^^^^^^^^^^^^^^^
NVS 按顺序存储键值对,新的键值对添加在最后。因此,如需更新某一键值对,实际是在日志最后增加一对新的键值对,同时将旧的键值对标记为已擦除。
页面和条目
^^^^^^^^^^^^^^^^^
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 为 0xffversion-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 字节。
API 参考
-------------
.. include-build-file:: inc/nvs_flash.inc
.. include-build-file:: inc/nvs.inc