ARTICLE DETAIL

资讯详情

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

ESP32 WiFi配置新方案:NVS存储与浏览器工具实现免拆机修改

ESP32 WiFi配置新方案:NVS存储与浏览器工具实现免拆机修改 1. 项目缘起一个让人抓狂的日常痛点做过 ESP32 产品落地的朋友大概率都遇到过这个场景设备已经装到现场了客户突然说WiFi 密码换了你帮我改一下。这时候你怎么办如果代码里 WiFi 账号密码是写死在固件里的那就只能重新编译、重新烧录运气好现场有 USB 口还能接电脑运气不好设备封在壳子里、装在吊顶上那就只能拆机。我见过最离谱的一次同事为了改一个 WiFi 密码开车来回两百公里去客户现场就为了插一根 USB 线。这个问题的根源在于很多人把 WiFi 配置当成了代码而不是数据。代码是编译进固件的改代码就得重刷固件但 WiFi 账号密码本质上是配置数据它应该存在一个可以独立于固件被读写的地方。ESP32 恰好提供了这么一个地方——NVSNon-Volatile Storage非易失性存储。NVS 是 ESP-IDF 里的一套键值存储系统跑在 SPI Flash 的一个独立分区上掉电不丢数据。你可以把它理解成 ESP32 内部的一个小数据库用 key-value 的形式存东西比如wifi_ssid MyHome、wifi_pass 12345678。固件启动的时候从 NVS 里读这些值去连 WiFi而不是用写死的常量。这样一来改 WiFi 密码就变成了改 NVS 里的一个键值跟固件本身没关系了。那问题又来了怎么在不拆机、不接 USB 的情况下改 NVS答案就是标题里说的——浏览器工具。ESP32 本身可以跑一个轻量级的 HTTP 服务器手机或电脑连上它的热点或者它连上局域网后通过 IP 访问打开浏览器就能看到一个配置页面填好新的 WiFi 账号密码点保存工具通过 HTTP 接口把值写进 NVS重启后设备就用新密码连网了。整个过程不需要任何上位机软件不需要数据线一部手机就搞定。这篇文章我会把整套方案的来龙去脉讲清楚为什么选 NVS 而不是其他存储方式、浏览器工具怎么和 ESP32 通信、NVS 读写有哪些坑、完整的实操步骤和代码骨架、以及我在实际项目中踩过的那些坑。适合正在做 ESP32 联网产品、被现场配置问题折磨过的开发者也适合刚接触 ESP-IDF、想搞明白 NVS 到底怎么用的朋友。哪怕你之前只会用 Arduino 写WiFi.begin(ssid, password)看完也能把这套方案落地。2. 方案整体设计为什么是 NVS 浏览器工具2.1 存储选型NVS、SPIFFS、EEPROM 到底选哪个ESP32 上能存配置数据的地方不止一个常见的有三种NVS、SPIFFS/LittleFS 文件系统、以及模拟 EEPROM。很多人第一反应是用 EEPROM毕竟 Arduino 时代就是这么干的。但在 ESP32 上EEPROM.h其实是对 Flash 的一层模拟底层还是走 NVS 或者直接操作 Flash 扇区性能和可靠性都不如直接用 NVS。我把三者的对比整理成一张表方便你按场景选存储方式数据结构适合场景读写粒度磨损均衡推荐度NVS键值对配置参数、WiFi 凭据、校准值单键读写内置高SPIFFS/LittleFS文件网页资源、日志、大块数据文件级需自行处理中模拟 EEPROM字节数组简单小数据、兼容老代码字节级无低选 NVS 的核心理由有三个。第一它是为配置数据设计的天生就是 key-value 模型读写单个键非常方便不用像文件系统那样打开、定位、写入、关闭一整套流程。第二NVS 内置了磨损均衡和掉电保护Flash 的擦写寿命是有限的一般 10 万次左右NVS 会自动把写操作分散到不同扇区避免某个扇区被写坏。第三NVS 支持命名空间namespace你可以把 WiFi 配置放一个命名空间把设备参数放另一个互不干扰读取的时候也不会串。注意NVS 单个 value 有大小限制字符串类型nvs_set_str最大约 4000 字节 blob 类型nvs_set_blob单个分区内也有限制。存 WiFi 账号密码这种几十字节的数据完全够用但别拿它存图片或者大段 JSON。2.2 通信方式为什么用浏览器而不是 App 或串口改 NVS 的方式有好几种我逐一分析一下为什么最终选了浏览器工具这条路。串口方式最直接接 USB 用串口终端发命令。但问题就是标题里吐槽的——现场设备不一定有 USB 口就算有你也得带电脑、装驱动、开终端软件。对现场施工人员来说门槛太高。专用 App 方式写个手机 App 通过蓝牙或局域网连设备改配置。功能是强但你要维护 Android 和 iOS 两个版本还要处理权限、兼容性、上架审核成本太高。而且客户手机装个来路不明的 App心理上也会抵触。浏览器方式设备自己跑一个 HTTP 服务器手机连上后打开浏览器输入 IP 就能访问配置页。零安装、跨平台、任何有浏览器的设备都能用。这是它最大的优势。你甚至可以用手机热点让 ESP32 连上然后手机浏览器直接访问连局域网都不用配。浏览器方案的技术栈也很清晰ESP-IDF 自带esp_http_server组件起一个 HTTP 服务只需要几十行代码前端页面可以用最朴素的 HTML JavaScript不需要任何框架因为功能就是填表单、点保存这么简单。整个页面的 HTML 可以直接以字符串形式编译进固件或者放到 SPIFFS 里按需读取。2.3 整体架构从浏览器点击到 NVS 落盘把整个链路串起来看数据流是这样的设备上电先尝试从 NVS 读取 WiFi 凭据读到就连接读不到就进入配网模式开热点或等待配置。设备启动 HTTP 服务器监听某个端口比如 80。用户手机连上设备热点或同一局域网浏览器访问设备 IP。设备返回一个 HTML 配置页面页面上有 SSID 和密码输入框。用户填写后点击保存前端 JS 把数据以 POST 请求发给设备。设备的 HTTP handler 收到请求解析出 SSID 和密码调用 NVS API 写入。写入成功后返回响应前端提示保存成功设备即将重启。设备重启重新从 NVS 读取新凭据连接新 WiFi。这个架构的关键设计点是配置写入和 WiFi 连接是解耦的。HTTP 服务器和 NVS 操作不依赖 WiFi 是否连上所以即使当前 WiFi 密码错了、连不上网你依然能通过设备自己的热点访问配置页改密码。这一点非常重要否则一旦密码错了就变成死锁只能重刷固件。3. 核心细节解析NVS 读写与 HTTP 服务的实操要点3.1 NVS 初始化与命名空间管理用 NVS 之前必须先初始化。ESP-IDF 里 NVS 的初始化分两步先初始化分区再打开命名空间。#include nvs_flash.h #include nvs.h // 第一步初始化 NVS 分区 esp_err_t ret nvs_flash_init(); if (ret ESP_ERR_NVS_NO_FREE_PAGES || ret ESP_ERR_NVS_NEW_VERSION_FOUND) { // 分区满了或者版本不匹配擦除后重新初始化 ESP_ERROR_CHECK(nvs_flash_erase()); ret nvs_flash_init(); } ESP_ERROR_CHECK(ret); // 第二步打开命名空间不存在会自动创建 nvs_handle_t my_handle; ret nvs_open(wifi_cfg, NVS_READWRITE, my_handle); if (ret ! ESP_OK) { printf(打开 NVS 命名空间失败: %s\n, esp_err_to_name(ret)); }这里有几个细节值得展开说。nvs_flash_init()返回ESP_ERR_NVS_NO_FREE_PAGES是新手最常遇到的错误意思是 NVS 分区里没有空闲页了。这通常发生在你反复擦写、或者分区表里 NVS 分区设得太小的时候。处理办法就是擦除整个 NVS 分区重新初始化但要注意擦除会丢失所有已存数据所以生产环境里要谨慎最好在擦除前先尝试读取并备份关键配置。命名空间的名字最长 15 个字符这是 NVS 的硬性限制超了会返回错误。我一般用wifi_cfg、dev_param这种简短又表意的名字。命名空间一旦打开后续所有读写都通过这个 handle 进行用完记得nvs_close(my_handle)释放资源。实操心得如果你的项目里多个任务都要读写 NVS建议用一个全局的 handle 加互斥锁保护或者干脆把 NVS 操作封装成一个单独的模块所有读写都走这个模块的接口。我见过因为两个任务同时写 NVS 导致数据错乱的案例排查了大半天。3.2 字符串读写WiFi 凭据的存取WiFi 的 SSID 和密码都是字符串用nvs_set_str和nvs_get_str最合适。// 写入 SSID nvs_set_str(my_handle, ssid, MyHomeWiFi); // 写入密码 nvs_set_str(my_handle, pass, 12345678); // 提交更改必须调用否则不落盘 nvs_commit(my_handle);读取的时候有个坑nvs_get_str需要你先提供一个缓冲区并且传入缓冲区大小的指针。如果缓冲区太小函数会返回ESP_ERR_NVS_INVALID_LENGTH同时把实际需要的长度写回你传入的长度变量。所以正确的做法是先查长度再分配缓冲区size_t len 0; // 第一次调用len 传 0函数会返回所需长度 esp_err_t err nvs_get_str(my_handle, ssid, NULL, len); if (err ESP_OK len 0) { char *ssid malloc(len); nvs_get_str(my_handle, ssid, ssid, len); // 使用 ssid... free(ssid); }很多人图省事直接开一个 64 字节的数组大部分情况没问题但 SSID 理论上最长 32 字节、密码最长 63 字节加上结束符64 字节其实刚好卡在边界上。我建议缓冲区至少给 128 字节留足余量。nvs_commit()这一步绝对不能省。NVS 的写入是先在内存里改调用 commit 才真正写到 Flash。如果你写完不 commit掉电后数据就没了。我早期就犯过这个错调试的时候发现明明写进去了重启就没了查了半天才发现漏了 commit。3.3 HTTP 服务器搭建从注册路由到处理请求ESP-IDF 的esp_http_server用起来很顺手核心就是注册 URI handler。#include esp_http_server.h static esp_err_t config_get_handler(httpd_req_t *req) { const char *html htmlbody h2WiFi 配置/h2 form action/save methodpost SSID: input namessidbr 密码: input namepass typepasswordbr input typesubmit value保存 /form/body/html; httpd_resp_send(req, html, HTTPD_RESP_USE_STRLEN); return ESP_OK; } static esp_err_t config_post_handler(httpd_req_t *req) { char buf[256]; int ret httpd_req_recv(req, buf, sizeof(buf) - 1); if (ret 0) return ESP_FAIL; buf[ret] \0; // 解析 buf 里的表单数据写入 NVS... httpd_resp_send(req, 保存成功设备即将重启, HTTPD_RESP_USE_STRLEN); return ESP_OK; } httpd_uri_t uri_get { .uri /, .method HTTP_GET, .handler config_get_handler }; httpd_uri_t uri_post { .uri /save, .method HTTP_POST, .handler config_post_handler }; httpd_handle_t server NULL; httpd_config_t config HTTPD_DEFAULT_CONFIG(); httpd_start(server, config); httpd_register_uri_handler(server, uri_get); httpd_register_uri_handler(server, uri_post);这里的关键点是表单数据的解析。浏览器提交表单时Content-Type 是application/x-www-form-urlencoded数据格式是ssidMyHomepass12345678。你需要自己解析这个字符串按分割成键值对再按分割键和值还要处理 URL 编码比如空格会变成%20中文会变成%XX形式。ESP-IDF 没有内置的表单解析函数得自己写或者用httpd_query_key_value这个辅助函数。char ssid[128], pass[128]; if (httpd_query_key_value(buf, ssid, ssid, sizeof(ssid)) ESP_OK httpd_query_key_value(buf, pass, pass, sizeof(pass)) ESP_OK) { // 写入 NVS nvs_set_str(my_handle, ssid, ssid); nvs_set_str(my_handle, pass, pass); nvs_commit(my_handle); }httpd_query_key_value会自动处理 URL 解码省了不少事。但要注意它只能解析keyvaluekeyvalue这种格式如果你的前端用 JSON 提交就得换cJSON之类的库来解析。注意HTTP 服务器的接收缓冲区大小要设够。默认配置下httpd_req_recv一次能收的数据有限如果表单数据超过缓冲区需要循环接收。WiFi 配置这种小表单一般不会超但如果你以后要传更大的配置记得处理分片接收。3.4 前端页面设计极简但要好用前端页面不需要花哨但有几个细节能大幅提升体验。第一密码框用typepassword避免明文显示。第二加一个显示密码的复选框方便用户核对输入。第三保存后给明确的反馈比如弹一个提示或者跳转到一个保存成功页面然后自动重启。form action/save methodpost labelWiFi 名称/label input namessid maxlength32 required labelWiFi 密码/label input namepass typepassword maxlength63 labelinput typecheckbox onclicktogglePwd(this) 显示密码/label button typesubmit保存并重启/button /form script function togglePwd(cb) { var pwd document.querySelector(input[namepass]); pwd.type cb.checked ? text : password; } /script页面可以直接以字符串形式嵌在固件里优点是简单、不依赖文件系统缺点是改页面要重新编译。如果页面比较复杂建议放到 SPIFFS 里通过httpd_resp_send读取文件发送。我一般用嵌入字符串的方式因为配置页就那么点内容没必要引入文件系统。4. 完整实操流程从零搭一个可用的配置系统4.1 分区表配置给 NVS 留足空间NVS 是跑在 Flash 分区上的所以第一步是在分区表里定义 NVS 分区。ESP-IDF 默认的分区表里已经有一个nvs分区大小通常是 24KB0x6000。对于只存 WiFi 配置的场景24KB 绰绰有余因为 NVS 一个键值对也就几十字节。但如果你还要存其他参数或者担心磨损均衡可以适当加大到 64KB 甚至 128KB。自定义分区表的话在项目根目录建一个partitions.csv# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x6000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 0x100000,然后在menuconfig里把Partition Table设为Custom partition table CSV并指定文件名。改完分区表一定要idf.py erase-flash全擦一次否则旧分区表和新分区表冲突会出现各种奇怪的错误。4.2 代码骨架一个最小可用的实现把前面的片段整合起来一个最小可用的 WiFi 配置系统大概长这样void app_main(void) { // 1. 初始化 NVS esp_err_t ret nvs_flash_init(); if (ret ESP_ERR_NVS_NO_FREE_PAGES || ret ESP_ERR_NVS_NEW_VERSION_FOUND) { nvs_flash_erase(); nvs_flash_init(); } // 2. 尝试读取 WiFi 凭据 nvs_handle_t handle; nvs_open(wifi_cfg, NVS_READWRITE, handle); char ssid[128] {0}, pass[128] {0}; size_t len sizeof(ssid); bool has_cfg (nvs_get_str(handle, ssid, ssid, len) ESP_OK); len sizeof(pass); nvs_get_str(handle, pass, pass, len); // 3. 启动 WiFi wifi_init_sta(ssid, pass); // 4. 启动 HTTP 服务器无论 WiFi 是否连上都启动 start_http_server(handle); }这里有个设计决策HTTP 服务器在 WiFi 连接之前就启动。这样即使 WiFi 连不上用户依然能通过设备热点访问配置页。具体做法是设备同时开一个 AP 模式热点SSID 类似ESP32_Config用户连上这个热点后访问192.168.4.1就能看到配置页。这个STA AP 共存的模式是现场配置的标准做法。4.3 参数计算NVS 分区大小怎么定很多人不知道 NVS 分区该给多大这里给一个估算方法。NVS 的存储单位是页一页 4096 字节。每个键值对占用的空间包括条目头约 32 字节 key 字符串 value 数据。假设你存 10 个配置项每项平均 100 字节总共 1000 字节一页都用不满。但 NVS 需要预留磨损均衡的空间一般建议实际数据量的 4 到 8 倍。所以 10 个配置项给 8KB 到 16KB 就够了默认的 24KB 完全够用。如果你要存大量数据比如设备日志、历史记录那就不适合用 NVS 了应该用文件系统。NVS 的定位就是少量、频繁读、偶尔写的配置数据。4.4 现场操作流程一部手机搞定代码烧进去之后现场操作流程是这样的设备上电如果没有存过 WiFi 凭据自动进入 AP 模式热点名类似ESP32_Config_XXXX。施工人员用手机连上这个热点默认无密码或固定密码。手机浏览器打开192.168.4.1看到配置页面。填写客户现场的 WiFi 名称和密码点保存。设备收到配置写入 NVS返回保存成功1 秒后自动重启。重启后设备读取新凭据连接客户 WiFi连接成功后关闭 AP 模式。如果连接失败设备重新开启 AP 模式等待重新配置。整个过程不需要电脑、不需要数据线、不需要装任何 App。我实测下来从连热点到配置完成熟练的话 30 秒搞定。实操心得AP 模式的热点名建议带上设备 MAC 后四位比如ESP32_Config_A1B2这样现场有多个设备时不会连错。另外配置页面最好显示当前设备的一些信息比如 MAC、固件版本方便施工人员确认连对了设备。5. 常见问题与排查技巧实录5.1 NVS 相关的高频问题问题一nvs_flash_init返回ESP_ERR_NVS_NO_FREE_PAGES这是最常见的 NVS 错误原因是分区里没有空闲页了。触发场景通常是反复擦写导致页耗尽或者分区表里 NVS 分区太小。解决办法是擦除后重新初始化但要注意数据会丢。预防措施是合理设置分区大小并且避免高频写 NVS比如不要每秒写一次。问题二写入成功但重启后数据丢失九成是漏了nvs_commit()。NVS 的写入是两阶段的nvs_set_xxx只改内存nvs_commit才落盘。另一个可能是写入后立刻断电Flash 写入需要时间虽然 NVS 有掉电保护但极端情况下仍可能丢数据。建议写入后延时 100ms 再重启。问题三读取字符串返回ESP_ERR_NVS_INVALID_LENGTH缓冲区太小。解决办法是先传 NULL 查长度再分配足够大的缓冲区。或者干脆给一个足够大的固定缓冲区128 字节起步。问题四命名空间打开失败命名空间名字超过 15 字符或者 NVS 分区没初始化。检查名字长度确认nvs_flash_init已调用。5.2 HTTP 服务的典型故障问题一浏览器访问超时先确认设备和手机在同一个网络。如果是 AP 模式确认手机连的是设备热点而不是其他 WiFi。再确认 HTTP 服务器确实启动了可以在串口日志里看有没有 httpd started 之类的输出。还要检查防火墙有些手机浏览器会拦截局域网访问。问题二表单提交后没反应大概率是 POST handler 没注册或者 URI 不匹配。检查httpd_register_uri_handler注册的 URI 和表单action是否一致。另外如果 handler 里处理时间太长比如写 NVS 卡住浏览器会超时建议先返回响应再执行耗时操作。问题三中文 SSID 乱码URL 编码问题。浏览器提交中文时会做百分号编码httpd_query_key_value会自动解码但如果你的解析是自己写的就要手动处理%XX。建议直接用httpd_query_key_value省心。5.3 问题速查表现象可能原因排查方向NVS 初始化失败分区满/分区表错误擦除重试、检查分区表数据重启后丢失漏 commit/写入后立即断电补 commit、加延时读取长度错误缓冲区太小先查长度再分配浏览器打不开页面网络不通/服务器没启动检查网络、看串口日志表单提交无响应URI 不匹配/handler 未注册核对 URI 和注册代码中文乱码URL 编码未处理用 httpd_query_key_value5.4 独家避坑技巧第一个技巧给配置页加一个恢复出厂设置按钮。现场经常遇到密码改错了、连不上的情况如果有个按钮能一键清空 NVS 并重启能省很多事。实现就是调用nvs_erase_all(handle)然后重启。第二个技巧配置写入后不要立即重启先返回响应再延时重启。如果先重启浏览器可能收不到响应用户以为没保存成功会重复提交。正确做法是返回保存成功页面页面里用 JavaScript 延时 2 秒后跳转或提示重启。第三个技巧在 NVS 里存一个配置版本号。固件升级后如果配置结构变了可以通过版本号判断是否需要迁移或重置配置。这个习惯在长期维护的产品里特别有用。第四个技巧AP 模式的热点加个简单密码。虽然配置页本身没什么敏感信息但开放热点容易被路人连上乱改配置。设个固定密码比如设备序列号后六位就能挡住大部分误操作。6. 方案扩展与个人体会这套 NVS 浏览器工具的方案核心价值在于把配置和固件解耦。一旦你习惯了这种模式会发现它能扩展出很多玩法。比如把设备的所有可调参数采样频率、上报间隔、阈值都放进 NVS配置页做成多个 tab现场调试就不用反复烧固件了。再比如加一个 OTA 升级入口配置页上直接上传固件文件连升级都不用接线。我在实际项目里还做过一个变种设备连上 WiFi 后把配置页也暴露在局域网里这样在办公室就能通过设备 IP 访问配置页不用连热点。实现上就是 HTTP 服务器同时监听 STA 和 AP 两个接口ESP-IDF 默认就支持不需要额外配置。踩过的坑里最深刻的一次是 NVS 分区设太小设备跑了三个月后突然起不来串口日志显示 NVS 初始化失败。后来查出来是某个参数被高频写入把页耗尽了。从那以后我养成了两个习惯一是 NVS 分区至少给 64KB二是任何高频写入的数据都不放 NVS改用内存缓存加定期落盘。这个教训分享出来希望你别再踩一遍。最后再分享一个小技巧调试 NVS 的时候可以用nvs_tool.py这个脚本ESP-IDF 自带直接读取分区内容不用写代码就能看到里面存了什么。命令是python $IDF_PATH/components/nvs_flash/nvs_partition_tool/nvs_tool.py --partition 分区文件配合esptool.py read_flash把 NVS 分区读出来排查问题非常方便。
返回列表