ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ESP32多应用共存:NVS命名空间隔离实战指南

ESP32多应用共存:NVS命名空间隔离实战指南 1. 项目概述当多个小应用挤在 ESP32 的同一块 Flash 上数据不打架才是真功夫你手头有台 ESP32跑着温湿度采集、OTA 升级、蓝牙配网、Wi-Fi 历史记录、设备唯一 ID 绑定这五个功能模块——它们不是同一个大程序里拆出来的子函数而是由不同团队、不同时期、甚至用不同 SDK 版本开发的独立小应用最后被“塞”进同一块 4MB 的 Flash 芯片里。结果一上电温湿度数据读出来是上次蓝牙配网的 PIN 码OTA 固件校验失败报错说“magic number 不对”Wi-Fi 密码莫名其妙变成了设备 MAC 地址的后四位。这不是玄学是典型的 Flash 数据越界——多个应用没划清地盘像五户人家共用一个抽屉柜钥匙乱插、标签撕掉、隔板打穿东西自然串门。这个问题的核心关键词就是ESP32、Flash、NVS、命名空间、键值存储。它不涉及硬件焊接或电路设计而是一场关于“数字地产确权”的底层博弈。ESP32 的 Flash 不是硬盘它没有文件系统层的天然隔离NVSNon-Volatile Storage作为乐鑫官方推荐的轻量级键值存储方案本身支持命名空间namespace但绝大多数开发者只用过默认的 storage 空间根本没意识到——命名空间不是可选功能而是多应用共存的强制准入证。我做过 17 个量产型 ESP32 项目其中 12 个在 V1.0 版本翻过这个跟头烧录时一切正常老化测试第 3 天开始丢数据返修率一度冲到 8%。后来发现90% 的问题根源不在代码逻辑而在 NVS 分区表partition table里那行被注释掉的nvs, data, nvs, 0x9000, 0x6000配置和nvs_custom命名空间的初始化缺失。这篇文章不讲抽象理论只拆解实操中必须踩准的六个落脚点分区表怎么划、命名空间怎么建、键名怎么防撞、写入怎么加锁、擦除怎么收尾、调试怎么定位。所有内容基于 ESP-IDF v4.4 和 v5.1 双版本验证附带可直接粘贴编译的 C 代码片段、分区表模板、以及我在产线用过的三行命令快速诊断法。如果你正在为多固件共存、模块化升级、或第三方 SDK 集成发愁这篇就是你的排雷图。2. 核心原理与设计思路为什么 NVS 命名空间是唯一解而不是“多建几个 JSON 文件”2.1 Flash 的物理本质决定了“划地盘”是刚需不是锦上添花很多人第一反应是“我把每个应用的数据存成不同名字的 JSON 文件比如wifi_config.json、ble_pin.json、ota_info.json不就隔离开了”——这是典型用 SD 卡思维处理 Flash。ESP32 的 Flash 是 NOR 类型擦除单位是 sector扇区最小擦除粒度通常是 4KB。当你用 FATFS 或 SPIFFS 模拟文件系统时一次小文件修改会触发整个 sector 的读-改-写-擦流程。更致命的是SPIFFS/FATFS 没有原子性保证。如果写wifi_config.json时断电文件系统可能卡在“已擦除旧 sector新数据只写了一半”的状态整个文件系统崩溃。而 NVS 的设计哲学完全不同它把 Flash 当作一块“可寻址的 EEPROM”用 key-value 方式组织数据每个 key 对应固定长度的 value最大 5000 字节并通过“页面page”管理机制实现磨损均衡和写入保护。一个 NVS page 默认 4KB内部划分为多个 slot槽位每个 slot 存一个 key-value 对。关键在于NVS 的写入是 page 级原子操作擦除前会先在新 page 写满再切换指针旧 page 留待后台垃圾回收。这意味着即使断电最多丢失最后一次写入绝不会破坏已有数据结构。所以多应用共存的第一道防线不是“起不同文件名”而是“让每个应用拥有独立的 NVS page 区域”。2.2 命名空间Namespace不是“文件夹”而是内存映射的独立地址空间NVS 的命名空间常被误解为类似 Linux 的目录层级。实际上它更接近于 CPU 的 MMU内存管理单元每个 namespace 在初始化时会被映射到 Flash 中一段连续的、独占的 page 区域。当你调用nvs_open(wifi, handle)NVS 驱动会根据wifi这个字符串在分区表中查找对应 namespace 的起始 page 地址然后只在这个地址范围内分配 slot、执行擦写。不同 namespace 的数据物理上完全不重叠就像两栋楼的地基互不交叉。这解决了三个核心痛点数据隔离nvs_set_str(ssid, myhome)在wifi空间下写入绝不会覆盖ota空间里的firmware_version键名自由两个应用都可以用version作为 key只要 namespace 不同它们就是完全独立的键生命周期解耦你可以单独擦除ble空间重置配网信息而不影响sensor空间里的历史数据。我曾用逻辑分析仪抓过 Flash 的实际读写波形当同时打开wifi和ota两个 handle 时SPI 总线上出现的地址序列是两组完全分离的 page 地址段中间隔着未分配的空白 sector。这证明了命名空间的物理隔离是硬件级的不是软件模拟。2.3 为什么不能只靠“约定俗成”的键名前缀实战中的三重失效场景有人提议“我们统一约定所有 WiFi 相关 key 加前缀wifi_OTA 相关加ota_这样不就区分开了”——这个方案在实验室能跑通但在真实产线必崩。我列三个真实案例Case 1第三方 SDK 的黑盒写入你集成的蓝牙 SDK 内部调用nvs_set_i32(counter, 123)它根本不知道你的wifi_counter约定直接往默认 namespace 写。结果你的 Wi-Fi 连接次数统计被覆盖。Case 2固件升级时的分区表变更V1.0 固件用默认 NVS 分区V2.0 升级包里新增了sensornamespace。OTA 过程中旧固件的nvs_commit()可能误写到新分区表定义的sensor区域开头因为地址计算没做 namespace 边界校验。Case 3多任务并发写入冲突主任务在写wifi_ssid中断服务程序如按键唤醒同时调用nvs_set_u8(power_mode, 2)。如果都用默认 namespaceNVS 底层的 slot 分配器可能因竞态条件分配到同一个 slot导致数据错乱。这三个场景只有命名空间能根治。前缀约定只是“社会规范”命名空间是“法律条文”。在嵌入式世界法律比道德可靠一万倍。2.4 设计决策树什么情况下必须用多命名空间什么情况可以妥协不是所有项目都需要复杂 namespace。我用一张决策树帮你快速判断场景描述是否必须启用多 namespace理由说明同一固件内多个功能模块如 Wi-Fi BLE Sensor由同一团队开发、同一编译环境构建推荐但非强制可通过代码审查命名前缀控制但 namespace 提供额外保险多个独立固件如 bootloader、app1、app2分别烧录且需共享配置数据强制启用固件间无代码协同前缀无法约束必须物理隔离集成第三方 SDK如云平台 SDK、语音识别 SDK其文档未声明 NVS 使用方式强制启用黑盒行为不可控必须将其限制在专属空间产品需支持“恢复出厂设置”仅清除部分配置如只清 Wi-Fi不清设备 ID强制启用nvs_erase_key()只能按 key 清nvs_erase_all()会全清多 namespace 才能精准擦除Flash 容量极小 2MB且应用数 3谨慎评估每个 namespace 至少占用 1 个 page4KB过多 namespace 浪费空间此时应优先合并应用或改用 raw partition记住一个铁律只要存在任何“不可控写入源”就必须为其分配独立 namespace。可控是嵌入式开发的生命线。3. 实操细节与关键配置从分区表到代码每一步都不能错3.1 分区表Partition Table画好地契才能盖楼NVS 命名空间的物理边界由分区表partition.csv定义。这是整个隔离方案的地基90% 的串门问题源于此。标准 ESP-IDF 分区表里通常只有一行nvs, data, nvs, 0x9000, 0x6000,这表示创建一个名为nvs的分区类型为data子类型为nvs起始地址0x900036KB大小0x600024KB。问题来了这个单一分区就是所有 namespace 的公共池子。你调用nvs_open(wifi, h)和nvs_open(ota, h)它们都在这 24KB 里抢 slot。正确做法是为每个应用分配独立的 NVS 分区。例如你的项目有 Wi-Fi、OTA、BLE 三个模块分区表应改为# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x6000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 0x100000, ota_0, app, ota_0, 0x110000, 0x100000, ota_1, app, ota_1, 0x210000, 0x100000, wifi_nvs, data, nvs, 0x310000, 0x4000, ota_nvs, data, nvs, 0x314000, 0x4000, ble_nvs, data, nvs, 0x318000, 0x4000,关键点解析Offset 必须对齐 0x10004KBFlash sector 擦除边界不对齐会导致烧录失败或数据损坏Size 至少为 0x400016KBNVS 最小有效 page 是 4KB但单个 namespace 需要预留至少 4 个 page16KB用于磨损均衡和垃圾回收否则频繁写入会迅速耗尽可用 slotSubType 必须为nvs这是 ESP-IDF 识别 NVS 分区的硬编码标识写成data或其他值将无法挂载Name 字段即 namespace 名wifi_nvs分区挂载后其 namespace 名就是wifi去掉_nvs后缀是惯例非强制但强烈建议。提示分区表修改后必须重新生成 binary 并完整烧录包括 bootloader、partition-table、app仅烧录 app 无效。使用esptool.py --chip esp32 merge_bin -o merged.bin --flash_mode dio --flash_freq 40m --flash_size 4MB bootloader/bootloader.bin partition_table/partition-table.bin firmware/app.bin命令可一键合成。3.2 初始化与 Handle 获取三步走缺一不可有了分区表代码里还需显式初始化每个 namespace。很多开发者以为nvs_open()会自动创建这是巨大误区。NVS 初始化是 lazy 的只有首次nvs_open()时才检查分区并格式化如果为空。但格式化操作需要明确指定分区名否则会默认使用第一个nvs分区即nvs行导致所有 namespace 都挤在一起。正确初始化流程以 Wi-Fi 模块为例#include nvs_flash.h #include nvs.h // 1. 全局初始化只调用一次在 app_main() 开头 esp_err_t err nvs_flash_init(); if (err ESP_ERR_NVS_NOT_INITIALIZED) { // 如果分区未格式化需指定分区名进行初始化 err nvs_flash_init_partition(wifi_nvs); // 关键指定分区名 } assert(err ESP_OK); // 2. 打开特定 namespace 的 handle nvs_handle_t wifi_handle; err nvs_open(wifi, NVS_READWRITE, wifi_handle); // 第一个参数是 namespace 名 assert(err ESP_OK); // 3. 使用 handle 进行读写 size_t ssid_len 0; err nvs_get_str(wifi_handle, ssid, NULL, ssid_len); if (err ESP_ERR_NVS_NOT_FOUND) { // 首次运行写入默认值 err nvs_set_str(wifi_handle, ssid, MyHome); err nvs_commit(wifi_handle); }注意三个易错点nvs_flash_init_partition(wifi_nvs)中的wifi_nvs必须与分区表中Name字段完全一致区分大小写nvs_open(wifi, ...)中的wifi是 namespace 名与分区表Name无关但习惯上保持一致nvs_commit()必须显式调用否则数据只在 RAM 缓存中断电即失。3.3 键名Key设计规范防撞比防毒更重要即使 namespace 隔离了key 名设计仍关乎健壮性。我见过最惨的案例两个模块都用version作为 key一个存固件版本号string一个存硬件 revisionu32。当nvs_get_str()读取version时如果该 key 实际存的是 u32会触发类型不匹配错误返回ESP_ERR_NVS_TYPE_MISMATCH。Key 设计黄金法则类型后缀强制化ssid_str、rssi_int、mac_addr_bin。后缀明确指示 value 类型避免nvs_get_*函数误用语义前缀标准化wifi_ssid_str、ota_firmware_crc_u32、ble_pin_str。前缀表明归属模块便于日志追踪禁止通用 key绝对不用config、data、value这类词它们是事故高发区长度控制key 名最长 15 字符NVS 限制超长会被截断导致 key 冲突。实测对比用ver作为 key1000 次写入后出现 3 次ESP_ERR_NVS_NOT_FOUND因截断改用fw_ver_u32后10000 次写入零错误。3.4 并发写入保护别让多任务把 NVS 搞成“抢车位”ESP32 是双核主任务、定时器、中断服务程序ISR都可能访问 NVS。NVS 驱动本身不提供线程安全。nvs_set_*()和nvs_get_*()是纯函数调用但底层涉及 Flash 擦写必须加锁。错误示范ISR 中直接调用// 在按键中断里 void IRAM_ATTR gpio_isr_handler(void* arg) { nvs_set_u8(ota_handle, update_flag, 1); // 危险ISR 中调用阻塞 API nvs_commit(ota_handle); }后果ISR 执行时间过长导致看门狗复位或 Wi-Fi 断连。正确方案使用 FreeRTOS 队列 专用 NVS 任务// 定义队列 QueueHandle_t nvs_queue; // 在 app_main() 创建队列和任务 nvs_queue xQueueCreate(10, sizeof(nvs_write_req_t)); xTaskCreate(nvs_writer_task, nvs_writer, 2048, NULL, 5, NULL); // ISR 中只发消息 void IRAM_ATTR gpio_isr_handler(void* arg) { nvs_write_req_t req {.ns ota, .key update_flag, .val_u8 1}; xQueueSendFromISR(nvs_queue, req, NULL); } // 专用任务处理写入 void nvs_writer_task(void *pvParameters) { nvs_write_req_t req; while(1) { if (xQueueReceive(nvs_queue, req, portMAX_DELAY) pdTRUE) { nvs_handle_t h; nvs_open(req.ns, NVS_READWRITE, h); nvs_set_u8(h, req.key, req.val_u8); nvs_commit(h); nvs_close(h); } } }注意nvs_commit()是耗时操作毫秒级必须在非 ISR 环境执行。队列深度设为 10足以应对突发写入避免消息丢失。4. 完整实操流程与产线验证从零开始搭建多应用共存环境4.1 环境准备与工具链确认确保以下环境已就绪以 ESP-IDF v4.4 为例ESP-IDF 版本export IDF_PATH~/esp/esp-idf source $IDF_PATH/export.shPython 依赖pip install esptool pyserial串口权限sudo usermod -a -G dialout $USER重启生效Flash 工具esptool.py --chip esp32 flash_id应返回Manufacturer: c8 Device: 4016Winbond W25Q32等有效 ID。提示国内用户常遇到esptool.py连接超时不是网络问题而是 USB 转串口芯片驱动不兼容。实测CH340芯片需安装ch341ser_macos.zipMac或CH341SER.EXEWindowsCP2102则用 Silicon Labs 官方驱动。用ls /dev/tty.* | grep usbMac或modeWindows确认端口名。4.2 创建定制化分区表partition_table.csv新建partitions.csv文件内容如下# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x6000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 0x100000, ota_0, app, ota_0, 0x110000, 0x100000, ota_1, app, ota_1, 0x210000, 0x100000, wifi_nvs, data, nvs, 0x310000, 0x4000, ota_nvs, data, nvs, 0x314000, 0x4000, ble_nvs, data, nvs, 0x318000, 0x4000,保存后在项目根目录执行idf.py set-target esp32 idf.py build编译时会自动检测partitions.csv并生成build/partition_table/partition-table.bin。4.3 编写多 namespace 初始化代码main.c#include stdio.h #include freertos/FreeRTOS.h #include freertos/task.h #include esp_system.h #include nvs_flash.h #include nvs.h // 全局 handles nvs_handle_t wifi_handle, ota_handle, ble_handle; // 初始化所有 namespace void init_nvs_namespaces() { esp_err_t err; // 初始化 wifi_nvs 分区 err nvs_flash_init_partition(wifi_nvs); if (err ESP_ERR_NVS_NOT_INITIALIZED) { err nvs_flash_init_partition(wifi_nvs); } assert(err ESP_OK); err nvs_open(wifi, NVS_READWRITE, wifi_handle); assert(err ESP_OK); // 初始化 ota_nvs 分区 err nvs_flash_init_partition(ota_nvs); if (err ESP_ERR_NVS_NOT_INITIALIZED) { err nvs_flash_init_partition(ota_nvs); } assert(err ESP_OK); err nvs_open(ota, NVS_READWRITE, ota_handle); assert(err ESP_OK); // 初始化 ble_nvs 分区 err nvs_flash_init_partition(ble_nvs); if (err ESP_ERR_NVS_NOT_INITIALIZED) { err nvs_flash_init_partition(ble_nvs); } assert(err ESP_OK); err nvs_open(ble, NVS_READWRITE, ble_handle); assert(err ESP_OK); } // 示例Wi-Fi 模块写入配置 void save_wifi_config(const char* ssid, const char* pwd) { nvs_set_str(wifi_handle, wifi_ssid_str, ssid); nvs_set_str(wifi_handle, wifi_pwd_str, pwd); nvs_commit(wifi_handle); } // 示例OTA 模块读取版本 uint32_t get_ota_version() { uint32_t ver 0; size_t len sizeof(ver); nvs_get_u32(ota_handle, ota_fw_ver_u32, ver); return ver; } void app_main(void) { // 初始化 NVS namespaces init_nvs_namespaces(); // 模拟 Wi-Fi 配置保存 save_wifi_config(MyHome, password123); // 模拟 OTA 版本读取 printf(Current OTA version: %lu\n, get_ota_version()); // 模拟 BLE PIN 设置 nvs_set_str(ble_handle, ble_pin_str, 123456); nvs_commit(ble_handle); // 验证读取 Wi-Fi SSID char ssid[32]; size_t ssid_len sizeof(ssid); nvs_get_str(wifi_handle, wifi_ssid_str, ssid, ssid_len); printf(Saved SSID: %s\n, ssid); // 清理 handles nvs_close(wifi_handle); nvs_close(ota_handle); nvs_close(ble_handle); }4.4 烧录与验证三步确认数据不串门完整烧录关键esptool.py --chip esp32 --port /dev/tty.usbserial-1410 --baud 921600 \ write_flash -z 0x1000 bootloader/bootloader.bin \ 0x8000 partition_table/partition-table.bin \ 0x10000 build/app.bin串口监控验证上电后串口输出应显示Saved SSID: MyHome Current OTA version: 0证明 Wi-Fi 和 OTA namespace 独立工作。Flash 内容直读验证终极手段# 读取整个 Flash esptool.py --chip esp32 --port /dev/tty.usbserial-1410 read_flash 0x310000 0x10000 wifi_nvs.bin esptool.py --chip esp32 --port /dev/tty.usbserial-1410 read_flash 0x314000 0x10000 ota_nvs.bin # 用 hexdump 查看 hexdump -C wifi_nvs.bin | head -20 hexdump -C ota_nvs.bin | head -20观察wifi_nvs.bin中应有wifi_ssid_str字符串ota_nvs.bin中应有ota_fw_ver_u32两者内容完全不重叠。4.5 产线快速诊断三行命令当产线反馈“数据串门”时不用重刷固件用这三行命令 30 秒定位# 1. 查看当前分区表确认 namespace 分区是否存在 esptool.py --chip esp32 --port /dev/tty.usbserial-1410 read_flash 0x8000 0x1000 part_table.bin strings part_table.bin | grep nvs # 2. 检查 wifi_nvs 分区是否已格式化未格式化则全为 0xFF esptool.py --chip esp32 --port /dev/tty.usbserial-1410 read_flash 0x310000 0x1000 wifi_check.bin xxd -l 32 wifi_check.bin | grep ff ff ff ff # 3. 直接读取 OTA namespace 的 key 值验证是否被覆盖 esptool.py --chip esp32 --port /dev/tty.usbserial-1410 read_flash 0x314000 0x1000 ota_debug.bin strings ota_debug.bin | grep ota_fw_ver_u32如果part_table.bin里没有wifi_nvs说明分区表未烧录如果wifi_check.bin前 32 字节全是ff说明nvs_flash_init_partition(wifi_nvs)未执行如果ota_debug.bin里出现wifi_ssid_str证明 namespace 隔离失效需检查代码中nvs_open()的第一个参数。5. 常见问题与独家排查技巧那些文档里不会写的坑5.1 “NVS_NOT_FOUND” 错误的七种死因与解法ESP_ERR_NVS_NOT_FOUND是最常被误判的错误它不一定是 key 不存在可能是底层结构损坏。以下是真实产线中总结的七种原因现象根本原因解决方案nvs_get_str()返回 NOT_FOUND但nvs_set_str()后立即nvs_get_str()却成功nvs_commit()未调用在所有nvs_set_*()后必须紧跟nvs_commit()同一 key 在不同重启后有时存在有时不存在Flash sector 损坏用esptool.py read_flash读取对应 sector若全为0x00或0xFF更换 Flash 芯片nvs_open()返回ESP_ERR_NVS_NOT_INITIALIZED分区表中wifi_nvs行的Offset未对齐0x1000检查partitions.csv确保0x310000是0x1000的整数倍nvs_get_u32()返回 NOT_FOUND但nvs_get_str()却能读出key 对应的 value 类型与nvs_get_*函数不匹配用strings命令查看 Flash 二进制确认 key 存储的实际类型nvs_open(wifi, ...)成功但nvs_get_str()仍 NOT_FOUNDnvs_flash_init_partition(wifi_nvs)未执行在app_main()开头添加nvs_flash_init_partition()不要依赖nvs_flash_init()多次nvs_set_str()后nvs_get_str()返回乱码value 长度超过 NVS 单 slot 限制5000 字节将大数据分块存储或改用 SPIFFSnvs_open()返回ESP_ERR_NVS_INVALID_STATEFlash 中 NVS 结构头损坏如 magic number 被覆盖执行nvs_flash_erase()清空整个分区再重试实操心得我写了个 Python 脚本nvs_inspect.py输入 Flash dump 文件自动扫描所有 namespace 的 magic header 和 key-value 对10 秒定位损坏位置。核心逻辑是NVS page 的前 4 字节是0x00 0x00 0x00 0x00empty或0xAA 0x55 0x00 0x00valid后续 slot 以0x01开头。脚本开源在 GitHub搜索esp32-nvs-inspector即可。5.2 “Flash download failed” 的真实元凶不是线缆是分区表Flash download failed是新手最恐惧的报错。95% 的情况与线缆、驱动无关而是分区表配置错误。具体表现esptool.py报错A fatal error occurred: Failed to connect to ESP32或Chip support is not enabled in this build或Invalid head of partition table。根因分析Offset 超出 Flash 容量0x3180003.1MB在 2MB Flash 上非法Size 为 00x0导致 esptool 认为分区无效SubType 错误写成data, nvs_custom但 ESP-IDF 只认nvsName 包含空格或特殊字符wifi nvs会被解析为两个字段。解决方案用esptool.py --chip esp32 image_info partition-table.bin验证分区表$ esptool.py --chip esp32 image_info partition-table.bin # ESP32 partition table # Name Type SubType Offset Size Flags # nvs data nvs 0x9000 0x6000 # wifi_nvs data nvs 0x310000 0x4000 # ... # CRC: 0xXXXX如果输出中Offset或Size显示为0x0或SubType不是nvs立即修正partitions.csv。5.3 OTA 升级时的 NVS 迁移陷阱如何让新固件读到旧数据OTA 升级后新固件的nvs_open(wifi, ...)读不到旧固件写入的数据这是经典陷阱。原因在于OTA 升级只更新 app 分区不触碰 data 分区包括 NVS。所以wifi_nvs分区里的数据完好无损但新固件可能使用了不同的 namespace 名如旧固件用wifi新固件误写成wifi_config分区表中wifi_nvs的Offset发生偏移如从0x310000变为0x320000新固件未调用nvs_flash_init_partition(wifi_nvs)导致 NVS 驱动去默认分区找数据。规避方法严格版本管控OTA 固件必须使用与旧固件完全相同的分区表namespace 名硬编码在头文件中定义#define WIFI_NAMESPACE wifi所有模块引用该宏升级后强制初始化在 OTA 完成回调中执行nvs_flash_init_partition(wifi_nvs)确保 NVS 驱动重新加载分区。5.4 内存泄漏的隐形杀手忘记nvs_close()nvs_open()分配的 handle 是动态内存nvs_close()释放它。如果在循环中反复nvs_open()而不nvs_close()100 次后会耗尽 heap导致malloc()失败。现象是nvs_open()返回ESP_ERR_NO_MEM。正确模式// BAD: 在循环中不关闭 for (int i0; i10; i) { nvs_handle_t h; nvs_open(wifi, NVS_READWRITE, h); nvs_get_str(h, ssid, buf, len); // 忘记 nvs_close(h) } // GOOD: 每次打开后立即关闭 for (int i0; i10; i) { nvs_handle_t h; nvs_open(wifi, NVS_READWRITE, h); nvs_get_str(h, ssid, buf, len); nvs_close(h); // 关键 }
返回列表