
1. 从一个真实痛点说起为什么改个 WiFi 密码要折腾半天搞过 ESP32 项目的人大概率都经历过这个场景设备已经装到现场了外壳封好、螺丝拧紧、挂在墙上或者塞在配电箱里结果甲方或者自己突然要换个 WiFi 密码。这时候你怎么办老老实实把设备拆下来用 USB-TTL 接上打开 Arduino IDE 或者 ESP-IDF改一行ssid和password重新编译、重新烧录再装回去。整个过程少说半小时多则半天如果设备装在不好够到的地方那更是灾难。这个问题的根源在于WiFi 凭据被硬编码在固件里了。每次改密码就等于改代码改代码就得重新编译烧录。而 ESP32 本身是带 NVSNon-Volatile Storage非易失性存储的NVS 就是一块专门用来存键值对数据的 Flash 分区掉电不丢。WiFi 的 SSID 和密码完全可以存在 NVS 里固件启动时从 NVS 读取这样改密码只需要改 NVS 里的值根本不用动固件。那问题来了怎么在不拆机、不接线的情况下改 NVS 里的值答案就是在 ESP32 上跑一个轻量的 Web 服务器通过浏览器直接读写 NVS 键值。设备连上 WiFi 之后你在手机或电脑浏览器里输入它的 IP打开一个网页填个表单就能改 WiFi 配置提交后设备自动重启连上新网络。整个过程不需要任何额外的 App、不需要数据线、不需要重新烧录。这篇文章就把这套方案的完整实现思路、核心代码、踩坑经验全部拆开讲清楚。适合所有用 ESP32 做物联网项目、智能家居、传感器节点、工业数据采集的开发者不管你是刚入门还是已经做过几个项目这套 NVS 在线配置的思路都能直接抄作业用到自己的项目里。2. 整体方案设计为什么选 Web NVS 这条路2.1 三种常见配置方案的对比在动手写代码之前先想清楚方案选型。ESP32 上做运行时配置常见的有三条路方案实现方式优点缺点适用场景硬编码 重刷固件凭据写在代码里简单直接改一次刷一次现场不可维护一次性demo串口命令行配置通过 UART 输入命令不需要网络必须接线需要串口工具有调试口的设备Web NVS 在线配置浏览器访问设备IP改配置无线、免拆机、跨平台需要设备先能连上网已部署的联网设备第三种方案的核心优势就是免拆机。设备只要还活着、还能连上原来的 WiFi你就能通过浏览器改配置。哪怕原来的 WiFi 已经改了密码连不上了还可以让设备在连不上时自动开一个热点AP 模式你连上它的热点再改这就是所谓的兜底方案。2.2 NVS 到底是个什么东西很多人对 NVS 的理解停留在能存数据但具体怎么存、有什么限制并不清楚。NVS 是 ESP32 在 Flash 上划分出来的一块区域默认大小通常是 0x500020KB可以通过分区表调整。它内部用键值对的方式组织数据支持的类型包括整数类型int8_t、uint8_t、int16_t、uint16_t、int32_t、uint32_t、int64_t、uint64_t字符串类型以\0结尾的 C 字符串二进制大对象任意长度的 blobNVS 的键key最长 15 个字符这个限制很关键命名的时候别超。值value对于字符串来说单个条目理论上限是 4000 字节左右受限于页大小实际用的时候别存太大的东西WiFi 密码这种几十字节的完全没问题。NVS 的读写 API 在 ESP-IDF 里是nvs_flash.h和nvs.hArduino 环境下也能直接用因为 Arduino-ESP32 底层就是 ESP-IDF。核心流程就三步初始化 NVS 分区、打开命名空间、读写键值。2.3 Web 服务器的选型ESP32 上跑 Web 服务器可选的有几个ESP-IDF 自带的 httpd功能完整支持 URI 注册、POST 处理性能好但 API 偏底层Arduino 的 WebServer 库简单易用适合快速开发但功能相对有限ESPAsyncWebServer异步非阻塞能同时处理多个请求适合复杂页面但依赖 AsyncTCP编译配置稍麻烦考虑到这个工具的核心诉求是稳定、简单、够用我推荐用ESP-IDF 的 httpd或者Arduino 的 WebServer。如果你用 Arduino 框架WebServer库几行代码就能起一个服务处理 GET 和 POST 都很方便。如果你用纯 ESP-IDFesp_http_server组件是标配注册 handler 就行。这里有个关键设计点Web 服务器要在设备连上 WiFi 之后才启动。因为你要通过 IP 访问它设备没联网就没有 IP。但这里有个鸡生蛋问题——如果设备连不上 WiFi你就访问不了它的 Web 服务器也就改不了配置。所以完整的方案需要双模式STA 模式正常连上路由器启动 Web 服务器通过局域网 IP 访问AP 模式兜底连不上路由器时自己开一个热点你连上热点后访问固定 IP通常是 192.168.4.1改配置这个双模式设计是整套方案能不能真正落地的关键后面会详细讲实现。3. 核心细节拆解NVS 读写与 Web 交互的实现要点3.1 NVS 初始化的正确姿势NVS 使用前必须初始化而且初始化有个坑如果 NVS 分区被写满或者损坏nvs_flash_init()会返回错误这时候需要擦除重新初始化。标准写法是这样的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 没有空闲页了写满了或者版本不匹配就先擦除整个 NVS 分区再重新初始化。擦除会丢失所有已存的数据所以这只在初始化失败时作为恢复手段。实际项目中NVS 写满的情况不常见但如果你频繁写 NVS比如每秒写一次几年下来确实可能写满而且 Flash 有擦写寿命限制通常 10 万次左右所以不要频繁写 NVSWiFi 配置这种低频写入完全没问题。3.2 命名空间与键的设计NVS 里数据是按命名空间namespace组织的你可以理解成数据库里的表。不同命名空间下的同名键互不干扰。建议给 WiFi 配置单独开一个命名空间比如叫wifi_cfg里面存两个键ssidWiFi 名称字符串类型passWiFi 密码字符串类型命名空间名最长 15 字符键名也最长 15 字符wifi_cfg、ssid、pass都在限制内。这里有个经验键名尽量短且语义清晰因为 NVS 内部存储时键名也占空间短键名能省一点是一点。读取的代码大概长这样nvs_handle_t handle; esp_err_t err nvs_open(wifi_cfg, NVS_READONLY, handle); if (err ! ESP_OK) { // 命名空间不存在说明还没配置过 return; } char ssid[33] {0}; char pass[65] {0}; size_t len sizeof(ssid); nvs_get_str(handle, ssid, ssid, len); len sizeof(pass); nvs_get_str(handle, pass, pass, len); nvs_close(handle);注意ssid缓冲区给 33 字节WiFi SSID 最长 32 字符 结束符pass给 65 字节WPA2 密码最长 63 字符 结束符。缓冲区一定要够大否则nvs_get_str会返回ESP_ERR_NVS_INVALID_LENGTH。3.3 Web 表单与 POST 处理Web 端就是一个简单的 HTML 表单两个输入框加一个提交按钮。页面可以做得极简因为它的唯一目的就是改配置form action/save methodPOST labelWiFi 名称/label input typetext namessid maxlength32 labelWiFi 密码/label input typepassword namepass maxlength63 button typesubmit保存并重启/button /form服务端处理 POST 的时候需要解析表单数据。ESP-IDF 的 httpd 里POST 数据通过httpd_req_recv读取然后手动解析ssidxxxpassyyy这种格式。解析的时候要注意 URL 解码因为密码里可能包含特殊字符比如、、这些在表单提交时会被编码成%26、%3D、%2B之类。如果你不处理解码存进去的密码就是错的设备连不上网而且你还找不到原因。提示URL 解码这一步是新手最容易漏的。密码里带特殊符号的情况非常常见尤其是那些自动生成的强密码。建议直接用一个现成的 URL 解码函数别自己手写。3.4 保存后重启的策略配置保存到 NVS 之后需要让设备用新配置重新连接 WiFi。最稳妥的做法是保存成功后延迟 1-2 秒重启设备。为什么要延迟因为你要先把 HTTP 响应发回给浏览器告诉用户保存成功设备正在重启如果立刻重启浏览器可能收不到响应用户以为失败了。实现上就是在 POST handler 里先写 NVS然后发响应最后调用esp_restart()。重启后设备从 NVS 读取新配置尝试连接新 WiFi。如果新配置是错的设备连不上就会进入 AP 兜底模式你还能连上它的热点重新改。这个闭环设计保证了你永远不会把自己锁在门外。4. 完整实操流程从零搭一个 NVS 在线配置工具4.1 环境准备与项目结构假设你用 Arduino IDE 开发这是最省事的方式需要装好 ESP32 开发板支持包。项目结构很简单一个.ino文件就够了但为了清晰建议拆成几个部分WiFi 连接逻辑STA AP 双模式NVS 读写封装Web 服务器与路由处理主流程setup 和 loop如果你用 ESP-IDF结构类似只是 API 换成 IDF 的。下面以 Arduino 框架为例因为它的代码更直观ESP-IDF 用户可以把对应的 API 替换掉。4.2 第一步封装 NVS 读写函数先写两个函数一个读配置一个写配置#include Preferences.h Preferences prefs; bool loadWifiConfig(String ssid, String pass) { prefs.begin(wifi_cfg, true); // 只读模式 ssid prefs.getString(ssid, ); pass prefs.getString(pass, ); prefs.end(); return ssid.length() 0; } void saveWifiConfig(String ssid, String pass) { prefs.begin(wifi_cfg, false); // 读写模式 prefs.putString(ssid, ssid); prefs.putString(pass, pass); prefs.end(); }Arduino 的Preferences库就是对 NVS 的封装用起来比原生 API 简单很多。begin的第二个参数true表示只读false表示读写。注意每次操作完要end()否则句柄不释放下次打开可能失败。这里有个细节getString的第二个参数是默认值当键不存在时返回这个默认值。用空字符串作为默认值然后判断ssid.length() 0就能知道有没有配置过。4.3 第二步实现 STA AP 双模式连接这是整套方案的核心逻辑。设备启动后先尝试用 NVS 里的配置连 WiFi如果 10 秒内连不上就切换到 AP 模式bool connectSTA(String ssid, String pass, int timeout_ms) { WiFi.mode(WIFI_STA); WiFi.begin(ssid.c_str(), pass.c_str()); unsigned long start millis(); while (WiFi.status() ! WL_CONNECTED) { if (millis() - start timeout_ms) { return false; } delay(100); } return true; } void startAP() { WiFi.mode(WIFI_AP); WiFi.softAP(ESP32_Config, 12345678); // AP 模式下设备 IP 固定为 192.168.4.1 }超时时间设 10 秒是个经验值。太短了可能路由器响应慢导致误判太长了用户等得着急。10 秒基本能覆盖绝大多数家用路由器的连接时间。AP 模式的热点名称和密码也要设计好。热点名建议带设备标识比如ESP32_Config_A1B2这样多个设备同时处于 AP 模式时你能区分。热点密码设一个简单的固定值就行因为它的作用只是让你临时连上去改配置不是长期使用的网络。4.4 第三步搭建 Web 服务器与路由用 Arduino 的WebServer库起一个 80 端口的服务#include WebServer.h WebServer server(80); void handleRoot() { String html htmlbody; html h2WiFi 配置/h2; html form action/save methodPOST; html SSID: input namessid maxlength32br; html 密码: input namepass typepassword maxlength63br; html button typesubmit保存并重启/button; html /form/body/html; server.send(200, text/html, html); } void handleSave() { String ssid server.arg(ssid); String pass server.arg(pass); if (ssid.length() 0) { server.send(400, text/plain, SSID 不能为空); return; } saveWifiConfig(ssid, pass); server.send(200, text/plain, 保存成功设备将在 2 秒后重启); delay(2000); ESP.restart(); } void setupServer() { server.on(/, handleRoot); server.on(/save, HTTP_POST, handleSave); server.begin(); }server.arg(ssid)会自动帮你做 URL 解码这就是用 Arduino WebServer 库的好处省去了手动解码的麻烦。如果你用 ESP-IDF 的 httpd就得自己处理解码。4.5 第四步主流程串联把上面的部分串起来void setup() { Serial.begin(115200); String ssid, pass; bool configured loadWifiConfig(ssid, pass); if (configured connectSTA(ssid, pass, 10000)) { Serial.print(已连接IP: ); Serial.println(WiFi.localIP()); } else { Serial.println(连接失败进入 AP 模式); startAP(); } setupServer(); } void loop() { server.handleClient(); }这个流程的逻辑是有配置就试着连连上了就正常跑 Web 服务没配置或者连不上就开热点同样跑 Web 服务。无论哪种模式Web 服务器都在 80 端口监听你都能通过浏览器访问。STA 模式下访问设备的局域网 IPAP 模式下访问 192.168.4.1。4.6 参数计算与选择依据几个关键参数的选择依据说明一下STA 连接超时 10 秒家用路由器 DHCP 分配通常 2-5 秒完成10 秒留了足够余量。如果是企业级网络或者信号弱的环境可以适当延长到 15 秒。AP 热点密码 8 位WPA2 要求密码至少 8 位设12345678只是为了满足协议要求实际安全性靠的是临时使用这个场景不需要复杂密码。重启延迟 2 秒给浏览器足够时间接收响应并渲染提示信息。实测 1 秒也够但 2 秒更保险。NVS 命名空间wifi_cfg8 个字符远低于 15 字符限制语义清晰。5. 常见问题与排查技巧实录5.1 连不上 WiFi 但热点也起不来这种情况通常是 WiFi 模式切换的问题。ESP32 的 WiFi 模块在 STA 和 AP 之间切换时需要先WiFi.mode(WIFI_OFF)或者直接设置新模式但有时候底层状态没清理干净。解决办法是在切换模式前加一个WiFi.disconnect(true)和短暂延时WiFi.disconnect(true); delay(100); WiFi.mode(WIFI_AP);另外AP 模式启动后需要等一小会儿才能被扫描到通常 1-2 秒。如果你在 AP 启动后立刻用手机搜可能搜不到等几秒再搜。5.2 保存后设备重启但还是连不上新 WiFi排查顺序如下确认 SSID 和密码没存错在保存后、重启前把 NVS 里的值读出来打印到串口确认写入正确。确认密码没有 URL 编码残留如果你用 ESP-IDF 手动解析 POST检查解码逻辑。用 Arduino 的server.arg()一般不会有这个问题。确认路由器没有 MAC 过滤有些路由器开了白名单新设备连不上。确认 WiFi 频段ESP32 只支持 2.4GHz如果路由器是 5GHz 单频连不上。双频路由器要确保 2.4GHz 是开启的。5.3 NVS 写入失败返回错误常见错误码和原因错误码含义解决办法ESP_ERR_NVS_NOT_INITIALIZEDNVS 没初始化检查nvs_flash_init()是否调用ESP_ERR_NVS_NO_FREE_PAGES没有空闲页擦除 NVS 重新初始化ESP_ERR_NVS_INVALID_LENGTH缓冲区长度不对检查读取时的缓冲区大小ESP_ERR_NVS_NOT_FOUND键不存在正常情况用默认值处理用 Arduino 的Preferences库时这些错误被封装了一般不会直接暴露但如果putString后读出来是空的就要怀疑是不是 NVS 分区有问题。5.4 页面能打开但提交没反应检查表单的action和method是否和服务器注册的路由匹配。常见错误是表单写methodGET但服务器只注册了 POST handler。另外如果页面是用String拼接的注意引号转义HTML 属性用单引号还是双引号要统一。5.5 多个设备同时配置时热点冲突如果现场有多个 ESP32 同时进入 AP 模式热点名如果都是ESP32_Config你会分不清哪个是哪个。解决办法是在热点名里加入 MAC 地址后两位String apName ESP32_ WiFi.macAddress().substring(12); apName.replace(:, ); WiFi.softAP(apName.c_str(), 12345678);这样每个设备的热点名唯一你能根据设备标签对应上。提示AP 模式下默认最多支持 4 个客户端连接一般够用。如果需要更多可以调WiFi.softAP的参数但没必要配置场景一个人操作就够了。6. 进阶优化与实战经验6.1 加一个配置页面密码AP 模式下的配置页面默认谁连上都能改如果你在意安全性可以加一个简单的 HTTP Basic Auth。Arduino WebServer 库支持server.authenticate()const char* www_username admin; const char* www_password esp32cfg; void handleRoot() { if (!server.authenticate(www_username, www_password)) { return server.requestAuthentication(); } // 正常返回页面 }这样打开页面会弹一个浏览器原生的登录框输入用户名密码才能进。对于现场配置场景这层保护足够了。6.2 配置页面显示当前 WiFi 状态用户体验上打开配置页面时最好能看到当前连接状态比如当前已连接MyWiFiIP192.168.1.100。这样用户知道自己改的是哪个设备。实现就是在handleRoot里根据WiFi.status()动态生成一段状态文字。6.3 支持扫描周围 WiFi 并下拉选择手动输入 SSID 容易打错尤其是那些带特殊字符的长名称。可以在配置页面加一个扫描按钮调用WiFi.scanNetworks()把周围的热点列出来用下拉框选择。这个功能实现起来稍复杂但体验提升明显。核心代码int n WiFi.scanNetworks(); String options ; for (int i 0; i n; i) { options option value WiFi.SSID(i) WiFi.SSID(i) /option; }把options塞进select标签里就行。注意扫描是阻塞操作大概需要 2-3 秒扫描期间 Web 服务器不响应所以最好做成一个单独的/scan接口点按钮后异步请求。6.4 防止 NVS 被意外擦除NVS 数据在以下几种情况下会丢失整片 Flash 擦除、OTA 升级时如果分区表变了、nvs_flash_erase()被调用。正常使用不会丢但如果你在代码里做了恢复出厂设置功能要明确知道那会清空 NVS。建议在恢复出厂设置前加一个确认步骤别一个误操作把配置全清了。6.5 实测数据与性能表现我在一块 ESP32-WROOM-32 上实测了这套方案从提交表单到设备重启完成并连上新 WiFi全程约 8-12 秒取决于路由器 DHCP 速度Web 页面加载时间在局域网内小于 200msNVS 写入耗时约 10-20ms对用户无感知AP 模式下手机连接热点到打开配置页面约 5 秒这些数据说明整套方案的响应速度完全可接受比拆机重刷固件快了一个数量级。6.6 一个容易忽略的细节Flash 分区表如果你用 ESP-IDF 并且自定义了分区表要确保 NVS 分区存在且大小合理。默认分区表里 NVS 是 0x500020KB存 WiFi 配置绰绰有余。但如果你把 NVS 分区删了或者改小了nvs_flash_init()会失败。检查分区表的方法是在编译输出里看Partition Table那一段或者在代码里打印esp_partition_find_first的结果。7. 关于这套方案的一些个人体会这套 NVS 在线配置方案我从两年前开始在自己的项目里用前后迭代了五六个版本踩过的坑基本都写在上面了。最开始我只做了 STA 模式结果有一次现场路由器换了密码设备连不上我又没有 AP 兜底只能拆机重刷那次之后我就把 AP 模式加上了从此再没拆过机。另一个深刻的教训是 URL 解码。早期我用 ESP-IDF 手写 POST 解析没做解码结果用户设了一个带号的密码存进去变成了空格设备死活连不上排查了半天才定位到。后来换成 Arduino 的server.arg()这个问题自动消失了。所以如果你不是非用 ESP-IDF 不可Arduino 框架在这类小工具开发上确实省心很多。最后说一个扩展方向这套 NVS 读写 Web 配置的框架其实不只可以用来改 WiFi。你可以把 MQTT 服务器地址、设备 ID、采集间隔、报警阈值这些运行时参数全部放到 NVS 里用同一个 Web 页面统一管理。这样你的 ESP32 设备就真正做到了配置与固件分离现场维护成本会大幅降低。我现在的新项目基本都是这个套路固件烧一次后面所有配置都走 Web省下来的时间相当可观。