
朋友拿了一块 ESP32 做小气象站结果 WiFi 密码一换他第一反应是又要拆壳重刷固件。我相信不少人都有同样经历ESP32 改个 WiFi 密码代码里改一下重新编译接串口烧录有时候还要先擦除折腾半小时起步。事实上NVS非易失性存储就是 ESP32 里专门用来存这类运行时配置的地方WiFi 凭据、设备参数全都住在里面。问题在于绝大多数固件根本没有提供修改 NVS 键值的入口。这篇文章就来讲一个浏览器工具的完整思路把 NVS 键值变成网页上的表单连上设备热点就能直接改从此告别为了一个密码反复重刷固件的日子。1. 为什么改个 WiFi 密码要重刷固件1.1 传统方案的痛点硬编码与重编译早期玩 ESP32 的时候大部分人写的都是这种代码WiFi.begin(MyWiFi, 12345678);SSID 和密码直接写死在程序里。这种写法对开发板测试没问题但一旦设备部署出去问题就来了。WiFi 密码换了维护人员就得把设备拆下来带回有编译环境的地方改源码、重新编译、接线烧录。如果设备装在弱电箱、屋顶或者户外防水盒里光是拆装就是一轮体力活。更麻烦的是频繁烧录本身也会带来风险。虽然 ESP32 的 flash 寿命足够长但每次烧录都要擦除、写入操作不当还会把分区表搞乱。而且烧录前如果忘了备份设备里的校准数据、设备编号一次全片擦除可能把之前保存在 flash 里的所有业务数据清掉。所以“改密码就要重刷固件”这件事本质上不是因为技术做不到而是因为固件里没有一个“运行时配置入口”。1.2 NVS为运行时配置而生的存储区NVS 全称 Non-Volatile Storage是 ESP-IDF 提供的一套基于 flash 的键值对存储系统。它和我们熟悉的 Preferences 库是同一个底层实现Preferences 只是对 NVS 做了一层更方便的封装。NVS 支持命名空间、字符串、整型、二进制块掉电不丢失特别适合存 WiFi 凭据、设备编号、传感器校准值、上报周期这类需要经常变动的小数据。默认分区表里NVS 分区的大小是 0x6000也就是 24KB 左右。别看不大存几十个键值对绰绰有余。NVS 的读写流程一般是先nvs_open打开一个命名空间然后调用nvs_set_str、nvs_get_str读写键值最后nvs_commit提交。每次写操作都会经过磨损均衡机制尽量让写入均匀分布在 flash 页面上避免某个扇区被反复擦写提前报废。关键点在于ESP32 自带的 WiFi 驱动本来就会把当前连接过的 WiFi 配置写进 NVS 的一个内部命名空间。也就是说设备其实一直具备“记住 WiFi 密码”的能力。只是普通固件没有把修改这个键值的入口暴露出来所以我们只能傻乎乎地回去改代码重编译。1.3 核心思路把“改配置”这件事从编译期搬到运行期想通上面这点解决思路就非常清晰了。我们需要在固件里常驻一个小型配置服务设备启动后先尝试连接已保存的 WiFi如果连不上就自动开启一个 AP 热点。用户用手机连上这个热点浏览器访问一个固定 IP页面上列出当前 NVS 里的关键键值填上新的 WiFi 密码点击保存设备自动重启并连上新的网络。这样做的好处是显而易见的第一次烧录过后以后所有配置变更都走浏览器不需要再碰编译器和烧录器。而且这个工具不只改 WiFi 密码MQTT 服务器地址、设备名称、传感器阈值、上报间隔凡是存进 NVS 的键值都可以在这个网页上直接改。一套工具解决所有运行时配置问题。2. 工具的整体设计与核心原理2.1 为什么用浏览器而不是串口助手很多人第一反应是用串口助手不就行了加几条 AT 命令或者自定义串口协议也能改 NVS 键值。确实可以这么做但实际用下来会发现几个很现实的问题。串口方案要求维护人员带着笔记本电脑去现场还要装 USB 转串口驱动打开串口助手软件选对 COM 口和波特率再输入命令。这套操作对熟悉嵌入式开发的人来说不难但很多时候负责现场维护的并不是开发者本人。而浏览器方案就友好得多手机连上设备热点打开浏览器输一个 IP页面上直接就是输入框和保存按钮是个人就会用。另外浏览器方案几乎没有平台兼容性问题。Windows、macOS、Linux、安卓、iOS 都能打开同一个网页。Windows 上常见的 CP210x、CH340 这类串口芯片驱动问题在浏览器方案里完全不存在因为网络配置根本不需要碰串口。配置方式环境要求操作难度适合场景串口助手电脑、驱动、串口线较高开发调试、固件底层维护WebSerial浏览器、串口线中等批量写入、开发调试设备内置 Web 页面手机连设备热点低现场维护、用户自助配置2.2 关键设计通用 NVS 键值读写接口既然是“直接改 NVS 键值”工具的核心就应该是一个通用的键值读写接口而不是只针对 WiFi 做一个死页面。我建议在固件里提供两个 HTTP 接口GET /api/nvs?ns命名空间key键名读取指定键值返回 JSON。POST /api/nvs写入指定键值参数包括命名空间、键名、类型、值。这两个接口就是整个工具的“通用后端”。页面上展示什么、隐藏什么完全由前端决定。无论是 WiFi 密码、服务器地址还是设备编号只要按照参数格式提交后端统一写到 NVS 里。不过有个经验要提醒一下对于 WiFi 配置这种驱动内部管理的键值千万别让用户直接通过通用接口去改“sta.ssid”、“sta.pswd”这类内部键名。一是不同 SDK 版本的键名和存储格式不完全一致容易写错二是 WiFi 驱动在运行时有自己的缓存直接改了 NVS 里的值驱动不一定立刻生效。正确做法是提供独立的 WiFi 配置接口内部调用驱动 API驱动自己会完成持久化。通用 NVS 接口留给业务配置项使用。2.3 三种典型运行模式我在实际测试中总结了三种运行模式按使用场景灵活切换。第一种是 AP 配置模式。设备启动后如果在 10 秒内没有连上已保存的 WiFi就自动开启热点热点名称可以做成“ESP32-Config-xxxx”这种格式。用户连上热点后访问 192.168.4.1看到的就是配置页面。这个模式最适合首次部署和 WiFi 密码修改。第二种是 STA 局域网模式。设备已经连上路由器局域网内的电脑或手机通过设备 IP 访问配置页面。这个模式适合远程修改 MQTT 参数、设备阈值人不用走过去只要在同一局域网内就能操作。第三种是 WebSerial 模式。浏览器通过 Web Serial API 直接连到 ESP32 的串口设备端跑一个简单的命令解析服务。这种方式适合批量生产场景一条 USB 线连着电脑浏览器页面自动逐台写入设备编号。后面我会单独讲这个模式的坑。3. 实战从零实现一个 NVS Web 配置工具3.1 准备环境与开发板选择我平时习惯用 PlatformIO配置起来比较干净。板子如果是普通的 ESP32 DevKitC选择esp32dev如果是 ESP32-S3 核心板选择esp32-s3-devkitc-1。注意 ESP32-S3 有些板子用的是 USB 串口芯片驱动和普通板子不一样但烧录流程没有区别。烧录设置方面串口波特率可以开到 921600速度快很多。如果供电不稳建议外接 5V 电源不要完全依赖 USB 口否则烧录大固件的时候经常报连接超时。环境准备好之后核心依赖就三个WiFi 库、WebServer 库、NVS 库。ESP32 的 Arduino 核心已经全部内置不需要额外装第三方库。如果你用 ESP-IDF直接在组件里启用nvs_flash和esp_http_server即可。3.2 核心代码初始化与 WebServer先写初始化部分。NVS 在 ESP-IDF 环境需要手动初始化Arduino 环境系统已经初始化过一次但为了代码通用性我还是建议带上完整的错误处理#include nvs.h #include nvs_flash.h #include WiFi.h #include WebServer.h WebServer server(80); void initNVS() { esp_err_t err nvs_flash_init(); if (err ESP_ERR_NVS_NO_FREE_PAGES || err ESP_ERR_NVS_NEW_VERSION_FOUND) { nvs_flash_erase(); err nvs_flash_init(); } if (err ! ESP_OK) { Serial.printf(NVS init failed: %s\n, esp_err_to_name(err)); } }这段代码里最关键的判断是ESP_ERR_NVS_NO_FREE_PAGES。这个错误的意思是 NVS 分区里找不到足够的连续页来初始化常见原因是分区表被改过或者 NVS 里的数据结构版本不一致。遇到这个错误常规做法是先擦除再重新初始化。但要注意擦除会把 NVS 里所有键值清空所以生产环境的固件不应该自动执行擦除最好只在配置模式下手动触发。接着初始化 WebServer并注册路由void setup() { Serial.begin(115200); initNVS(); server.on(/api/nvs, HTTP_GET, handleNvsRead); server.on(/api/nvs, HTTP_POST, handleNvsWrite); server.on(/api/wifi, HTTP_POST, handleWifiSave); server.on(/, handleRoot); server.begin(); }/返回配置页面的 HTML/api/nvs处理通用键值读写/api/wifi是专门给 WiFi 配置用的独立接口。路由分开的好处是职责清晰不容易把通用接口用错地方。3.3 核心代码通用 NVS 键值读写读取键值的逻辑很简单封装成一个函数String readNvsString(const char* ns, const char* key) { nvs_handle_t h; char buf[128] {0}; if (nvs_open(ns, NVS_READONLY, h) ! ESP_OK) return ; size_t len sizeof(buf); if (nvs_get_str(h, key, buf, len) ! ESP_OK) { nvs_close(h); return ; } nvs_close(h); return String(buf); }写入键值稍微注意一点必须调用nvs_commit否则数据不一定真正落盘bool writeNvsString(const char* ns, const char* key, const char* value) { nvs_handle_t h; if (nvs_open(ns, NVS_READWRITE, h) ! ESP_OK) return false; esp_err_t err nvs_set_str(h, key, value); if (err ESP_OK) err nvs_commit(h); nvs_close(h); return err ESP_OK; }两个 HTTP handler 再包一层void handleNvsRead() { String ns server.arg(ns); String key server.arg(key); String value readNvsString(ns.c_str(), key.c_str()); String json {\ns\:\ ns \,\key\:\ key \,\value\:\ value \}; server.send(200, application/json, json); } void handleNvsWrite() { String ns server.arg(ns); String key server.arg(key); String value server.arg(value); if (ns.length() 0 || key.length() 0 || value.length() 128) { server.send(400, text/plain, bad request); return; } if (writeNvsString(ns.c_str(), key.c_str(), value.c_str())) { server.send(200, text/plain, ok); } else { server.send(500, text/plain, nvs write failed); } }这里有个容易忽略的坑NVS 的键名最长不能超过 15 个字符不是 32、不是 64就是 15。命名空间最长也是 15 个字符。如果你的业务键名超过这个长度写入会直接报错。我第一次做的时候用了一个叫sensor_calibration_offset的键结果保存按钮点了没反应查了半天日志才发现是键名太长改成calib_offset就正常了。3.4 核心代码WiFi 密码保存专用接口通用接口我们已经有了但 WiFi 密码不建议走通用接口。我在前面说过WiFi 驱动在运行时有自己的配置缓存直接改 NVS 内部键名不靠谱。正确的做法是调用驱动 API让驱动来完成持久化。void handleWifiSave() { String ssid server.arg(ssid); String pswd server.arg(pswd); if (ssid.length() 0 || pswd.length() 8) { server.send(400, text/plain, invalid ssid or password); return; } WiFi.begin(ssid.c_str(), pswd.c_str()); server.send(200, text/plain, saved, rebooting...); delay(2000); ESP.restart(); }几个细节说明一下。WPA2 个人版密码最短 8 位所以少于 8 位直接拒绝。WiFi.begin 会触发连接同时把凭据交给驱动保存到 NVS。我加了 2 秒延时再重启是为了给驱动一点时间把配置完整写入 flash避免刚写完就断电导致配置丢失。这个问题我在真机上踩过保存后马上重启重启起来发现密码还是旧的。3.5 前端页面与完整操作流程前端页面不需要复杂框架一个表单页就够用。我通常是内嵌一个很精简的 HTML包含三个区块WiFi 配置表单、通用键值读写表单、保存按钮和状态提示。页面整体不到 4KB不占用多少 flash 空间。实际使用流程是这样的给设备上电等待 10 秒左右如果设备没连上之前保存的 WiFi就会自动开启 AP 热点。用手机打开 Wi-Fi 列表找到类似ESP32-Config-xxxx的热点并连接。手机浏览器访问192.168.4.1。页面显示当前保存的 WiFi SSID在密码框里输入新密码点击保存。设备提示保存成功自动重启并连接新 WiFi。手机切回原来的网络完成。整个过程不需要电脑不需要串口线更不需要重新编译固件。3.6 首次烧录与后续维护这里要诚实地说一点这个工具并不能让设备“永远不用烧录”。第一次还是要烧一次因为固件里得先有这个配置服务。但烧录一次之后后续所有配置变更都走浏览器再也不碰编译器和烧录器。如果你自己做产品还可以把这个配置模块长期保留在固件里。之后 OTA 升级只更新应用层代码配置服务始终保持可用。这样即使设备换了个环境、换了 WiFi维护人员也不需要拆机长按复位再连热点就能重新配置。4. 常见问题与排查技巧实录4.1 保存后设备连不上 WiFi怎么排查这是最常遇到的问题我整理了一个排查顺序。先看页面返回什么。如果保存时提示“invalid”说明输入被前端拦了最常见是密码少于 8 位。如果提示“ok”但设备重启后还是连不上建议先打开串口监视器看日志。设备启动日志里会打印 WiFi 连接状态码常见的NO_AP_FOUND表示附近搜不到这个 SSIDAUTH_FAIL表示密码错误。还有个特别容易踩的坑ESP32 只支持 2.4GHz 频段不支持 5GHz WiFi。如果你的手机开的移动热点是 5GHz设备永远连不上。路由器也需要确认是不是开了双频合一把 5G 频段信号和 2.4G 频段信号混在一个 SSID 下。解决方法是登录路由器把 2.4G 和 5G 的 SSID 分开或者直接建一个专门的 2.4G IoT 网络。4.2 NVS 初始化失败与空间不足如果你改了分区表或者烧录了不同 SDK 版本的固件启动时可能看到这串日志E (123) nvs: The NVS partition contains data in new format这个问题的本质是旧版本 NVS 和新版本不兼容。处理办法和前面代码里写的一样调用nvs_flash_erase()后重新初始化。但注意这操作会清空所有键值设备会变成“出厂状态”。所以我建议把恢复出厂功能做成一个独立按钮放在页面最底部而不是让固件每次开机都自动擦除。如果日志里出现ESP_ERR_NVS_NO_FREE_PAGES而你已经擦除过那很可能是 NVS 分区真的太小了。默认 24KB 对大部分场景够用但如果一个命名空间里存了大量键值还是可能写完。这时候需要改分区表把 NVS 分区调大。改完分区表后记得重新烧录 partition table只烧应用固件不烧分区表是没用的。4.3 浏览器打不开配置页面手机连上了热点但浏览器访问 192.168.4.1 没反应这个现象也出现过不少次。第一确认设备是不是真的进入了 AP 模式。如果设备已经连上了旧 WiFi它不会开热点。这时候可以按一下板子上的 EN 键复位或者在代码里加一个强制进入配置模式的逻辑比如开机时按住某个 GPIO 不放持续 3 秒就强制进入 AP 模式。第二确认手机有没有自动跳回原来的网络。很多手机会在锁屏或信号差的时候自动切换到蜂窝网络导致设备和热点之间的连接断开。建议连接热点后把手机的“自动切换网络”关掉。第三注意 192.168.4.1 这个地址。在 AP 模式下 WebServer 绑定这个地址确实能访问但如果固件里做了其它网络配置IP 可能不一样。最简单的方法是看串口日志设备启动时会打印类似AP started, IP: 192.168.4.1的信息。4.4 WebSerial 模式的应用与坑如果你的场景是批量配置比如生产线上一次烧 100 块板子每块板子的设备编号不一样那用 WebSerial 模式会比较顺手。WebSerial 是浏览器里的一个 API可以像串口助手一样直接和 ESP32 通信。使用限制是浏览器必须是 Chrome 或 Edge而且页面必须在 HTTPS 环境下打开或者 localhost 豁免。本地调试的时候直接在浏览器地址栏输http://localhost就行。如果你把网页部署到服务器上那就必须用 HTTPS否则浏览器不会弹出串口选择框。设备端需要实现一个简单的串口命令解析。比如收到READ ns:key就返回当前键值收到WRITE ns:key:value就写 NVS。这个方案的好处是网页可以批量处理一条 USB 线插上浏览器自动识别串口写入设备编号后提示完成再换下一块板子。坑也有一个WebSerial 需要先占用串口此时 Arduino IDE 的串口监视器必须关闭否则浏览器连不上。另外 CH340 这类芯片在某些系统上驱动不稳定如果网页一直不弹串口选择框先检查驱动。4.5 固件安全与误操作提醒NVS 键值能改带来了很多便利但同时也带来了误操作的风险。我给几个建议。配置接口一定要做白名单和长度限制。通用 NVS 接口应该只允许写入预先定义好的键名不能放开来者不拒。不然前端一个误操作把一个不该改的键写坏了设备启动可能直接崩溃。不要把这个 HTTP 配置服务暴露到公网。AP 配置模式只在热点下开放问题不大但 STA 局域网模式下如果设备直接暴露在公网 IP 上配置接口就是攻击面。至少加一个简单的访问口令页面上输入密码才能打开配置功能。页面上的“恢复默认配置”按钮要谨慎实现最好加二次确认对话框。因为 NVS 一旦擦除校准数据、设备编号这些关键信息就找不回来了。5. 这个工具还能拿来做什么5.1 业务参数全面 Web 化WiFi 密码只是最典型的场景。一旦你有了通用 NVS 读写接口你会发现设备里几乎所有和“运行参数”相关的东西都可以搬到网页上。比如一块环境监测板上报间隔、温度阈值、湿度阈值、MQTT 服务器地址、设备名称都是 NVS 里的键值。以前改这些参数要重新编译现在打开页面直接改。对于低成本物联网项目来说这是很实用的省事方案。5.2 批量生产与现场部署同一份固件烧到所有设备然后再通过 WebSerial 或局域网页面逐台写入设备编号、密钥、服务器地址。这样固件是统一的不用每台设备编一个版本生产管理会轻松很多。而且后期换服务器、换项目也不用返厂远程指导维护人员在浏览器里改几个键值就搞定了。5.3 与 OTA 配合使用NVS 不只是用来存 WiFi 和业务参数还可以存 OTA 升级需要用到的临时信息。比如 OTA 下载地址、升级包版本号这些都可以通过配置页面提前写入。这样每次升级就不用重新编译固件去改地址直接在页面里填新地址触发一次 OTA 就能完成。你甚至可以把这个工具和 OTA 联动做成整套管理方案先通过配置页面写入 OTA URL再提交一个重启指令设备重启时检查到 NVS 里的 OTA 地址有变化自动进入升级流程。5.4 我踩过的坑和一点经验最后分享几个实际测试中踩过的坑希望你能绕开。第一个坑是保存 WiFi 密码后立即重启。第一次实现时我点击保存后马上就ESP.restart()结果 10 次里有 3 次设备重启后用的还是旧密码。后来加了delay(2000)问题解决了。WiFi 驱动写 NVS 是异步落盘的断电或者重启太快数据可能还在 flash 缓存里没写出去。第二个坑是中文 SSID。之前有个朋友拿着这套工具去配一个名字带中文的 WiFi结果页面保存显示成功设备却一直连不上。排查半天发现是前端 HTML 页面没有声明 UTF-8 编码浏览器把中文 SSID 按照别的编码格式提交了写进 NVS 的是乱码。后来页面里加了一行meta charsetUTF-8问题才解决。第三个坑是键值类型不匹配。NVS 里存的时候是字符串读取的时候如果用了nvs_get_blob返回错误不说还可能读出长度不对的数据。所以通用接口一定要带上类型字段页面里也明确标出这个键是字符串还是整数避免读写类型不一致。这个工具做完之后我最多的感慨是ESP32 的 NVS 本身就提供了很好的运行时配置能力缺的只是一个好用的入口。把入口做成浏览器页面设备维护的成本会低很多。如果你也想试试建议从最小的版本开始先做一个只改 WiFi 密码的页面跑通了再加通用 NVS 接口、WebSerial、OTA 这些扩展。一步一步来稳定最重要。