☰
ESP32免拆机在线修改WiFi配置:Web+NVS方案详解
2026/10/7 11:38:16 网站建设 项目流程

1. 从一个真实痛点说起:为什么改个 WiFi 密码要折腾半天

搞过 ESP32 项目的人大概率都经历过这个场景:设备已经装到现场了,外壳封好、螺丝拧紧、挂在墙上或者塞在配电箱里,结果甲方或者自己突然要换个 WiFi 密码。这时候你怎么办?老老实实把设备拆下来,用 USB-TTL 接上,打开 Arduino IDE 或者 ESP-IDF,改一行ssid和password,重新编译、重新烧录,再装回去。整个过程少说半小时,多则半天,如果设备装在不好够到的地方,那更是灾难。

这个问题的根源在于:WiFi 凭据被硬编码在固件里了。每次改密码就等于改代码,改代码就得重新编译烧录。而 ESP32 本身是带 NVS(Non-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 上划分出来的一块区域,默认大小通常是 0x5000(20KB),可以通过分区表调整。它内部用键值对的方式组织数据,支持的类型包括:

  • 整数类型:int8_t、uint8_t、int16_t、uint16_t、int32_t、uint32_t、int64_t、uint64_t
  • 字符串类型:以\0结尾的 C 字符串
  • 二进制大对象:任意长度的 blob

NVS 的键(key)最长 15 个字符,这个限制很关键,命名的时候别超。值(value)对于字符串来说,单个条目理论上限是 4000 字节左右(受限于页大小),实际用的时候别存太大的东西,WiFi 密码这种几十字节的完全没问题。

NVS 的读写 API 在 ESP-IDF 里是nvs_flash.h和nvs.h,Arduino 环境下也能直接用,因为 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-IDF,esp_http_server组件是标配,注册 handler 就行。

这里有个关键设计点:Web 服务器要在设备连上 WiFi 之后才启动。因为你要通过 IP 访问它,设备没联网就没有 IP。但这里有个鸡生蛋问题——如果设备连不上 WiFi,你就访问不了它的 Web 服务器,也就改不了配置。所以完整的方案需要双模式:

  1. STA 模式:正常连上路由器,启动 Web 服务器,通过局域网 IP 访问
  2. 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 万次左右),所以不要频繁写 NVS,WiFi 配置这种低频写入完全没问题。

3.2 命名空间与键的设计

NVS 里数据是按"命名空间"(namespace)组织的,你可以理解成数据库里的"表"。不同命名空间下的同名键互不干扰。建议给 WiFi 配置单独开一个命名空间,比如叫wifi_cfg,里面存两个键:

  • ssid:WiFi 名称,字符串类型
  • pass:WiFi 密码,字符串类型

命名空间名最长 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" method="POST"> <label>WiFi 名称</label> <input type="text" name="ssid" maxlength="32"> <label>WiFi 密码</label> <input type="password" name="pass" maxlength="63"> <button type="submit">保存并重启</button> </form>

服务端处理 POST 的时候,需要解析表单数据。ESP-IDF 的 httpd 里,POST 数据通过httpd_req_recv读取,然后手动解析ssid=xxx&pass=yyy这种格式。解析的时候要注意 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 = "<html><body>"; html += "<h2>WiFi 配置</h2>"; html += "<form action='/save' method='POST'>"; html += "SSID: <input name='ssid' maxlength='32'><br>"; html += "密码: <input name='pass' type='password' maxlength='63'><br>"; html += "<button type='submit'>保存并重启</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 模式下访问设备的局域网 IP,AP 模式下访问 192.168.4.1。

4.6 参数计算与选择依据

几个关键参数的选择依据说明一下:

  • STA 连接超时 10 秒:家用路由器 DHCP 分配通常 2-5 秒完成,10 秒留了足够余量。如果是企业级网络或者信号弱的环境,可以适当延长到 15 秒。
  • AP 热点密码 8 位:WPA2 要求密码至少 8 位,设12345678只是为了满足协议要求,实际安全性靠的是"临时使用"这个场景,不需要复杂密码。
  • 重启延迟 2 秒:给浏览器足够时间接收响应并渲染提示信息。实测 1 秒也够,但 2 秒更保险。
  • NVS 命名空间wifi_cfg:8 个字符,远低于 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

排查顺序如下:

  1. 确认 SSID 和密码没存错:在保存后、重启前,把 NVS 里的值读出来打印到串口,确认写入正确。
  2. 确认密码没有 URL 编码残留:如果你用 ESP-IDF 手动解析 POST,检查解码逻辑。用 Arduino 的server.arg()一般不会有这个问题。
  3. 确认路由器没有 MAC 过滤:有些路由器开了白名单,新设备连不上。
  4. 确认 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是否和服务器注册的路由匹配。常见错误是表单写method="GET"但服务器只注册了 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 状态

用户体验上,打开配置页面时最好能看到当前连接状态,比如"当前已连接:MyWiFi,IP:192.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 页面加载时间在局域网内小于 200ms
  • NVS 写入耗时约 10-20ms,对用户无感知
  • AP 模式下手机连接热点到打开配置页面约 5 秒

这些数据说明整套方案的响应速度完全可接受,比拆机重刷固件快了一个数量级。

6.6 一个容易忽略的细节:Flash 分区表

如果你用 ESP-IDF 并且自定义了分区表,要确保 NVS 分区存在且大小合理。默认分区表里 NVS 是 0x5000(20KB),存 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,省下来的时间相当可观。

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

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

立即咨询