☰
ESP32不刷固件改WiFi密码:NVS+Web配置方案详解
2026/10/28 1:02:21 网站建设 项目流程

1. 项目缘起:一个让嵌入式开发者集体头疼的老问题

搞 ESP32 的朋友大概率都经历过这个场景:设备已经装到现场了,外壳封好了,甚至灌了胶,突然甲方来一句“WiFi 密码换了,你帮忙改一下”。这时候你脑子里第一反应是什么?重新连串口、打开 Arduino IDE 或者 ESP-IDF、改代码里的宏定义、编译、烧录……一套流程下来,快则十分钟,慢则半小时,还得拆壳接线。如果设备装在吊顶里、配电箱里、或者某个你根本够不着的地方,那画面就更美了。

这个项目的出发点就一句话:能不能不刷固件,直接改掉 ESP32 里存的 WiFi 密码?

答案是可以的。ESP32 的 WiFi 配置信息默认存在NVS(Non-Volatile Storage,非易失性存储)里,NVS 本质上是芯片内部 Flash 上的一块键值对分区。既然它是键值对,那就有办法在运行时去读写它。我们要做的,就是让 ESP32 自己起一个轻量的 HTTP 服务,浏览器打开一个页面,输入新的 SSID 和密码,点保存,设备重启后自动连上新网络。整个过程不需要数据线,不需要编译环境,甚至不需要你懂代码——只要设备还能通电、还能被访问到。

这篇文章我会把这个方案的设计思路、NVS 的底层机制、Web 服务的实现细节、参数计算、踩坑记录全部拆开讲清楚。适合三类人看:一是手上有一堆 ESP32 设备要维护的物联网开发者;二是刚入门 ESP32、想搞懂 NVS 到底怎么回事的初学者;三是做智能家居 DIY、希望给设备留一个“后门配置入口”的玩家。读完你就能把这套东西直接抄到自己项目里。

2. 方案整体设计:为什么选 NVS + Web 而不是别的路子

2.1 先搞清楚 ESP32 的 WiFi 配置到底存在哪

很多人以为WiFi.begin(ssid, password)之后,密码就“记住”了。其实不是。ESP32 的 Arduino 核心和 ESP-IDF 里,WiFi 驱动默认会把最近一次成功连接的网络信息写进 NVS,分区名通常叫nvs,命名空间是nvs.net80211。这里面存的键包括sta.ssid、sta.pswd、sta.bssid等等。下次上电时,如果代码里调用的是WiFi.begin()不带参数,驱动就会去 NVS 里读这些值。

这就意味着:只要我们能改掉 NVS 里sta.ssid和sta.pswd这两个键,设备下次启动就会连上新网络。这是整个方案的理论基石。

那为什么不用别的方式?我对比过几种常见做法:

方案是否需要重刷固件是否需要拆机实现难度适用场景
改代码宏定义重刷是通常需要低开发阶段
串口 AT 指令配置否需要接串口中有串口引出
SmartConfig / 配网否否中首次配网
BLE 配网否否高带蓝牙场景
NVS + Web 改键值否否中已联网设备维护

SmartConfig 和 BLE 配网更适合“首次配网”,也就是设备还没连上任何网络的时候。但我们的场景是设备已经在网,只是要换密码。这时候设备本身有一个可访问的 IP,直接起个 Web 服务最直接。而且 Web 方案跨平台,手机、电脑、平板都能操作,不需要装 App。

2.2 核心思路:运行时读写 NVS + 轻量 Web 服务

整体架构分三层:

  • 底层:用Preferences库(Arduino 对 NVS 的封装)或者直接调nvs_flashAPI 读写键值。
  • 中间层:起一个WebServer(Arduino 自带)或者esp_http_server(ESP-IDF),提供两个接口——一个 GET 返回配置页面,一个 POST 接收新 SSID/密码并写入 NVS。
  • 上层:一个极简的 HTML 页面,包含输入框和提交按钮,用原生 fetch 发 POST 请求。

为什么用Preferences而不是直接怼nvs_flash?因为Preferences把命名空间、类型、提交(commit)都封装好了,代码量少一半,出错概率低。对于只需要读写字符串的场景,完全够用。如果你要操作nvs.net80211这个系统命名空间,Preferences也能打开,但要注意系统命名空间的键名是固定的,不能乱改。

注意:直接改nvs.net80211里的sta.pswd是可行的,但不同 IDF 版本键名可能略有差异。更稳妥的做法是自己建一个命名空间(比如wifi_cfg),存自己的 SSID 和密码,然后在setup()里优先读自己的命名空间,读到了就用它去WiFi.begin()。这样不依赖系统内部实现,升级 IDF 也不怕。

2.3 为什么这个方案“稳”

我实测下来,这套方案有几个让人放心的地方。第一,NVS 写入是带磨损均衡和掉电保护的,写一次密码不会把 Flash 写坏。第二,Web 服务只在需要配置时启动,平时可以关掉,不占资源。第三,即使密码写错了,设备连不上网,你还可以通过串口兜底,或者让设备在连不上时自动开启一个 AP 热点,让你重新配置。这个“AP 兜底”我后面会讲怎么加。

3. NVS 键值存储机制:把 Flash 当小数据库用

3.1 NVS 到底是什么,为什么适合存配置

NVS 是 ESP32 在 Flash 上划出来的一块区域,默认大小 24KB(可通过分区表调整)。它把这块区域组织成一个个“页”,每页 4KB,页内是键值对条目。键最长 15 个字符,值可以是整数、字符串、二进制 blob。它内部做了磨损均衡,写入时会先写到新位置再标记旧位置失效,所以寿命比直接擦写 Flash 长得多。

你可以把 NVS 理解成一个极简的、掉电不丢的小型键值数据库。它不适合存大量数据(比如日志、图片),但存 WiFi 密码、设备配置、校准参数这类“少量、关键、需要持久化”的数据,简直完美。

3.2 Preferences 库的常用操作

Arduino 环境下,Preferences是最顺手的封装。基本用法:

#include <Preferences.h> Preferences prefs; // 打开命名空间,false 表示读写模式 prefs.begin("wifi_cfg", false); // 写字符串 prefs.putString("ssid", "MyHomeWiFi"); prefs.putString("pswd", "12345678"); // 读字符串,第二个参数是默认值 String ssid = prefs.getString("ssid", ""); String pswd = prefs.getString("pswd", ""); // 用完关闭 prefs.end();

几个关键点:begin()的第二个参数readOnly一定要传false才能写;每次写完最好end()一下,虽然不 end 也会在析构时提交,但显式关闭更稳;putString写入后并不是立即落盘,end()或者putString内部达到阈值时才 commit,所以写完一定要 end 或者手动 commit。

3.3 键名和命名空间的命名规范

NVS 的键名有长度限制(15 字符),命名空间也有长度限制(15 字符)。我踩过的坑是:一开始用了wifi_configuration这种超长名字,编译不报错,运行时begin()返回 false,查了半天才发现是长度超了。所以命名要短:wifi_cfg、net_cfg、dev_cfg都是好选择。

另外,键名不要用中文、不要用特殊符号,用下划线分隔的英文小写最稳。我一般用ssid、pswd、host、port这种。

3.4 写入次数与寿命的实测数据

有人担心“改密码会不会把 Flash 写坏”。我查过 Espressif 的文档,NVS 所在 Flash 区域的擦写寿命典型值是 10 万次。假设你一天改一次密码,一年 365 次,10 万次能用 270 多年。就算你一天改 100 次,也能用 2.7 年。而且 NVS 有磨损均衡,实际寿命更长。所以正常使用完全不用担心寿命问题,真正要担心的是写入时掉电导致数据损坏——这个后面讲。

4. Web 配置服务实现:从零搭一个能改密码的页面

4.1 服务端:WebServer 的初始化和路由设计

Arduino 的WebServer库用起来很简单。核心代码结构:

#include <WiFi.h> #include <WebServer.h> #include <Preferences.h> WebServer server(80); Preferences prefs; void handleRoot() { String html = R"rawliteral( <!DOCTYPE html> <html> <head><meta charset="utf-8"><title>WiFi 配置</title></head> <body> <h2>修改 WiFi 配置</h2> <form id="cfgForm"> <label>SSID: <input type="text" id="ssid" name="ssid"></label><br> <label>密码: <input type="password" id="pswd" name="pswd"></label><br> <button type="submit">保存并重启</button> </form> <script> document.getElementById('cfgForm').addEventListener('submit', async (e) => { e.preventDefault(); const ssid = document.getElementById('ssid').value; const pswd = document.getElementById('pswd').value; const resp = await fetch('/save', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: 'ssid=' + encodeURIComponent(ssid) + '&pswd=' + encodeURIComponent(pswd) }); alert(await resp.text()); }); </script> </body> </html> )rawliteral"; server.send(200, "text/html", html); } void handleSave() { String ssid = server.arg("ssid"); String pswd = server.arg("pswd"); if (ssid.length() == 0) { server.send(400, "text/plain", "SSID 不能为空"); return; } prefs.begin("wifi_cfg", false); prefs.putString("ssid", ssid); prefs.putString("pswd", pswd); prefs.end(); server.send(200, "text/plain", "保存成功,设备将在 3 秒后重启"); delay(3000); ESP.restart(); } void setup() { Serial.begin(115200); prefs.begin("wifi_cfg", true); String ssid = prefs.getString("ssid", ""); String pswd = prefs.getString("pswd", ""); prefs.end(); if (ssid.length() > 0) { WiFi.begin(ssid.c_str(), pswd.c_str()); unsigned long start = millis(); while (WiFi.status() != WL_CONNECTED && millis() - start < 15000) { delay(500); } } if (WiFi.status() == WL_CONNECTED) { server.on("/", handleRoot); server.on("/save", HTTP_POST, handleSave); server.begin(); Serial.println(WiFi.localIP()); } else { // 连不上,起 AP 兜底 WiFi.softAP("ESP32_Config", "12345678"); server.on("/", handleRoot); server.on("/save", HTTP_POST, handleSave); server.begin(); } } void loop() { server.handleClient(); }

这段代码就是完整可跑的最小实现。逻辑很直白:启动时先读 NVS,有配置就尝试连接,连上了就起 Web 服务;连不上就起 AP 热点,同样起 Web 服务。用户访问页面,填新密码,POST 到/save,写入 NVS,重启。

4.2 前端页面:为什么不用框架,越简单越可靠

前端我坚持用原生 HTML + 原生 fetch,不引入任何 CDN 资源。原因有三个:第一,ESP32 的 Web 服务性能有限,加载大文件慢;第二,现场环境可能没有外网,CDN 加载失败页面就废了;第三,原生代码体积小,可以直接内嵌在固件里,不依赖 SPIFFS 文件系统。

页面里唯一需要注意的是表单提交用 fetch 而不是默认的 form action,因为默认提交会跳转页面,用户体验差,而且 ESP32 处理跳转容易出问题。用 fetch 异步提交,拿到结果弹个 alert,干净利落。

4.3 参数校验:别让空密码和超长 SSID 把设备搞挂

NVS 的字符串值有长度限制,单个值最长 4000 字节左右(受页大小限制)。SSID 标准最长 32 字节,密码最长 63 字节。所以服务端要校验:

  • SSID 长度 1~32
  • 密码长度 0~63(开放网络密码为空是合法的)
  • 拒绝包含非法字符的输入

我一开始没做校验,结果有人输入了一个 200 字符的 SSID,写入 NVS 时返回错误,但代码没检查返回值,导致 NVS 里存了个半截数据,设备重启后连不上也读不出,只能串口救。后来加了校验和返回值检查,再没出过问题。

if (ssid.length() == 0 || ssid.length() > 32) { server.send(400, "text/plain", "SSID 长度必须在 1-32 之间"); return; } if (pswd.length() > 63) { server.send(400, "text/plain", "密码长度不能超过 63"); return; } size_t written = prefs.putString("ssid", ssid); if (written == 0) { server.send(500, "text/plain", "写入失败,请重试"); return; }

4.4 安全考量:加个简单认证,别让邻居随便改

Web 服务默认没有认证,同一局域网内任何人访问设备 IP 都能改密码。这在家庭环境可能无所谓,但在办公或公共环境就是隐患。最简单的做法是加 HTTP Basic Auth:

if (!server.authenticate("admin", "esp32cfg")) { return server.requestAuthentication(); }

在handleRoot和handleSave开头都加上这段。用户名密码可以硬编码,也可以存在 NVS 里让用户自己改。虽然 Basic Auth 是明文传输,但对于内网配置场景,挡住随手乱点的人足够了。真要更安全,可以上 HTTPS,但 ESP32 跑 TLS 会吃不少内存,配置页面这种低频操作,Basic Auth 性价比最高。

5. 实操全流程:从烧录到改密码的完整走一遍

5.1 环境准备与依赖确认

我用的环境是 Arduino IDE 2.x + ESP32 核心 2.0.14。需要的库:WiFi、WebServer、Preferences,这三个都是核心自带的,不用额外装。如果你用 PlatformIO,在platformio.ini里确认platform = espressif32,框架用arduino即可。

开发板选择上,ESP32-WROOM-32、ESP32-S3、ESP32-C3 都行。S3 和 C3 的 NVS 机制一样,代码通用。唯一要注意的是 S3 的 USB CDC 串口和普通 UART 不一样,Serial输出可能需要额外配置,但这不影响 NVS 和 Web 部分。

5.2 首次烧录:把带 Web 配置的固件写进去

首次烧录还是得用数据线。把上面的完整代码贴进 IDE,选好板子和端口,点上传。烧录完成后打开串口监视器,波特率 115200。如果 NVS 里没有配置,设备会起 AP 热点ESP32_Config,密码12345678。用手机连上这个热点,浏览器访问192.168.4.1,就能看到配置页面。

填入你家 WiFi 的 SSID 和密码,点保存。设备会写入 NVS 并重启,这次它会去连你填的网络。连上后串口会打印出分配的 IP,比如192.168.1.105。记住这个 IP,以后改密码就靠它了。

5.3 日常改密码:浏览器三步搞定

假设现在 WiFi 密码从oldpass123改成了newpass456。设备还在线,但连不上路由器了(因为密码变了)。这时候你有两个入口:

入口一:设备还没重启,Web 服务还在跑。直接浏览器访问设备 IP,输入新密码,保存,重启,完事。

入口二:设备已经重启,连不上网,起了 AP 热点。手机连ESP32_Config,访问192.168.4.1,输入新密码,保存,重启,完事。

整个过程不需要数据线,不需要电脑,手机就能操作。我实测从打开浏览器到设备重新上线,大概 30 秒。

5.4 验证 NVS 是否写入成功

怎么确认密码真的写进去了?两个办法。一是串口打印,在handleSave里写完 NVS 后读出来打印:

prefs.begin("wifi_cfg", true); Serial.print("NVS SSID: "); Serial.println(prefs.getString("ssid", "")); Serial.print("NVS PSWD: "); Serial.println(prefs.getString("pswd", "")); prefs.end();

二是用nvs_flash的工具,但那个麻烦。串口打印最直接。

5.5 参数计算:NVS 分区大小够不够用

默认分区表里 NVS 是 24KB。我们存两个字符串,SSID 最多 32 字节,密码最多 63 字节,加上键名和元数据,总共不到 200 字节。24KB 绰绰有余。如果你还要存别的配置,比如 MQTT 服务器地址、端口、设备 ID,每个也就几十字节,存几十个键值对没问题。

如果项目里 NVS 分区被改小了(有些定制分区表会压缩 NVS),要确保至少留 4KB。低于 4KB 时 NVS 可能无法正常初始化。这个在partitions.csv里能看到,nvs那一行的 size 字段。

6. 常见问题与排查技巧实录

6.1 写入 NVS 后设备重启连不上,怎么办

这是最常见的问题。排查顺序:

  1. 确认 SSID 和密码没有多余空格。浏览器输入框里很容易带首尾空格,putString会原样存进去。加个trim()处理。
  2. 确认路由器是 2.4GHz。ESP32 不支持 5GHz,如果路由器双频合一,SSID 一样但设备可能连到 5G 频段失败。建议在路由器里把 2.4G 和 5G 的 SSID 分开。
  3. 确认密码里的特殊字符。有些字符在 URL 编码时出问题,server.arg()解码后应该是对的,但保险起见用encodeURIComponent前端编码。
  4. 看串口日志。WiFi.begin()后打印WiFi.status(),不同状态码对应不同问题。WL_NO_SSID_AVAIL是找不到网络,WL_WRONG_PASSWORD是密码错,WL_CONNECT_FAILED是其他失败。

6.2 NVS 写入失败返回 0 是什么原因

putString返回写入的字节数,返回 0 表示失败。原因通常是:命名空间没打开成功(长度超限或分区满)、值太长、Flash 操作被中断。我遇到过一次是命名空间用了wifi_configuration(18 字符),超过 15 字符限制,begin()返回 false,后续putString全失败。改成wifi_cfg就好了。

6.3 Web 页面打不开的几种可能

  • 设备没连上网也没起 AP。检查setup()里的逻辑,确保连不上时有 AP 兜底。
  • IP 地址变了。DHCP 分配的 IP 可能变,建议在路由器里给设备绑定静态 IP,或者在代码里用WiFi.config()设静态 IP。
  • 防火墙拦截。有些企业网络禁止设备间互访,换个网络试试。
  • 端口被占。80 端口如果被其他服务占了,改成 8080。

6.4 改完密码后旧密码还能连上吗

不能。NVS 里的pswd被覆盖了,设备下次启动读的是新值。但要注意,如果路由器还开着旧密码的访客网络,设备可能连到那个。所以改密码后最好确认路由器只开了一个网络。

6.5 常见问题速查表

现象可能原因解决方法
保存后重启连不上SSID/密码有空格加 trim()
保存后重启连不上5GHz 网络改用 2.4GHz
putString 返回 0命名空间名超长改短到 15 字符内
Web 页面打不开设备没起 AP检查 setup 兜底逻辑
Web 页面打不开IP 变了绑定静态 IP
认证失败用户名密码错检查 Basic Auth 配置
NVS 初始化失败分区太小检查 partitions.csv

6.6 独家避坑技巧

技巧一:写入前先擦除旧值。虽然putString会覆盖,但如果新值比旧值短,旧值的尾部可能残留。用prefs.remove("ssid")先删再写,更干净。

技巧二:加一个“恢复出厂设置”按钮。在页面上加个按钮,POST 到/reset,调用prefs.clear()清空命名空间,然后重启。这样设备会回到 AP 配网模式,相当于硬件复位。这个功能在设备被改乱时救命。

技巧三:AP 热点加密码。默认 AP 是开放的,任何人都能连。给softAP加个密码,比如WiFi.softAP("ESP32_Config", "config123"),防止别人乱连。

技巧四:限制 Web 服务启动时间。如果不想让 Web 服务一直跑着占资源,可以在启动后 5 分钟自动关闭,或者只在特定条件下启动(比如某个 GPIO 拉低时)。我一般让它一直跑着,因为 ESP32 跑个 Web 服务内存占用不到 10KB,无所谓。

技巧五:OTA 升级和 NVS 配置分开。如果你同时用 OTA,注意 OTA 升级不会动 NVS 分区,所以配置会保留。但如果你 OTA 的固件里改了命名空间名,那就读不到旧配置了。所以命名空间名一旦定了就别改。

7. 进阶扩展:让这套方案更实用

7.1 加一个配置页面显示当前 SSID

用户打开页面时,最好能看到当前连的是哪个网络。在handleRoot里读 NVS,把 SSID 填到输入框的value里:

prefs.begin("wifi_cfg", true); String curSsid = prefs.getString("ssid", ""); prefs.end(); String html = "<input type='text' id='ssid' value='" + curSsid + "'>";

密码框不填,留空表示不修改。保存时如果密码为空,就只更新 SSID,保留原密码。这个逻辑很实用,避免用户每次都要重输密码。

7.2 支持静态 IP 配置

有些场景需要固定 IP。在页面上加几个输入框:IP、网关、子网掩码。存到 NVS 的另一个命名空间net_cfg。启动时读出来,用WiFi.config()设置。注意WiFi.config()必须在WiFi.begin()之前调用。

7.3 配置版本号与迁移

如果以后配置项变多了,加个版本号键cfg_ver。启动时读版本号,如果是旧版本,做数据迁移。比如 v1 只有 ssid/pswd,v2 加了 mqtt 地址,迁移逻辑就是给旧配置补上默认 mqtt 地址。这样升级固件不会丢配置。

7.4 用 ESP-IDF 原生 API 替代 Preferences

如果你不用 Arduino,纯 ESP-IDF 环境,可以用nvs_flashAPI:

nvs_handle_t handle; nvs_open("wifi_cfg", NVS_READWRITE, &handle); nvs_set_str(handle, "ssid", "MyWiFi"); nvs_set_str(handle, "pswd", "12345678"); nvs_commit(handle); nvs_close(handle);

逻辑一样,只是 API 不同。nvs_commit是必须的,不 commit 不落盘。

7.5 结合外部中断做“一键恢复”

热词里有人搜“esp32外部中断实战”,这里正好用得上。接一个按钮到 GPIO,配置下降沿中断。中断里设置一个标志位,loop()里检测到标志就清空 NVS 并重启。这样设备装在外壳里,用户按一下按钮就能恢复出厂,不用拆机。中断服务程序要短,只设标志,不做耗时操作。

volatile bool resetFlag = false; void IRAM_ATTR onResetBtn() { resetFlag = true; } void setup() { pinMode(0, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(0), onResetBtn, FALLING); } void loop() { if (resetFlag) { prefs.begin("wifi_cfg", false); prefs.clear(); prefs.end(); delay(500); ESP.restart(); } server.handleClient(); }

GPIO0 在很多开发板上就是 BOOT 按钮,直接拿来用最方便。

8. 一些实测数据和性能观察

我用 ESP32-WROOM-32 实测了几组数据。Web 服务启动后,空闲时内存占用约 8KB,处理一次 POST 请求峰值约 15KB。NVS 写入一次耗时约 20ms,读取约 2ms。从点击保存到设备重启完成并连上新网络,平均 8 秒,其中重启本身占 3 秒,WiFi 连接占 4~5 秒。

如果设备连的是隐藏 SSID,WiFi.begin()需要额外指定信道和 BSSID,否则连不上。这个在 NVS 里也要存sta.bssid和sta.channel,但比较麻烦。我的建议是别用隐藏 SSID,没必要,安全性提升有限,兼容性坑一堆。

另外,ESP32-S3 的 NVS 读写速度和 WROOM-32 差不多,但 S3 的 Web 服务响应更快,因为主频高。C3 是 RISC-V 架构,NVS 机制一样,代码通用,但注意 C3 只有 400KB SRAM,跑 Web 服务要省着点用,别开太多缓冲区。

9. 我个人在实际操作中的几点体会

这套方案我前后在十几个项目里用过,从智能插座到环境监测节点,最远的设备装在郊区的基站机柜里,改密码全靠它。有几点体会比较深。

第一,AP 兜底是必须的。我早期版本没做兜底,结果有次密码改错了,设备连不上网,Web 服务也没起,只能派人去现场拆机。后来加了 AP 兜底,再没出过这种事故。

第二,NVS 命名空间名要短且固定。我吃过一次亏,项目重构时把wifi_cfg改成了wifi_config,结果旧设备的配置读不出来,全部要重新配。从那以后命名空间名我都在头文件里定义成宏,改的时候全局替换,避免手误。

第三,Web 页面越简单越好。我试过用 Vue 写配置页,打包出来 200KB,ESP32 加载要好几秒,还容易内存不足。后来全部退回原生 HTML,页面不到 3KB,秒开。

第四,加个“测试连接”按钮很实用。在保存之前,先让设备尝试用新密码连接,连上了再写入 NVS。这样避免写错密码导致设备失联。实现方式是 POST 到/test,设备用WiFi.begin()试连,返回结果,但不写 NVS。用户确认没问题再点保存。

最后再分享一个小技巧:如果你有很多台设备要批量改密码,可以在 Web 接口基础上加一个 UDP 广播监听,收到特定广播包就自动改密码。这样一台一台点浏览器太慢,广播一发全部搞定。不过这个要小心,广播包容易被误触发,加个校验码就行。

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

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

立即咨询