Matter connectedhomeip 实战:ESP32 Persistent Storage 示例 —— 基于 NVS 的 Key Value Store 测试与 API 使用指南
【免费下载链接】connectedhomeipMatter (formerly Project CHIP) creates more connections between more objects, simplifying development for manufacturers and increasing compatibility for consumers, guided by the Connectivity Standards Alliance.项目地址: https://gitcode.com/GitHub_Trending/co/connectedhomeip
本指南围绕 connectedhomeip(Matter/Project CHIP)仓库中的persistent-storageESP32 示例展开,完整介绍该示例的设计目标、入口代码、KeyValueStoreManager公共 API 与平台实现、8 个 KVS 测试用例的含义,以及从环境准备、构建、烧录到串口监控的全流程操作。读者完成阅读后,将能够在自己的 ESP32 Matter 应用中正确调用持久化键值存储接口,并理解 NVS 底层实现的能力边界与当前限制。
示例概览与设计目标
示例 README 明确指出:这是一个用于测试和演示 key value storage(KVS)API 的示例程序。它的价值体现在两个层面:
- 平台验证:当 KVS 实现在不同平台上适配(bring-up)时,用这套统一测试用例检验各平台实现的正确性;
- API 教学:为开发者提供一个"如何调用
KeyValueStoreMgr()接口读写持久化数据"的完整参考实现。
README 同时说明了一个重要的演进方向:未来当所有平台都具备可用的 KVS 后,这个示例可以被迁移为单元测试(unit test),从"示例工程"演变为"平台回归测试"。
注意:该文档还明确标注了当前平台的已知限制——ESP32 平台的 KVS 尚未完全实现,特别是不支持 offset 读取与 partial(部分)读取。这一限制与源码实现完全吻合,后文会从实现层面详细解释。
环境准备与前置文档
示例 README 指引读者先完成两件事,对应的完整操作分别位于:
- ESP-IDF 与 CHIP/Matter 环境搭建
- 构建与配对(commissioning)指南
其中构建指南(docs/platforms/esp32/build_app_and_commission.md)给出了完整的环境变量配置流程,这是所有 ESP32 示例(包括本示例)共用的基础步骤:
# 1) 激活 ESP-IDF 工具链 $ cd path/to/esp-idf $ source export.sh # 2) 激活 Matter 环境(需在 export.sh 之后执行) $ cd path/to/connectedhomeip $ source scripts/activate.sh # 3) 可选:启用 Ccache 加速 IDF 构建 $ export IDF_CCACHE_ENABLE=1示例代码结构与入口分析
persistent-storage目录在仓库中是一个跨平台共享示例,ESP32 只是其中一个平台实现。整体结构如下:
- examples/persistent-storage/KeyValueStorageTest.h —— 跨平台共享的测试声明,定义
RunKvsTest()入口和TestConfigurations枚举; - examples/persistent-storage/KeyValueStorageTest.cpp —— 跨平台共享的测试实现,共 7 个测试函数;
- examples/persistent-storage/esp32/main/main.cpp —— ESP32 平台入口
app_main(); - examples/persistent-storage/esp32/CMakeLists.txt 与 main/CMakeLists.txt —— ESP-IDF 构建配置;
- examples/persistent-storage/esp32/sdkconfig.defaults 与 partitions.csv —— 工程默认配置与分区表。
app_main 入口:初始化 NVS 后循环跑测试
ESP32 入口代码(main.cpp)的逻辑非常清晰:
extern "C" void app_main() { esp_err_t err = nvs_flash_init(); if (err != ESP_OK) { ESP_LOGE(TAG, "nvs_flash_init() failed: %s", esp_err_to_name(err)); return; } ESP_LOGI(TAG, "============================================="); ESP_LOGI(TAG, "chip-esp32-persitent-storage-example starting"); ESP_LOGI(TAG, "============================================="); // Run tests while (true) { ESP_LOGI(TAG, "Running Tests:"); // Partial and offset reads are not currently supported on the ESP32 // platform, skip these tests, chip::RunKvsTest(chip::SKIP_MULTI_READ_TEST); vTaskDelay(60000); // Run every minute } }关键点:
- 必须先
nvs_flash_init():ESP32 的 KVS 底层基于 ESP-IDF 的 NVS(Non-Volatile Storage)子系统,使用前必须初始化 flash 上的 NVS 分区。初始化失败直接退出,不做任何后续操作; - 以
SKIP_MULTI_READ_TEST参数运行测试:这与 README 所述的平台限制一一对应——ESP32 尚不支持 offset/partial 读取,因此跳过了专门验证多段读取的TestMultiRead用例; - 测试每 60 秒循环执行一次:
vTaskDelay(60000)让设备可以反复对同一片 flash 进行写入/读取/擦除压力测试,便于观察 NVS 的磨损与稳定性表现。
共享测试的入口与配置枚举
KeyValueStorageTest.h 定义了测试配置枚举:
enum TestConfigurations { RUN_ALL_TESTS, // 运行全部 7 个测试(含 TestMultiRead) SKIP_MULTI_READ_TEST // 跳过多段读取测试(ESP32 当前使用) }; void RunKvsTest(TestConfigurations test_config = RUN_ALL_TESTS);支持多段读取的平台可以传入RUN_ALL_TESTS运行全部用例;不支持的平台(如当前的 ESP32)则传入SKIP_MULTI_READ_TEST。
KeyValueStoreManager:统一的 KVS 公共 API
所有平台共享的公共 API 定义在 src/include/platform/KeyValueStoreManager.h 中,位于命名空间chip::DeviceLayer::PersistedStorage。应用侧通过单例访问接口KeyValueStoreMgr()获取平台实现。
三个核心操作
| API | 说明 | 关键返回码 |
|---|---|---|
Put(key, value, value_size) | 写入键值对;key 已存在则覆盖 | CHIP_NO_ERROR;CHIP_ERROR_INVALID_ARGUMENT(key 为空/过长或 value 过大);CHIP_ERROR_PERSISTED_STORAGE_FAILED |
Get(key, buffer, buffer_size, read_bytes_size, offset_bytes) | 读取键对应的值到缓冲区,可指定起始偏移 | CHIP_ERROR_BUFFER_TOO_SMALL(缓冲区放不下全部数据时返回,并返回已拷贝字节数);CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND(key 不存在);CHIP_ERROR_INTEGRITY_CHECK_FAILED(数据损坏) |
Delete(key) | 删除键值对 | CHIP_NO_ERROR;CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND |
Get的一个特别用法是探测 key 是否存在:传入nullptr缓冲区与0大小,此时返回CHIP_NO_ERROR或CHIP_ERROR_BUFFER_TOO_SMALL都说明 key 存在(详见测试用例TestKeyExistence)。
模板重载:对任意平凡可拷贝类型透明读写
除了原始指针版本,KeyValueStoreManager还提供了基于模板的重载(KeyValueStoreManager.h),让uint32_t、数组、结构体等**平凡可拷贝类型(trivially copyable)**可以直接读写:
template <typename T> CHIP_ERROR Get(const char * key, T * value) { static_assert(std::is_trivially_copyable<T>(), "KVS values must copyable"); static_assert(!std::is_pointer<T>(), "KVS values cannot be pointers"); static_assert(CHAR_BIT == 8, "Current implementation assumes 8 bit."); return Get(key, value, sizeof(T)); } template <typename T> CHIP_ERROR Put(const char * key, const T & value) { static_assert(std::is_trivially_copyable<T>(), "KVS values must copyable"); static_assert(!std::is_pointer<T>(), "KVS values cannot be pointers"); static_assert(CHAR_BIT == 8, "Current implementation assumes 8 bit."); return Put(key, &value, sizeof(T)); }三个static_assert约束了类型边界:必须平凡可拷贝、不能是指针、假定字节宽度为 8 位。对象大小由编译器从类型自动推导,调用方无需手工指定。
平台实现的分发机制
公共类通过静态分发把调用转发给平台实现(KeyValueStoreManager.h):
inline CHIP_ERROR KeyValueStoreManager::Put(const char * key, const void * value, size_t value_size) { return static_cast<ImplClass *>(this)->_Put(key, value, value_size); }平台实现类KeyValueStoreManagerImpl以friend class KeyValueStoreManager的方式声明,提供_Put/_Get/_Delete前缀方法。ImplClass由CHIP_DEVICE_LAYER_TARGET宏决定,构建时通过KeyValueStoreManagerImpl.h按目标平台引入对应实现。这正是本示例能在 Linux、QPG、Infineon PSOC6、ESP32 等多个平台复用的架构基础。
七大 KVS 测试用例逐项解析
KeyValueStorageTest.cpp 中共定义了 7 个测试函数,RunKvsTest()逐个执行并通过RUN_TEST宏打印PASSED/FAILED(失败时附带FormatCHIPError格式化的错误信息)。每个用例的模式都是"写入 → 读取 → 校验 → 删除",因此可以反复运行而不污染存储。
| 测试函数 | 测试的 key | 验证内容 |
|---|---|---|
TestEmptyString() | str_key | 写入空字符串(长度为 0 的值)后再读回,校验读回内容与长度;覆盖空值边界 |
TestKeyExistence() | str_key | 用Get(key, nullptr, 0)探测 key 是否存在,允许返回CHIP_NO_ERROR或CHIP_ERROR_BUFFER_TOO_SMALL |
TestString() | str_key | 字符串"test_value"的完整写入/读回/删除往返 |
TestUint32() | uint32_key | 通过模板重载读写单个uint32_t数值 |
TestArray() | array_key | 通过模板重载读写uint32_t[5]数组,用memcmp逐字节比对 |
TestStruct() | struct_key | 自定义结构体{uint8_t value1; uint32_t value2;}的序列化读写,逐字段校验 |
TestUpdateValue() | update_key | 连续 10 次更新同一个 key(值 0~9),每次写后立即读回校验,验证覆盖更新语义 |
TestMultiRead() | multi_key | 以i * sizeof(uint32_t)为 offset 分 5 次读取数组元素,验证偏移读取;前 4 次应返回CHIP_ERROR_BUFFER_TOO_SMALL,最后一次返回CHIP_NO_ERROR |
其中TestMultiRead()是唯一被 ESP32 跳过的用例(KeyValueStorageTest.cpp),其注释与实现都依赖offset_bytes参数——而该参数正是 ESP32 当前未实现的能力。
ESP32 平台实现:NVS 之上的 KVS 适配层
ESP32 的实现位于 src/platform/ESP32/KeyValueStoreManagerImpl.cpp 与 KeyValueStoreManagerImpl.h,它把 Matter 的KeyValueStoreManager抽象直接映射到 ESP-IDF 的 NVS API 之上。
命名空间与单例
实现类持有静态单例sInstance,并使用固定的 NVS 命名空间:
class KeyValueStoreManagerImpl final : public KeyValueStoreManager { private: static inline const char kNamespace[] = "CHIP_KVS"; static KeyValueStoreManagerImpl sInstance; };即所有 Matter KVS 数据都存放在 NVS 命名空间CHIP_KVS下。同时提供两个访问函数(KeyValueStoreManagerImpl.h):
KeyValueStoreMgr()—— 返回公共接口单例,应用通用代码使用;KeyValueStoreMgrImpl()—— 返回平台专属接口单例,需要访问 ESP32 特有能力时使用。
写/读/删与 NVS API 的对应关系
| Matter 方法 | 底层 NVS 调用 | 说明 |
|---|---|---|
_Put | nvs_set_blob()+nvs_commit() | 以 blob 形式写入原始字节,nvs_commit确保落盘(持久化) |
_Get | nvs_get_blob() | 读取 blob;value可为nullptr以探测 key 是否存在 |
_Delete | nvs_erase_key()+nvs_commit() | 擦除指定 key |
EraseAll | nvs_erase_all()+nvs_commit() | 清空CHIP_KVS命名空间下全部数据 |
所有 NVS 句柄通过 RAII 类Internal::ScopedNvsHandle管理(见 src/platform/ESP32/ScopedNvsHandle.h),以只读(NVS_READONLY)或读写(NVS_READWRITE)模式打开,作用域退出自动释放。错误码通过ReturnMappedErrorOnFailure从esp_err_t映射为CHIP_ERROR。
长 key 的 SHA1 哈希处理
实现中最具工程细节的部分是HashIfLongKey()(KeyValueStoreManagerImpl.cpp)。ESP-IDF NVS 对 key 名称长度有硬性上限(NVS_KEY_NAME_MAX_SIZE,即 15 字符),而 Matter 的 KVS 抽象并不限制 key 长度。为解决该矛盾:
- 当 key 长度 ≥ 15 时,对 key 做SHA1 哈希,取前 7.5 字节转换为十六进制字符串作为实际 NVS key;
- 哈希结果只包含十六进制字符(
0-9a-f),而正常的 Matter KVS key 前缀通常包含/,因此哈希生成的 key 不会与普通 key 冲突; - 函数通过返回值区分是否发生了哈希:
true表示已哈希,调用方随后用哈希结果替换原始 key。
代码注释中明确记录了设计权衡:虽然 SHA1 取 8 字节存在理论上的冲突概率,但在实际使用场景中可能性很低。
偏移读取:明确的未实现边界
_Get的第一行就给出了 README 所述限制的代码级证据(KeyValueStoreManagerImpl.cpp):
// Offset and partial reads are not supported in nvs, for now just return NOT_IMPLEMENTED. Support can be added in the // future if this is needed. VerifyOrReturnError(offset_bytes == 0, CHIP_ERROR_NOT_IMPLEMENTED);任何offset_bytes != 0的读取都会直接返回CHIP_ERROR_NOT_IMPLEMENTED。头文件同样在注释中说明:"Currently this platform does not support partial and offset reads, these will returnCHIP_ERROR_NOT_IMPLEMENTED"。这就是示例入口必须传SKIP_MULTI_READ_TEST的根本原因。
工程构建配置解析
顶层 CMakeLists(examples/persistent-storage/esp32/CMakeLists.txt)
cmake_minimum_required(VERSION 3.20) set(PROJECT_VER "v1.0") set(PROJECT_VER_NUMBER 1) include($ENV{IDF_PATH}/tools/cmake/project.cmake) include(${CMAKE_CURRENT_LIST_DIR}/third_party/connectedhomeip/examples/common/cmake/idf_flashing.cmake) set(EXTRA_COMPONENT_DIRS "${CMAKE_CURRENT_LIST_DIR}/third_party/connectedhomeip/config/esp32/components" ) project(chip-persistent-storage) idf_build_set_property(CXX_COMPILE_OPTIONS "-std=gnu++17;-Os;-DCHIP_HAVE_CONFIG_H" APPEND) idf_build_set_property(C_COMPILE_OPTIONS "-Os" APPEND) # For the C3, project_include.cmake sets -Wno-format, but does not clear various # flags that depend on -Wformat idf_build_set_property(COMPILE_OPTIONS "-Wno-format-nonliteral;-Wno-format-security" APPEND) # -Wmaybe-uninitialized has too many false positives, including on std::optional # and chip::Optional. Make it nonfatal. idf_build_set_property(COMPILE_OPTIONS "-Wno-error=maybe-uninitialized" APPEND) flashing_script()要点:
- 工程名
chip-persistent-storage,要求CMake ≥ 3.20; - C++ 编译采用
-std=gnu++17与-Os(尺寸优化,适合 flash 受限的嵌入式环境),并定义-DCHIP_HAVE_CONFIG_H; - 通过
EXTRA_COMPONENT_DIRS引入 connectedhomeip 的 ESP32 公共组件(config/esp32/components),这是所有 ESP32 示例复用 SDK 代码的通用手法; -Wno-error=maybe-uninitialized用于规避 GCC 对std::optional/chip::Optional的误报(源码注释引用了 gcc.gnu.org bug 80635);flashing_script()会生成chip-persistent-storage.flash.py烧录脚本,供后续一键烧录。
main 组件(examples/persistent-storage/esp32/main/CMakeLists.txt)
main 组件把跨平台共享的persistent-storage目录(含KeyValueStorageTest.cpp)一并纳入编译:
idf_component_register(PRIV_INCLUDE_DIRS "${CMAKE_CURRENT_LIST_DIR}" "${CMAKE_SOURCE_DIR}/third_party/connectedhomeip/examples/persistent-storage" SRC_DIRS "${CMAKE_CURRENT_LIST_DIR}" "${CMAKE_SOURCE_DIR}/third_party/connectedhomeip/examples/persistent-storage") target_compile_options(${COMPONENT_LIB} PRIVATE "-DCHIP_HAVE_CONFIG_H")注意此处引用的third_party/connectedhomeip是该示例目录内部的 vendored SDK 副本(examples/persistent-storage/esp32/third_party/connectedhomeip),它保持了 SDK 的标准目录布局。
sdkconfig 默认配置(examples/persistent-storage/esp32/sdkconfig.defaults)
# Use a custom partition table CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" # Vendor and product id CONFIG_DEVICE_VENDOR_ID=0xFFF1 CONFIG_DEVICE_PRODUCT_ID=0x8009 # Enable HKDF in mbedtls CONFIG_MBEDTLS_HKDF_C=y- 使用自定义分区表(见下方
partitions.csv); - 预设厂商 ID
0xFFF1与产品 ID0x8009(开发用途的测试 VID/PID); - 启用 mbedtls 的 HKDF 支持(Matter 加密密钥派生所需)。
分区表(examples/persistent-storage/esp32/partitions.csv)
# Name, Type, SubType, Offset, Size, Flags # Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap nvs, data, nvs, , 0xC000, phy_init, data, phy, , 0x1000, # Factory partition size about 1.9MB factory, app, factory, , 1920K,三个分区各司其职:nvs分区(48 KB)存放键值数据(Matter KVS 的CHIP_KVS命名空间就落在这里);phy_init存放 WiFi/蓝牙射频校准数据;factory为应用固件分区(约 1.9 MB)。由于 KVS 测试会持续写入 NVS,该分区大小直接影响可容纳的键值条目数与擦写寿命。
构建、烧录与运行
根据 docs/platforms/esp32/build_app_and_commission.md 的标准流程,本示例的构建运行步骤如下:
1. 进入示例目录并设定目标芯片
$ cd examples/persistent-storage/esp32 $ idf.py set-target esp32 # 或 esp32c3 / esp32s3 等目标该文档指出所有 Matter demo 应用支持 ESP32、ESP32C3、ESP32S3 芯片变体;ESP32H2/ESP32C6 目前仅针对 lighting-app、lit-icd-app、all-clusters-app 验证过,本示例请以仓库实际支持为准。
2. 构建
默认使用sdkconfig.defaults,直接执行:
$ idf.py build如需自定义配置,可先运行idf.py menuconfig调整,或通过idf.py -D 'SDKCONFIG_DEFAULTS=...' build指定其他 defaults 文件。
3. 烧录与监控
$ idf.py -p (PORT) erase_flash $ idf.py -p (PORT) flash monitor将(PORT)替换为实际串口设备名(Linux 下通常是/dev/ttyUSB0)。首次烧录前建议erase_flash清空整片 flash,避免残留数据干扰测试。监控模式下按Ctrl+]退出。
4. 查看测试输出
启动后串口会周期性打印:
============================================= chip-esp32-persitent-storage-example starting ============================================= Running Tests: KeyValueStoreMgr().Put(...): PASSED ...每 60 秒循环一轮,全部用例(除被跳过的TestMultiRead)输出PASSED即说明 ESP32 平台的 KVS 基础读写/更新/删除能力正常。
5. 使用生成脚本烧录(可选)
构建过程中flashing_script()已生成chip-persistent-storage.flash.py,可一键烧录:
$ export ESPPORT=/dev/tty.SLAB_USBtoUART $ idf.py build $ idf.py flashing_script $ python chip-persistent-storage.flash.py当前限制与后续演进
综合 README 与源码,当前版本的要点总结如下:
- offset / partial 读取未实现:
KeyValueStoreManagerImpl::_Get对非零偏移直接返回CHIP_ERROR_NOT_IMPLEMENTED,示例因此以SKIP_MULTI_READ_TEST跳过TestMultiRead; - key 长度限制的适配:NVS key 最长 15 字符,Matter 层通过 SHA1 哈希 + 十六进制截断来支持长 key;
- 持久化语义:写入与删除均显式调用
nvs_commit,保证数据真正落盘; - 演进方向:README 明确该示例未来在平台条件成熟后可转为单元测试,成为 CI 中校验各平台 KVS 实现一致性的回归用例。
对于需要在 ESP32 上使用持久化存储的 Matter 开发者,可以直接借鉴本示例的调用模式:nvs_flash_init()初始化 →KeyValueStoreMgr().Put/Get/Delete读写 → 通过返回码判断结果,同时在设计业务时避开对偏移读取的依赖,或关注 SDK 后续版本对该能力的支持。
延伸阅读
- KeyValueStoreManager 公共 API 头文件
- ESP32 KVS 平台实现
- 跨平台共享测试用例
- ESP32 平台配置选项总览
- ESP32 构建与配对完整指南
【免费下载链接】connectedhomeipMatter (formerly Project CHIP) creates more connections between more objects, simplifying development for manufacturers and increasing compatibility for consumers, guided by the Connectivity Standards Alliance.项目地址: https://gitcode.com/GitHub_Trending/co/connectedhomeip
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考