1. 从一个让人抓狂的场景说起
如果你玩过 ESP32,大概率经历过这个场景:设备已经焊好、装进壳子、挂在墙上,突然要换个 WiFi 密码。按照传统流程,你得打开电脑、连上开发板、改代码里的ssid和password、重新编译、重新烧录,运气不好还得按住 BOOT 键手动进下载模式。整个过程少说五分钟,多则半小时,如果设备装在吊顶或者户外,那更是灾难。
我手上有一批基于 ESP32 的温湿度采集节点,分散在几个房间里,最早那批固件把 WiFi 凭据硬编码在代码里。后来路由器换了一次密码,我花了整整一个下午挨个拆下来重刷。那次之后我就下定决心,一定要找一个不用重刷固件就能改 WiFi 配置的方案。
后来我摸索出一套基于NVS(Non-Volatile Storage)加浏览器工具的做法:把 WiFi 凭据存在 NVS 分区里,设备启动时从 NVS 读取;同时设备跑一个轻量 HTTP 服务,提供一个网页界面,你在浏览器里填新密码提交,设备写入 NVS 然后重启,完事。整个过程不需要数据线、不需要编译环境,手机连上设备热点就能操作。
这套方案解决的核心问题就一个:把"改配置"和"刷固件"这两件事彻底解耦。固件负责逻辑,NVS 负责数据,浏览器负责交互。适合所有做 ESP32 物联网项目、需要现场调试或远程维护的开发者,哪怕你刚入门,照着做也能跑通。
2. 为什么是 NVS 加浏览器工具这套组合
2.1 硬编码、SPIFFS 文件、NVS 三种方案对比
在决定用 NVS 之前,我把常见的几种配置存储方案都试了一遍,这里直接给结论。
| 方案 | 修改方式 | 是否需要重刷 | 掉电保存 | 磨损寿命 | 适合场景 |
|---|---|---|---|---|---|
| 代码硬编码 | 改源码重编译 | 是 | 是 | 不涉及 | 一次性量产、配置永不变 |
| SPIFFS/LittleFS 文件 | 串口或网页上传文件 | 否 | 是 | 一般 | 存大块配置、网页资源 |
| NVS 键值对 | API 读写 | 否 | 是 | 高(带磨损均衡) | 小配置项、频繁读写 |
| EEPROM 模拟 | API 读写 | 否 | 是 | 较低 | 老项目兼容 |
硬编码的问题不用多说,改一次哭一次。SPIFFS 文件方案其实也能用,但它的定位是存文件系统,用来存几个字符串有点杀鸡用牛刀,而且文件系统挂载本身有失败风险。NVS 是 ESP-IDF 原生提供的键值存储,底层跑在 flash 的一个独立分区上,自带磨损均衡和掉电保护,专门为存配置参数设计,读写几个字符串又快又稳。
提示:NVS 分区大小在分区表里定义,默认
nvs分区通常是 0x6000(24KB),存 WiFi 凭据、设备 ID、校准参数这类小数据绰绰有余。如果你要存几百条记录,那得考虑加大分区或者换文件系统。
2.2 为什么交互层选浏览器而不是串口或蓝牙
串口改配置需要电脑和线,蓝牙需要装 App 或者用通用 BLE 调试工具,体验都不如浏览器。浏览器的优势在于:任何有屏幕和浏览器的设备都能用,手机、平板、电脑通吃,不需要装任何东西。ESP32 本身就能跑 HTTP 服务器,用WebServer库或者 ESP-IDF 的httpd组件,几十行代码就能起一个配置页面。
具体流程是这样的:设备上电后先尝试用 NVS 里的凭据连 WiFi,如果连不上或者 NVS 里没有凭据,就切换成 AP 模式,自己发一个热点。你用手机连上这个热点,浏览器打开192.168.4.1,就能看到配置页面。填好新的 SSID 和密码提交,设备写入 NVS,重启,用新凭据连网。整个过程设备不用拆、不用接线。
2.3 这套方案能解决哪些真实痛点
我总结下来,它主要解决四类问题。第一类是现场部署,设备装好了才发现密码填错,不用拆机。第二类是批量维护,路由器换密码后,设备自动进 AP 模式等你改,不用挨个找。第三类是多环境切换,同一块板子拿到公司、家里、客户现场,连不同 WiFi 不用改代码。第四类是给非技术人员用,你把设备给客户,客户自己就能配网,不用你远程指导烧录。
这里有个设计上的取舍值得说:设备启动时是"先连再退"还是"先判断再连"。我的做法是启动后先读 NVS,如果有凭据就尝试连接,超时 10 秒连不上就切 AP 模式;如果 NVS 里压根没凭据,直接进 AP 模式。这样正常使用时启动快,异常时又能自动兜底。
3. NVS 读写的核心细节与避坑要点
3.1 NVS 的命名空间、键名和数据类型
NVS 的结构是"命名空间(namespace)+ 键(key)+ 值(value)"。命名空间类似文件夹,键类似文件名,值就是数据。命名空间和键名都有长度限制,命名空间最长 15 个字符,键名最长 15 个字符,超了会报错。这个坑我踩过,当时用wifi_configuration当命名空间,编译能过但运行时报ESP_ERR_NVS_KEY_TOO_LONG,查了半天才发现是长度问题。
数据类型方面,NVS 支持整数(i8/u8/i16/u16/i32/u32/i64/u64)、字符串、二进制大对象(blob)。WiFi 的 SSID 和密码用字符串存就行。注意字符串是以 null 结尾的,实际占用空间是长度加一。
// ESP-IDF 下写入 WiFi 凭据的典型代码 #include "nvs_flash.h" #include "nvs.h" void save_wifi_credentials(const char *ssid, const char *password) { nvs_handle_t handle; esp_err_t err = nvs_open("wifi_cfg", NVS_READWRITE, &handle); if (err != ESP_OK) { printf("nvs_open failed: %s\n", esp_err_to_name(err)); return; } nvs_set_str(handle, "ssid", ssid); nvs_set_str(handle, "pass", password); nvs_commit(handle); // 必须 commit 才真正落盘 nvs_close(handle); }注意:
nvs_set_str之后一定要调用nvs_commit,否则数据可能还在缓存里,掉电就丢了。这个和数据库的事务提交是一个道理,很多人第一次用会漏掉。
3.2 读取时的容错处理
读取比写入更需要小心,因为首次上电时 NVS 里是空的,直接读会返回ESP_ERR_NVS_NOT_FOUND。如果不处理这个错误,程序可能崩溃或者读到乱码。正确的做法是判断返回值,读不到就用默认值或者进 AP 模式。
bool load_wifi_credentials(char *ssid, size_t ssid_len, char *pass, size_t pass_len) { nvs_handle_t handle; if (nvs_open("wifi_cfg", NVS_READONLY, &handle) != ESP_OK) { return false; } esp_err_t e1 = nvs_get_str(handle, "ssid", ssid, &ssid_len); esp_err_t e2 = nvs_get_str(handle, "pass", pass, &pass_len); nvs_close(handle); return (e1 == ESP_OK && e2 == ESP_OK); }这里有个细节:nvs_get_str的第三个参数是缓冲区大小,传入的是指针,函数会把它更新为实际读取的长度。如果你传的缓冲区太小,会返回ESP_ERR_NVS_INVALID_LENGTH。所以缓冲区至少给 33 字节(WiFi SSID 最大 32 字节加结尾符),密码给 64 字节比较稳妥。
3.3 NVS 分区被写满或损坏怎么办
NVS 虽然带磨损均衡,但如果你频繁写入(比如每秒写一次),分区迟早会满。满了之后nvs_set_str会返回ESP_ERR_NVS_NOT_ENOUGH_SPACE。解决办法有两个:一是调用nvs_erase_key删掉旧键再写,二是用nvs_erase_all清空整个命名空间。我的做法是写入前先 erase 对应的键,避免旧数据占空间。
还有一种情况是 NVS 分区损坏,通常发生在固件升级或者 flash 操作异常之后。这时候nvs_flash_init会返回ESP_ERR_NVS_NO_FREE_PAGES或ESP_ERR_NVS_NEW_VERSION_FOUND。标准处理方式是擦除后重新初始化:
esp_err_t ret = nvs_flash_init(); if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { nvs_flash_erase(); ret = nvs_flash_init(); }提示:
nvs_flash_erase会清掉所有命名空间的数据,包括你可能存的其他配置。所以如果你的项目里 NVS 还存了别的关键数据,擦除前要三思,或者把 WiFi 配置单独放一个分区。
4. 浏览器配置页面的完整实现
4.1 HTTP 服务器的搭建与路由设计
在 ESP32 上起 HTTP 服务,Arduino 环境用WebServer库最省事,ESP-IDF 环境用httpd组件。我这里以 Arduino 为例,因为它对新手更友好。核心思路是注册两个路由:GET /返回配置页面 HTML,POST /save接收表单数据并写入 NVS。
#include <WiFi.h> #include <WebServer.h> #include <Preferences.h> WebServer server(80); Preferences prefs; void handleRoot() { String html = "<!DOCTYPE html><html><body>"; html += "<h2>WiFi Config</h2>"; html += "<form action='/save' method='POST'>"; html += "SSID:<br><input name='ssid'><br>"; html += "Password:<br><input name='pass' type='password'><br><br>"; html += "<input type='submit' value='Save'>"; html += "</form></body></html>"; server.send(200, "text/html", html); } void handleSave() { String ssid = server.arg("ssid"); String pass = server.arg("pass"); prefs.begin("wifi_cfg", false); prefs.putString("ssid", ssid); prefs.putString("pass", pass); prefs.end(); server.send(200, "text/html", "<h3>Saved. Rebooting...</h3>"); delay(1000); ESP.restart(); }Arduino 的Preferences库其实就是对 NVS 的封装,begin对应nvs_open,putString对应nvs_set_str,end会自动 commit。用起来比原生 API 简洁不少,适合快速开发。
4.2 AP 模式与 STA 模式的自动切换逻辑
设备启动后的状态机是这样的:先读 NVS,有凭据就进 STA 模式尝试连接,连上就正常跑业务;连不上或者没凭据,就进 AP 模式起热点和 HTTP 服务。这里的关键是连接超时要设合理,太短了网络慢的时候连不上,太长了用户等得着急。我实测 10 到 15 秒比较合适。
void setup() { Serial.begin(115200); prefs.begin("wifi_cfg", true); String ssid = prefs.getString("ssid", ""); String pass = prefs.getString("pass", ""); prefs.end(); if (ssid.length() > 0) { WiFi.mode(WIFI_STA); WiFi.begin(ssid.c_str(), pass.c_str()); unsigned long start = millis(); while (WiFi.status() != WL_CONNECTED && millis() - start < 15000) { delay(500); } } if (WiFi.status() != WL_CONNECTED) { WiFi.mode(WIFI_AP); WiFi.softAP("ESP32_Config", "12345678"); server.on("/", handleRoot); server.on("/save", HTTP_POST, handleSave); server.begin(); } }注意:AP 模式的热点密码别设太简单也别不设,不设密码的话任何人都能连上来改你的配置。设个
12345678这种临时密码就行,反正配完就重启了。
4.3 配置页面的安全与体验优化
基础版本能用,但有几个地方可以优化。第一是表单校验,SSID 不能为空,密码长度至少 8 位(WPA2 要求),前端用required和minlength属性就能做。第二是提交后反馈,别让用户点了按钮没反应,返回一个"保存成功,设备重启中"的页面。第三是防止误操作,可以加一个确认弹窗。
还有一个容易被忽略的点:AP 模式下 DNS 劫持。默认情况下用户得手动输入192.168.4.1,体验不好。可以起一个 DNS 服务器,把所有域名解析到192.168.4.1,这样用户连上热点后随便打开一个网页都会跳到配置页。Arduino 环境下用DNSServer库几行就能搞定。
#include <DNSServer.h> DNSServer dnsServer; dnsServer.start(53, "*", WiFi.softAPIP());这个技巧在商业产品里很常见,用户连上热点自动弹配置页,不用记 IP。不过要注意 DNS 服务器会占用资源,配置完成后记得关掉。
5. 实操全流程与关键环节记录
5.1 从零搭建的完整步骤
我把整个流程拆成可复现的步骤,你照着做就行。
准备开发环境。Arduino IDE 装好 ESP32 支持包,或者用 PlatformIO。我推荐 PlatformIO,依赖管理更清晰,编译也快。开发板选
ESP32 Dev Module,如果是 S3 核心板就选对应的 S3 型号。确认分区表。默认分区表里已经有
nvs分区,一般不用改。如果你用的是自定义分区表,确保nvs分区存在且不小于 0x5000。写代码。把上面的 NVS 读写、HTTP 服务、模式切换逻辑整合到一个
.ino或.cpp文件里。代码量大概 150 行左右。烧录测试。第一次烧录后,设备因为没有 NVS 凭据会直接进 AP 模式。手机连上
ESP32_Config热点,浏览器打开192.168.4.1,填 WiFi 信息提交。验证。设备重启后应该连上你填的 WiFi,串口打印出获取到的 IP 地址。再改一次密码,确认不用重刷就能生效。
5.2 关键参数的计算与选择
有几个参数需要根据实际情况调整。连接超时我设 15 秒,因为有些企业级 AP 的认证流程比较慢。AP 热点信道默认是 1,如果周围干扰严重可以改成 6 或 11。HTTP 服务器端口用 80,方便用户直接输 IP 不用带端口号。
NVS 分区大小方面,存两个字符串大概占用不到 100 字节,加上 NVS 自身的元数据开销,24KB 的分区能存几百条记录。如果你还要存其他配置,算一下总量别超就行。计算公式很简单:每条记录占用约键名长度 + 值长度 + 32 字节元数据。
5.3 实测记录与性能数据
我在 ESP32-WROOM-32 上实测了一组数据。从提交表单到设备重启完成并连上新 WiFi,整个过程约 8 到 12 秒,其中 NVS 写入耗时不到 10 毫秒,重启约 3 秒,WiFi 连接约 5 到 8 秒。HTTP 页面加载时间在 AP 模式下约 200 毫秒,完全可接受。
功耗方面,AP 模式比 STA 模式费电,实测 AP 模式电流约 120mA,STA 模式约 80mA。所以配置完成后一定要切回 STA 模式,别一直挂着 AP。我的代码里是重启后自然切回 STA,如果你不想重启,也可以动态切换模式,但要注意先关 HTTP 服务和 DNS 服务再切。
6. 常见问题与排查技巧实录
6.1 连不上 WiFi 的排查顺序
设备连不上 WiFi 是最常见的问题,我整理了一个排查顺序。先看串口日志,确认是 NVS 读取失败还是连接超时。如果是 NVS 读取失败,检查命名空间和键名有没有写错。如果是连接超时,检查 SSID 和密码有没有多余空格,特别是从网页表单复制粘贴的时候容易带空格。
还有一个隐蔽的坑:WiFi 密码里的特殊字符。如果密码里有&、=、+这些字符,在 HTTP POST 表单里需要 URL 编码,否则server.arg拿到的数据是错的。Arduino 的WebServer库会自动解码,但如果你自己拼 HTML 表单,要注意enctype设置。
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 串口报 NVS_NOT_FOUND | 首次上电,NVS 为空 | 正常,进 AP 模式配置即可 |
| 串口报 NVS_KEY_TOO_LONG | 命名空间或键名超 15 字符 | 改短命名空间和键名 |
| 连 WiFi 一直超时 | SSID/密码错误或信号弱 | 检查凭据,靠近路由器测试 |
| 提交表单后无反应 | HTTP 路由未注册或方法不对 | 确认server.on注册了 POST |
| 重启后还是 AP 模式 | NVS 写入未 commit | 检查prefs.end()是否调用 |
6.2 NVS 数据读出来是乱码怎么办
乱码通常有两个原因。一是读取缓冲区没初始化,nvs_get_str只写入实际长度的数据,如果你缓冲区里是脏数据,超出部分就是乱码。解决办法是读取前memset清零。二是数据类型不匹配,比如你用nvs_set_i32写进去,却用nvs_get_str读,那肯定乱。写入和读取的类型必须一致。
提示:调试 NVS 的时候,可以用
nvs_get_stats查看分区使用情况,包括已用条目数、剩余空间等。这个 API 在排查"分区满"问题时特别有用。
6.3 浏览器页面打不开的几种情况
页面打不开,先确认手机是不是真的连上了 ESP32 的热点。有些手机会自动切换到移动数据,导致访问192.168.4.1走的是蜂窝网络。解决办法是在手机 WiFi 设置里关掉"自动切换网络"或者"智能网络切换"。
如果热点连上了但页面还是打不开,检查 HTTP 服务器有没有server.begin(),以及端口是不是 80。还有一种情况是浏览器缓存了旧页面,强制刷新或者换个浏览器试试。我遇到过 Chrome 缓存导致改了代码但页面没变的情况,折腾了半小时才发现是缓存问题。
6.4 几个我踩过的坑和独家技巧
第一个坑:AP 模式和 STA 模式不能同时用同一个 WiFi 库实例。如果你先WiFi.softAP再WiFi.begin,行为是不确定的。正确做法是先WiFi.mode(WIFI_AP_STA)再分别配置,或者干脆分时复用,配置完重启。
第二个技巧:在配置页面加一个"恢复出厂设置"按钮。调用nvs_erase_all清空配置然后重启,设备就回到 AP 模式。这个功能在设备换环境时特别有用,不用重新烧录。
第三个技巧:把设备 IP 显示在串口和 OLED 上。如果你的设备带屏幕,连上 WiFi 后把 IP 显示出来,方便你找到它。没有屏幕的话,串口打印也行,至少调试的时候不用猜。
第四个坑:NVS 写入频率。有人在循环里不停写 NVS,结果几个月就把分区写坏了。NVS 虽然有磨损均衡,但也是有寿命的。配置类数据只在用户提交时写一次,千万别在循环里写。
7. 方案扩展与进阶玩法
7.1 加上 mDNS 让设备有个好记的名字
每次输 IP 太麻烦,可以加 mDNS,让设备有个域名。ESP32 的 Arduino 环境用ESPmDNS库,两行代码就能注册一个esp32.local这样的名字。这样你在浏览器里输http://esp32.local就能访问配置页,不用记 IP。不过 mDNS 在部分安卓手机上支持不好,iOS 和电脑上没问题。
#include <ESPmDNS.h> MDNS.begin("esp32"); MDNS.addService("http", "tcp", 80);7.2 配置项扩展:不止 WiFi
NVS 这套机制不只用来存 WiFi,你可以把设备 ID、服务器地址、上报间隔、校准系数都存进去。配置页面相应扩展成多个输入框,提交时一起写入。这样设备的所有可调参数都能通过浏览器改,真正做到"零重刷维护"。
我现在的项目里,NVS 存了 WiFi 凭据、MQTT 服务器地址、设备别名、采样间隔四个配置项,配置页面用一个大表单搞定。用户拿到设备后自己配一次,之后除非换环境,否则不用再动。
7.3 远程配置的思路
AP 模式配置需要人到设备旁边,如果设备装在够不着的地方怎么办?可以在 STA 模式下也开一个 HTTP 服务,连上 WiFi 后通过局域网 IP 访问配置页。这样只要设备和你在同一个网络里,就能远程改配置。更进一步,可以接一个 MQTT 主题,收到配置消息后写入 NVS 并重启,实现真正的远程配置。
不过远程配置要注意安全,至少加个简单的 token 校验,别让局域网里任何人都能改你的设备。我的做法是在 NVS 里存一个随机生成的 token,配置请求必须带上正确的 token 才处理。
7.4 和 OTA 升级的配合
NVS 配置和 OTA 升级是绝配。OTA 升级固件不会动 NVS 分区,所以升级后 WiFi 配置还在,设备自动连网。这样你就能实现"远程推固件、配置不丢失"的完整闭环。ESP32 的 OTA 用ArduinoOTA库或者 ESP-IDF 的esp_https_ota组件都行,升级前记得检查固件签名,别把设备刷成砖。
我现在的维护流程是:设备出厂时烧一个带 AP 配置和 OTA 的基础固件,客户自己配网。后续固件更新通过 OTA 推送,配置存在 NVS 里不受影响。客户全程不用碰设备,体验很好。
这套 NVS 加浏览器配置的方案,我从最早的硬编码一路迭代过来,现在已经是所有 ESP32 项目的标配。它不复杂,但确实解决了实际维护中的大麻烦。如果你还在用改代码重刷的方式改 WiFi,真的建议花半天时间把这套机制加上,后面省下的时间远超这点投入。