ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ESP32免重刷固件:浏览器直接修改NVS键值实现WiFi配置更新

ESP32免重刷固件:浏览器直接修改NVS键值实现WiFi配置更新 1. 为什么改个 WiFi 密码要折腾重刷固件搞过 ESP32 项目的人大概率都经历过这个场景设备已经装到现场了外壳封好、螺丝拧紧、甚至打了胶固定在配电箱里结果甲方或者自己家里换了路由器WiFi 名字和密码全变了。这时候你怎么办把设备拆回来、接上 USB 转串口、打开 Arduino IDE 或者 ESP-IDF改两行代码重新编译、重新烧录再装回去。一套流程下来半小时起步如果设备装在不好够到的地方那更是折磨。这个痛点的根源在于绝大多数 ESP32 入门项目和教程都是把 WiFi 的 SSID 和密码直接写死在代码里的const char* ssid xxx;这种形式。代码编译成固件烧进 Flash 之后这些字符串就固化在程序的只读数据段里了运行时根本没法改。想改只能改代码重新编译。但 ESP32 其实自带了一个非常适合存这类配置数据的地方——NVSNon-Volatile Storage非易失性存储。它是乐鑫在 ESP-IDF 里提供的一套键值对存储系统底层跑在 Flash 的某个分区上掉电不丢数据专门用来存 WiFi 配置、设备参数、校准数据这类需要持久保存又经常变动的东西。ESP32 的 WiFi 驱动本身在配网成功后也会把 SSID 和密码存进 NVS下次上电自动重连。问题就变成了既然 NVS 里本来就存着 WiFi 信息那我能不能不重刷固件直接改 NVS 里的键值答案是能而且可以做得非常优雅——用一个浏览器工具通过串口或者设备自己开的 Web 服务直接读写 NVS 的键值对。这就是标题里说的浏览器工具能直接改 NVS 键值的核心思路。这篇文章我会把整套方案的来龙去脉讲清楚NVS 到底怎么组织的、为什么它能被外部工具改、浏览器工具是怎么跟 ESP32 通信的、具体怎么落地实现、以及我在实际项目里踩过的那些坑。适合已经会用 ESP32 做联网项目、但被现场改配置这件事折磨过的朋友也适合想深入理解 NVS 机制的人。2. NVS 到底是个什么东西为什么它能被直接改2.1 NVS 的存储结构和键值模型很多人第一次听到 NVS 会以为是文件系统其实不是。NVS 是一套基于分区的键值数据库它在 Flash 上占用一个独立的分区通常在分区表里叫nvs默认大小 0x600024KB可以自己调。它的数据组织方式跟 FAT、SPIFFS、LittleFS 这些文件系统完全不同没有目录树没有文件概念就是纯粹的namespace key - value。一个 NVS 条目由这几部分组成Namespace命名空间相当于一个分组比如wifi_config、device_cfg不同命名空间下的同名 key 互不干扰。Key键字符串最长 15 个字符不含结尾的\0这是硬限制超了会报错。Value值支持多种类型整型i8/u8/i16/u16/i32/u32/i64/u64、字符串、二进制 blob。字符串和 blob 有长度限制单条最大受分区大小约束。底层实现上NVS 把每个 namespace 映射成一个页page页大小固定 4096 字节每个页里存若干条目entry每个 entry 32 字节。写入的时候是追加式的删除只是把 entry 标记为 erased真正回收空间靠垃圾回收GC机制在页写满时触发。这个设计的好处是写入磨损均衡坏处是频繁写会触发 GC有性能抖动。理解这个结构很关键因为它决定了外部工具能做什么只要工具能按 NVS 的二进制格式解析和生成 entry就能直接读写 Flash 上的 NVS 分区完全绕过应用程序。2.2 为什么 WiFi 密码存在 NVS 里就能被改这里有个容易被忽略的事实ESP32 的 WiFi 驱动默认就会把配网信息存进 NVS。当你调用esp_wifi_set_config()并连接成功后驱动会把 SSID、密码、信道等信息写进nvs分区里一个叫nvs.net80211的命名空间。下次上电esp_wifi_start()之后驱动会自己去读这个命名空间尝试自动重连。也就是说你的应用程序代码里写死的 SSID 密码和 NVS 里驱动存的那份是两套东西。如果你的代码是WiFi.begin(写死的SSID, 写死的密码)那它每次都会用代码里的值覆盖 NVS但如果你改成WiFi.begin()不带参数或者用esp_wifi_set_config之前先读 NVS那 NVS 里的值就是权威来源。这就给了我们操作空间把 WiFi 配置的读取逻辑改成优先读 NVS读不到再用默认值然后外部工具只需要改 NVS 里的键值设备重启后就会用新配置。整个过程不需要动固件。2.3 直接改 NVS vs 重刷固件的成本对比我把两种方式的实际成本列个表你一看就明白为什么值得折腾这套方案对比项重刷固件直接改 NVS需要拆设备是否Web 方式或仅需接串口需要编译环境是否单次耗时5-30 分钟10 秒-2 分钟操作门槛需要开发者普通运维/用户即可出错风险烧录失败可能变砖只改数据风险低适合场景功能升级配置变更注意直接改 NVS 只适合配置类变更如果是要改业务逻辑、加功能那还是得重刷固件。别把这两件事混为一谈。3. 浏览器工具是怎么跟 ESP32 搭上话的3.1 两条技术路线串口直连 vs 设备内嵌 Web浏览器工具改 NVS这个说法落地时有两条完全不同的路线我分别说一下你根据场景选。路线一Web Serial API 串口直连。现代浏览器Chrome、Edge 89支持 Web Serial API网页可以直接调用navigator.serial.requestPort()拿到串口句柄然后以任意波特率跟 ESP32 通信。这条路线的本质是浏览器替代了传统的串口助手你在网页上输入新的 SSID 和密码网页通过串口把命令发给 ESP32 上运行的一个配置服务固件由它去写 NVS。优点是设备不需要联网、不需要额外服务缺点是必须物理接触设备插 USB。路线二设备内嵌 Web 服务器。ESP32 自己跑一个 HTTP 服务器用WebServer.h或ESPAsyncWebServer手机或电脑连上设备的热点或者设备已在局域网里浏览器访问设备的 IP打开一个配置页面提交表单后设备自己写 NVS。优点是完全无线设备装在高处也能改缺点是设备得先能连上要么开 AP 热点要么已经在网里。实际项目里我一般两个都做首次配网用 AP 内嵌 Web后续改配置用 Web Serial 兜底。因为万一设备连不上网、AP 也进不去串口是最后的救命稻草。3.2 Web Serial API 的关键约束用 Web Serial 有几个坑必须提前知道必须 HTTPS 或 localhostWeb Serial 属于敏感权限 APIhttp://的页面调不起来本地测试用localhost或者起个本地 https 服务。必须用户手势触发requestPort()必须在 click 等用户操作的回调里调用不能在页面加载时自动弹窗。一次只能一个页面占用串口是独占资源串口助手开着网页就打不开。波特率要匹配ESP32 默认 115200但如果你改过Serial.begin()的参数网页这边也要跟着改。3.3 内嵌 Web 服务器的实现要点如果用内嵌 Web 方案ESP32 端要处理的事情更多起一个WebServer server(80)注册/返回配置页面 HTML注册/save处理 POST 表单。配置页面 HTML 可以直接以字符串形式嵌在固件里PROGMEM也可以用 SPIFFS/LittleFS 存后者更灵活但多一层文件系统。保存时调用nvs_open/nvs_set_str/nvs_commit写入然后ESP.restart()重启生效。如果设备当前连不上目标 WiFi要先起 AP 模式WiFi.softAP(ESP32-Config, 12345678)让用户连上来配置。提示内嵌 Web 方案里配置页面一定要做表单校验SSID 不能为空、密码长度符合 WPA2 要求8-63 字符否则写进去连不上用户还得再折腾一次。4. 手把手实现从 NVS 读写到浏览器交互4.1 ESP32 端封装一套 NVS 配置读写函数先把 NVS 的读写封装好这是整个方案的地基。下面这段代码我用了很多项目稳定可靠#include nvs_flash.h #include nvs.h #define CFG_NAMESPACE wifi_cfg #define KEY_SSID ssid #define KEY_PASS pass bool save_wifi_config(const char* ssid, const char* pass) { nvs_handle_t handle; esp_err_t err nvs_open(CFG_NAMESPACE, NVS_READWRITE, handle); if (err ! ESP_OK) { Serial.printf(nvs_open failed: %s\n, esp_err_to_name(err)); return false; } err nvs_set_str(handle, KEY_SSID, ssid); if (err ESP_OK) err nvs_set_str(handle, KEY_PASS, pass); if (err ESP_OK) err nvs_commit(handle); nvs_close(handle); return err ESP_OK; } bool load_wifi_config(char* ssid, size_t ssid_len, char* pass, size_t pass_len) { nvs_handle_t handle; if (nvs_open(CFG_NAMESPACE, NVS_READONLY, handle) ! ESP_OK) return false; esp_err_t e1 nvs_get_str(handle, KEY_SSID, ssid, ssid_len); esp_err_t e2 nvs_get_str(handle, KEY_PASS, pass, pass_len); nvs_close(handle); return (e1 ESP_OK e2 ESP_OK); }几个关键点解释一下。nvs_open的第二个参数决定读写权限读配置用NVS_READONLY更安全。nvs_set_str之后必须调用nvs_commit否则数据只在缓存里掉电就没了——这是新手最常犯的错。nvs_get_str传入的len是传入传出参数调用前要填缓冲区大小调用后会被改成实际长度所以别传个常量进去。4.2 启动逻辑优先读 NVS读不到用默认值void connect_wifi() { char ssid[33] {0}; char pass[65] {0}; if (load_wifi_config(ssid, sizeof(ssid), pass, sizeof(pass))) { Serial.printf(Using NVS config, SSID%s\n, ssid); } else { strncpy(ssid, DEFAULT_SSID, sizeof(ssid) - 1); strncpy(pass, DEFAULT_PASS, sizeof(pass) - 1); Serial.println(No NVS config, using default); } WiFi.mode(WIFI_STA); WiFi.begin(ssid, pass); // ... 等待连接 }这段逻辑是整个方案的灵魂NVS 优先默认值兜底。这样即使 NVS 是空的比如刚烧录的新设备设备也能用默认配置启动不会变砖。4.3 内嵌 Web 配置页面的完整实现下面是一个能直接用的最小实现包含 AP 模式和配置页面#include WebServer.h WebServer server(80); const char CONFIG_PAGE[] PROGMEM RHTML( !DOCTYPE htmlhtmlheadmeta charsetutf-8 meta nameviewport contentwidthdevice-width,initial-scale1 titleWiFi Config/title/headbody h2WiFi 配置/h2 form action/save methodPOST SSID: input namessid maxlength32 requiredbr 密码: input namepass typepassword maxlength63br button typesubmit保存并重启/button /form/body/html )HTML; void handleRoot() { server.send_P(200, text/html, CONFIG_PAGE); } void handleSave() { String ssid server.arg(ssid); String pass server.arg(pass); if (ssid.length() 0) { server.send(400, text/plain, SSID empty); return; } if (save_wifi_config(ssid.c_str(), pass.c_str())) { server.send(200, text/plain, Saved, rebooting...); delay(500); ESP.restart(); } else { server.send(500, text/plain, Save failed); } } void start_config_portal() { WiFi.mode(WIFI_AP); WiFi.softAP(ESP32-Config, 12345678); server.on(/, handleRoot); server.on(/save, HTTP_POST, handleSave); server.begin(); Serial.print(Config portal at ); Serial.println(WiFi.softAPIP()); }PROGMEM把 HTML 字符串放到 Flash 而不是 RAM省内存。send_P是配套的发送函数。表单提交后写 NVS 再重启重启后connect_wifi()就会读到新配置。4.4 Web Serial 网页端的实现如果走串口路线网页端核心代码是这样let port, writer, reader; async function connectSerial() { port await navigator.serial.requestPort(); await port.open({ baudRate: 115200 }); writer port.writable.getWriter(); readLoop(); } async function readLoop() { const decoder new TextDecoderStream(); port.readable.pipeTo(decoder.writable); reader decoder.readable.getReader(); while (true) { const { value, done } await reader.read(); if (done) break; if (value) console.log(ESP32:, value); } } async function sendConfig(ssid, pass) { const cmd SETCFG:${ssid}:${pass}\n; await writer.write(new TextEncoder().encode(cmd)); }ESP32 端则要解析这个SETCFG:协议void handleSerialCommand() { if (!Serial.available()) return; String line Serial.readStringUntil(\n); if (line.startsWith(SETCFG:)) { int first line.indexOf(:); int second line.indexOf(:, first 1); String ssid line.substring(first 1, second); String pass line.substring(second 1); if (save_wifi_config(ssid.c_str(), pass.c_str())) { Serial.println(OK); } else { Serial.println(FAIL); } } }协议设计上我建议用带前缀的文本行别用二进制调试方便用串口助手也能手动发。分隔符选:或者|都行只要 SSID 和密码里不会出现这个字符——注意 SSID 里理论上可以出现冒号所以更稳妥的是用长度前缀或者 JSON但为了简单实际项目里用:也够因为带冒号的 SSID 极少见。5. 实操中踩过的坑和排查技巧5.1 NVS 写入失败的那些原因我遇到过好几次nvs_set_str返回ESP_ERR_NVS_NOT_ENOUGH_SPACE排查下来无非几种分区太小默认 24KB 看着够但如果你存了很多 blob很快就满。改分区表把nvs调到 0x1000064KB能解决大部分问题。key 太长超过 15 字符直接报ESP_ERR_NVS_KEY_TOO_LONG这个错误信息很明确但新手容易忽略。没 commit前面强调过nvs_set_str不 commit 等于没写。namespace 名太长namespace 也有 15 字符限制别起太长的名字。5.2 改了 NVS 但设备还是连旧 WiFi这个现象特别常见原因通常是代码里还在用写死的 SSID 覆盖 NVS。检查你的WiFi.begin()调用如果带了参数那 NVS 里的值就被忽略了。改成WiFi.begin()无参形式或者干脆自己读 NVS 再WiFi.begin(ssid, pass)。还有一种情况是WiFi 驱动自己缓存了配置。ESP32 的 WiFi 驱动在nvs.net80211命名空间里也存了一份如果你改的是自己的wifi_cfg命名空间驱动那份没变某些情况下驱动会优先用自己那份。解决办法是改完配置后调用esp_wifi_restore()清掉驱动缓存或者干脆用WiFi.disconnect(true)再重连。5.3 Web Serial 打不开串口的排查顺序按这个顺序查基本能定位现象可能原因解决按钮点了没反应页面不是 HTTPS/localhost换 localhost 或起 https弹窗里没有设备驱动没装/线是充电线装 CP210x/CH340 驱动换数据线打开报错串口被占用关掉串口助手、Arduino 监视器能连但收不到数据波特率不对确认两边都是 115200收到乱码波特率或数据位不匹配检查 8N1 配置5.4 一个容易被忽略的细节NVS 的擦除粒度NVS 底层是 FlashFlash 的擦除粒度是 4KB 的扇区。这意味着即使你只改一个字节底层也可能擦掉整个扇区再重写。如果你的应用频繁写 NVS比如每秒写一次传感器数据Flash 寿命会消耗得很快。ESP32 的 Flash 标称擦写寿命约 10 万次按每秒一次算不到 28 小时就到一个扇区的寿命上限了。所以我的经验是NVS 只存配置不存高频数据。传感器数据要存用 RTC 内存或者外部存储。配置类数据一天改不了几次完全不用担心寿命。提示如果确实需要频繁写考虑用nvs_set_blob批量写减少 commit 次数或者加一层 RAM 缓存定时落盘。6. 这套方案的边界和扩展玩法6.1 什么场景适合什么场景别用适合的场景很明确配置项变更频繁、设备部署后不易物理接触、操作者不一定是开发者。比如智能家居节点、工业传感器、共享设备、教学实验板。不适合的场景也要说清楚需要改业务逻辑、需要升级功能、需要修复 bug这些必须重刷固件。另外如果设备有安全要求比如金融、医疗NVS 明文存 WiFi 密码是有风险的得考虑加密存储或者用安全芯片。6.2 扩展一把配置项做成通用的别只盯着 WiFiNVS 可以存任何配置。我一般会做一个通用的配置框架struct DeviceConfig { char wifi_ssid[33]; char wifi_pass[65]; char mqtt_host[64]; uint16_t mqtt_port; uint32_t report_interval; };然后写一套save_config/load_config把整个结构体序列化进 NVS。这样 Web 页面就能一次性配置所有参数不用为每个参数单独写代码。6.3 扩展二配置版本号和回滚生产环境里我会加一个config_version字段。每次保存配置时版本号加一设备启动时如果发现配置版本比固件期望的低就走迁移逻辑。这样即使配置格式变了老设备升级固件后也能平滑过渡不会因为读不懂旧配置而挂掉。6.4 扩展三OTA 和 NVS 配置的配合OTA空中升级升级的是固件NVS 配置默认是保留的。但要注意如果新固件的 NVS 命名空间或 key 变了旧配置就读不到了。所以 OTA 升级时最好带上配置迁移逻辑或者干脆在升级前把配置导出、升级后导入。这个细节很多 OTA 教程都不提但实际项目里踩过坑的人都知道有多重要。7. 我个人在实际项目中的几点体会这套浏览器改 NVS的方案我从最早的纯串口助手手动改到后来做 Web Serial 页面再到内嵌 Web 服务器前后迭代了好几版。最大的体会是别追求一步到位做全功能先把能改这件事跑通。很多人一上来就想做漂亮的配置页面、做多设备管理、做权限控制结果卡在 NVS 读写这一步就放弃了。另外一个血泪教训是默认配置一定要留。我见过有人把默认 SSID 设成空字符串结果 NVS 一空设备就彻底连不上只能拆机重刷。默认值是你的安全网哪怕设成ESP32-Default这种明显是占位的名字也比空着强。最后分享一个小技巧调试 NVS 的时候用nvs_get_stats()打印一下当前分区的使用情况能看到已用条目数、剩余空间、namespace 数量比盲猜高效得多。这个函数在nvs.h里很多人不知道它的存在。nvs_stats_t stats; nvs_get_stats(NULL, stats); Serial.printf(Used: %d, Free: %d, Total: %d\n, stats.used_entries, stats.free_entries, stats.total_entries);后续如果要做多设备批量配置可以把这套逻辑封装成一个独立的配置服务固件所有项目共用通过串口协议或者 HTTP 接口暴露配置能力。这样每个新项目只要集成这个服务就自动获得了免重刷改配置的能力省下的时间相当可观。
返回列表