
前阵子帮朋友调一个 ESP32 的小网关设备已经摆在现场了突然说 WiFi 密码换了。我第一反应是完了又得把设备拿回来重新编译、重新烧录。冷静下来一想不对固件代码一个字节都没变变的只是 WiFi 凭据为什么要让整个固件重走一遍编译烧录流程这个问题的根源是我们把“配置”和“程序”焊死在一起了。ESP32 上其实有一块专门用来存配置的独立区域叫 NVSNon-Volatile Storage非易失性存储WiFi 的 SSID、密码这类键值完全可以放在里面。程序还是那个程序只需要改 NVS 中的键值设备重启后读到的就是新配置。最近我把自己常用的方式整理成了一份实战记录用浏览器打开一个工具页面通过 USB 串口连接到 ESP32直接读取和修改 NVS 里的键值。整个过程不再需要碰编译器和烧录器。这篇文章会把原理、实操步骤、踩坑经历一起写清楚适合所有做 ESP32 开发、尤其是经常需要在现场改设备配置的工程师参考。1. 为啥改个 WiFi 密码非要重刷固件NVS 分区表示不背锅1.1 固件是打印好的说明书NVS 才是那张便利贴先说一个基础但很多人没想透的点ESP32 的 flash 并不是一整块都拿来放程序。它按照分区表被切成了好几段常用分区布局大致长这样分区名称起始地址大小作用bootloader0x10000x8000引导程序partition table0x80000xC00分区表nvs0x90000x6000非易失性键值存储phy_init0xF0000x1000射频校准数据factory0x10000约 3MB应用程序不同工程的地址和大小会有差别但 nvs 分区始终是独立存在的它专门负责保存那些“不想因为刷固件而丢失”的东西。NVS 的本质是一个键值对数据库支持整数、字符串、二进制块等类型。它内部有磨损均衡机制每次写入不是直接覆盖旧数据而是先找空闲位置写新条目再把旧条目标记成废弃类似日志文件的追加模式所以频繁改配置也不太容易把同一块 flash 磨坏。我习惯把固件和 NVS 的关系比作“说明书”和“便利贴”。固件是打印好的使用说明书NVS 就是贴在设备外壳上的便利贴上面写着当前 WiFi 叫什么、密码是什么。说明书一个字都不用改你只需要撕掉旧便利贴换上写有新信息的便利贴。很多开发者之所以觉得“改 WiFi 必须刷固件”是因为代码里把 WiFi 凭据写成了编译期常量等于把便利贴上的内容印进了说明书里自然每次改动都要重新印刷。1.2 重刷固件、AP 配网、串口命令传统配置方式的账本在没有 NVS 编辑工具的时候改一个 WiFi 密码通常有三条路走起来都谈不上舒服。第一条路是重刷固件。打开 Arduino IDE 或者 VS Code确认开发板型号和端口重新编译再等待烧录完成整个过程即使一切顺利也要好几分钟。如果现场没有电脑或者设备装在机箱里需要拆盖接线那就更折腾了。而且这里有个很讽刺的地方你的程序逻辑完全没变烧录器把一模一样的代码再写一遍只是为了让一个字符串常量变成另一个字符串常量。第二条路是 AP 配网。设备开机进入 softAP 模式手机连上它的热点打开网页填 WiFi 信息。这套交互本身没问题很多消费级设备都在用但代价是固件里要多实现一套 WebServer、DNS 劫持、配置状态管理逻辑代码复杂度明显上涨。而且在工业内网、无手机环境、批量设备部署场景下AP 配网并不实用。第三条路是留串口命令。如果固件在设计之初就预留了“通过 UART 修改配置”的指令那确实很直接。但问题在于很多单品固件并没有预留这种能力等设备都已经量产了才发现要改配置这时候再补串口命令本质上又回到了重新编译、重新烧录的老路。这三条路绕来绕去核心矛盾都一样配置和程序没有解耦。而 NVS 的分区设计本来就是为了解决这个问题的只是缺一个趁手的修改工具而已。所以我后来专门折腾了一套“浏览器直接改 NVS 键值”的方案相当于把那张便利贴从一个别扭的位置挪到了随手就能改的地方。2. 浏览器直改 NVS 的原理Web Serial 配置固件2.1 Web Serial浏览器第一次拿到了串口钥匙能让网页直接操作串口的是 Web Serial API。从 Chrome 89 开始Chromium 内核的浏览器内置了这套接口Edge 也一样。以前我们想在浏览器里和设备通信只能走 WebSocket 到服务器再转发绕一大圈现在浏览器自己就能直接读写本地串口。这个 API 的使用方式和 Node.js 里的串口库有点像核心代码就四步if (serial in navigator) { // 1. 用户手动选择要打开的串口设备 const port await navigator.serial.requestPort(); // 2. 设置波特率等参数 await port.open({ baudRate: 115200 }); // 3. 拿 writer 发命令给设备 const writer port.writable.getWriter(); await writer.write(new TextEncoder().encode(list\r\n)); // 4. 拿 reader 读设备返回的内容 const reader port.readable.getReader(); const { value, done } await reader.read(); }有两个设计细节值得多说一句。第一页面必须是安全上下文才能调用 Web Serial也就是要么用 localhost 打开页面要么通过 HTTPS 访问。直接双击 HTML 文件用 file:// 协议打开在很多浏览器里会看到navigator.serial是 undefined这是最容易踩的坑之一。第二浏览器强制要求用户手动点击“连接”并在弹窗里选择端口网页不能偷偷扫描你电脑上的串口设备。这个交互虽然多了一步但它是很必要的安全边界防止任意网页在你插着开发板的时候偷偷读取数据。2.2 别指望网页直接写 flash配置固件才是真正的执行者Web Serial 只是给了网页一条数据通路真正读写 NVS 的仍然是 ESP32 上的固件。原因很简单Web Serial 发出去的是一串文本而这串文本最终要调用 ESP-IDF 里的 nvs 库函数才能把数据落盘这一步必须由设备端程序完成。所以这套工具通常是一个网页加一个配套固件网页负责展示和交互固件负责执行命令。我用的通信协议非常简单每个命令占一行以回车换行结尾命令功能示例list遍历 NVS 里的命名空间和键值listget读取指定键值get wifi ssidset写入键值set wifi pass string NewPwd123erase擦除键值erase wifi passrestart重启设备restart命令之所以设计成一行一个主要是方便现场调试你可以直接在串口监视器里手敲命令也可以在网页端写一个对应的输入框。固件端核心代码不需要很多示意如下static void handle_command(char *line) { char *cmd strtok(line, \r\n); if (!cmd) return; if (strcmp(cmd, list) 0) { nvs_iterator_t it nvs_entry_find(nvs, NULL, NVS_TYPE_ANY); while (it ! NULL) { nvs_entry_info_t info; nvs_entry_info(it, info); printf([%s] %s type%d\n, info.namespace_name, info.key, info.type); it nvs_entry_next(it); } } else if (strcmp(cmd, set) 0) { char *ns strtok(NULL, ); char *key strtok(NULL, ); char *type strtok(NULL, ); char *value strtok(NULL, \r\n); nvs_handle_t handle; nvs_open(ns, NVS_READWRITE, handle); if (strcmp(type, string) 0) { nvs_set_str(handle, key, value); } else if (strcmp(type, u32) 0) { nvs_set_u32(handle, key, (uint32_t)atoi(value)); } // 注意要 commit 才会真正落盘 nvs_commit(handle); nvs_close(handle); printf([NVS] OK\n); } else if (strcmp(cmd, restart) 0) { esp_restart(); } }这里有一个实际工程里很重要的小习惯命令响应最好带一个统一前缀比如[NVS] OK这样网页端只用匹配前缀就能区分哪一行是命令响应哪一行是 ESP32 自己打的日志。不然 WiFi 驱动、BLE 协议栈的日志混在一起前端解析会很痛苦。2.3 修改一个键值而不是重编一个固件这套方案的本质是把“改配置”这个动作从开发流程里剥离出来。开发阶段配置是写死在代码里的开发完了配置应该交给 NVS 管代码只用关心“从 NVS 读什么、读到之后怎么用”。当你把 WiFi 密码做成一个 NVS 键值之后改密码就只是改一个小数据项而不是重新发布一个固件。它特别适合三类场景。第一类是产线预配置一批设备出厂前需要写入不同的 SSID、密码、服务器地址用网页工具逐台设置比每台都烧录不同固件高效得多。第二类是现场维护设备已经装好客户换了路由器技术人员带个笔记本就能在浏览器里搞定。第三类是开发调试我经常需要让同一个开发板在不同 WiFi 环境之间切换以前每次都要重烧现在一条命令就完事。当然它也有边界。NVS 工具改的是“配置”不是“程序”。如果固件本身有 bug 或者要加新功能那该升级固件还是得升级固件。另外如果设备的 flash 加密、安全启动等机制没有完全配置好NVS 里的内容可能是明文可见的这点后面安全部分会专门说。3. 实操浏览器里改 WiFi 密码全程五分钟3.1 第一步给设备留一个配置模式入口要让浏览器改 NVS设备上必须先有一个能响应命令的入口。我强烈建议不要在正式固件里把串口命令常开而是设计成一个“非正常启动”才进入的配置模式。最简单的触发方式是检测 BOOT 键也就是 GPIO0开机时按住它再上电。void setup() { Serial.begin(115200); pinMode(0, INPUT_PULLUP); delay(100); if (digitalRead(0) LOW) { // 按住 BOOT 开机进入 NVS 配置模式 nvsConfigMode(); } // 正常启动流程 loadWifiConfigFromNvs(); }这样设计的好处是命令通道只在按按键开机的那一小段时间里打开平时设备就是一个正常运行的 WiFi 设备不会一直暴露调试接口。如果你的固件已经刷好了里面没有配置模式也没关系可以走备用方案先把一个独立的 NVS 编辑器固件烧进去用浏览器改完键值再烧回正式固件。缺点是需要来回烧录两次但至少不用改源码、不用重新编译。另外提醒一句很多 ESP32 模组本身不带 USB 转串口比如只是把 GPIO16、GPIO17 引出来的板子你需要外接一个 USB-TTL 工具并把 RX、TX 交叉连接才能让浏览器看到设备。3.2 第二步浏览器连上串口先看看 NVS 里有什么网页文件是一个静态 HTML不需要复杂的服务端。在项目目录下执行python3 -m http.server 8080然后用 Chrome 或 Edge 打开http://localhost:8080点击页面上的“连接”按钮在弹出的列表里选择对应的 COM 口macOS 下通常是/dev/cu.usbserial-xxxx这样的设备。波特率默认 115200和固件保持一致。连接成功后页面上的串口输出区域通常能看到设备启动日志。此时发送list工具会返回当前 NVS 里所有的命名空间和键值类似这样 NVS entries namespace: wifi [ssid] typestring valueMyHomeWiFi [pass] typestring value12345678 namespace: app [boot_count] typeu32 value12如果你的固件用的是 Arduino 的 Preferences 库那preferences.begin(wifi, false)里的wifi就是命名空间preferences.putString(ssid, xxx)里的ssid就是键名。网页工具看到的 namespace 和 key跟代码里写的是严格对应的不需要额外转换。3.3 第三步改 WiFi 键值重启验证接下来就是重头戏。把网页上显示的ssid改成新的 WiFi 名称把pass改成新密码点击写入。工具底层执行的就是前面说的set命令写入成功后设备端会返回[NVS] OK。为了保险我习惯写完立刻再发一次get wifi ssid确认值确实变了。改完键值之后点击“重启”按钮或者手动按一下复位键设备就会带着新配置启动。串口日志里如果出现WiFi connected, IP: 192.168.x.x说明修改成功。这个环节有几个约束要特别注意NVS 的键名最长 15 个字符超过会返回ESP_ERR_NVS_KEY_NAME_TOO_LONG所以命名的时候尽量短一点。命名空间和键名都区分大小写Wifi和wifi是两个不同的命名空间。字符串值里不要塞回车换行串口是按行解析的值里混入换行会让后面的命令全部错位。修改之后如果仍然连不上 WiFi先别怀疑写入失败用 get 命令再看一眼很多时候是值里带了空格或者引号。不同项目里 WiFi 键值怎么命名完全由你的固件决定我给一个常见的参考用途命名空间键名类型WiFi SSIDwifissidstringWiFi 密码wifipassstring设备别名devicenamestring上报间隔appintervalu323.4 如果你的固件用的是 ESP-IDF 自带的 Wi-Fi 存储这里要区分两种存储方式。第一种是应用自定义存储也就是程序启动时自己从 NVS 里读字符串键值然后手动拼出wifi_config_t结构体。这种方式对 NVS 工具最友好因为键值都是可读的字符串直接改就行。第二种是 ESP-IDF 内建的 WiFi 存储。调用过esp_wifi_set_storage(WIFI_STORAGE_FLASH)之后WiFi 库会把配置存到 NVS 里一个叫sta_config的键中类型是二进制 blob里面是序列化之后的wifi_static_config_t结构体。这个格式跟固件编译时的结构体定义强相关你用文本工具硬改极容易写坏一旦数据不完整WiFi 初始化阶段可能直接崩溃或者起不来。我的建议是如果你的固件还在开发期尽早把 WiFi 配置改成应用自定义存储的方式。启动流程大致是// 先从 NVS 读取自定义键 nvs_get_str(handle, ssid, ssid, len); nvs_get_str(handle, pass, pass, len); // 再设置给 WiFi 库 wifi_config_t cfg {}; strcpy((char *)cfg.sta.ssid, ssid); strcpy((char *)cfg.sta.password, pass); esp_wifi_set_config(WIFI_IF_STA, cfg);这样一来浏览器工具直接修改wifi/ssid和wifi/pass这两个字符串键值设备重启后逻辑清晰、格式可控比去解析二进制 blob 靠谱得多。4. 翻车现场这些坑我替你踩过了4.1 网页连不上串口从浏览器版本到 USB 驱动逐个查这是出现频率最高的一类问题我整理了一张排查表现象原因解决办法页面里没有串口相关按钮不是安全上下文用 localhost 启动页面或部署到 HTTPSnavigator.serial是 undefined浏览器不支持 Web Serial换最新版 Chrome 或 Edge弹窗里看不到开发板端口USB 驱动没装安装 CP210x、CH340、FTDI 对应驱动选完端口后立刻断连波特率不匹配把网页和固件都统一到 115200端口一直在使用中有软件占用了串口关闭串口监视器、VS Code 调试终端等补充一个很容易被忽略的点如果你用的是虚拟机里的 Linux或者远程桌面USB 设备可能没有被正确透传到当前环境。这时候先把串口接到物理机上测试能排除很多环境层面的干扰。4.2 改了键值却不生效先排查这三个地方第一种可能是没重启。WiFi 库往往只在启动时读取一次配置运行中改 NVS 键值不会热生效必须重启设备或者调用esp_wifi_restart()。第二种可能是改错了命名空间或键名。比如固件读的是preferences.begin(wifi)你在网页里却写到preferences.begin(config)下面了虽然都是 string 类型但完全是两个地方。解决方案很简单写完用list或get回读一遍确认你看到的键名、命名空间和固件代码里一致。第三种可能是类型不匹配。程序里用getString读出来的键如果你用 NVS 工具写成了u32读取会直接失败反过来也一样。所以工具里选择类型的时候要跟固件里的读写 API 对齐不能随手填。这里分享一个我这半年用下来的小技巧在配置里加一个config_version键每次用网页工具改完配置顺手把它加 1。固件启动时打印这个值一眼就能看出设备到底有没有读到新配置排查效率高很多。4.3 NVS 写不进去、空间不够怎么处理常见的 NVS 报错有这么几个ESP_ERR_NVS_NO_FREE_PAGES分区空间满了或者有太多废弃待回收的条目。ESP_ERR_NVS_KEY_NAME_TOO_LONG键名超过 15 字符。ESP_ERR_NVS_VALUE_TOO_LONG字符串或 blob 超过单个键的长度上限。如果确实需要清理最干净的办法是擦除整个 NVS 分区。用 esptool 的话以默认分区表为例esptool.py -p /dev/ttyUSB0 erase_region 0x9000 0x6000不同分区方案起始地址不同动手之前先用分区表工具确认一下。擦除后所有配置都会丢失设备会回到出厂状态如果没有默认配置逻辑WiFi 可能根本连不上。所以擦除这个操作只建议在调试阶段用别在对端客户的设备上轻易尝试。多说一个冷知识NVS 措施本身有磨损均衡但如果固件里反复高频地写入同一个键flash 寿命仍然会被消耗。实际项目中别把 NVS 当成日志系统高频擦写否则设备可能用几个月就出现莫名其妙的 NVS 初始化失败。4.4 安全提醒调试入口别裸奔最后这部分虽然放在“常见问题”里但它比前几个问题都重要。第一NVS 里的 WiFi 密码是明文存储的。任何能通过 USB 串口连上设备的人只要发送一个list命令就能把密码读出来。所以不要把 NVS 编辑器固件烧到量产设备上不管更不要在产品里常开配置模式。第二配置模式的命令通道要能关。我用“按住 BOOT 键开机进入配置模式”这种方式核心目的就是让命令通道默认关闭只有在手动干预时才打开。如果你一定要在正式固件里保留配置入口至少要加一个编译期开关同一个固件可以分为开发版和生产版生产版直接把整段配置模式代码从编译里剔掉。第三合规定位要清楚。这套工具是用来管理你自己设备的配置的不管是自己的开发板还是自己负责维护的产品都没有问题。但是不要把它当成一个通用工具去操作不属于你的设备这既涉及网络安全边界也涉及基本的设备管理权限。做个合格的开发者先把自己的东西管好。说到这再分享一点实际使用心得。那个“开机按住 BOOT 进入配置模式”的设计我坚持用了半年最大的体会是它把配置管理变成了一件可以放心交给非嵌入式人员操作的事情。现场同事只需要插上 USB、按住按键开机、打开网页改两个字段、重启全程不需要知道什么叫 NVS也不需要碰编译器和烧录器。后来我还给固件加了一个“把 NVS 键值导出成文本文件”的功能换设备时直接把备份灌进新板子省掉了现场手敲 WiFi 密码的环节。如果你的 ESP32 项目也经常卡在“改配置必须重烧固件”这个环节可以按这个思路试试。改动量不大但日常便利性提升非常明显。