From ffaf1d2968756eecbe23a00edd449a09abd1ec37 Mon Sep 17 00:00:00 2001 From: "radek.tandler" Date: Thu, 2 Nov 2023 11:08:31 +0100 Subject: [PATCH] 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. --- components/nvs_flash/Kconfig | 10 +++ .../nvs_flash/src/nvs_memory_management.hpp | 33 ++++++++- .../nvs_flash/test_apps/main/CMakeLists.txt | 2 +- .../nvs_flash/test_apps/main/app_main.c | 47 ++++++++++++- .../nvs_flash/test_apps/main/test_nvs.c | 69 +++++++++++++++++++ .../nvs_flash/test_apps/pytest_nvs_flash.py | 7 +- .../nvs_flash/test_apps/sdkconfig.ci.spiram | 7 ++ components/nvs_flash/test_nvs_host/Makefile | 2 +- docs/en/api-reference/storage/nvs_flash.rst | 21 ++++++ .../zh_CN/api-reference/storage/nvs_flash.rst | 21 ++++++ 10 files changed, 212 insertions(+), 7 deletions(-) create mode 100644 components/nvs_flash/test_apps/sdkconfig.ci.spiram diff --git a/components/nvs_flash/Kconfig b/components/nvs_flash/Kconfig index a432384ad9..5862f3af2b 100644 --- a/components/nvs_flash/Kconfig +++ b/components/nvs_flash/Kconfig @@ -35,4 +35,14 @@ menu "NVS" in the NVS remains active and the new value is just stored, actually not accessible through corresponding nvs_get() call for the key given. Use this option only when your application relies on such NVS API behaviour. + + config NVS_ALLOCATE_CACHE_IN_SPIRAM + bool "Prefers allocation of in-memory cache structures in SPI connected PSRAM" + depends on SPIRAM && (SPIRAM_USE_CAPS_ALLOC || SPIRAM_USE_MALLOC) + default n + help + Enabling this option lets NVS library try to allocate page cache and key hash list in SPIRAM + instead of internal RAM. It can help applications using large nvs partitions or large number + of keys to save heap space in internal RAM. SPIRAM heap allocation negatively impacts speed + of NVS operations as the CPU accesses NVS cache via SPI instead of direct access to the internal RAM. endmenu diff --git a/components/nvs_flash/src/nvs_memory_management.hpp b/components/nvs_flash/src/nvs_memory_management.hpp index 933f08256e..2936365a7b 100644 --- a/components/nvs_flash/src/nvs_memory_management.hpp +++ b/components/nvs_flash/src/nvs_memory_management.hpp @@ -1,10 +1,11 @@ /* - * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ #include +#include "esp_heap_caps.h" #pragma once @@ -41,6 +42,8 @@ struct ExceptionlessAllocatable { */ static void *operator new( std::size_t ) = delete; + static void *operator new[]( std::size_t ) = delete; + /** * Simple implementation with malloc(). No exceptions are thrown if the allocation fails. * To use this operator, your type must inherit from this class and then allocate with: @@ -50,13 +53,39 @@ struct ExceptionlessAllocatable { * @endcode */ void *operator new (size_t size, const std::nothrow_t&) noexcept { +#ifdef CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM + return heap_caps_malloc_prefer(size, 2, MALLOC_CAP_DEFAULT | MALLOC_CAP_SPIRAM, + MALLOC_CAP_DEFAULT | MALLOC_CAP_INTERNAL); +#else return std::malloc(size); +#endif + } + + void *operator new [](size_t size, const std::nothrow_t&) noexcept { +#ifdef CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM + return heap_caps_malloc_prefer(size, 2, MALLOC_CAP_DEFAULT | MALLOC_CAP_SPIRAM, + MALLOC_CAP_DEFAULT | MALLOC_CAP_INTERNAL); +#else + return std::malloc(size); +#endif } /** * Use \c delete as normal. This operator will be called automatically instead of the global one from libstdc++. */ void operator delete (void *obj) noexcept { - free(obj); +#ifdef CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM + return heap_caps_free(obj); +#else + return std::free(obj); +#endif + } + + void operator delete [](void *obj) noexcept { +#ifdef CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM + return heap_caps_free(obj); +#else + return std::free(obj); +#endif } }; diff --git a/components/nvs_flash/test_apps/main/CMakeLists.txt b/components/nvs_flash/test_apps/main/CMakeLists.txt index 093fc198ba..22e8d8fbd0 100644 --- a/components/nvs_flash/test_apps/main/CMakeLists.txt +++ b/components/nvs_flash/test_apps/main/CMakeLists.txt @@ -1,6 +1,6 @@ idf_component_register(SRC_DIRS "." PRIV_REQUIRES cmock test_utils nvs_flash nvs_sec_provider - bootloader_support spi_flash + bootloader_support spi_flash esp_psram EMBED_TXTFILES encryption_keys.bin partition_encrypted.bin partition_encrypted_hmac.bin sample.bin WHOLE_ARCHIVE) diff --git a/components/nvs_flash/test_apps/main/app_main.c b/components/nvs_flash/test_apps/main/app_main.c index 43cab1db04..43ade3807b 100644 --- a/components/nvs_flash/test_apps/main/app_main.c +++ b/components/nvs_flash/test_apps/main/app_main.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Unlicense OR CC0-1.0 */ @@ -12,6 +12,50 @@ #endif #include "memory_checks.h" +#include "esp_heap_caps.h" +#include "time.h" + +// recorded heap free sizes (MALLOC_CAP_INTERNAL and MALLOC_CAP_SPIRAM) +static size_t recorded_internal_heap_free_size = 0; +static size_t recorded_spiram_heap_free_size = 0; + +// stores heap free sizes for internal and spiram pools +void record_heap_free_sizes(void) +{ + recorded_internal_heap_free_size = heap_caps_get_free_size(MALLOC_CAP_INTERNAL); + recorded_spiram_heap_free_size = heap_caps_get_free_size(MALLOC_CAP_SPIRAM); +} + +// returns difference between actual heap free size and recorded heap free size +// parameter nvs_active_pool controls whether active or inactive heap will be examined +// if CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM is not set, active pool is MALLOC_CAP_INTERNAL and inactive is MALLOC_CAP_SPIRAM +// if CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM is set, active pool is MALLOC_CAP_SPIRAM and inactive is MALLOC_CAP_INTERNAL +int32_t get_heap_free_difference(const bool nvs_active_pool) +{ + int32_t recorded_heap_free_size = 0; + int32_t actual_heap_free_size = 0; + + bool evaluate_spiram = false; +#ifdef CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM + // here active means spiram + evaluate_spiram = nvs_active_pool; +#else + // here active means internal + evaluate_spiram = !nvs_active_pool; +#endif + + if(evaluate_spiram) { + recorded_heap_free_size = recorded_spiram_heap_free_size; + actual_heap_free_size = heap_caps_get_free_size(MALLOC_CAP_SPIRAM); + } + else { + recorded_heap_free_size = recorded_internal_heap_free_size; + actual_heap_free_size = heap_caps_get_free_size(MALLOC_CAP_INTERNAL); + } + + return actual_heap_free_size - recorded_heap_free_size; +} + /* setUp runs before every test */ void setUp(void) { @@ -47,7 +91,6 @@ void tearDown(void) test_utils_finish_and_evaluate_leaks(test_utils_get_leak_level(ESP_LEAK_TYPE_WARNING, ESP_COMP_LEAK_ALL), test_utils_get_leak_level(ESP_LEAK_TYPE_CRITICAL, ESP_COMP_LEAK_ALL)); - } static void test_task(void *pvParameters) diff --git a/components/nvs_flash/test_apps/main/test_nvs.c b/components/nvs_flash/test_apps/main/test_nvs.c index 688722fe9f..9e7958c6ac 100644 --- a/components/nvs_flash/test_apps/main/test_nvs.c +++ b/components/nvs_flash/test_apps/main/test_nvs.c @@ -26,6 +26,9 @@ #include "unity.h" #include "memory_checks.h" +#include "esp_heap_caps.h" +#include "esp_random.h" + #ifdef CONFIG_NVS_ENCRYPTION #include "mbedtls/aes.h" #endif @@ -34,8 +37,74 @@ #include "esp_hmac.h" #endif +extern void record_heap_free_sizes(void); +extern int32_t get_heap_free_difference(const bool nvs_active_pool); + static const char* TAG = "test_nvs"; +TEST_CASE("Kconfig option controls heap capability allocator for NVS", "[nvs_ram]") +{ + // number of keys used for test + const size_t max_key = 400; + + char key_name[sizeof("keyXXXXX ")]; + int32_t out_val = 0; + nvs_handle_t handle; + + esp_err_t err = nvs_flash_init(); + if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_LOGW(TAG, "nvs_flash_init failed (0x%x), erasing partition and retrying", err); + ESP_ERROR_CHECK(nvs_flash_erase()); + err = nvs_flash_init(); + } + ESP_ERROR_CHECK( err ); + + TEST_ESP_OK(nvs_open("test_namespace1", NVS_READWRITE, &handle)); + TEST_ESP_OK(nvs_erase_all(handle)); + record_heap_free_sizes(); + + for(size_t i=0; i None: # Erase the nvs_key partition dut.serial.erase_partition('nvs_key') dut.run_all_single_board_cases() + + +@pytest.mark.esp32 +@pytest.mark.psram +def test_nvs_flash_ram(dut: IdfDut) -> None: + dut.run_all_single_board_cases(group='nvs_ram') diff --git a/components/nvs_flash/test_apps/sdkconfig.ci.spiram b/components/nvs_flash/test_apps/sdkconfig.ci.spiram new file mode 100644 index 0000000000..822d159214 --- /dev/null +++ b/components/nvs_flash/test_apps/sdkconfig.ci.spiram @@ -0,0 +1,7 @@ +# Restricting to ESP32 +CONFIG_IDF_TARGET="esp32" + +CONFIG_SPIRAM=y +CONFIG_SPIRAM_USE_CAPS_ALLOC=y +CONFIG_INT_WDT_TIMEOUT_MS=3000 +CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM=y diff --git a/components/nvs_flash/test_nvs_host/Makefile b/components/nvs_flash/test_nvs_host/Makefile index 044a39e72a..856b9aba5a 100644 --- a/components/nvs_flash/test_nvs_host/Makefile +++ b/components/nvs_flash/test_nvs_host/Makefile @@ -34,7 +34,7 @@ else COMPILER := gcc endif -CPPFLAGS += -I../private_include -I../include -I../src -I../../esp_rom/include -I../../esp_rom/include/linux -I../../log/include -I./ -I../../esp_common/include -I../../esp32/include -I ../../mbedtls/mbedtls/include -I ../../spi_flash/include -I ../../esp_partition/include -I ../../hal/include -I ../../xtensa/include -I ../../soc/linux/include -I ../../../tools/catch -fprofile-arcs -ftest-coverage -g2 -ggdb +CPPFLAGS += -I../private_include -I../include -I../src -I../../heap/include -I../../esp_rom/include -I../../esp_rom/include/linux -I../../log/include -I./ -I../../esp_common/include -I../../esp32/include -I ../../mbedtls/mbedtls/include -I ../../spi_flash/include -I ../../esp_partition/include -I ../../hal/include -I ../../xtensa/include -I ../../soc/linux/include -I ../../../tools/catch -fprofile-arcs -ftest-coverage -g2 -ggdb CFLAGS += -fprofile-arcs -ftest-coverage -DLINUX_TARGET -DLINUX_HOST_LEGACY_TEST CXXFLAGS += -std=c++11 -Wall -Werror -DLINUX_TARGET -DLINUX_HOST_LEGACY_TEST LDFLAGS += -lstdc++ -Wall -fprofile-arcs -ftest-coverage diff --git a/docs/en/api-reference/storage/nvs_flash.rst b/docs/en/api-reference/storage/nvs_flash.rst index 0796b6fd1b..572fe59c9d 100644 --- a/docs/en/api-reference/storage/nvs_flash.rst +++ b/docs/en/api-reference/storage/nvs_flash.rst @@ -19,6 +19,23 @@ Future versions of this library may have other storage backends to keep data in .. note:: NVS works best for storing many small values, rather than a few large values of the type 'string' and 'blob'. If you need to store large blobs or strings, consider using the facilities provided by the FAT filesystem on top of the wear levelling library. +.. note:: NVS component includes flash wear levelling by design. Set operations are appending new data to the free space after existing entries. Invalidation of old values doesn't require immediate flash erase operations. The organization of NVS space to pages and entries effectively reduces the frequency of flash erase to flash write operations by a factor of 126. + +Large Amount of Data in NVS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Although not recommended, NVS can store tens of thousands of keys and NVS partition can reach up to megabytes in size. + +.. note:: NVS component leaves RAM footprint on the heap. The footprint depends on the size of the NVS partition on flash and the number of keys in use. For RAM usage estimation, please use the following approximate figures: each 1 MB of NVS flash partition consumes 22 KB of RAM and each 1000 keys consumes 5.5 KB of RAM. + +.. note:: Duration of NVS initialization using :cpp:func:`nvs_flash_init` is proportional to the number of existing keys. Initialization of NVS requires approximately 0.5 seconds per 1000 keys. + +.. only:: SOC_SPIRAM_SUPPORTED + + By default, internal NVS allocates a heap in internal RAM. With a large NVS partition or big number of keys, the application can exhaust the internal RAM heap just on NVS overhead. + Applications using modules with SPI-connected PSRAM can overcome this limitation by enabling the Kconfig option :ref:`CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM` which redirects RAM allocation to the SPI-connected PSRAM. + This option is available in the nvs_flash component of the menuconfig menu when SPIRAM is enabled and and :ref:`CONFIG_SPIRAM_USE` is set to ``CONFIG_SPIRAM_USE_CAPS_ALLOC``. + .. note:: Using SPI-connected PSRAM slows down NVS API for integer operations by an approximate factor of 2.5. Keys and Values ^^^^^^^^^^^^^^^ @@ -33,6 +50,10 @@ NVS operates on key-value pairs. Keys are ASCII strings; the maximum key length String values are currently limited to 4000 bytes. This includes the null terminator. Blob values are limited to 508,000 bytes or 97.6% of the partition size - 4000 bytes, whichever is lower. +.. note:: + + Before setting new or updating existing key-value pair, free entries in nvs pages have to be available. For integer types, at least one free entry has to be available. For the String value, at least one page capable of keeping the whole string in a contiguous row of free entries has to be available. For the Blob value, the size of new data has to be available in free entries. + Additional types, such as ``float`` and ``double`` might be added later. Keys are required to be unique. Assigning a new value to an existing key replaces the old value and data type with the value and data type specified by a write operation. diff --git a/docs/zh_CN/api-reference/storage/nvs_flash.rst b/docs/zh_CN/api-reference/storage/nvs_flash.rst index 8f05d495d5..65a2149195 100644 --- a/docs/zh_CN/api-reference/storage/nvs_flash.rst +++ b/docs/zh_CN/api-reference/storage/nvs_flash.rst @@ -19,6 +19,23 @@ NVS 库后续版本可能会增加其他存储器后端,来将数据保存至 .. 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 倍。 键值对 ^^^^^^^^^^^^^^^ @@ -33,6 +50,10 @@ NVS 的操作对象为键值对,其中键是 ASCII 字符串,当前支持的 字符串值当前上限为 4000 字节,其中包括空终止符。BLOB 值上限为 508,000 字节或分区大小的 97.6% 减去 4000 字节,以较低值为准。 +.. note:: + + 在设置新的或更新现有的键值对之前,需要确保 NVS 页面具备可用的空闲条目。对于整数类型,确保至少有一个可用的空闲条目。对于字符串值,确保至少有一个 NVS 页面,页面中有足够的连续空闲条目,以便能够完整地存储整个字符串。对于 Blob 值,确保在 NVS 中有足够的空闲条目,以容纳新数据的大小。 + 后续可能会增加对 ``float`` 和 ``double`` 等其他类型数据的支持。 键必须唯一。为现有的键写入新值时,会将旧的值及数据类型更新为写入操作指定的值和数据类型。