前阵子维护一套 ESP32 环境监测节点,十几块板子分散在不同位置,有的塞在天花板检修口里,有的藏在配电箱旁边。路由器一换 WiFi 密码,我按老思路挨个拆机、接串口线、重新编译烧录固件,光是把板子从安装位置弄下来就花了半天。后来想明白一件事:SSID 和密码一直是作为键值对存在 ESP32 的 NVS 存储区里的,只要能在设备运行状态下直接改写这个键值,就完全没必要重刷固件。于是我在固件里内置了一个浏览器可访问的配置页面,局域网内打开手机浏览器直接修改并保存 NVS 键值,一分钟完成改密。这篇就聊聊这个工具的完整实现思路和踩过的坑,适合批量维护 ESP32 设备、或者做产品后需要就地改 WiFi 配置的同学参考。
1. 改个密码为什么要动固件?NVS 这套键值存储机制先说透
很多人一听"改 WiFi 密码要重刷固件"就觉得离谱,其实根源在于对 NVS 的理解不够。NVS 全称 Non-Volatile Storage,是 ESP32 内部 flash 上划分出来的一块键值对存储区域。它的设计初衷就是用来存那些"掉电不能丢、但又经常要改"的小数据——WiFi 账号密码、设备编号、校准系数、用户设置,都属于这一类。
1.1 NVS 不是文件系统,是带磨损均衡的小仓库
你可以把 NVS 理解成一个小储物柜,每个柜子有一个名字(键),柜子里可以放一段数据(值)。和 SPIFFS/LittleFS 这类文件系统不同,NVS 不关心文件名、目录、打开关闭,它只提供最基础的"按名字存、按名字取"操作。这个设计的优势是:接口简单、存储效率高、内置磨损均衡。
ESP32 内部 flash 的擦写寿命通常在十万次级别,如果每次都往固定地址写,很快会把某个扇区写坏。NVS 的做法是把数据分散写入多个扇区,写之前自动找空闲位置,旧数据先标记失效,等碎片积累到一定程度再整体压缩回收。所以只要你规范使用 NVS API,正常改 WiFi 密码的频率根本不用操心寿命问题。
1.2 WiFi 配置到底存在哪里
ESP32 使用 WiFi 库连接网络时,如果你用的是官方配置流程,连接参数最终会被写进 NVS 的某个命名空间里。在 Arduino 环境下最常见的封装是 Preferences 库,它底层调的正是 NVS API。你调用prefs.begin("wifi", false)的时候,实际上是在 NVS 里打开(或创建)一个叫wifi的命名空间,后续所有putString、putInt都落在这个命名空间下。
之所以说"改密码要重刷固件"是个误区,是因为很多人以为 SSID 和密码写在固件源码里。实际上,只要你在源码里用的是动态读取方式,比如:
prefs.getString("ssid", ""); prefs.getString("password", "");那么固件本身只是一份"怎么连接"的执行逻辑,具体连哪个 WiFi、用什么密码,完全取决于 NVS 里的键值。重刷固件只是把 flash 整个擦掉重写,顺带把 NVS 也清空了,所以你重新配一次密码后又好了——但这不是唯一路径,直接改 NVS 键值才是更精准的做法。
1.3 重刷固件为什么能"解决",代价有多大
重刷固件走的是这样一条链路:找板子、接串口、设置下载参数、编译整个工程、烧录、重启、重新配网。每次至少十分钟,如果板子装在不好拆的位置,半小时起步。更要命的是,重刷会清空你之前存好的所有 NVS 数据,比如传感器校准值、设备序列号、上报间隔,这些参数全得重新设置一遍。
所以问题的本质不是"刷固件能解决问题",而是"刷固件恰好把 NVS 里的旧配置也冲掉了"。我见过不少产品把用户配置、设备凭证也一股脑写死在固件里,后期运维只能靠 OTA 整包升级来改参数,这是最笨重的做法。
2. 传统改密方式逐个拆:刷固件、AT 指令、蓝牙配网各有各的难堪
既然直接改 NVS 是精准解法,那为什么大家第一反应还是刷固件?因为其他几种常见方式各有痛点,让人形成了惯性。
2.1 刷固件的隐性成本:编译环境与现场条件
很多做硬件的朋友,板子烧完就交付了,客户现场根本没有串口线,也不一定有 Arduino IDE 环境。哪怕你远程让对方刷,对方还要装驱动、选板卡型号、下载离线包,折腾一圈下来,用户早没耐心了。我甚至遇到过把板子从墙上拆下来、刷完固件再装回去,结果螺丝滑牙装不回去的情况。这就是重刷固件最典型的隐性成本——它不只是花时间,还会引入物理损坏风险。
2.2 AT 指令和蓝牙配网各自的瓶颈
使用 AT 固件的模组改 WiFi 参数确实不用重刷,但前提是你得有一条可用的串口通路。板子已经封装进设备外壳后,串口往往没有引出来,或者被其他功能占用。蓝牙配网(如 ESP32 的 SmartConfig、BLE 配网)虽然免了串口,但依赖手机 App,设备要进入配网模式,App 和模块之间的协议还得统一维护。对于已经部署在客户手里的设备,让客户下载一个 App 再配对,教育成本同样不低。
2.3 为什么浏览器配网页在运维场景里最舒服
浏览器是任何设备都自带的能力,不需要额外安装软件。ESP32 本身就能起一个 WebServer,把配网页嵌入固件里,用户连上设备发出的热点(或者同网段 IP),打开浏览器就能操作。这个思路在商用路由器、智能家居单品里其实已经很常见——只是很多 ESP32 开发者还停留在"配网必须依赖 App 或串口"的思维里。
我在自己的方案里做了个分工:首次上电用 SoftAP 配网,日常改密则走局域网 Web 页面。两者共用同一套 NVS 写入逻辑,只是入口不同。这个分工让"改个密码"这个高频操作变得极其轻量,完全绕开了烧录器和编译环境。
3. 浏览器直改 NVS 键值的整体方案:从 HTTP 请求到 flash 落盘
方案整体思路不复杂,但要做好几件事:启动时从 NVS 读配置并尝试连接 WiFi;提供 HTTP 接口接收新的 SSID 和密码;校验后写入 NVS;重启生效。下面拆开讲每一步的关键点。
3.1 两种入口模式:SoftAP 与局域网共存
设备上电后先读取 NVS 里保存的 WiFi 配置,如果能连上,就同时启动 STA 模式(连接路由器)和 SoftAP 模式(发出一个独立热点),两个网络都提供配置页面。如果连不上(比如密码错了),SoftAP 仍然工作,用户连上ESP32-Config这个热点就能重新配置。这样做的好处是:设备在路由器下可以直接用 IP 访问,设备不在身边或者路由器换了,也能通过直连热点完成修改。
3.2 从 HTTP 请求到 NVS 落盘的完整链路
用户填完表单点提交,浏览器向 ESP32 发送一个 HTTP POST 请求,参数是ssid和password。WebServer 库收到请求后,在回调函数里取出参数,先做基本校验(非空、长度限制),然后调用 Preferences 库写入。代码大致长这样:
#include <WiFi.h> #include <WebServer.h> #include <Preferences.h> Preferences prefs; WebServer server(80); void handleSave() { String ssid = server.arg("ssid"); String password = server.arg("password"); if (ssid.length() == 0 || ssid.length() > 32) { server.send(400, "text/plain", "SSID 参数不合法"); return; } if (password.length() > 64) { server.send(400, "text/plain", "密码参数过长"); return; } prefs.begin("wifi", false); prefs.putString("ssid", ssid); prefs.putString("password", password); prefs.end(); server.send(200, "text/plain", "配置已保存,设备即将重启"); delay(500); ESP.restart(); }这段代码看起来简单,但有三个细节值得注意。
第一,prefs.begin必须在写入前调用,读取时建议用只读模式prefs.begin("wifi", true),写入时才用false,避免不必要的 flash 写入开销。
第二,String类型在 Arduino 上有内存碎片问题,频繁创建和销毁会导致堆内存碎片化,长期运行时尤其明显。写入 NVS 这种低频操作无所谓,但在 WebServer 回调里解析参数时,尽量复用同一个String变量或者直接用char数组,能有效减少碎片。
第三,保存后必须给浏览器一个明确的响应,再延迟一小段时间重启。如果立刻重启,HTTP 响应可能还没发完,浏览器那边会显示连接中断。我实测delay(500)比较稳,太短会有概率失败。
3.3 修改完成后的重启策略
改完配置不重启的话,WiFi 库不会自动重新连接新网络。所以保存后直接ESP.restart()是最干净的做法。重启后 setup 里重新读取 NVS 的 SSID 和密码,尝试连接新的网络,一切自动完成。
我建议在重启前把当前配置打印到串口,方便调试时确认写入是否成功:
Serial.printf("[Config] Saved -> SSID: %s, Password: %s\n", ssid.c_str(), password.c_str());另外要设置一个连接超时。WiFi 密码填错时,WiFi.begin不会返回失败,只会长时间处于WL_DISCONNECTED状态。我一般用一个循环最多等 20 秒,超时后自动进入 SoftAP 模式,避免设备变成"既连不上网、又没有可操作入口"的死状态。
4. Web 配网页完整实现:NVS 读写、表单处理、自动重启一把梭
上面那段只展示了保存接口,完整的配网页还差两部分:展示当前配置的首页,以及带有基础访问限制的入口逻辑。这里给出一个可以直接复制的完整示例。
4.1 首页显示当前配置状态
配网页最理想的状态是:用户打开页面时,一眼能看到设备当前连的是哪个 WiFi、IP 是多少、距离上次修改多久,然后再决定要不要改。所以我在首页读取 NVS 中已有的配置并渲染到页面里:
void handleRoot() { prefs.begin("wifi", true); String currentSSID = prefs.getString("ssid", ""); prefs.end(); String html = "<!DOCTYPE html><html><head><meta charset=\"UTF-8\">"; html += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"; html += "<title>ESP32 WiFi 配置</title></head><body>"; html += "<h2>设备 WiFi 配置</h2>"; html += "<p>当前状态: " + String(WiFi.status() == WL_CONNECTED ? "已连接" : "未连接") + "</p>"; html += "<p>当前 IP: " + (WiFi.status() == WL_CONNECTED ? WiFi.localIP().toString() : "无") + "</p>"; html += "<p>已保存 SSID: " + currentSSID + "</p>"; html += "<form method=\"POST\" action=\"/save\">"; html += "<p>SSID: <input type=\"text\" name=\"ssid\" value=\"" + currentSSID + "\"></p>"; html += "<p>密码: <input type=\"password\" name=\"password\"></p>"; html += "<p><input type=\"submit\" value=\"保存并重启\"></p>"; html += "</form></body></html>"; server.send(200, "text/html", html); }这里有个细节:SSID 回显到 value 属性时需要做 HTML 转义,否则 SSID 里如果包含引号,页面会直接被截断。我一般写一个简单的转义函数,替换<、>、&、"。虽然大多数家用 WiFi 名不会这么变态,但做产品必须考虑。
4.2 启动逻辑:配网模式与服务端初始化
回到 setup,我常用的启动序列是这样的:
void setup() { Serial.begin(115200); delay(100); prefs.begin("wifi", true); String ssid = prefs.getString("ssid", ""); String password = prefs.getString("password", ""); prefs.end(); if (ssid.length() > 0) { WiFi.mode(WIFI_STA); WiFi.begin(ssid.c_str(), password.c_str()); unsigned long start = millis(); while (WiFi.status() != WL_CONNECTED && millis() - start < 20000) { delay(500); Serial.print("."); } } WiFi.mode(WIFI_AP_STA); WiFi.softAP("ESP32-Config", "12345678"); server.on("/", handleRoot); server.on("/save", HTTP_POST, handleSave); server.begin(); Serial.println("Web 配置服务已启动"); }注意这里有个坑:如果先WiFi.mode(WIFI_AP_STA)再WiFi.begin,有些版本的 ESP32 库会切换模式打断连接过程,所以正确顺序是先连 WiFi,最后再开 SoftAP。另外,WIFI_AP_STA模式下 SoftAP 和 STA 可以共存,但两者共享 RF 资源,实际吞吐会互相影响,只是配个网页完全够用。
4.3 用最低成本实现访问控制
配网页如果不做任何限制,局域网里任何人都能改你设备的 WiFi,这显然不行。最简单的办法是要求用户在 URL 里带一个固定 token,比如/save?token=mytoken&ssid=...&password=...,或者在表单里加一个隐藏字段,提交时校验。
我在表单里加了一个"管理口令"输入框,和 SSID、密码一起 POST 到后端,后端比对 NVS 里预先存好的口令:
const char* adminToken = "your-secret-token"; void handleSave() { String token = server.arg("token"); if (token != adminToken) { server.send(403, "text/plain", "管理口令错误"); return; } // ...校验和写入逻辑 }这个方案虽然简朴,但已经能挡住绝大多数误操作。对于局域网内的恶意攻击者,ESP32 的性能和存储都不适合做高强度的加密认证,最好的策略是基本防护加网络层面隔离——配网页只在内网开放,不要主动暴露到公网。
提示:如果设备部署在公开场所,建议把配网页的端口改成一个非常用端口(比如 8080),再配合访问口令,能显著降低被扫描器随机篡改的概率。
4.4 为什么用 POST 而不是 GET 传密码
我在最初搭建这套方案的时候,图省事用的是 GET 请求,把参数全放在 URL 里。结果发现两个问题:一是密码会出现在浏览器历史记录和 WebServer 日志里,二是部分特殊字符在 URL 拼接时需要额外编码。改成 POST 之后,数据放在请求体里,既不会出现在历史记录,也避开了 URL 长度限制。这个习惯建议从一开始就养成。
5. 实测踩坑记录:中文 SSID、特殊字符、flash 寿命与配置回滚
方案跑起来之后,我陆续遇到了一些实际使用中的坑。这一节是文章里最值钱的部分,因为这些经验在官方文档里基本不会写。
5.1 中文 SSID 与 URL 编码问题
第一次在客户现场用手机浏览器配置时,客户的 WiFi 名是中文。表单提交后设备连不上,串口打印显示 SSID 是一串乱码。查了半天发现,手机浏览器默认用 UTF-8 编码提交表单,Arduino 的server.arg("ssid")拿到的是已经解码后的字符串,理论上没问题,但问题出在 WebServer 库对 URL 解码的处理上——如果 SSID 中间有字符被解析成非法 UTF-8 序列,整个字符串会被截断。
解决办法是保存前做一次严格校验:UTF-8 编码的中文 SSID 字节长度通常在 6 到 30 字节之间,如果解码后长度小于原始字节数,说明解析出错,直接返回错误:
String ssid = server.arg("ssid"); if (ssid.length() == 0 || ssid.length() > 32) { // 传入的编码格式有问题,拒绝写入 }另外,Arduino 的String.length()返回的是字节数,不是字符数。中文 SSID 在 UTF-8 下每个汉字占 3 字节,所以一个 8 个汉字的中文 SSID,length()返回的是 24。判断上限时要用 32 字节来判断,而不是 32 个字符。
5.2 特殊字符密码:&、+、= 这些别踩
密码里如果有&、+、=这类字符,在传统 GET 请求里会被当成参数分隔符,导致密码截断。改用 POST 后,WebServer 库会自动做 URL 解码,基本不会出问题。但有一点容易被忽略:如果密码里有空格,浏览器会提交+,而后端解码时+应该被还原成空格。ESP32 的 WebServer 库在这个细节上实现得比较迷,有些固件版本会把空格保持为空格,有些不处理。
我给出的实战建议是:如果发现密码包含特殊字符连不上,先不要怀疑 NVS 写入失败,先在串口把保存的密码完整打印出来,确认实际写入的内容和你填的一致,再检查别的环节。
5.3 NVS 擦写寿命与频繁改密
前面说过 NVS 有磨损均衡,但这不代表可以无限制写。单键的重复写入会持续消耗 flash 的擦写次数。有人会问,我一天改十次密码会不会有问题?答案是不会,十万次的寿命够你改两万多年。真正需要注意的是调试阶段的自动化脚本。
我在调试 Web 接口时写了个脚本,每两秒循环提交一次配置,结果跑了半小时后,这块 flash 那一个扇区的写入计数涨了几千次。虽然不至于马上坏,但养成好习惯还是有必要的:程序里避免在循环任务里调用putString,只在配置真正变化的时候才写入。
5.4 配置回滚:新 WiFi 连不上怎么办
这是整套方案里最值得讲的增强功能。如果用户手滑把密码填错了,设备重启后连不上新 WiFi,这时候 SoftAP 还在工作,用户可以通过热点重新配置。但问题来了:如果现场没人盯着设备,它就会一直处于"配置错误、离线"的状态,你远程根本没法救。
我的做法是保存前先把旧配置备份到一个backup命名空间,重启后如果 30 秒内连不上新 WiFi,自动从 backup 恢复旧配置并再次重启。这个回滚机制在实际部署中救过我两次,强烈建议加上。代码结构大概这样:
// 保存时 prefs.begin("wifi", false); prefs.putString("backup_ssid", oldSSID); prefs.putString("backup_password", oldPassword); prefs.putString("ssid", newSSID); prefs.putString("password", newPassword); prefs.end(); // 启动后 if (WiFi.status() != WL_CONNECTED && 等待超时) { prefs.begin("wifi", false); prefs.putString("ssid", prefs.getString("backup_ssid", "")); prefs.putString("password", prefs.getString("backup_password", "")); prefs.end(); ESP.restart(); }这里有个小技巧:备份用的命名空间和主配置共用一个wifi命名空间,键名用backup_ssid、backup_password,这样读取时一次begin就能同时拿到当前配置和备份配置,不用切换命名空间。
5.5 浏览器缓存导致页面不刷新
WebServer 的响应头默认没有加Cache-Control,有些浏览器(尤其是手机内置浏览器)会对页面做缓存。改了配置后重新打开,看到的还是旧页面。我的解决方式是在响应头里显式加上:
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");或者在 HTML 的<head>里加<meta http-equiv="Cache-Control" content="no-cache">。虽然对 ESP32 这种小内存设备来说缓存能省流量,但配置页面需要实时状态,所以禁用缓存更合适。
6. 给这个工具补上安全与可用性扩展
基础版本的 Web 配网页已经能解决"改密码要刷固件"的问题,但在实际产品化过程中,我还陆续加了几个功能,让这套方案更接近一个可交付的工具。
6.1 修改后自动更新 SoftAP 名称
默认的 SoftAP 叫ESP32-Config,如果有两台设备同时开热点,现场的人根本分不清连的是哪一台。我的做法是读取 NVS 里的设备名称(比如部署位置编号)拼到热点名后面:
String apName = "ESP32-" + deviceID; WiFi.softAP(apName.c_str());这样每台设备的热点名称唯一,用户在手机 WiFi 列表里一眼就能找到目标设备。
6.2 独立的 NVS 诊断接口
除了配置页面,我还留了一个/status接口,返回纯文本的设备状态:当前 WiFi 信号强度、IP 地址、NVS 剩余可用空间、固件编译时间。这个接口对远程排查问题非常有用——用户反馈设备离线时,我让他打开/status,把返回内容发给我,很多问题一眼就能定位。
比如 NVS 剩余空间不足会导致prefs.putString失败,这个错误在串口可能不明显,但通过诊断接口就能看到。我遇到过一种情况:某个键反复写入太长字符串,把 NVS 分区写满了,WiFi 配置也存不进去,设备一直处于"连不上网也没有配置入口"的状态。有了诊断接口,这类问题几秒钟就能确认。
6.3 保留串口配置作为最后手段
Web 配网再方便,也总会有 WebServer 起不来的极端情况。所以我在代码里保留了串口配置的逻辑:串口收到CONFIG ssid password指令时,同样走 NVS 写入和重启流程。这套双通道设计让设备无论处于什么状态都有办法恢复,Web 通道服务于现场非技术人员,串口通道留给开发者自己应急。
6.4 关于安全性的个人结论
我见过有人把配网页直接暴露到公网,理由是方便远程改配置,但 ESP32 的 WebServer 库本身没有 TLS 支持,跑 HTTP 裸奔的风险很大。如果你的设备确实需要公网远程改配置,建议用反向代理加 HTTPS 在前面做一层,或者干脆用 MQTT 远程下发配置指令,Web 页面只作为局域网入口。这个边界一定要守住,配网页的目的是便利,不是给自己挖安全漏洞。
最后说一点个人体会。整套方案做下来,最大的收获不是省了刷固件的时间,而是理解了"配置与程序分离"的价值。固件负责功能逻辑,NVS 负责状态与参数,两者解耦之后,设备的运维灵活性会提升一个量级。现在这套 Web 配网页已经跑在我所有 ESP32 设备上,路由器换密码、换 SSID、调整上报间隔,全都在浏览器里完成。如果你也在维护多台 ESP32 设备,不妨把这篇文章里的方案抄过去,改改页面样式就能直接用,省下的时间足够你把更多的精力放在真正有价值的功能上。