☰
ESP32 NVS + 浏览器配置WiFi:彻底告别重编译烧录
2026/10/8 7:30:17 网站建设 项目流程

说实话,搞 ESP32 开发的这两年,我最烦的一件事就是:家里路由器一换密码,所有靠 WiFi 联网的设备都得重新编译一遍固件。改个字符串、点一下编译、再插线烧录,来回少说十分钟,多则半小时。如果设备已经装进外壳、焊在电路板上,还得拆机。后来我换了个思路——把 WiFi 的 SSID 和密码存进 NVS,再在 ESP32 上跑一个极简的浏览器配置页面。需要换网时直接手机连上设备的热点,打开网页改一下键值就行,完全不用重刷固件。这个方案我已经在几个项目里稳定跑了大半年,今天把它拆开讲清楚,包括原理、代码、避坑点,给同样被"改密码重刷"折磨过的朋友一个可以直接抄作业的参考。适合正在做 WiFi 联网项目、又不想每次部署后都靠串口线或重编译维护设备的开发者。

1. 痛点场景:改个 WiFi 密码为什么这么麻烦

1.1 传统改密码的三种姿势,没一个省心的

先说最常见的做法:很多初学 ESP32 的朋友喜欢把 WiFi 信息直接硬编码在代码里。

const char* ssid = "MyHome_WiFi"; const char* password = "password123";

这种方式开发阶段很爽,连上就能跑。但真到设备装好之后,问题就来了:路由器改密码、加了个信号更好的热点、带去朋友家临时蹭个网……任何一次网络变动,这行字符串就得变。于是流程变成:改代码 -> 打开编译 -> 等待 -> 插 USB 线 -> 烧录 -> 重启。如果设备放在天花板上、嵌在墙里、或者螺丝封死在外壳里,每次都得拆机,脾气再好也会崩溃。

第二种方式,是把 WiFi 信息存进 SPIFFS / LittleFS 里的配置文件。这样改密码不用重编译,但需要把配置文件单独上传到 flash 文件系统。听起来不错,实操起来还是要连数据线,而且对小白要解释"怎么用 Arduino IDE 的插件上传文件到 SPIFFS",这一步就能劝退一大半人。

第三种方式,就是串口改。接一根 USB-TTL,打开串口监视器,发送指令,比如set ssid MyHome、set pass pass123。这个方案给了我们一个新思路:把配置数据从代码里剥离出来、放到一个可随时修改的存储区。但它仍然依赖物理连接,而且用户需要记住一套命令。对开发者还行,对使用设备的家人朋友来说,完全不现实。

所以真正方便的方案,应该是:不需要数据线、不需要额外 App、不需要让用户理解任何底层概念,打开一个网页填两个框,点保存,完事。

1.2 NVS 是什么?为什么它正好解决这个问题

NVS 的全称是 Non-Volatile Storage,非易失性存储。它位于 ESP32 内部 flash 的一个分区里,专门用来保存键值对数据。你可以把它理解成一个带标签的小抽屉柜:每个抽屉有一个名字(key),抽屉里可以放整数、字符串、二进制数据等。断电之后数据不会丢,下次上电照样能读出来。

在 ESP-IDF 和 Arduino 框架里,NVS 的读写 API 是现成的。它相比 SPIFFS / LittleFS 最大的优势,是天生为"保存配置项"设计,比如 WiFi 账号、设备名称、传感器校准值这一类量少、但需要频繁修改的数据。NVS 内部还做了磨损均衡和掉电保护,你不用操心哪块 flash 被写坏了,也不用担心写一半断电导致数据损坏。

项目标题里说的"浏览器工具能直接改 NVS 键值",其本质就是:ESP32 同时扮演两个角色——一个 WiFi 设备,一个微型 Web 服务器。设备启动时,从 NVS 读取 WiFi 配置去连接路由器。如果你需要改配置,设备就进入 AP 热点模式,你手机连上它的热点,在浏览器里打开一个配置页面,填上新的 WiFi 密码,提交给 ESP32 的 Web 接口,ESP32 再把数据写回 NVS 并重启。整个过程,零数据线、零重编译、几分钟搞定。

这也是我在项目里最终采用的方案。它没有引入任何复杂的框架,就是 ESP32 自带的能力,却能把"改密码"从一次开发流程变成一次日常操作。

2. 方案选型:为什么是"浏览器 + NVS",而不是其他组合

2.1 设备端跑 Web 服务器的思路

ESP32 本身带 WiFi 硬件,既支持 STA(连接路由器)模式,也支持 AP(开热点)模式,甚至两者可以共存。利用这一点,我们可以在设备固件里内置一个极小的 HTTP 服务。硬件上唯一的额外要求是闪存空间,几十 KB 的代码量对 ESP32 来说九牛一毛。

整个交互逻辑非常简单:

  1. 设备上电,尝试从 NVS 读取 WiFi 配置。
  2. 如果读到有效的 SSID 和密码,就 STA 模式连路由器;连不上或者超时,回退到 AP 模式。
  3. 如果 NVS 里根本没有配置,直接进入 AP 模式,热点名比如ESP-Config-XXXX。
  4. 手机或电脑连上这个热点后,浏览器访问http://192.168.4.1,得到网页表单,填入新的 WiFi 信息。
  5. 设备端收到 POST 请求后,把内容写入 NVS、提交、重启,然后以新的配置尝试连接路由器。

这套逻辑在任何支持 WiFi 的嵌入式平台上都是通用的。ESP32 的优势在于它自带 WebServer 相关的库,用 Arduino 框架时可以直接用WebServer.h,不需要自己拼 HTTP 报文,大大降低实现门槛。

2.2 四种方案对比:为什么浏览器方案胜出

我自己把这几种方式放在一张表里对比过,选型时很直观:

方案需要数据线需要安装软件对普通用户友好度开发成本部署后维护成本
改代码重刷固件是是极差低极高,每次改都要重编译
串口命令修改是是差,要懂指令中高,现场维护麻烦
手机 App 配网否是中,要装 App高,要改双端中
浏览器 Web 配置否否好,有手就会中低极低,手机浏览器即可

从表格能看出,浏览器方案在"部署后维护成本"这个维度上几乎是最优的。它没有一个特定的客户端需要维护,任何带浏览器的设备都能操作。开发成本比手机 App 低得多,因为整个交互界面只是一个 HTML 页面加几个 HTTP 接口。

另外一个隐性优点是排障方便。网页是你自己写的,调试时可以直接用浏览器开发者工具看请求是否成功、POST 参数是否正常,比调 App 和串口程序都要直观。我在开发过程中遇到问题,基本是打开浏览器 F12 就能定位,效率高很多。

2.3 为什么不直接用现成的 WiFiManager 库

我知道有人会说:tzapu/WiFiManager 这个库不是早就实现了类似功能吗?确实,WiFiManager 是 ESP32/ESP8266 社区很成熟的配网方案,它也支持把自定义参数保存到 NVS。但我在实际使用中,发现它有两个让我不太舒服的地方。

第一,WiFiManager 的功能很全,全到很多项目用不到,代码体积和逻辑复杂度都比自己写一个小模块要高。如果项目本身很精简,引入一整套 WiFiManager 反而有点重。第二,它的配网页面和保存逻辑是封装好的,我要想定制页面,比如加一个"设备名称"输入框、加一个"MQTT 服务器地址"栏,就得去读它的回调接口文档,折腾一圈下来,不如自己写一个三十行表单加十行处理函数来得直接。

当然,如果你不想写任何代码,直接用 WiFiManager 也是一个非常靠谱的选择。我这里讲的是"浏览器工具改 NVS 键值"这一思路的最小实现,顺便也能让你彻底理解它背后的机制,以后不管是自己写还是用库,心里都有底。

3. 核心细节:NVS 键值操作与 Web 配置页的配合

3.1 NVS 键值读写中最容易踩的三个细节

NVS 的 API 形态在 Arduino 和 ESP-IDF 下略有不同,但核心逻辑都是围绕几个函数展开的:nvs_open/nvs_get_str/nvs_set_str/nvs_commit/nvs_close。我在写代码时发现三个细节,新手非常容易忽略。

第一,写入后一定要调用nvs_commit。很多人写完nvs_set_str就直接断电,发现数据没存住。原因是nvs_set_str只是把数据写入缓存,nvs_commit才真正落盘到 flash。虽然某些情况下系统会自动提交,但这是不确定行为,不能依赖。

第二,读取字符串前要先查询长度。NVS 读字符串不是直接给一个目标缓冲区就完事,它需要你告诉它缓冲区多大。如果你不知道字符串多长,正确做法是先调用nvs_get_str(handle, key, NULL, &length),拿到实际长度后,再用这个长度分配缓冲区,然后第二次调用读取。我看到很多初学者固定一个 64 字节数组,结果密码一长就溢出,数据被截断,WiFi 怎么都连不上。

第三,键名有限制。NVS 的 key 最长 15 个字符,不能包含特殊符号,命名空间最长也是 15 个字符。我在早期项目里用过Router_Password_2023这种键名,结果写入直接返回错误,查了半天文档才发现是长度超限。现在统一用简短风格,比如wifi_ssid、wifi_pass。

3.2 Web 页面到底该放哪:嵌入固件还是放文件系统

要让浏览器能打开配置页面,页面本身有两种存放方式。一种是直接把 HTML 作为字符串常量放进固件,比如 Arduino 里的const char index_html[] PROGMEM = R"rawliteral(...)rawliteral";。好处是页面总是存在,不依赖文件系统,烧录用一种方式就完事;坏处是如果你想把页面做得复杂一点,比如加个 SVG 图标、加个 JS 脚本,代码文件会变得很长,阅读和维护都不舒服。

另一种方式是放在 LittleFS / SPIFFS 分区里,通过LittleFS.begin()和server.serveStatic()来提供访问。好处是页面和固件解耦,改页面不用重新烧录固件;坏处是首次烧录时要额外上传一次文件系统镜像,对不熟悉这套流程的人来说多了一道手续。

我自己目前偏好嵌入固件,因为配置页本身很轻量,一个表单加一小段说明文字就够了。让用户少一个操作步骤,维护成本也更低。如果你的页面比较复杂、需要经常调整样式,再考虑 LittleFS 方案不迟。

3.3 表单提交:GET 还是 POST

浏览器页面提交数据到 ESP32,有 GET 和 POST 两种方式。网上一搜一大把示例用的是 GET,URL 长这样:http://192.168.4.1/save?ssid=MyHome&pass=password123。

GET 的好处是代码少,服务器端处理简单,server.arg("ssid")就能拿到值。但它有两个明显的坑:一是 URL 有长度限制,二是 WiFi 密码会出现在浏览器历史记录、路由器日志里。虽然这只是局域网内的一次配置操作,风险有限,但养成用 POST 的习惯总是好的。

POST 的处理同样不复杂,在 Arduino 的 WebServer 库中,你只需要在表单里写method="post",然后服务端用server.arg("ssid")照样能取到参数,保存逻辑没有任何额外负担。所以我会建议直接上 POST,别在局域网里也把密码挂在 URL 上。

4. 手把手实操:把"NVS 浏览器配置工具"集成进你的 ESP32 工程

4.1 搭建最小工程结构

我用的是 PlatformIO + Arduino 框架,因为它的platformio.ini管理依赖更干净,编译速度的瓶颈主要体现在首次编译,后续增量编译其实也挺快。工程整体结构如下:

esp32_nvs_web_config/ ├── platformio.ini ├── src/ │ └── main.cpp └── include/ └── config.h

platformio.ini里最核心的内容就是选择开发板和上传方式。我用的是esp32dev,烧录速度默认 921600 波特率,实测很稳,不用特地去改。

[env:esp32dev] platform = espressif32 board = esp32dev framework = arduino monitor_speed = 115200 board_upload.flash_size = 4MB

如果你用的是 Arduino IDE,新建工程后记得在"开发板管理器"里选好对应芯片型号,比如 ESP32-S3 要选ESP32S3 Dev Module。剩下的代码逻辑完全一样,只是编译和烧录入口不同。

4.2 核心代码:NVS 读取 + Web 服务

直接上关键代码,这些代码我在多个项目里复用,实测稳定。先定义两个全局变量存 WiFi 配置,再写 NVS 读写函数:

#include <WiFi.h> #include <WebServer.h> #include <Preferences.h> Preferences prefs; String wifi_ssid = ""; String wifi_pass = ""; void loadWiFiConfig() { prefs.begin("wifi", true); // 只读模式打开命名空间 wifi wifi_ssid = prefs.getString("ssid", ""); wifi_pass = prefs.getString("pass", ""); prefs.end(); } void saveWiFiConfig(String ssid, String pass) { prefs.begin("wifi", false); // 读写模式 prefs.putString("ssid", ssid); prefs.putString("pass", pass); prefs.end(); // Preferences 库在 end 时会自动 commit,不需要手动调 }

这里我用的是Preferences库,它是 Arduino 对 NVS 的封装,API 比原生 ESP-IDF 的nvs_*函数友好得多。prefs.end()内部会做提交操作,所以写完后不用担心数据没落盘。

接下来是 Web 服务部分。用WebServer对象监听 80 端口,注册两个路径:根路径/返回配置页表单,/save接收 POST 并保存:

WebServer server(80); void handleRoot() { String html = "<!DOCTYPE html><html><head>" "<meta charset='UTF-8'>" "<meta name='viewport' content='width=device-width, initial-scale=1'>" "<title>ESP32 WiFi配置</title></head><body>" "<h2>WiFi 配置</h2>" "<form method='post' action='/save'>" "<label>WiFi 名称</label><br>" "<input type='text' name='ssid' value='" + wifi_ssid + "'><br><br>" "<label>WiFi 密码</label><br>" "<input type='password' name='pass' value='" + wifi_pass + "'><br><br>" "<input type='submit' value='保存并重启'>" "</form></body></html>"; server.send(200, "text/html", html); } void handleSave() { if (server.hasArg("ssid") && server.hasArg("pass")) { String newSSID = server.arg("ssid"); String newPass = server.arg("pass"); saveWiFiConfig(newSSID, newPass); server.send(200, "text/html", "<h3>已保存,设备正在重启...</h3>"); delay(1000); ESP.restart(); } else { server.send(400, "text/html", "<h3>缺少参数</h3>"); } }

这段代码里的一个细节是,密码框用了type='password',不至于让旁边的人一眼看到密码。另一个细节是保存成功后先server.send再delay再重启,如果直接ESP.restart(),浏览器可能还没收到响应页面就断了,用户体验会差一些。

4.3 启动逻辑:没有配置就进入 AP 模式

整个工具的灵魂是启动逻辑。设备上电后,先读 NVS,判断有没有可用的 WiFi 配置。如果没有,或者连接路由器失败,就打开 AP 模式并启动 Web 服务。

void setup() { Serial.begin(115200); loadWiFiConfig(); if (wifi_ssid.length() > 0) { WiFi.mode(WIFI_STA); WiFi.begin(wifi_ssid.c_str(), wifi_pass.c_str()); unsigned long start = millis(); while (WiFi.status() != WL_CONNECTED && millis() - start < 10000) { delay(500); Serial.print("."); } } if (WiFi.status() != WL_CONNECTED) { // 没有配置或连接失败,进入 AP 配置模式 WiFi.mode(WIFI_AP); WiFi.softAP("ESP-Config-1234"); server.on("/", handleRoot); server.on("/save", HTTP_POST, handleSave); server.begin(); Serial.println("AP Mode: http://192.168.4.1"); } else { Serial.print("Connected, IP: "); Serial.println(WiFi.localIP()); } } void loop() { server.handleClient(); }

这段代码有一个细节值得展开讲:连接超时时间我设的是 10 秒。如果设得太短,比如 3 秒,路由器握手慢一点就会误判连接失败,导致设备频繁进入 AP 模式,用户刚把手机连上热点,设备又重启了,体验很差。如果设得太长,比如 30 秒,又会让"切换 WiFi"这个动作变得很笨重。10 秒是我试下来比较平衡的值,当然你如果知道自己的路由器握手很快,也可以缩短到 5 秒。

另一个容易忽略的点是:设备连接成功后,我故意没有启动 Web 服务。也就是说,正常情况下设备是不会对外开放配置页面的——只有当你需要改密码、让设备进不了网络的时候,它才会开启配置模式。这样既符合直觉,也减少了一个可以被随意改动配置的暴露面。

4.4 浏览器端实际操作,完整流程演示

假设你现在已经烧录好了这个固件,接下来就是普通用户视角的操作了。

  1. 路由器改了密码,ESP32 连不上网络,自动进入 AP 模式。此时设备上的 LED 如果是可编程的,建议做成快闪提示,方便用户感知"它在等配置"。
  2. 手机打开 Wi-Fi 列表,找到ESP-Config-1234这个热点,连接。注意这个热点不需要密码,因为它的作用只是让你进入配置页。
  3. 打开手机浏览器,输入http://192.168.4.1,注意有些安卓手机会因为页面不满足 HSTS 强制跳转 HTTPS,如果打不开,手动换成http://就正常了。
  4. 页面上有两个输入框:WiFi 名称、WiFi 密码。填好新路由器的信息,点击"保存并重启"。
  5. 浏览器提示"已保存,设备正在重启...",设备随即进入 STA 模式,尝试连接新的 WiFi。回到手机 Wi-Fi 列表,选回你家里的路由器,整个改动就完成了。

实测下来,整个操作耗时不到 1 分钟。对比原来的"重编译 + 找线 + 拆机"流程,这就是降维打击。

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

5.1 修改完密码,设备还是连不上原来那个 WiFi

这是我踩过最多次的坑。排查看似简单,其实涉及好几层。首先确认设备是否真的进入了"配置模式"而不是"正常连接模式"。如果你用了串口监视器,能看到打印信息,比如Connected, IP: 192.168.x.x说明它已经连上了旧网络,这时你在 AP 热点里根本找不到它。如果串口打印的是AP Mode,那就说明它确实在等待配置。

其次,检查你是否写对了键名。Preferences库里的命名空间和键名是大小写敏感的,ssid和SSID会被识别为完全不同的两个 key。我在项目里统一用小写,避免混用。如果读取和保存用的键名不一致,保存永远成功,但启动时读到的还是空的。

最后,如果保存成功但连接还是失败,大概率是密码本身输错了。这边建议在配置页上额外增加一个"密码可见"的复选框,用 JavaScript 切换type='password'和type='text',能显著减少手滑输错的概率。别小看这个小功能,它能省掉你大量远程排障的时间。

5.2 手机怎么都打不开 192.168.4.1

这个问题有几个常见原因。第一个原因最普遍:手机连上 ESP32 的热点后,安卓系统检测到这个热点"没有互联网",会在系统层面拦截流量,很多手机会弹出一个"是否保持连接"的提示。如果你点了"断开"或者没留意,手机就会自动切回有网络的 WiFi,导致配置页根本访问不了。正确做法是:在系统提示弹出来时,选择"保持连接 / 不切换网络"。

第二个原因跟浏览器有关。有些手机浏览器默认走 HTTPS,访问http://192.168.4.1时反而会报错或者一直转圈。解决办法是在地址栏完整输入http://192.168.4.1,如果还不行,换一个浏览器试试,比如 Chrome 或 Safari 一般都没问题。

第三个原因就比较冷门了:如果你电脑上开着别的虚拟网卡,比如 VMware、VirtualBox 的虚拟适配器,访问 192.168.4.1 时流量可能走错网卡。这种场景下把虚拟网卡先禁用,再访问一般就通了。我在 Windows 笔记本上遇到过几次,禁用后秒开。

5.3 关于安全和权限:自己的设备也要留点心

用浏览器工具改配置,本质上是把一个 Web 服务暴露给了局域网。虽然设备只在 AP 模式下提供配置页,但如果有别有用心的设备连着你的热点,理论上也能打开这个页面把你的 WiFi 密码改掉。这种场景在城市里概率不高,但我还是建议做两步加固。

第一步,给配置页加一个简易访问密码,比如在 URL 上加一个 token,http://192.168.4.1/?token=1234,或者直接让 AP 热点本身带密码。热点密码可以固定写进固件,或者用设备唯一 ID 生成,比如ESP-Config-1234本身就是一个可传播的标识,热点密码同步显示在串口打印和外壳标签上即可。

第二步,不要在产品页面上明文回显密码。默认情况下,我的示例代码是回显wifi_pass的,这是为了方便调试。但在真正给别人用的产品里,我会把密码输入框留空,仅显示"已配置,如需修改请输入新密码"。这一来防止旁观者偷看,二来避免用户在服务器端日志里留下明文密码。

还有一点是固件本身的安全:NVS 里的数据默认不加密,如果有人拿到设备、读出 flash,仍然可以提取到 WiFi 密码。如果这个设备用在安全性要求较高的环境,可以考虑启用 ESP32 的 flash 加密特性,但这是另一个话题了,普通家用场景不必过度担心。

5.4 频繁重启、NVS 写入磨损问题

你可能会担心:万一用户手滑,一天改 10 次 WiFi 密码,NVS 会不会被写坏?实际上 NVS 底层有磨损均衡机制,会把写入操作分散到不同 flash 扇区。按照 ESP-IDF 的文档说明,NVS 可以承受上万次甚至更多次写入。一天改 10 次的话,理论上可以用好几年,根本不用焦虑。

真正需要留意的是,不要用这个机制去存高频变化的数据,比如传感器每秒钟上报一次就把当前值写入 NVS,那才是加速损耗的用法。配置类数据,本质上就是"偶尔改一次、经常读一次",和 NVS 的定位完美匹配。

6. 写在最后的一些体会

这套"浏览器改 NVS 键值"的方案,我现在已经集成到了三个项目里,一个是智能家居网关,一个是阳台自动浇灌控制器,还有一个是给朋友做的桌面温湿度计。每次他们换路由器,只需要在微信上问我一句"怎么改",我把操作步骤发过去,两分钟就搞定,再也不用上门拆机了。

我觉得它最核心的价值,不在于代码本身有多复杂,而在于它改变了设备维护的交互方式:把"配置"从开发者的编译烧录流程中解放出来,变成普通用户也能完成的日常操作。你仔细想想,NVS 只是提供了一个可靠的存储池,浏览器的价值在于它成为了一种跨平台的配置面板。你甚至不需要额外写 App,不需要学习什么协议,就用了互联网世界最通用的"网页 + 表单"这一套语言,把嵌入式设备和普通用户连接了起来。

最后分享一个小彩蛋:这个思路不只是能改 WiFi 密码。你可以把 MQTT 服务器地址、设备名称、上报间隔、GPIO 引脚定义,甚至温度校准值,都可以用同样的方式存储到 NVS 并通过网页修改。等你想扩展功能的时候,只需要在表单里加几个输入框、在保存函数里多写两行putString,配置能力立刻翻倍。我现在的新项目默认就会做一个"高级配置页",把所有需要在现场调、但又不常见到的参数都塞进去,开发调试时省了非常多事。

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

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

立即咨询