
在嵌入式开发群里隔三差五就会看到这样的求助WiFi 密码换了手上的 ESP32 设备是不是只能重新编译、重新烧录固件很多人的第一反应是打开 Arduino IDE把 ssid/password 改掉编译、烧录、再跑一遍串口监视器。折腾半小时其实只改了两个字符串。痛点就在这——固件的业务逻辑一点没动但为了改两个字符串你花了成倍的时间还要把设备拆卸、接线、重新烧录。今天分享的浏览器工具就是为了解决这个问题通过浏览器直接读写 ESP32 的 NVS 键值把 WiFi 名、密码这些配置项像编辑本地配置文件一样改掉不用动固件更不用重编译。这个工具适合两类人一是量产过 ESP32 硬件、想给使用者一个简单配置入口的开发者和创客二是喜欢折腾无线设备、经常在工地或客户现场调 WiFi 的嵌入式工程师。下面我先把“为什么改密码要重刷固件”这件事拆清楚再给出完整可复现的实操方案中间会穿插一些我在实际项目里踩过的坑希望能让你少走弯路。1. 痛点拆解改个密码为什么非要重刷固件1.1 你们遇到的场景大概率是这个先描述一个非常典型的画面一块 ESP32 开发板首次上电setup()里把 WiFi 写死成HomeWiFi/12345678跑得挺顺。换了一个路由器或者用户把管理后台密码改了设备就连不上了。这时候你翻代码WiFi.begin(ssid, password)就像一根刺扎在那不改源码就永远连不上新网络。于是你被迫做一次“重编译 重烧录”的全套流程。看起来也不复杂但对实际开发来说这个动作的代价远比你想象的大编译固件需要时间。Arduino 环境第一次编译可能要一两分钟大型工程配合一堆第三方库甚至奔着五分钟去。烧录要在物理上连接串口哪怕用 OTA 也得先把设备保持在能联网的状态。如果新密码没生效你连第一步都做不了。更难受的是现场如果要快速切换两个热点比如从办公室切到客户演示网络你不能每次都掏出笔记本插线吧真正的问题不是“重刷”这个动作本身而是它把一个高频的配置操作和低频的固件升级操作绑在了一起。你已经站在 ESP32 的门槛上却还在用最野蛮的方式改配置。实际上ESP32 自己就带了一个专门干这事的存储区叫 NVS。WiFi 参数、电源校准、设备标识这类需要“断电不丢”的数据都应该放进去。可惜很多人一开始不知道活生生把“配置”写进了源代码然后陷入无尽的重新编译循环。1.2 NVS 与“改密码重刷固件”的真相NVS 的全称是 Non-Volatile Storage非易失性存储。你可以把它理解成一个跑在 Flash 分区里的小型 KV 数据库key-value 形式支持字符串、整数、二进制数据等类型。ESP32 上电后代码可以通过nvs_get_str、nvs_set_str这类接口读写它断电后数据还在。Arduino 官方套件的 WiFi 库表面上写着WiFi.begin(ssid, pass)实际上内部会把当前这套 WiFi 配置存到 NVS 的WIFI命名空间里。常见的键名是键名含义sta.ssid站点模式下保存的 WiFi 名称sta.pass站点模式下保存的 WiFi 密码sta.channelWiFi 通道参数sta.bssidBSSID 值有时也会被保存所以结论很反直觉你每次重刷固件其实只是在把一份新的默认配置覆盖到 NVS 上。如果设备端早就把当前连接参数放进了 NVS那么真正要改的只是这两个键值而已。那为什么大家第一反应还是重刷固件因为没有“看得见”的入口。你没地方去看 NVS 里到底是什么也没地方改。串口监视器打印的只是日志不是配置面板。于是最笨的方案反而成了最“可靠”的方案。浏览器工具要做的就是补上这块缺失的入口把 NVS 变成一个可视化、可交互的表单。你在网页里输入新 WiFi 密码网页通过串口向 ESP32 发一条写 NVS 的指令设备端把sta.pass更新掉然后自动重启连接新网络。整个过程固件没有动过一个字节。2. 破局思路浏览器工具怎么做到“直接改 NVS 键值”2.1 工具的整体流向先说一个容易误解的点浏览器是运行在电脑里的网页它不能凭空直接访问 ESP32 的 Flash。工具能跑通前提是 ESP32 的固件里预先放进了一段“串口命令处理程序”专门负责解析从串口来的 NVS 操作指令再返回结果。整个数据流向大概是这个脉络在 Chrome/Edge 这类支持 Web Serial API 的浏览器里打开一个 HTML 页面。页面调用navigator.serial.requestPort()弹出一个串口选择列表你选 ESP32 对应的 COM 口。网页把要执行的指令比如NVS_SET WIFI sta.pass 12345678按行写入串口。ESP32 收到这一行调用 NVS API 更新对应键值然后返回一个结果比如NVS_SET_RESULT OK。网页读取返回结果更新界面展示。这里最关键的是第 3 步的“指令协议”。没人规定必须叫NVS_SET你可以自定义成CFG:WIFI:PASS:12345678这种风格。核心原则只有一条两端约定好帧格式。我习惯用换行符\n作为一条指令的结束标志这样组织缓冲区最简单不容易出现半包错乱。选择浏览器做客户端优点很明显跨平台。Windows、macOS、Linux 都行甚至平板刷新一下页面就能用。不用装驱动不用配环境最适合给同事、客户或者生产线上的人充当一个低成本配置工具。2.2 两种浏览器链路串口直连与设备内嵌 Web提到“浏览器工具”其实有两条完全不同的实现路径第一条串口直连。这是本文主角。设备用 USB 线和电脑连在一起网页通过 Web Serial API 直接操作串口。它的最大好处是不依赖网络。哪怕 ESP32 现在连不上任何 WiFi只要 USB 能连上电脑你就能改 NVS、把 WiFi 配好。这对于第一次配置新设备来说几乎是唯一靠谱的入口。第二条设备内嵌 Web 页面。ESP32 开启一个 Web Server用户用手机或电脑浏览器访问设备 IP页面上放一个表单提交后由设备内部把参数写入 NVS。这种方式适合“设备已经在线”的场景。但它有个死穴新设备第一次上电就配错了 WiFi你根本访问不到那个页面。所以很多成熟方案会把两条链路都做上未联网时用串口联网后用 Web。有朋友还问过我 Web Bluetooth 行不行技术上可以但 BLE 的 MTU 很小传大一点的值容易分片而且浏览器兼容性和配对流程都更麻烦。一般配置项量不大用串口直连是最省心的。还有人问那能不能直接改 NVS 分区比如把 Flash 里的 NVS 读出来在电脑本地改完再刷回去能但太折腾。你得知道 NVS 分区在 flash 里的偏移和大小用parttool.py把它导出成 bin 文件再用专门工具解析 key-value 结构改完后再烧回去。中间的格式封装、CRC 校验任何一个细节错了都会导致设备启动异常。浏览器直连这种方式是“只改需要的键”而不是“重写整块分区”风险和操作成本都小得多。3. 从 0 到 1 的实操不重刷固件修改 WiFi 凭据3.1 固件端准备对外开放一个 NVS 读写命令先明确一点下文示例基于 Arduino 环境因为这是国内相当一部分 ESP32 玩家的入口。如果你用 ESP-IDF原理完全一样只是相应库名和项目结构不同。第一步在 Arduino 工程里加入 NVS 操作函数并在串口回调里解析指令。下面这段代码足够跑通#include WiFi.h #include nvs.h #include nvs_flash.h static String serialBuf ; bool nvsSetString(const char* ns, const char* key, const char* val) { nvs_handle_t handle; if (nvs_open(ns, NVS_READWRITE, handle) ! ESP_OK) return false; esp_err_t err nvs_set_str(handle, key, val); if (err ESP_OK) err nvs_commit(handle); nvs_close(handle); return err ESP_OK; } String nvsGetString(const char* ns, const char* key) { nvs_handle_t handle; char buf[128] {0}; if (nvs_open(ns, NVS_READONLY, handle) ESP_OK) { size_t len sizeof(buf); nvs_get_str(handle, key, buf, len); nvs_close(handle); } return String(buf); } void handleSerialNVS(String line) { // 指令格式NVS_SET namespace key value // 指令格式NVS_GET namespace key if (line.startsWith(NVS_SET)) { int p1 line.indexOf( ); int p2 line.indexOf( , p1 1); int p3 line.indexOf( , p2 1); String ns line.substring(p1 1, p2); String key line.substring(p2 1, p3); String val line.substring(p3 1); bool ok nvsSetString(ns.c_str(), key.c_str(), val.c_str()); Serial.printf(NVS_SET_RESULT %s\n, ok ? OK : FAIL); } else if (line.startsWith(NVS_GET)) { int p1 line.indexOf( ); int p2 line.indexOf( , p1 1); String ns line.substring(p1 1, p2); String key line.substring(p2 1); String val nvsGetString(ns.c_str(), key.c_str()); Serial.printf(NVS_GET_RESULT %s\n, val.c_str()); } } void setup() { Serial.begin(115200); // Arduino 环境下通常在启动流程里已初始化 NVS // 若是 ESP-IDF 裸工程请记得自行调用 nvs_flash_init()。 WiFi.mode(WIFI_STA); } void loop() { while (Serial.available()) { char c Serial.read(); if (c \n) { handleSerialNVS(serialBuf); serialBuf ; } else { serialBuf c; } } }先在串口监视器里手动验证一下等到工作了再打开网页工具连接。例如发送NVS_SET WIFI sta.ssid MyOfficeWiFi NVS_GET WIFI sta.ssid如果收到NVS_GET_RESULT MyOfficeWiFi说明 NVS 写入和读取链路都正常。密码更新之后为了让新配置立刻生效可以在写入完最后一条命令后触发 WiFi 重连。注意WiFi.begin()会去 NVS 里读最新配置所以现场应用可以用这样的函数收尾void applyWiFiConfig() { WiFi.disconnect(); delay(200); WiFi.mode(WIFI_STA); // 确保处于 STA 模式 WiFi.begin(); // 从 NVS 读取 sta.ssid / sta.pass 并连接 }有一点必须强调写完 NVS 后别急着断电重启看效果。串口命令里最后再发一条NVS_GET WIFI sta.pass确认密码已经被正确写进去了再去重启。如果密码里带了空格或特殊字符命令行解析的时候要特别小心最好用引号或者自定义分隔符做边界处理。我在日志里见过不少人因为 WiFi 密码末尾多了一个空格导致一连串莫名其妙的连接失败。3.2 Web 端实现连接串口 读取/写入 NVS固件端准备好以后剩下的就是写一个单页面 HTML。把下面这个 HTML 保存为nvs_tool.html用 Chrome 打开即可。!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleESP32 NVS 浏览器工具/title style body { font-family: monospace; max-width: 720px; margin: 40px auto; } button, input { font-size: 16px; padding: 6px 12px; margin: 4px; } pre { background: #f4f4f4; padding: 12px; white-space: pre-wrap; } /style /head body h2ESP32 NVS 键值工具/h2 button idconnect连接串口/button button iddisconnect断开/button hr input idssid placeholder新 WiFi 名称 input idpass placeholder新 WiFi 密码 button idwriteWifi写入 WiFi 凭据/button hr pre idlog等待串口连接.../pre script let port, reader, writer; async function log(text) { document.getElementById(log).textContent \n text; } document.getElementById(connect).onclick async () { port await navigator.serial.requestPort(); await port.open({ baudRate: 115200 }); writer port.writable.getWriter(); reader port.readable.getReader(); await log(串口已连接); readLoop(); }; document.getElementById(disconnect).onclick async () { await reader.cancel(); await writer.releaseLock(); await port.close(); await log(串口已断开); }; async function readLoop() { const decoder new TextDecoder(); while (true) { const { value, done } await reader.read(); if (done) break; await log(decoder.decode(value).trim()); } } async function sendCommand(cmd) { const encoder new TextEncoder(); await writer.write(encoder.encode(cmd \n)); await log( cmd); } document.getElementById(writeWifi).onclick async () { const ssid document.getElementById(ssid).value; const pass document.getElementById(pass).value; if (!ssid || !pass) { await log(请填写完整); return; } await sendCommand(NVS_SET WIFI sta.ssid ssid); await new Promise(r setTimeout(r, 200)); await sendCommand(NVS_SET WIFI sta.pass pass); await new Promise(r setTimeout(r, 200)); await sendCommand(NVS_GET WIFI sta.ssid); await new Promise(r setTimeout(r, 200)); await sendCommand(NVS_GET WIFI sta.pass); await log(写入完成设备可重启验证。); }; /script /body /html这段代码有几个点值得展开讲讲一是textEncoder.encode(cmd \n)末尾的换行必须带上。固件端的Loop是以\n作为指令结束标记的少了它ESP32 会一直等页面就会看起来像“死掉了一样”。二是写入两次NVS_SET之间加 200ms 延时。因为串口 FIFO 有缓冲两个命令连续快速发送时有时会在 ESP32 端被粘包处理。加一个短延时能让每条指令都被完整解析实测下来更稳。三是reader.cancel()和writer.releaseLock()的顺序不能搞反否则浏览器会报“端口被锁定”的错误。遇到这种情况先把页面刷新一遍重新请求串口连接即可。如果你不喜欢写 HTML也可以在 Chrome 开发者工具里直接执行 JavaScript 调 Web Serial API。但实际配置别人去用的时候没有一个可视化页面是真不方便所以我强烈建议把上面这个页面托管到本地或者放在你自己的服务器上。页面不依赖任何第三方库纯原生代码跨平台直接跑。3.3 实测验证把固件烧录到 ESP32 后打开nvs_tool.html点击“连接串口”选择 ESP32 对应的端口。此时页面日志应该会显示串口已连接。然后填入新 WiFi 名称和密码点击“写入 WiFi 凭据”。日志中会出现 NVS_SET WIFI sta.ssid MyOfficeWiFi NVS_SET_RESULT OK NVS_SET WIFI sta.pass NewPass123 NVS_SET_RESULT OK NVS_GET WIFI sta.ssid NVS_GET_RESULT MyOfficeWiFi NVS_GET WIFI sta.pass NVS_GET_RESULT NewPass123看到NVS_SET_RESULT OK说明写入成功。此时按一下 ESP32 的复位键如果固件里在setup()阶段有WiFi.begin()设备会尝试用新配置连接 WiFi。串口监视器里能看到类似WiFi connected, IP: 192.168.x.x的输出那就是真正生效了。特别注意这个过程完全没碰固件。你在网页里改完固件没重编译Flash 里的程序区也没变变的只是 NVS 分区里的字符串数据。这也是这个方案最有价值的地方。如果网络没有连上先不要急着怀疑工具写坏了 NVS。优先检查WiFi 名称是否区分大小写。2.4G 还是 5G。ESP32 最常见的型号只支持 2.4GHz如果新网络是纯 5G怎么改都连不上。路由器有没有开启 MAC 过滤。密码中是否有隐藏字符比如从网页复制粘贴时带进了换行。4. 常见问题与避坑清单4.1 常见问题速查表下面这个表是我在多个项目里反复遇到的现场问题按照出现频率排了个序。建议先收藏用工具的时候对照着看。现象可能原因排查与解决点“连接串口”没反应Chrome 版本过旧或站点不是 HTTPSWeb Serial API 要求 89 以上本地 file:// 有时会被限制改用本地服务器或部署到 HTTPS 页面ESP32 收不到任何命令串口波特率不匹配固件和网页里必须都用 115200两端保持一致指令返回 FAILNVS 命名空间不合法或已满NVS 命名空间只允许字母数字下划线长度不能超过 15 个字符写入了新密码但连接不上路由器 5G 频段不支持ESP32 常规型号只支持 2.4G改成 2.4G 热点测试网页返回 OK 但重启后丢失写入后没有 commitnvs_set_str后必须调用nvs_commit不然可能没落到 Flash串口监视器正常网页读写乱码两个程序占用了同一串口关闭 Arduino 的串口监视器再刷新网页重连连接时频繁断连USB 供电不稳换数据线或换 USB 口尤其是 ESP32-WROVER 这类功耗稍高的板子这些坑在文档里很难看到都是实操逼出来的。比如串口占用那一条我头一回写这个工具时就是一边开着 Arduio 串口监视器一边点网页按钮死活没响应排查了半天才发现是两个程序在抢同一个 COM 口。4.2 我踩过的几个坑第一个坑是nvs_flash_init()的使用问题。在 ESP-IDF 裸工程里初始化 NVS 之前必须先确保 Flash 分区表里确实有 NVS 分区。如果你用默认分区表一般没问题但如果你自己魔改过分区表把 NVS 分区砍掉了初始化会直接返回错误。正确的做法是先nvs_flash_erase()再nvs_flash_init()不过这个erase会把整个 NVS 清空务必提前备份有价值的数据。第二个坑是命令解析里的空格问题。我第一版协议是这样设计的NVS_SET WIFI sta.ssid MyOfficeWiFi问题来了万一 WiFi 密码本身包含空格呢比如密码是Abc 123解析逻辑按空格切分会把它拆成Abc和123两个值直接写坏。如果设备面向最终用户我建议你别用空格分隔改用|或;NVS_SET|WIFI|sta.pass|Abc 123这样就不用担心密码里有空格了。网页端发送时也把字符串按同样的分隔符拼接。解析端用split()去切分后取最后一个字段作为整体 value就不会截断。第三个坑是 Arduino 环境里有时候改了 NVS 键值但WiFi.begin()读取到的还是旧值。这并不是写入失败而是 Arduino 网络库有缓存。常见的处理是先WiFi.disconnect()再调WiFi.mode(WIFI_STA)重置状态最后调用WiFi.begin()重新拉取配置。这个过程最好等 200ms 再继续否则内核中 WiFi 任务还没切换到新状态很容易出现”看起来没生效“的假象。第四个坑也是很多人忽略的Arduino 编译时有自己的默认 Flash 分区方案不一定给 NVS 留了足够空间。如果设备里存了大量自定义数据NVS 分区可能被写满此时nvs_set_str会返回ESP_ERR_NVS_NOT_ENOUGH_SPACE。排查时可以在程序里打印nvs_get_free_space看看剩余空间必要的时候改用一个更大的分区表。4.3 安全与备份建议浏览器直接改 NVS 这件事能力越大责任也越大。它是一把双刃剑用好了是配置神器用不好就是设备变砖的快捷方式。首要是备份。在修改 WiFi 凭据之前建议把整块 NVS 分区先读出来。ESP-IDF 自带的parttool.py可以完成这个操作python parttool.py --port COM3 --partition-name nvs read_partition --output nvs_backup.bin不要小看这个备份它救过我一次。有一次我在一个设备上写错了某个自定义键值导致设备每次开机都进入错误模式。当时因为有备份直接写回 NVS 分区就恢复了前后不到一分钟。要是没有备份就只能重新擦除整片 Flash 再烧框架麻烦得多。其次是权限意识。如果你把这个网页工具交给非技术人员使用最好在页面里加一个二次确认按钮比如“我确认我已备份当前配置”。有些团队还会在 NVS 工具里加一个简单的访问口令避免现场作业时有人拿手机对着你的网页接口胡乱发送命令。网上有人会做密码校验思路是用一个额外的 NVS 键保存 token网页写入前先验证 token验证通过才允许写主配置。这种方式在多人协作现场真的有用。最后说一种常见误解不要在未确认键名的情况下把整个 NVS 清空。NVS 里不止存 WiFi还可能有 ESP32 蓝牙配对的绑定数据、RF 校准数据等。把这些数据清掉设备虽然还能启动但某些传感器或无线性能参数会退化。应该做的是只操作你明确知道的键比如sta.ssid、sta.pass。这个工具后续还可以扩展成很多形态加上nvs_list命令一次性把所有键值导出来做成一个本地服务器版本方便同事通过内网页面访问甚至可以在浏览器工具里集成 OTA 升级功能把“配置修改”和“固件升级”做成两个独立入口。我在实际使用中最大的体会是嵌入式设备不该再让用户为了改一个配置而动编译器配置和程序分离才是长期维护的正确姿势。希望这套思路也能帮你把手上的 ESP32 项目变得更灵活。