☰
ESP-ADF 配置持久化实战:基于 esp_config_manager 实现多配置组、双槽冗余与 CRC 校验
2026/10/14 2:25:41 网站建设 项目流程
  • 音视频
  • 音频处理
  • 嵌入式
  • 物联网
  • 语音

【免费下载链接】esp-adf

Espressif Advanced Development Framework for Multimedia Applications

项目地址:https://gitcode.com/gh_mirrors/es/esp-adf
点击查看免费下载

本文围绕 ESP-ADF(Espressif Advanced Development Framework)中的esp_config_manager组件及其官方示例config_manager_example展开,讲解如何在嵌入式设备上以「主/备双槽 + CRC 校验 + 默认值合并」的方式可靠地保存多组应用配置。读完本文,你将掌握该组件的完整使用流程:如何创建配置组、绑定 NVS/SPIFFS/raw flash 三种存储后端、切换自测模式验证掉电恢复与结构体升级场景,以及如何解读运行日志与定位故障。

例程概览:它解决什么问题

esp_config_manager是一个面向嵌入式场景的配置管理组件,其核心定位是让配置数据在掉电、写入被中断、结构体/schema 扩展等情况下依然「可用、可恢复、可演进」。组件 README 明确列出了它的关键能力(见 组件 README):

  • 主/备双槽存储(Primary + Backup);
  • 记录头校验(magic / schema_version / payload_len / crc32);
  • 自动加载回退(primary → backup → defaults);
  • 支持自定义merge_fn的默认值合并(用于迁移/演进);
  • 同步的主槽修复(把 backup 复制回 primary);
  • 可插拔存储后端(NVS / filesystem / raw flash / 自定义);
  • 可选加解密钩子(encrypt/decrypt)。

官方示例config_manager_example(例程 README)演示了如何用该组件在持久化介质中维护audio、lifecycle、net 三个相互独立的配置组:首次启动加载默认值并写入存储,随后修改字段、保存、再次读取,并打印每个配置的来源(defaults / primary / backup)。

典型场景

该例程适用于需要可靠本地配置保存的产品,例如:

  • 音频参数(音量、亮度、profile 名等);
  • 启动计数(如 boot counter)、会话标志位;
  • 网络相关标志位(接口名、MTU、路由标识);
  • 需要兼容结构体扩展的配置升级场景(新增字段时旧配置仍可加载并补齐默认值)。

例程运行机制(工作流程)

例程的完整流程如下(源码见 config_manager_example.c 中的app_main()):

  1. 初始化 NVS;当选择 filesystem 存储后端时,同时初始化并挂载 SPIFFS;
  2. 创建audio、lifecycle、net三个独立配置组;
  3. 将每个配置组绑定到选定的存储介质(通过esp_config_storage_init_nvs()/init_fs()/init_flash()打开后端句柄);
  4. 调用esp_config_manager_load()读取配置;
  5. 修改每个配置组中的一个字段,调用esp_config_manager_save()保存,再次读取以验证持久化结果;
  6. 释放 manager handle 与 storage handle,打印Done。

在app_main()的开头还包含标准的 NVS 初始化容错逻辑:当nvs_flash_init()返回ESP_ERR_NVS_NO_FREE_PAGES或ESP_ERR_NVS_NEW_VERSION_FOUND时,先nvs_flash_erase()再重新初始化;当选择 FS 后端时,则通过esp_vfs_spiffs_register()挂载storage分区到/spiffs(max_files=8、format_if_mount_failed=true),退出前再esp_vfs_spiffs_unregister()。

深入底层:记录格式与双槽存储布局

理解例程行为的前提是掌握组件的底层存储格式。每个配置组维护两个逻辑槽(见 esp_config_storage.h):

  • ESP_CONFIG_SLOT_PRIMARY:主副本;
  • ESP_CONFIG_SLOT_BACKUP:冗余副本。

每个槽保存一个二进制 blob(加密之前),格式为「16 字节私有记录头 + 变长 payload」:

+--------------------------------------------------------------+ | private record header (16 bytes) | | - magic (uint32_t) | | - schema_version (uint32_t) | | - payload_len (uint32_t) | | - crc32 (uint32_t) | +--------------------------------------------------------------+ | payload (variable length) | +--------------------------------------------------------------+

在源码 esp_config_manager.c 中,magic 定义为0x30666765u,crc32的计算覆盖schema_version + payload_len + payload三个部分(crc_record(),基于esp_rom_crc32_le)。加载时若magic或 CRC 校验失败,该槽即被视为无效记录。

加载回退与主槽修复

esp_config_manager_load()在源码中的实际执行流程(esp_config_manager.c):

  1. 读取 primary 槽;成功则直接返回,info.source = ESP_CONFIG_LOAD_SOURCE_PRIMARY;
  2. 若 primary 缺失(ESP_ERR_NOT_FOUND)或 CRC/magic 校验失败(ESP_ERR_INVALID_CRC),尝试读取 backup 槽;
  3. 若 backup 有效:把 backup 内容同步写回 primary 完成修复,info.source = ESP_CONFIG_LOAD_SOURCE_BACKUP,且info.primary_repair_scheduled = true;
  4. 若两槽均无效:回退到默认值,先经merge_fn(默认是前缀覆盖)构造运行时配置,再写回 primary 与 backup 两个槽,info.source = ESP_CONFIG_LOAD_SOURCE_DEFAULTS。

esp_config_manager_save()则通过write_both_slots()把同一份记录同时写入 primary 与 backup,任一槽写失败即返回ESP_FAIL。加载来源可通过esp_config_load_info_t::source区分:

  • ESP_CONFIG_LOAD_SOURCE_PRIMARY
  • ESP_CONFIG_LOAD_SOURCE_BACKUP
  • ESP_CONFIG_LOAD_SOURCE_DEFAULTS

例程中的print_audio()/print_lifecycle()/print_net()正是根据该枚举打印日志中的from primary / from backup / from defaults字样。

默认值合并:面向结构体升级的演进机制

当 primary 与 backup 都无效时,管理器会用default_config重建配置。esp_config_manager_cfg_t中的merge_fn提供自定义合并入口(原型见 esp_config_manager.h)。

若merge_fn为NULL,组件使用内置的前缀覆盖策略(default_merge(),esp_config_manager.c):

  • 输出缓冲区先以default_config整体初始化;
  • 再用已加载 payload 覆盖前min(loaded_len, default_size)个字节。

这通常能保留结构体「尾部新增字段」的默认值——当旧 payload 比新结构体短时,新字段自动落回默认值。但需要注意:当新旧sizeof相同、仅字段语义变化时,旧 blob 的填充字节(padding)可能覆盖新字段。因此对于持久化结构体,建议使用packed布局或编写自定义merge_fn做显式迁移。

例程的 audio 配置组正是这一机制的演示对象(merge_tail_new_fields自测项):

typedef struct __attribute__((packed)) { uint32_t volume_percent; /*!< 音量 0–100 */ uint32_t feature_flags; /*!< 功能位掩码 */ char profile_name[16]; /*!< 标签 */ uint32_t brightness; /*!< UI 亮度 */ uint8_t volume_level; /*!< 离散电平 */ uint8_t array[5]; /*!< 演示字节数组 */ } example_audio_config_t;

自测中会构造一份「只写到volume_level之前(28 字节)」的旧 payload 存入存储,再加载,验证volume_level与array能从默认值正确补齐。三个配置组的 schema 版本分别为:audio2、lifecycle11、net12(见例程中的s_group_desc)。

可选加解密钩子

组件允许通过esp_config_crypto_ops_t注入加解密流程:

  • 保存时:先打包「头 + payload」,调用encrypt()后再写入;
  • 加载时:先读出 blob,调用decrypt(),再解析头与 CRC。

若不提供crypto,数据以明文存储;encrypt/decrypt完全由用户实现,因此可以接入任意算法或安全元件/HSM 流程。例程中的example_encrypt()/example_decrypt()是恒等透传实现(直接 memcpy),用于演示钩子接线方式:

static esp_err_t example_encrypt(void *ctx, const uint8_t *in, size_t in_len, uint8_t *out, size_t out_size, size_t *out_len) { if (out_size < in_len) { return ESP_ERR_NO_MEM; } memcpy(out, in, in_len); *out_len = in_len; return ESP_OK; }

配置项crypto_extra_size表示密文相对明文记录(16 字节头 + 至多default_size的 payload)可能多出的字节数,内部缓冲区按「明文大小 + 该值」分配;不使用encrypt时必须将其设为0(esp_config_manager_init()中会强制校验这一点,见 esp_config_manager.c)。

存储后端绑定:NVS / SPIFFS / raw flash / 自定义

底层持久化由esp_config_storage_ops_t定义,只有三个回调(esp_config_storage.h):

  • read(ctx, slot, buf, inout_len)
  • write(ctx, slot, buf, len)
  • erase(ctx, slot)(可选)

用esp_config_storage_init(ops, ctx, &handle)绑定后端(ctx不会被拷贝,必须存活到esp_config_storage_deinit(handle)之后),并把得到的 handle 传给esp_config_manager_cfg_t.storage。组件已内置三个适配器:

  • NVS:esp_config_storage_nvs_ops(esp_config_storage_nvs.c);
  • Filesystem(SPIFFS/FATFS/LittleFS 等):esp_config_storage_fs_ops(esp_config_storage_fs.c);
  • Raw flash 分区:esp_config_storage_flash_ops(esp_config_storage_flash.c)。

NVS 后端:namespace + 双 blob key

每个配置组占用一个独立 NVS namespace,槽位映射到两个 blob key。例程中三个配置组使用三个隔离 namespace:

static esp_config_storage_nvs_t s_nvs_groups[CFG_EX_NUM_GROUPS] = { {.nvs_namespace = "cfg_g1", .key_primary = "main", .key_backup = "bak"}, {.nvs_namespace = "cfg_g2", .key_primary = "main", .key_backup = "bak"}, {.nvs_namespace = "cfg_g3", .key_primary = "main", .key_backup = "bak"}, };

NVS 适配器读写最终落到nvs_set_blob()/nvs_get_blob()并显式nvs_commit()。

Filesystem 后端:/spiffs双文件

primary 与 backup 分别对应一个绝对路径文件。例程映射如下:

static esp_config_storage_fs_t s_fs_groups[CFG_EX_NUM_GROUPS] = { {.path_primary = "/spiffs/g1_pri.bin", .path_backup = "/spiffs/g1_bak.bin"}, {.path_primary = "/spiffs/g2_pri.bin", .path_backup = "/spiffs/g2_bak.bin"}, {.path_primary = "/spiffs/g3_pri.bin", .path_backup = "/spiffs/g3_bak.bin"}, };

值得注意的实现细节:FS 后端写入时先写一个同路径的.tmp文件,再rename()替换目标文件,以保证同一卷上的基本原子性(见 esp_config_storage_fs.c)。

Raw flash 后端:专用 data 分区

每个槽对应一个独立的data类型分区(通过 label 查找)。分区内布局为「小端uint32_t长度字段 + blob 数据」,写入时先整分区擦除再写(见 esp_config_storage_flash.c)。例程映射:

static esp_config_storage_flash_t s_flash_groups[CFG_EX_NUM_GROUPS] = { {.label_primary = "cfg_pri", .label_backup = "cfg_bak"}, {.label_primary = "cfg2_pri", .label_backup = "cfg2_bak"}, {.label_primary = "cfg3_pri", .label_backup = "cfg3_bak"}, };

自定义后端

你可以为任意介质(外部 EEPROM、远程 KV 等)自行实现ops,通过esp_config_storage_init()绑定后传入esp_config_manager_cfg_t.storage。单元测试 test_esp_config_manager.c 中就用一个内存 mock(mock_ram_ctx_t,可注入 primary/backup 读写失败)作为自定义后端来验证回退与修复逻辑。

环境配置与硬件要求

硬件要求

  • ESP-IDF 支持的 Espressif 开发板,例如 ESP32 或 ESP32-S3。例程的 pytest 目标芯片为esp32s3(见 pytest_esp_config_manager_simple.py)。

默认 IDF 分支

例程支持 ESP-IDFrelease/v5.5 及以后的分支;ADF 仓库默认使用 ESP-IDF release/v5.5。

构建准备

若尚未配置 ESP-IDF 环境,在 ESP-IDF 根目录执行:

./install.sh . ./export.sh

进入例程工程目录(注意实际目录名为config_manager_example):

cd $ADF_PATH/components/esp_config_manager/examples/config_manager_example

该目录下的顶层 CMakeLists.txt 通过include($ENV{IDF_PATH}/tools/cmake/project.cmake)引入工程构建;main/CMakeLists.txt声明依赖nvs_flash esp_config_manager spiffs,且idf_component.yml以override_path: ../../../指向组件源码,因此无需额外拉取依赖。

编译与烧录

  • 编译示例程序:
idf.py build
  • 烧录程序并运行 monitor 查看串口输出(替换PORT为实际端口名):
idf.py -p PORT flash monitor
  • 退出调试界面使用Ctrl-]。

项目配置:切换存储后端与自测模式

存储后端选择

默认存储方式为 NVS,定义在 config_manager_example.c:

#define EXAMPLE_STORAGE_NVS 1 #define EXAMPLE_STORAGE_FS 2 #define EXAMPLE_STORAGE_FLASH 3 /* 选择存储介质(编辑本行)。FS/FLASH 后端需要 partitions.csv + sdkconfig.defaults。 */ #define EXAMPLE_CFG_STORAGE_BACKEND EXAMPLE_STORAGE_NVS

如需测试其他后端,修改EXAMPLE_CFG_STORAGE_BACKEND为下列之一:

宏说明
EXAMPLE_STORAGE_NVS每个配置组使用独立 NVS namespace,各存 primary/backup 两个 blob key
EXAMPLE_STORAGE_FSprimary 与 backup 文件保存到/spiffs,使用 partitions.csv 中的storageSPIFFS 分区
EXAMPLE_STORAGE_FLASHprimary/backup 分别保存到partitions.csv中定义的 raw data 分区

例程的 sdkconfig.defaults 已启用自定义分区表(CONFIG_PARTITION_TABLE_CUSTOM=y、CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"),可满足 SPIFFS 与 raw flash 后端演示所需的分区布局。自定义分区表内容如下:

分区名类型子类型偏移大小用途
nvsdatanvs0x90000x6000NVS 主区域
phy_initdataphy0xf0000x1000PHY 校准数据
factoryappfactory0x100000x100000应用固件
cfg_pridata0x99自动0x2000audio 组主槽
cfg_bakdata0x99自动0x2000audio 组备槽
cfg2_pridata0x99自动0x1000lifecycle 组主槽
cfg2_bakdata0x99自动0x1000lifecycle 组备槽
cfg3_pridata0x99自动0x1000net 组主槽
cfg3_bakdata0x99自动0x1000net 组备槽
storagedataspiffs自动0xBC000SPIFFS 文件系统

自测模式

如需运行较完整的自测流程(而不只是短演示),在 config_manager_example.c 中设置:

#define CFG_EX_RUN_SELFTEST 1

自测模式会对每个配置组执行「擦除存储 → 验证默认值恢复 → 保存/加载往返 → 破坏主槽 → 验证备份回退与主槽修复」,并对 audio 组额外验证 packed 布局与尾字段合并。

运行效果与日志解读

烧录后例程自动运行。首次启动时,各配置组从默认值加载并写入存储;后续启动则优先从 primary 槽读取,并在上次保存值的基础上继续变化。

普通演示日志(默认 NVS 后端)

I (325) CFG_MANAGER_EXAMPLE: Storage backend: NVS (3 config groups) I (345) CFG_MANAGER_EXAMPLE: [audio] from defaults repair=0 I (345) CFG_MANAGER_EXAMPLE: vol=42 flags=0x00000001 profile=default bri=80 lvl=10 array=esp1 I (355) CFG_MANAGER_EXAMPLE: [audio] from primary repair=0 I (355) CFG_MANAGER_EXAMPLE: vol=43 flags=0x00000001 profile=default bri=80 lvl=0 array=esp1 I (365) CFG_MANAGER_EXAMPLE: --- group audio done --- I (375) CFG_MANAGER_EXAMPLE: [lifecycle] from defaults — boot_count=0 session_flags=0x0100 I (375) CFG_MANAGER_EXAMPLE: array=esp2 I (385) CFG_MANAGER_EXAMPLE: [lifecycle] from primary — boot_count=1 session_flags=0x0100 I (395) CFG_MANAGER_EXAMPLE: --- group lifecycle done --- I (405) CFG_MANAGER_EXAMPLE: [net] from defaults — route_tag=0 if=sta0 mtu=1500 I (415) CFG_MANAGER_EXAMPLE: [net] from primary — route_tag=305419896 if=sta0 mtu=1500 I (425) CFG_MANAGER_EXAMPLE: --- group net done --- I (425) CFG_MANAGER_EXAMPLE: Done

日志解读:

  • audio 组:首次从 defaults 读到vol=42,修改后保存,二次加载从 primary 读到vol=43、lvl=0(源码中volume_level = (volume_level + 1) % 11,即 10 → 0);
  • lifecycle 组:boot_count从默认 0 变为 1,演示「启动计数」类配置的跨重启累积;
  • net 组:route_tag从 0 变为0x12345678(305419896),演示网络相关标志位。

自测模式日志

开启 self-test 后,关键日志如下:

I (325) CFG_MANAGER_EXAMPLE: ======== selftest start (backend=NVS, 3 groups) ======== I (345) CFG_MANAGER_EXAMPLE: [TEST] g0.packed_layout PASS I (365) CFG_MANAGER_EXAMPLE: [TEST] g0.load_defaults_after_wipe PASS — ESP_OK I (385) CFG_MANAGER_EXAMPLE: [TEST] g0.save_load_roundtrip PASS — ESP_OK I (405) CFG_MANAGER_EXAMPLE: [TEST] g0.fallback_to_backup_crc PASS — ESP_OK I (425) CFG_MANAGER_EXAMPLE: [TEST] g0.primary_repaired_after_load PASS — ESP_OK I (445) CFG_MANAGER_EXAMPLE: [TEST] g0.merge_tail_new_fields PASS I (465) CFG_MANAGER_EXAMPLE: ======== selftest end ========

各测试项含义(对应源码run_selftests_audio()与run_selftests_short_group()):

测试项验证内容
g0.packed_layoutexample_audio_config_t的sizeof恰为 34 字节(packed 布局生效)
g0.wipe_storage三种后端下都能成功擦除存储
g0.load_defaults_after_wipe双槽被擦除后,加载返回默认值且source = DEFAULTS
g0.save_load_roundtrip修改字段(vol=55、lvl=7)保存后能原样读回,且来源为 primary
g0.corrupt_primary_blob能向主槽写入垃圾数据制造损坏
g0.fallback_to_backup_crc主槽 CRC 损坏时回退到 backup,且触发主槽修复
g0.primary_repaired_after_load再次加载时从修复后的 primary 成功读到数据
g0.merge_tail_new_fields旧 payload 缺少新字段时,尾部新字段从默认值补齐
g1/g2.defaults、g1/g2.u32_roundtrip其余配置组的默认值加载与 u32 往返验证

CI 侧的 pytest_esp_config_manager_simple.py 会逐条匹配上述 PASS 日志,任一 FAIL 都会导致集成测试失败,因此这些日志既是演示输出,也是自动化验收断言。

故障排除

SPIFFS 或 raw flash 后端加载失败

请确认构建时使用了sdkconfig.defaults(启用CONFIG_PARTITION_TABLE_CUSTOM),并且自定义partitions.csv已生效。这两种后端分别需要storage分区以及cfg_pri、cfg_bak、cfg2_pri、cfg2_bak、cfg3_pri、cfg3_bak等 raw config 分区,缺少任一分区都会导致加载失败(raw flash 适配器会以「partition not found」拒绝读写)。

重新烧录后配置值没有恢复默认

普通应用烧录不会清除 NVS 数据,因此二次烧录后各配置组仍从 primary 槽读回上次保存的值。如需重新观察「首次启动恢复默认值」的流程,先执行:

idf.py erase-flash

self-test 日志出现FAIL

  • 先执行idf.py erase-flash后重新运行,排除残留脏数据干扰;
  • 若使用 flash 后端,同时确认自定义分区表已生效,且partitions.csv中的每个 raw config 分区都存在;
  • 若使用 FS 后端,确认 SPIFFS 分区storage已正确挂载(日志中应无 SPIFFS 挂载错误)。

总结

esp_config_manager通过「双槽冗余写入 + 16 字节 CRC 记录头 + primary→backup→defaults 三级回退 + 同步主槽修复」的组合,为 ESP32 系列芯片上的本地配置提供了可靠的持久化保障;config_manager_example则以三个配置组、三种存储后端、可切换的自测模式,完整展示了该组件在产品中的典型接法。无论你的场景是音频参数、启动计数、网络标志位,还是需要兼容结构体扩展的配置升级,都可以直接以此例程为蓝本,替换其中的配置结构体与后端绑定即可落地使用。例程原文还列出了官方技术支持渠道(esp32.com 论坛与问题反馈入口),详细支持信息可参考例程 README 原文。

  • 音视频
  • 音频处理
  • 嵌入式
  • 物联网
  • 语音

【免费下载链接】esp-adf

Espressif Advanced Development Framework for Multimedia Applications

项目地址:https://gitcode.com/gh_mirrors/es/esp-adf
点击查看免费下载

相关推荐

上一篇:TD-MPC2安全性与鲁棒性分析:在真实世界中的可靠性
下一篇:hot-lib-reloader-rs实战:如何在Bevy游戏引擎中实现热重载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询