资讯动态

ESP32免烧录改WiFi配置:NVS与HTTP服务器实战

发布时间:2026/10/28 1:04:51 来源:尧图企业网站定制
1. 从一个让人抓狂的场景说起如果你玩过 ESP32大概率经历过这个场景设备已经焊好、装进壳子、挂在墙上跑了三个月突然要换个 WiFi 密码。你翻出数据线拆壳插上打开 Arduino IDE 或者 ESP-IDF改两行代码重新编译等两三分钟烧录再装回去。整个过程十分钟起步如果设备装在不好够到的地方那更是一场灾难。更别提有些项目是给别人做的客户自己不会编译固件每次改个 WiFi 就得找你远程或者上门。这种事情的频率一旦上来心态很容易崩。我最近在整理自己几个长期跑的 ESP32 节点时就又被这个问题恶心了一次。于是花了点时间研究了一下 NVSNon-Volatile Storage的读写机制最后搭了一个基于浏览器的小工具直接通过设备自己开的 HTTP 服务在网页上改 WiFi 的 SSID 和密码改完重启就生效完全不用碰固件。这篇文章就把整个思路、实现细节、踩过的坑以及可以直接抄的代码全部摊开讲一遍。这个方案适合谁适合所有用 ESP32 做联网设备、又不想每次改配置都重新烧录的人。不管你是用 Arduino 框架还是 ESP-IDF只要你能跑一个 HTTP Server就能用这套方法。哪怕你之前没接触过 NVS看完也能上手。2. 为什么改个 WiFi 密码这么麻烦2.1 传统方式的根本问题在哪大部分人写 ESP32 的 WiFi 连接代码都是这个套路const char* ssid MyWiFi; const char* password 12345678; WiFi.begin(ssid, password);SSID 和密码是硬编码在固件里的常量。这意味着它们被编译进了二进制文件烧录到 Flash 的代码段里。你要改它们就必须重新编译、重新烧录。这是最原始也最直接的方式对于一次性配置的设备来说没问题但对于需要长期维护、配置会变的设备来说就是纯粹的折磨。有人会说那我用WiFiManager库不就行了确实WiFiManager解决了一部分问题——它会在连不上 WiFi 的时候开一个热点让你用手机连上去配置。但这个方案有几个限制第一它只在首次配置或者连接失败时才会触发如果设备已经连上了旧 WiFi你想主动改它不会给你入口第二它的配置界面是固定的你没法通过它改其他自定义参数第三它依赖特定的库项目里多一个依赖就多一份维护成本。2.2 NVS 才是正确的存储位置ESP32 的 Flash 分区里有一块专门用来存键值对数据的区域叫 NVSNon-Volatile Storage。它独立于程序代码掉电不丢失而且可以通过 API 在运行时读写。WiFi 库本身其实也在用 NVS——当你调用WiFi.begin()并且连接成功后ESP32 的底层会把这次连接的 AP 信息存到 NVS 里下次上电可以更快连接。所以正确的思路是不要把 SSID 和密码写死在代码里而是启动时从 NVS 读取。如果 NVS 里没有就用一个默认值或者进入配置模式。这样只要我们能提供一个途径去修改 NVS 里的值就能实现不重新烧录改配置。NVS 的键值对支持多种数据类型整数、字符串、二进制大对象等。对于 WiFi 配置我们存两个字符串键就够了比如wifi_ssid和wifi_pass。命名空间namespace可以自定义比如叫wifi_cfg。2.3 浏览器工具的核心思路既然 NVS 可以在运行时读写那我们只需要在 ESP32 上跑一个轻量的 HTTP 服务器提供一个简单的网页表单用户填入新的 SSID 和密码提交后服务器把值写入 NVS然后重启设备。整个过程不需要任何额外的软件手机或电脑的浏览器就能完成。这个方案的核心组件只有三个一个 HTTP ServerESP32 自带WebServer库或者 ESP-IDF 的httpd、NVS 读写 API、一个嵌入的 HTML 页面。代码量不大但细节不少下面逐层拆解。3. NVS 读写机制与关键细节3.1 NVS 的基本操作流程在 Arduino 框架下NVS 的操作通过Preferences库封装用起来比原生 ESP-IDF 的nvs_flashAPI 简单很多。基本流程是#include Preferences.h Preferences prefs; // 打开命名空间false 表示读写模式 prefs.begin(wifi_cfg, false); // 写入字符串 prefs.putString(wifi_ssid, NewSSID); prefs.putString(wifi_pass, NewPassword); // 读取字符串第二个参数是默认值 String ssid prefs.getString(wifi_ssid, default_ssid); String pass prefs.getString(wifi_pass, default_pass); // 用完关闭 prefs.end();这里有几个关键点需要注意。begin()的第二个参数是只读标志传false表示可读写传true表示只读。如果你只需要读取传true更安全避免误写。putString在键不存在时会创建存在时会覆盖。getString的第二个参数是默认值当键不存在时返回这个值这个设计非常实用可以让你在首次启动时有一个兜底配置。3.2 命名空间与键名的选择命名空间namespace在 NVS 里相当于一个文件夹不同的命名空间之间的键是隔离的。ESP32 的 WiFi 库自己用的是nvs.net80211这个命名空间所以我们自定义的时候要避开它用wifi_cfg或者app_config这类名字。键名最长 15 个字符这个限制很容易被忽略。如果你写了一个超过 15 字符的键名编译不会报错但运行时会失败或者截断。我一开始用了wifi_ssid_config这种名字结果死活读不出来排查了半天才发现是长度超了。所以键名要短ssid和pass就够了配合命名空间已经足够清晰。另外NVS 的总空间是有限的默认分区表下大概有 20KB 左右具体取决于分区表配置。对于存几个字符串来说完全够用但如果你打算存大量数据就要注意分区表里 NVS 分区的大小。可以在partitions.csv里调整。3.3 写入后为什么要重启NVS 写入是立即生效的putString返回后数据就已经落到 Flash 里了。但 WiFi 连接是在启动时建立的你改了 NVS 里的 SSID 和密码当前运行的 WiFi 连接不会自动切换。所以需要重启设备让启动流程重新读取 NVS 并连接新的 WiFi。重启用ESP.restart()就行。但这里有个细节如果你在 HTTP 请求的处理函数里直接调用ESP.restart()浏览器可能收不到完整的响应用户看到的是连接中断而不是保存成功。所以正确的做法是先返回一个提示页面延迟一两秒再重启。可以用delay(1000)然后ESP.restart()但delay会阻塞 HTTP 服务器的其他请求。更好的方式是用一个标志位在主循环里检测到标志位后执行重启。4. 浏览器端配置页面的实现4.1 HTTP 服务器的搭建Arduino 框架下用WebServer库最方便ESP-IDF 下用httpd。这里以 Arduino 为例因为它的代码更简洁适合快速验证。#include WiFi.h #include WebServer.h #include Preferences.h WebServer server(80); Preferences prefs; String currentSSID; String currentPass; void loadConfig() { prefs.begin(wifi_cfg, true); currentSSID prefs.getString(ssid, ); currentPass prefs.getString(pass, ); prefs.end(); } void saveConfig(String ssid, String pass) { prefs.begin(wifi_cfg, false); prefs.putString(ssid, ssid); prefs.putString(pass, pass); prefs.end(); }loadConfig在启动时调用从 NVS 读取配置。如果读到的 SSID 为空说明是首次启动可以进入配网模式或者用默认值。saveConfig在表单提交时调用写入新的配置。4.2 配置页面的 HTML 设计页面不需要多漂亮但要能用。核心是一个表单两个输入框SSID 和密码一个提交按钮。密码框用typepassword避免肩窥但可以加一个显示密码的复选框方便核对。const char* configPage Rrawliteral( !DOCTYPE html html head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleWiFi Config/title style body { font-family: sans-serif; max-width: 400px; margin: 40px auto; padding: 0 20px; } input { width: 100%; padding: 10px; margin: 8px 0; box-sizing: border-box; } button { width: 100%; padding: 12px; background: #007bff; color: white; border: none; cursor: pointer; } .info { color: #666; font-size: 14px; margin-top: 20px; } /style /head body h2WiFi 配置/h2 form action/save methodPOST labelSSID/label input typetext namessid value%SSID% required label密码/label input typepassword namepass value%PASS% required button typesubmit保存并重启/button /form div classinfo保存后设备将自动重启并连接新的 WiFi。/div /body /html )rawliteral;这里用了Rrawliteral(...)原始字符串避免转义引号的麻烦。%SSID%和%PASS%是占位符在返回页面时替换成当前值这样用户打开页面就能看到当前配置只需要改要改的部分。4.3 路由处理与参数替换void handleRoot() { String page String(configPage); page.replace(%SSID%, currentSSID); page.replace(%PASS%, currentPass); server.send(200, text/html, page); } void handleSave() { if (server.hasArg(ssid) server.hasArg(pass)) { String newSSID server.arg(ssid); String newPass server.arg(pass); saveConfig(newSSID, newPass); server.send(200, text/html, h3保存成功设备将在 2 秒后重启.../h3); delay(2000); ESP.restart(); } else { server.send(400, text/plain, Missing parameters); } } void setup() { Serial.begin(115200); loadConfig(); if (currentSSID.length() 0) { WiFi.begin(currentSSID.c_str(), currentPass.c_str()); // 等待连接超时后进入 AP 模式 int retry 0; while (WiFi.status() ! WL_CONNECTED retry 20) { delay(500); retry; } } if (WiFi.status() WL_CONNECTED) { server.on(/, handleRoot); server.on(/save, HTTP_POST, handleSave); server.begin(); Serial.println(WiFi.localIP()); } else { // 连接失败开 AP 模式让用户配置 WiFi.softAP(ESP32-Config); server.on(/, handleRoot); server.on(/save, HTTP_POST, handleSave); server.begin(); } } void loop() { server.handleClient(); }这段代码的逻辑是启动时从 NVS 读配置尝试连接。连上了就正常跑 HTTP 服务访问设备 IP 就能看到配置页。连不上就开一个热点叫ESP32-Config用户连上后访问192.168.4.1也能看到同样的配置页。这样无论设备当前能不能联网你都有办法改配置。5. 实操过程中的坑与排查5.1 表单提交后浏览器卡住前面提到过在handleSave里直接delay(2000)再ESP.restart()浏览器会收到响应但这两秒内 HTTP 服务器是阻塞的。如果此时有别的请求进来会超时。对于单用户配置场景来说问题不大但如果你追求更优雅的实现可以用一个全局标志位bool shouldRestart false; void handleSave() { // ... 保存逻辑 ... server.send(200, text/html, h3保存成功设备将在 2 秒后重启.../h3); shouldRestart true; restartTime millis(); } void loop() { server.handleClient(); if (shouldRestart millis() - restartTime 2000) { ESP.restart(); } }这样 HTTP 服务器在等待期间仍然可以响应其他请求体验更好。5.2 NVS 写入失败但没报错Preferences的putString返回的是写入的字节数如果返回 0 表示写入失败。失败的原因可能是命名空间没打开成功、NVS 分区满了、或者键名超长。我遇到过一种情况在prefs.begin()之后没有检查返回值直接putString结果因为之前某个操作把 NVS 写满了导致新数据写不进去但代码没有任何提示。所以建议每次写入后检查返回值size_t written prefs.putString(ssid, ssid); if (written 0) { Serial.println(NVS write failed!); }另外NVS 分区满的情况可以通过nvs_flash_erase()清空整个分区来解决但这会丢失所有数据慎用。更好的做法是定期清理不再使用的键或者合理规划分区大小。5.3 特殊字符导致的连接失败WiFi 密码里如果有特殊字符比如、\、%在 HTML 表单提交和 URL 编码的过程中可能会出问题。WebServer库会自动处理 URL 解码但如果你在 HTML 里用value%PASS%回填密码而密码里包含就会破坏 HTML 结构。解决办法是在替换之前对特殊字符做 HTML 转义String htmlEscape(String input) { input.replace(, amp;); input.replace(, lt;); input.replace(, gt;); input.replace(\, quot;); return input; }然后在handleRoot里用转义后的值替换占位符。这个细节很容易被忽略但一旦密码里有特殊字符页面就会乱掉。5.4 AP 模式下的 IP 地址开 AP 模式时ESP32 默认的 IP 是192.168.4.1这个一般不会变。但如果你同时开了 STA 和 AP或者改了 AP 的配置IP 可能会不同。建议在串口里打印出来方便排查。另外有些手机在连接没有互联网的 WiFi 时会自动切换到移动数据导致访问不到配置页。这时候需要手动在手机设置里保持连接这个 WiFi或者关闭自动切换网络选项。6. 进阶玩法与扩展思路6.1 加个密码保护配置页默认情况下任何能访问设备 IP 的人都能改配置。在局域网里问题不大但如果你把设备暴露在公网比如做了端口转发那就很危险了。加一个简单的 HTTP Basic Auth 就能挡住大部分情况server.on(/, []() { if (!server.authenticate(admin, your_password)) { return server.requestAuthentication(); } handleRoot(); });WebServer库自带authenticate方法用起来很方便。密码可以存在 NVS 里也可以硬编码。对于家庭内网设备硬编码一个复杂密码就够了。6.2 支持修改更多参数这套框架不只可以用来改 WiFi任何需要持久化的配置都可以走同样的路子。比如 MQTT 服务器地址、设备名称、上报间隔等。你只需要在 HTML 表单里加对应的输入框在handleSave里多写几个putString或putInt就行。我自己的一个环境监测节点就用这种方式管理了 WiFi、MQTT 地址、上报间隔、温度校准偏移量四个参数。每次调整不用重新烧录网页上改完重启就行非常省事。6.3 用 mDNS 代替记 IP每次都要查设备 IP 很麻烦。可以用ESPmDNS库给设备注册一个域名比如esp32.local这样浏览器直接访问http://esp32.local就能打开配置页不用记 IP。#include ESPmDNS.h if (MDNS.begin(esp32)) { MDNS.addService(http, tcp, 80); }这个在局域网里非常实用尤其是设备多了之后每个设备一个域名管理起来清晰很多。6.4 OTA 与 NVS 配置的结合既然已经做到了不重新烧录改配置那离 OTA空中升级其实只有一步之遥。Arduino 框架下有ArduinoOTA库ESP-IDF 下有esp_ota组件。把 OTA 和 NVS 配置结合起来就能实现完全远程维护改配置不用烧录升级固件也不用插线。不过 OTA 涉及分区表调整需要两个 app 分区复杂度比 NVS 配置高不少。如果你的项目还在早期建议先把 NVS 配置这套跑通等稳定了再考虑 OTA。7. 一些实测下来的经验这套方案我在三个不同类型的 ESP32 设备上跑了大概半年稳定性没问题。有几点体会比较深。第一NVS 的读写寿命是有限的Flash 擦写次数大概在 10 万次左右。对于配置这种低频写入场景完全够用但千万不要在循环里频繁写 NVS那样很快就会把 Flash 写坏。如果需要频繁保存的数据考虑用 RAM 缓存加定期落盘的方式。第二配置页面的 HTML 尽量精简。ESP32 的内存有限如果页面太大比如带了很多 CSS 和 JS返回响应时会占用较多内存极端情况下可能导致崩溃。我一开始用了一个带 Bootstrap 的页面结果在 ESP32-C3 这种内存较小的芯片上偶尔会重启后来换成纯手写 CSS 就稳定了。第三AP 模式的 SSID 最好带上设备标识比如ESP32-Config-A1B2这样周围有多个设备时不会搞混。可以用 MAC 地址的后两位来生成唯一后缀。第四保存配置后一定要给用户明确的反馈。我见过有人做完之后用户点了保存页面刷新了一下但不知道到底有没有成功。加一个保存成功设备将在 2 秒后重启的提示体验会好很多。第五如果你用的是 ESP-IDF 而不是 ArduinoNVS 的 API 会稍微复杂一些需要先调用nvs_flash_init()然后nvs_open()、nvs_set_str()、nvs_commit()、nvs_close()。但逻辑是一样的只是多了一层错误处理。HTTP 服务器可以用esp_http_server组件性能比 Arduino 的WebServer好一些但代码量也更大。最后分享一个小技巧如果你不确定 NVS 里到底存了什么可以在启动时把所有键值对打印到串口。Preferences库没有直接列出所有键的方法但你可以预先知道有哪些键逐个读取打印。或者用nvs_flash的原生 API 遍历命名空间。这个在调试配置问题时非常有用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑