首页 / 资讯中心 / 文章详情

ESP32免重刷改配置:浏览器直接编辑NVS键值实战解析

ESP32免重刷改配置:浏览器直接编辑NVS键值实战解析 ★ FEATURED ARTICLE
先交代一个我经常遇到的场景手里有一批已经部署出去的 ESP32 设备用户说办公室 WiFi 密码换了设备全部离线。按老办法我得一台台拆回来重新编译固件、重新烧录再发回去。后来我发现一个让人又爽又气的真相——大多数情况下设备本身根本没坏固件也没问题只是 WiFi 凭据存在 NVS 里没跟着更新而重刷固件本质上只是“顺手”把 NVS 覆盖了一遍。所以后来我给自己做了一个浏览器工具专门用来直接改 ESP32 的 NVS 键值。改 WiFi 密码、改 MQTT 服务器地址、改设备编号都不用重新编译、不用重刷整个固件。这篇文章就从头到尾聊清楚这件事NVS 到底存了什么为什么能直接改浏览器工具怎么实现以及我踩过的那些坑。1. 为什么改个 WiFi 密码会变成“重刷固件”1.1 固件和配置本来是两回事很多入门 ESP32 的同学会习惯性地把 WiFi 账号密码直接写在app_main()里const char *ssid MyOfficeWiFi; const char *password password123;这么写确实简单粗暴但代价就是——你想换一个 WiFi就必须改代码、重新编译、重新烧录。哪怕你只是把密码里的一个数字从 1 换成 2也得走一遍完整流程。实际上ESP32 的固件和运行配置是完全可以分离的。固件是那一段段程序逻辑负责“怎么做事”配置是设备运行时的参数负责“做什么”。WiFi 账号密码、服务器地址、设备名称这些都属于配置不应该编译进固件里。比较合适的做法是程序启动时从 NVS 读取配置再根据配置初始化 WiFi 连接。这样改配置只需要改 NVS不需要动固件。那为什么“重刷固件”这个操作如此流行大部分原因是开发阶段贪方便把配置写死了。生产的时候也懒得区分反正几十台设备都是同一套固件同一个 WiFi顺便跑个烧录脚本全刷一遍。但设备一旦到了用户手里这种方案的维护成本就直线上升。用户改个 WiFi你得远程让设备进入配网模式或者干脆人肉上门重刷。这也是我下定决心做一个专门改 NVS 的工具的原因。1.2 NVS 到底存在哪长什么样NVS 全称 Non-Volatile Storage非易失性存储。在 ESP32 的 Flash 分区表里专门有一个类型为nvs的分区。你可以在partitions.csv里看到类似这样的一行nvs, data, nvs, 0x9000, 0x5000,含义是NVS 分区的起始地址是0x9000大小为0x500020 KB。这个分区被 ESP-IDF 的 NVS 组件管理用来保存键值对数据。Flash 是掉电不丢数据的所以保存在 NVS 里的配置重启后依然存在。NVS 的存储方式有点像一个小型数据库只是它的接口极简单只有“命名空间 键 值”。命名空间相当于分类目录键是具体名字值是数据。例如namespace: app_cfg key: wifi_ssid MyOfficeWiFi key: wifi_pwd password123 key: mqtt_host 192.168.1.100ESP32 的 WiFi 协议栈本身也依赖 NVS。乐鑫的esp_wifi组件在初始化时会把 WiFi 相关的配置参数存到 NVS 的私有命名空间里。这也是为什么很多人会发现明明代码里esp_wifi_set_config设置了新的 WiFi 参数但重启之后设备连的还是旧的——因为协议栈默认会把配置缓存回 NVS下次启动直接读取。所以重点来了既然 WiFi 配置本身就存在 NVS 里我们完全可以直接修改 NVS 里的键值让设备重新读取新的 WiFi 账号密码而不需要重刷固件。浏览器工具的本质就是绕开“重新编译重新烧录整片 Flash”这一步只对 NVS 分区做精准读改写。2. 浏览器直接改键值的两条路线标题里说的“浏览器工具”其实有两条截然不同的实现路线。我自己一开始也纠结过后来发现它们各有各的适用场景最好都懂。2.1 路线一ESP32 内嵌配置页自己“管理”自己的 NVS思路很简单在 ESP32 的固件里塞一个微型 Web 服务器手机或电脑浏览器直接访问设备 IP打开一个配置页面填好新的 WiFi 账号密码后提交。设备端收到请求后调用 NVS 接口写入键值然后自动重连 WiFi。这条路线的好处是不需要电脑不需要串口线手机浏览器就能操作。不需要知道 NVS 分区在哪不需要管 Flash 布局ESP32 自己处理。用户体验好适合“用户自己动手改”的场景比如智能家居设备。但要实现它你的固件里必须已经包含这个 Web 配置功能。换句话说你得在第一次烧录固件之前就把这个功能写进去。如果手里这批设备已经是旧固件没有内置配置页这条路就走不通了。2.2 路线二PC 浏览器通过 WebSerial 直连串口操作 Flash这个路线更符合标题说的“工具”感觉。它不需要改设备固件而是让 PC 浏览器通过 WebSerial API 直接连接 ESP32 的串口然后像 esptool 一样操作 Flash。只不过它不做整片烧录而是精准定位 NVS 分区读出里面的键值数据修改后再写回去。WebSerial 是 Chrome / Edge 等浏览器提供的串口 API。网页代码可以用 JavaScript 直接打开串口、读写数据配合 DTR/RTS 信号控制 ESP32 进入下载模式。也就是说浏览器变成了一台“图形化的串口工具”。这条路线的核心优势是不需要改动现有固件适用于已经部署的设备。可以查看 NVS 里到底存了哪些键值比 esptool 那种“盲烧整片”的方式精细得多。所有操作在网页上完成跨平台不用装 Python、不用装驱动某些系统下 CDC 驱动还是要装但比 esptool 环境省事。缺点也很明显需要一台电脑需要一根 USB 线而且 WebSerial 只在部分浏览器里可用。另外如果固件里把 Flash 分区表锁死了或者 NVS 分区有加密操作会变得复杂。2.3 两条路线怎么选我给出一个非常实际的判断标准场景推荐方案原因固件是我自己写的还没量产内嵌 Web 配置页一劳永逸用户后期自己改设备已经部署固件没有配置页WebSerial 浏览器工具免重刷现场电脑改一下需要批量修改几十台设备两者结合先脚本改 NVS再考虑要不要升级固件设备在局域网里但拿不到串口内嵌 Web 配置页远程改不用拆机我自己现在的做法是新固件一律内置配置页同时保留 WebSerial 工具作为兜底方案。这样既兼顾了用户体验又保证了自己在现场排查问题时能直接操作 NVS。3. 免重刷的最简单落地给固件加一个 NVS 修改接口如果你刚好处于“固件还没量产可以改代码”的阶段我强烈建议先在代码里把 NVS 配置体系搭好。这部分的投入很小但后期维护省下的时间惊人。3.1 保存 WiFi 凭据的命名空间设计先定一个规范。我用app_cfg作为应用配置的命名空间里面放 WiFi 相关键值namespace: app_cfg key: wifi_ssid (string) key: wifi_pwd (string)代码里这样写#include nvs.h #include nvs_flash.h #define NVS_NAMESPACE app_cfg static void save_wifi_config(const char *ssid, const char *pwd) { nvs_handle_t handle; if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, handle) ! ESP_OK) { return; } nvs_set_str(handle, wifi_ssid, ssid); nvs_set_str(handle, wifi_pwd, pwd); nvs_commit(handle); nvs_close(handle); }注意两点第一nvs_set_str之后必须调用nvs_commit。不 commit 的话数据只停留在内存缓存中掉电即丢。很多人第一次用 NVS 时踩过这个坑以为 set 完就保存了。第二字符串最大长度默认是 4000 字节不同 ESP-IDF 版本有差异保存 WiFi 密码这类短字符串完全够用。3.2 启动时读取配置并连接 WiFi启动流程应该是先初始化 NVS再读取配置拿到 WiFi 信息后调用esp_wifi_set_config连接。static bool load_wifi_config(char *ssid, size_t ssid_len, char *pwd, size_t pwd_len) { nvs_handle_t handle; if (nvs_open(NVS_NAMESPACE, NVS_READONLY, handle) ! ESP_OK) { return false; } bool ok true; if (nvs_get_str(handle, wifi_ssid, ssid, ssid_len) ! ESP_OK) { ok false; } if (nvs_get_str(handle, wifi_pwd, pwd, pwd_len) ! ESP_OK) { ok false; } nvs_close(handle); return ok ssid[0] ! \0; }然后在app_main里nvs_flash_init(); char ssid[32] {0}; char pwd[64] {0}; if (load_wifi_config(ssid, sizeof(ssid), pwd, sizeof(pwd))) { wifi_init_sta(ssid, pwd); } else { // 没有配置启动 SoftAP 配网模式 start_config_ap(); }这样设备就有了“没配置就进配网模式有配置就自动连接”的行为。用户后续改密码只需要走一遍配网流程根本不用碰固件。3.3 内嵌配置页的实际代码骨架为了验证“浏览器直接改 NVS”这件事我给设备加了一个极简 HTTP 接口。用的 ESP-IDF 自带esp_http_server不需要额外引入库。接口设计很简单GET /api/wifi 返回当前 WiFi 配置SSID密码隐藏 POST /api/wifi 提交新的 WiFi 配置核心处理函数static esp_err_t wifi_post_handler(httpd_req_t *req) { char buf[128]; int ret httpd_req_recv(req, buf, sizeof(buf) - 1); buf[ret] \0; // 假设前端提交的格式是 ssidxxxpasswordyyy char ssid[32] {0}; char pwd[64] {0}; parse_form_urlencoded(buf, ssid, ssid, sizeof(ssid)); parse_form_urlencoded(buf, password, pwd, sizeof(pwd)); save_wifi_config(ssid, pwd); // 触发 WiFi 重新连接 esp_wifi_disconnect(); wifi_config_t wifi_cfg {0}; strncpy((char *)wifi_cfg.sta.ssid, ssid, sizeof(wifi_cfg.sta.ssid) - 1); strncpy((char *)wifi_cfg.sta.password, pwd, sizeof(wifi_cfg.sta.password) - 1); esp_wifi_set_config(WIFI_IF_STA, wifi_cfg); esp_wifi_connect(); httpd_resp_send(req, OK, HTTPD_RESP_USE_STATIC_LEN); return ESP_OK; }前端页面不用复杂一个 HTML 表单就够。手机浏览器打开http://设备IP/api填表单点保存设备几秒后就连上新 WiFi 了。这里有个经验改完 WiFi 之后不要直接esp_restart()先 disconnect 再 connect 更平滑。如果直接重启设备会短暂离线用户会以为操作失败了。先尝试重连如果重连失败再考虑重启回落。4. 想用浏览器直接改 NVS 二进制先看底层结构如果你走 WebSerial 路线那就绕不开 NVS 的二进制存储格式。因为你需要自己解析分区内容定位到具体键值改完再写回。4.1 NVS 页的结构NVS 分区由若干个 Page 组成每个 Page 大小固定为 0x10004096字节。一个 Page 内部大致有四种区域区域大小作用Page Header32 字节页状态、页序号Entry State Table32 字节每个 entry 的状态标记Bitmap32 字节标记哪些 entry 是 key 项Entry 区剩余空间存储具体的键值对条目一个 Entry 是 32 字节所以每个 Page 最多可以容纳 (0x1000 - 32 - 32 - 32) / 32 ≈ 120 个 Entry。一个键值对至少占用一个 Entry 作为 Key Entry数据长度超过 16 字节的话还要额外占用 Data Entry。这个结构意味着你不能像读文本文件一样直接看 NVS 内容必须按格式解析。幸运的是ESP-IDF 源码里nvs_handle.c和nvs_page.cpp把整个布局写得清清楚楚照着实现就行。4.2 读取和查找的逻辑NVS 写入是一种“追加式”设计。你更新一个键时它不会原地修改旧数据而是在空闲 Entry 里写入新值然后把旧 Entry 标记为删除。所以同一个键在页里可能出现多次但不能存在两个同为 Active 状态的同名键。读取的时候NVS 会做一次线性查找扫描所有 Entry按照名字的哈希和命名空间定位到目标键取最新有效的值。哈希用的是 Jenkins 32-bit 哈希算法计算namespace key的哈希值后作为搜索依据。这个查找逻辑对浏览器工具的意义是如果你想在网页上“可视化编辑 NVS 键值”不能只做字符串替换必须严格按照 NVS 的条目布局去解析和重建。最简单的做法是不要尝试“原地修改某一个键”而是把整个 NVS 分区读出来解析成一份人类可读的 CSV 列表用户修改 CSV 后再重新生成一个全新的 NVS 分区镜像写回去。4.3 用 nvs_partition_gen.py 生成镜像的捷径ESP-IDF 自带了一个工具nvs_partition_gen.py可以用 CSV 文件生成 NVS 分区镜像。它的基本用法是python nvs_partition_gen.py generate nvs.csv nvs.bin 0x3000CSV 文件内容类似key,type,namespace,encoding,value wifi_ssid,namespace,app_cfg,,, wifi_pwd,data,app_cfg,string,password123生成出的nvs.bin就是一个完整干净的 NVS 分区镜像。你可以直接用 esptool 写进 Flashesptool.py --port COM3 write_flash 0x9000 nvs.bin很多浏览器 WebSerial 工具的核心流程就是读原分区 → 解析 CSV → 修改 CSV → 调用类似 nvs_partition_gen 的算法生成新镜像 → 写回原分区。但这里有一个大坑我后面专门讲直接写一个全新的 NVS 镜像会丢光原有分区里的其他键值。如果你只存了 WiFi 配置那无所谓但如果设备还有校准数据、用户数据、计数值存在同一个 NVS 分区就会出大问题。5. WebSerial 工具的完整流程设计下面进入正题自己写一个浏览器工具实现“不重刷固件直接改 NVS”。5.1 串口连接与进入下载模式浏览器端使用 WebSerial API。打开端口、设置波特率、发数据的代码大致如下const port await navigator.serial.requestPort(); await port.open({ baudRate: 115200 }); const writer port.writable.getWriter();要操作 Flash需要让 ESP32 进入 UART 下载模式。ESP32 的 bootrom 在收到特定时序的 DTR/RTS 信号后会进入下载模式。WebSerial 也可以控制这两个信号只要你的 USB 转串口芯片支持const signals await port.getSignals(); signals.dtr true; signals.rts true; await port.setSignals(signals);具体的进入下载模式的时序在不同芯片上略有不同核心思路是先拉低 EN复位脚再拉低 GPIO0再释放 EN最后释放 GPIO0。浏览器工具里通过 DTR 和 RTS 的组合逻辑来实现。这块我建议直接参考社区里已有的串口协议 JS 实现手写很容易在时序细节上卡住。5.2 读取、修改、写回三步走工具的操作流程我设计成三个大步骤第一步读取 NVS 分区。通过串口发送 ROM 的读 Flash 命令按地址和长度把 NVS 分区所在区域全部读出来。注意读的是分区内容不是整颗 Flash反正我们只关心 NVS。第二步解析并让用户修改。网页上把二进制数据解析成表格列出所有 namespace / key / value。用户直接在表格里改 SSID、密码、地址等。这一步我会在页面里塞一个校验逻辑比如字符串长度、是否非法字符避免把非法数据写进 NVS。第三步写回。生成新的 NVS 分区二进制用擦除命令先擦除 NVS 分区再把新数据块写进去。写完后复位设备。放在网页上就是一个“上传 CSV / 编辑表格 / 一键写入”的交互。为了避免误操作我在写回前会弹一个确认框显示“即将擦除 NVS 分区并写入新配置原配置将覆盖是否继续”。这一步一定要有我见过太多人手滑点一下就后悔的情况。5.3 为什么我推荐“只改 NVS 分区”而不是“全 Flash 备份再改”有同学会问既然都进入下载模式了为什么不把整个 Flash 读出来在镜像里改 NVS再整片写回去这样做不是最稳吗理论上是但实际上问题很多整片 Flash 备份可能要几 MB浏览器里处理起来慢而且容易出错。整片写回时如果分区表之外的区域如 OTA 分区有任何差异可能导致设备变砖。OTA 之后 Flash 布局可能和出厂时不一样你拿旧备份整片覆盖会挂得更惨。所以我的建议是只要目标明确就只操作 NVS 分区。分区表里 NVS 分区的起始地址和大小都是固定的读它、擦它、写它风险可控其他分区完全不碰。整个过程看起来像“深蹲完只擦桌子”精准而且安全。6. 实操中的那些坑6.1 分区表偏移对不上写入后设备起不来浏览器工具一共有两个地址一个是 NVS 分区在 Flash 里的偏移一个是分区大小。这两个值必须和固件实际使用的partitions.csv完全一致。我遇到过一台设备partitions.csv里 NVS 分区偏移是0x9000但旧固件编译时用的是另外一份分区表偏移是0x10000。我把新镜像写到0x9000设备起来后完全找不到配置连不上 WiFi表现非常诡异。排查结论固件初始化 NVS 时读取的地址是编译时烧进镜像的分区表不是你浏览器工具“以为”的地址。所以工具里最好做成让用户手动输入 NVS 偏移和大小不要写死默认值同时提示用户去partitions.csv里核对。6.2 字符串长度与编码问题NVS 的nvs_set_str保存的是字节序列没有强制 UTF-8但通常配置都用 UTF-8 字符串。浏览器 JavaScript 处理字符串时默认是 UTF-16写入 NVS 前必须转换成 UTF-8 字节。如果你不做编码转换直接拿 UTF-16 数据去写SSID 会变成乱码。另外 NVS 键名本身是有限制的最长 15 个字符且只能是字母数字下划线。我有个工具里让人自定义键名结果用户填了个带横杠的键名写入时一直失败。后来我在前端加了正则校验if (!/^[a-zA-Z0-9_]{1,15}$/.test(keyName)) { alert(键名只能包含字母、数字、下划线最长15位); return; }6.3 Flash 擦写寿命和掉电风险NVS 分区虽然做磨损均衡但它毕竟还是 Flash。频繁地整分区擦写会加速 Flash 老化。ESP32 的 Flash 擦写寿命通常在十万次级别日常改 WiFi 配置不至于写坏但如果是定时写入计数的应用就要特别注意了。还有一个更关键的问题擦写过程中掉电轻则丢配置重则损坏整页数据。浏览器工具在写回前一定要提醒用户“接好电源别拔线”。专业一点的实现是先写到一个全新的 NVS 分区镜像再通过原子替换的方式切换但那样需要额外预留分区空间普通小工具不必搞这么复杂。6.4 固件升级时千万别“顺手”覆盖用户改过的配置最后这点算是我额外想提醒的。如果你的设备后来支持 OTA 升级升级代码里千万不要无条件调用nvs_flash_erase()。有人为了“保证干净”在固件版本升级时把 NVS 整个擦掉结果用户刚通过浏览器工具改好的 WiFi 配置被全清空设备瞬间离线。正确的做法是升级过程中保留 NVS 分区不动启动时优先读取已有配置。只有在检测到“当前配置结构严重不兼容”时才引导用户重新配网。这一点看起来很小但在实际项目里因为“擦 NVS 导致用户重新配网”而售后爆炸的案例我见得太多了。最后分享两个小技巧第一个给浏览器工具加一个“导出当前配置”的按钮。操作 NVS 之前先把原始二进制导出一份存到本地。改坏了、写崩了至少还有原始数据可以恢复。这个习惯帮我避免了好几次现场事故。第二个把工具做成可以批量操作的。如果你有十几台设备要改 WiFi一台台连串口太痛苦。可以把“读取配置 → 修改 → 写回”做成一键流程换设备后点一下按钮就完成。配合串口转接线半小时改完全部设备不是梦。改 NVS 键值这件事本质上是把设备的运行配置从“固件的一部分”里剥离出来。当你真正完成这种分离后会发现很多以前让人抓狂的运维场景其实都只需要打开浏览器改一行配置。
阅读完成 · 觉得有帮助?
咨询建站