ESP32分区表详解:多应用共用Flash的数据隔离方案
发布时间:2026/10/1 9:02:31来源:尧图网络
1. 为什么要认真对待 ESP32 的分区表先把结论放在前面ESP32 的 Flash 是多个小应用共存的唯一命脉而分区表就是这套共同居住方案的“房契”和“地契”。我用 ESP32 做过几个实际项目最让人头疼的往往不是逻辑代码跑飞而是多个功能模块在同一个 Flash 上互相踩踏——今天这个模块存的配置把另一个模块的数据覆盖了明天升级固件时又把用户的历史记录刷没了。这种问题一旦上线排查起来非常痛苦因为代码逻辑上看起来完全没问题但数据就是莫名其妙地“串门”了。先说清楚一个问题ESP32 片上集成的是 SPI Flash用来存放固件、配置文件、日志、证书、Web 页面等一切掉电不能丢失的东西。常见的 ESP32 模块比如 WROOM-32默认带 4MB Flash也有 8MB、16MB 的版本。Flash 本身是一块连续的存储空间但同一时刻可能有多个“小应用”——比如一个 App 负责联网升级、一个 App 负责传感器数据记录、一个 App 负责蓝牙配网它们都往 Flash 里写东西。如果不把这块 Flash 提前划分清楚那后面发生的事情就是灾难。我最早吃过一次亏当时在做一个智能家居网关固件里集成了一套 Web 页面用于本地管理设备同时还有温度历史记录存储。我刚开始图省事直接把 Web 页面打包进固件分区把传感器数据也往同一个分区里写。结果运行了两个月后用户反馈设备升级后会变成“白屏”而且历史记录经常丢失。查了一圈才发现固件分区被 OTA 升级覆盖时连带把历史数据区也一并清掉了——因为这两段数据在 flash 地址上挨在一起而分区表又没有明确划分边界。从那以后我彻底弄明白了在 ESP32 上分区表绝不是可有可无的配置项而是整个 Flash 管理的地基。这篇文章会从分区表的原理讲起再结合常见的“多个小应用共用 Flash”场景给你一套可以直接抄作业的配置方法和代码示例。适合刚接触 ESP32 开发、以及已经在用 ESP32 做项目但是苦于数据互相干扰的开发者。2. ESP32 Flash 的整体结构解析2.1 Flash、分区表、分区之间的关系拿买房来打比方Flash 相当于一整栋毛坯楼分区表相当于房产局的登记册而每个分区就是一套已经划分好边界的房子。ESP32 启动时ROM 引导程序会先去读取 Flash 开头的分区表确认哪个区域存 Bootloader、哪个区域存应用固件、哪个区域存文件系统然后才敢动手干活。ESP32 的 Flash 地址从 0x000000 开始下面是典型的出厂布局起始地址长度分区类型分区子类型名称用途0x0000000x10000x000x00bootloader一级引导程序0x0100000x100000x010x00nvs键值存储NVS0x0200000x100000x010x01otadataOTA 信息记录0x0300000x2000000x000x10app0主应用固件OTA 槽位 00x2300000x2000000x000x11app1OTA 备份槽位0x4300000x1000000x010x82spiffs文件系统存储分区表本身是一张 CSV 格式的表格编译时通过“partition_table_CSV”配置文件生成二进制分区表烧录到 Flash 的 0x8000 地址附近。所有分区的总大小不能超过 Flash 实际容量而且每个分区都有自己的起始地址、长度、类型和子类型。这里有一个关键点分区表决定了 Flash 的“法律边界”但代码能不能守住这条边界就要靠你自己了。因为 ESP32 的读写 API比如 esp_partition_read/write确实会在分区内部做边界检查可如果你用裸的 SPI Flash 读写函数esp_flash_read/esp_flash_write那就是完全绕过分区表直接访问硬件地址了随意一个越界地址就可能把别的分区打得稀巴烂。2.2 分区类型和子类型到底怎么区分在 ESP-IDF 中分区类型type和子类型subtype是一个让人一开始容易懵的组合。常见的 type 有以下几种0x00app存放固件子类型 0x10 是 factory 出厂固件0x00 是 OTA 槽位 00x10 是 OTA 槽位 1以此类推。0x01data存放数据子类型非常多常见的有 0x00NVS、0x01OTA 信息、0x02PHY 初始化数据、0x81SPIFFS、0x82LittleFS等。0x02 ~ 0xFE自定义分区类型可以由开发者自由使用。0xFF表示未使用。我见过不少朋友有个误解以为只要把多个文件系统分区都设为 SPIFFS 类型就能互相隔离。实际上同类型的多个分区虽然物理上分开但如果你在代码里没有把它们各自挂载到不同的目录/标签那么操作时的路径冲突一样会造成串门。举个例子两个 SPIFFS 分区分别命名为“web”和“data”如果都用esp_vfs_spiffs_register挂载时报了相同的“/spiffs”前缀第二个挂载往往会失败就算成功读写路径也会彼此覆盖。所以分区规划完了代码里的挂载路径和标签也必须有严格的隔离意识。2.3 为什么 OTAdata 这个分区如此重要提到分区表绝对不能跳过 OTA 信息分区。它位于分区表中名为“otadata”专门记录当前应该从哪个 OTA 槽位启动。ESP32 的 OTA 机制是典型的 A/B 分区方案app0 是当前运行的槽位app1 是待升级槽位。升级时把新固件写入 app1写入完成后将 otadata 的状态标记为“app1 已验证可用”然后重启引导程序根据 otadata 的指示从 app1 启动。如果分区表里没有 otadata会怎么样OTA 功能直接不可用。我实际遇到过把出厂默认分区表删掉、自己重写一个简易分区表时忘了加 otadata结果升级时不断重启到 factory 分区连日志都是老的。排查了很久才想起这件事。所以凡是打算用 OTA 升级的项目分区表必须预留 otadata 分区哪怕它只有 0x1000064KB大小——实际上 otadata 只需要一个扇区大小就够但为了对齐和未来扩展一般给 0x10000 比较稳。3. “多个小应用共用 Flash”的实际场景推演3.1 典型场景一一套固件内集成多个功能模块这是最常见的情况。一个 ESP32 项目里同时有配网模块用 NVS 保存 Wi-Fi SSID/密码业务模块用 NVS 保存设备运行参数Web 服务器模块用文件系统保存前端页面日志模块用文件系统保存运行日志这四类数据都存放在同一块 Flash 上。如果不做任何规划全往 NVS 里塞那么 NVS 的键值空间很快会被不同的功能模块搅成一锅粥——Wi-Fi 模块可能把业务模块的键给覆盖了或者日志文件把 Web 页面文件顶掉了。最直接的办法就是将不同类型的数据分配到不同分区并且为每个模块建立独立的 NVS namespace。NVS 在 ESP-IDF 中的设计其实已经考虑到了这个问题它支持命名空间namespace。你在代码里调用nvs_open(wifi_config, NVS_READWRITE, handle)只会操作名为“wifi_config”的命名空间nvs_open(device_config, NVS_READWRITE, handle)操作的是“device_config”空间。两个空间的键值不会互相覆盖。我一个项目里最多开了五六个 namespace每个模块只管自己的互相之间完全无感而且不同 namespace 还可以设置为只读防止其他模块误写。3.2 典型场景二同一 Flash 上运行多个独立固件还有一种场景是“轮换式”同一块 Flash 上安装了多个独立编译的固件比如一个固件是传感器数据采集器另一个是网关模式通过某种方式在启动时根据外部开关或网络指令选择运行哪个固件。这种方案本质上就是使用多个 app 分区配合不同的分区表组合。但这里有一个硬约束同一时刻只有一个 app 分区能运行。因为 ESP32 的 CPU 只能执行 Flash 中映射到内存区域的内容不可能同时跑两份固件。如果你真的需要“两个应用同时跑”那就不是分区表的范畴得靠 FreeRTOS 多任务——在同一个固件里开两个任务。而如果是“多个固件轮替运行”那就可以用多个 factory/OTA 槽位实现切换每次只烧录到非当前运行的分区重启切换槽位。我做过一个录音笔类的设备里面存了“录音采集固件”和“播放器固件”两份 App按键长按 3 秒切换到播放器模式。实现方式就是在分区表里分配两个足够大小的 app 分区当前固件运行时会检查一个“模式切换”的 NVS 键然后调用esp_ota_set_boot_partition切换到另一个 app 分区。整个过程核心就是分区表要把两个 app 分区的边界画清晰并且给“模式标记”留一个专门的 NVS 分区。3.3 典型场景三文件系统空间互相隔离文件系统LittleFS / SPIFFS在 ESP32 项目里简直是刚需网页资源、图片、音频、证书文件都需要它。多个小应用如果都往同一个文件系统分区里写文件很容易把空间耗尽甚至出现一个应用删掉另一个应用文件的“惨案”。解决办法就是给每个应用分配独立的文件系统分区。比如定义两个分区web_fs用于 Web 页面挂载到/web、log_fs用于日志挂载到/log。这两个分区物理上是两块代码里分别挂载互不干扰。即使其中一个被写满、损坏另一个也不受影响。同样要在分区表里给日志分区也留一个独立空间。很多垃圾问题排查往往需要看日志如果日志和 Web 页面在同一个文件系统里一旦 Web 页面被 OTA 升级覆盖日志同时也没了下次想定位问题都没法查。把日志分区单独拉出来升级 App 时不动日志分区就能保留历史日志。4. 分区表设计一步步搭建防串门的隔离方案4.1 先画清楚内存地图分区表设计的第一步不是打开配置工具而是先算清楚你的需求。我建议你在纸上或表格里列一下需求大概需要大小类型/子类型期望的持久性系统固件含 OTA 备份槽根据编译后固件大小 ×2app升级后保留配网信息SSID、密码、设备ID16KB~64KBNVS升级后保留业务参数阈值、开关量16KB~64KBNVS升级后保留Web 页面资源1MB~3MBLittleFS升级后保留传感器历史日志256KB~1MBLittleFS升级后保留OTA 切换标记16KB~64KBotadata升级后保留估算好了之后再把这些需求映射到 Flash 的物理空间里。这里有个重要原则地址对齐到 Flash 扇区。ESP32 Flash 的扇区大小通常是 0x10004KB所以每个分区的起始地址和长度最好都是 0x1000 的整数倍否则分区工具可能会报警甚至烧录时出现奇怪的越界问题。另外一个小经验如果 Flash 总容量不大宁可将 app 的 OTA 槽压缩到刚好够放固件的大小也要把 NVS 和文件系统留足。因为固件升级后代码是可以重新烧录的但用户的数据一旦没了就是真没了。我在做 4MB Flash 的模块时通常会限制固件在 1.2MB 以内这样 app0、app1 各占 1.4MB留下约 1.2MB 给 NVS、文件系统和日志。当然具体分配要根据实际项目来没有一种“万能比例”。4.2 用 CSV 自定义分区表实操演示ESP-IDF 的分区表使用 CSV 文件编写默认位置是partitions.csv。下面是它最基本的结构# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x4000, otadata, data, ota, 0xd000, 0x2000, app0, app, ota_0, 0x10000, 0x200000, app1, app, ota_1, 0x210000,0x200000, web_fs, data, spiffs, 0x410000,0x100000, log_fs, data, littlefs,0x510000,0x80000,这个 CSV 里的字段依次是名称、类型、子类型、偏移地址、大小十六进制、标志位。实际使用中你不需要手动计算偏移。ESP-IDF 支持不带 Offset 的写法比如nvs, data, nvs, 0x9000, 0x4000 otadata, data, ota, , 0x2000 app0, app, ota_0, , 0x200000 app1, app, ota_1, , 0x200000 web_fs, data, spiffs, , 0x100000 log_fs, data, littlefs, , 0x80000此时分区表工具会自动按顺序把没有指定 Offset 的分区排到上一个分区之后。不过我还是建议至少把 nvs 的 Offset 显式写出来因为默认厂商分区表会把 nvs 放在 0x9000如果你把nvs放到最后可能会与默认的 bootloader 区域重叠。在 CMake 项目里你需要在CMakeLists.txt中指定set(PARTITION_TABLE_CSV partitions_custom.csv)或者在menuconfig的Partition Table选项中选择Custom partition table CSV并填入文件名。4.3 烧录时分区表如何生效分区表本身也是一个独立的烧录对象地址固定在 0x8000。烧录命令常见的有两种# 整片烧录包含 bootloader、分区表、应用固件 idf.py -p /dev/ttyUSB0 flash # 只烧分区表 idf.py -p /dev/ttyUSB0 partition-table如果你使用 esptool.py 手动烧录需要注意分区表偏移esptool.py --chip esp32 -p /dev/ttyUSB0 -b 460800 write_flash 0x8000 partition_table.bin这里有个隐藏坑如果你在 menuconfig 里完成了分区表配置但烧录时没有把新的分区表写入 Flash那么应用固件启动后可能按照旧分区表来解析导致明明代码里加了新分区Flash 里却没有对应空间一读写就报错。这个问题在我刚开始用自定义分区表时遇到过好几次后来我习惯在每次改了 CSV 之后先单独烧一遍 partition-table再烧 app确保 Flash 里的“房契”和代码里的“房契”是同一版本。5. 实践在代码层面真正守住数据边界5.1 使用 esp_partition API 进入专属区域分区表写清楚只是第一步代码里也要用对 API。正路是使用esp_partition_*系列函数它们会在操作前自动校验地址是否越界。示例代码如下#include esp_partition.h const esp_partition_t *my_part esp_partition_find_first( ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_LITTLEFS, log_fs); if (my_part NULL) { ESP_LOGE(TEST, log_fs partition not found); return; } // 写入数据length 不能超过分区大小 esp_err_t err esp_partition_write(my_part, 0, data, data_len); if (err ! ESP_OK) { ESP_LOGE(TEST, write failed: %s, esp_err_to_name(err)); }这里反复强调的是分区名必须和 CSV 里定义的名字完全一致。一个字母大小写错都不行比如 CSV 写的log_fs代码里用了LOG_FSesp_partition_find_first就会返回 NULL。我踩过这个坑排查了半天才发现是大小写问题。如果你要读写的分区是 app 类型必须确保该分区当前不在运行状态。不能直接往正在运行中的 app 分区写入数据因为 Flash 的代码映射区域会被破坏导致 CPU 取指错误。这几乎是所有 ESP32 新手都会问的为什么我对 app 分区调用esp_partition_write没有反应或者直接崩溃答案就是这个。5.2 NVS 的多命名空间隔离NVS 是大家最容易“串门”的地方。它虽然提供了命名空间隔离但很多人不注意把所有的配置全都扔进同一个默认的 namespace 里然后键名又起得非常通用比如“mode”“count”“data”后果就是 A 模块改了配置B 模块读到的是 A 的值。正确的做法是每个小应用都创建自己独立的 namespace。以一个包含配网模块和业务模块的项目为例#include nvs.h #include nvs_flash.h // Wi-Fi 配网模块 nvs_handle_t wifi_handle; nvs_open(wifi_config, NVS_READWRITE, wifi_handle); nvs_set_str(wifi_handle, ssid, my_wifi); nvs_set_str(wifi_handle, password, secret123); nvs_commit(wifi_handle); // 业务模块 nvs_handle_t device_handle; nvs_open(device_config, NVS_READWRITE, device_handle); nvs_set_i32(device_handle, fan_speed, 3); nvs_commit(device_handle);当业务模块想读 Wi-Fi 密码时即使它真的调用了nvs_get_str(..., password, ...)只要它还是用device_config这个 namespace就不会读到 Wi-Fi 模块存的值——因为两个 key 存在于不同命名空间互不可见。这就是“门牌号和房间号双重隔离”的意义。注意NVS 的一个键名最多 15 个字符命名空间本身最长也是 15 个字符。太长会被截断或返回错误。如果你的键名天然就很长请做缩写映射否则后面排错会是一场灾难。还有一个细节NVS 的读写在写入后需要调用nvs_commit才会真正落盘到 Flash。很多新手忘了这一步数据写到一半断电就丢了。不是大问题但如果你指望掉电保护数据必须在关键写操作后调用 commit。5.3 多个文件系统分区的挂载与使用文件系统隔离在代码层面也相当直接。每个分区单独挂载到不同的 VFS 路径避免冲突。使用 LittleFS 的典型代码#include esp_littlefs.h esp_vfs_littlefs_conf_t web_conf { .base_path /web, .partition_label web_fs, .format_if_mount_failed true, .dont_mount false, }; esp_vfs_littlefs_register(web_conf); esp_vfs_littlefs_conf_t log_conf { .base_path /log, .partition_label log_fs, .format_if_mount_failed true, .dont_mount false, }; esp_vfs_littlefs_register(log_conf);注册完成后你就可以用标准的 POSIX API 进行文件读写// Web 模块写自己的页面文件 FILE *fp fopen(/web/index.html, w); fwrite(html, 1, strlen(html), fp); fclose(fp); // 日志模块写自己的日志 FILE *lf fopen(/log/2025_04_01.log, a); fprintf(lf, temp25.6\n); fclose(lf);两个模块通过路径天然隔离一个模块再怎么乱写也只能在/web或/log这两个挂载点内部折腾。即使同一个分区下的目录可以随便创建跨分区的路径根本无法访问因为 VFS 层就把路封死了。注意一个事项当你使用partition_label挂载文件系统时该 label 必须和分区 CSV 里的名称一致。如果标签找不到esp_vfs_littlefs_register会返回错误。此外如果两个分区都定义成 SPIFFS 但挂载路径不同那也是没问题的不过 LittleFS 在掉电保护、目录操作上比 SPIFFS 更成熟新项目我建议直接使用 LittleFS。5.4 防止跨分区绕行别用底层 Flash API在代码中如果你想保持数据隔离的严格性最需要避免的是绕过esp_partition_*直接调esp_flash_read/write裸写 Flash。理由很简单esp_flash_read/write的地址是全局 Flash 地址分区表不会帮你做边界检查。一旦你算错偏移写入可能直接落在其他分区的地址范围内轻则破坏数据重则损坏 OTA 元数据导致设备无法启动。我见过一个真实案例某开发者想给日志模块提升写入速度直接用esp_flash_write往log_fs分区的绝对地址写数据结果因为偏移计算错误把一个扇区写到了app1分区中。当时运行正常但等到 OTA 升级时系统从 app1 启动就直接崩溃。查了很久才定位到是日志模块的越界写入惹的祸。所以只要你能用esp_partition_*就绝不要用裸 Flash API。只有在你真正处理自定义分区类型、且完全清楚边界条件时才考虑底层操作——而且即便如此我也建议你封装一层分区信息校验把“起始地址 偏移”做个边界判断。6. 升级与启动链路中的数据保护细节6.1 OTA 升级时哪些分区会被动哪些不会动这个问题在多个小应用共存的场景中极其关键OTA 升级默认只写 app 分区app0/app1和 otadata 分区不会动 nvs 和文件系统分区。也就是说正常情况下你的配置、历史记录、日志在升级后都会保留。这正是分区表分离的“数据护城河”。但实际项目中很多人会手滑将分区表“整体擦除”再烧录。比如用esptool.py erase_flash或者烧录工具勾选了“擦除整个 Flash”那抱歉你辛辛苦苦存的用户数据全部归零。所以如果你在生产环境发布固件千万别用整片擦除的方式。更隐蔽的一种情况是升级 app 时选错了分区表。比如你现在跑在 app0新版本固件把自己的 app 大小从原来的 1MB 扩大到 2MB而当前分区表里 app0 只有 1MB。这时候 OTA 写入 app1 同样大小也很可能不够导致写入超界直接被拒绝。所以固件体积变化时必须检查分区表是否同步调整。6.2 启动时检查分区表是否与固件匹配ESP32 启动时不会检查 app 固件是不是“按当前分区表编译”的它只会按照分区表内容去加载对应地址上的代码。如果你把不同分区表编译的两个固件交叉烧录系统会启动到一个意想不到的位置。轻则启动异常重则 panic。建议在你的应用启动早期添加一个“分区表自检”逻辑const esp_partition_t *running esp_ota_get_running_partition(); ESP_LOGI(MAIN, Running on partition: %s, subtype: %d, running-label, running-subtype);然后在日志里对比一下预期分区名。如果跑出来的分区名和你预想的不一致说明分区表或烧录流程已经出了问题。这条日志在项目交付时能帮你省下大量售后排查时间。对需要严格保护数据的设备可以在 NVS 里存一个“上次正常运行次数”的计数。每次启动先读计数启动成功后加 1如果发现计数比上次的数值少了 1 以上说明可能经历了一次启动失败后的自动回滚这时你就应该考虑是否需要把数据回退到某个安全版本。6.3 意外断电时的抗损能力Flash 写入过程断电会撕裂一个扇区。如果你的配置在 NVS、日志在 LittleFS那么断电只会损坏单个扇区不会祸及整个分区。但如果你把配置和日志放在同一个分区里断电时两个模块的数据可能同时受损。文件系统层面LittleFS 对掉电有比较强的保护设计目录项和文件数据均通过写入前日志实现原子操作而 SPIFFS 虽然也能恢复但有时会留下半截文件。如果你在开发阶段用的是 SPIFFS 上线我强烈建议尽早切到 LittleFS。NVS 本身具备“写前擦除 双页备份”的机制在一定范围内比如 NVS 每次最多约 4KB 的写缓冲可以保证掉电不损坏旧数据。但注意 NVS 对单键的频繁写入会消耗 Flash 寿命。ESP32 Flash 的可擦写次数一般在 1 万到 10 万次之间。如果你的日志是高频写入比如每秒一次那千万不能用 NVS 来存要放到 LittleFS 分区并且自己做文件轮转和磨损均衡——或者干脆用外部存储。6.4 自定义分区数据的安全擦除当你要在出厂前将敏感数据擦除干净时通常只会擦除 nvs 或 data 分区而保留 app 分区。使用 esptool.py 擦除指定分区# 擦除整个 Flash esptool.py --chip esp32 -p /dev/ttyUSB0 erase_flash # 擦除指定地址范围需要知道分区偏移和大小 esptool.py --chip esp32 -p /dev/ttyUSB0 erase_region 0x9000 0x4000擦除 NVS 分区比较简单但如果你只想擦除当前应用的 NVS namespace而不动其他模块的配置那么代码里可以调用nvs_erase_all(handle)这是按 handle 对应 namespace 删除非常精准。我在调试固件时经常用这个顺序先擦数据分区保留 app然后一键重启系统会自动重建 NVS 和文件系统。这样既保留了固件又恢复了出厂状态效率很高。7. 实操手记一个完整的四分区隔离项目配置7.1 场景设定为了让你更容易套用我拿一个现实中做过的“环境监测节点”来举例。设备功能固件支持 OTA 升级配网参数Wi-Fi SSID、密码、服务器地址存 NVS设备业务参数采样间隔、报警阈值存独立的 NVS namespaceWeb 页面资源1MB存放在web_fsLittleFS采样历史数据512KB存放在data_fsLittleFS7.2 分区表全量配置以 4MB Flash 为例我划分如下# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x6000, otadata, data, ota, 0xf000, 0x2000, app0, app, ota_0, 0x11000, 0x1D0000, app1, app, ota_1, 0x1E1000,0x1D0000, web_fs, data, littlefs, 0x3B1000,0x50000, data_fs, data, littlefs, 0x401000,0x80000,这里说明几个设计考虑nvs 放在 0x9000、大小 0x600024KB足够放配网和业务参数未来扩展空间还行。otadata 紧随其后大小为 0x20008KB实际一个扇区就够给双份是为了防止写坏时备份。app0/app1 各 0x1D0000约 1.8MB4MB Flash 留给应用 1.8MB ×2 3.6MB剩余空间约 0.4MB 给文件系统。如果你的固件小于 1.8MB这个分配没问题如果超过你需要扩大 app 区而削减文件系统。web_fs 0x50000320KB data_fs 0x80000512KB为了对齐我把 web_fs 和 data_fs 都放在后续地址中间没有空洞。当然这个方案有个缺陷web 资源 320KB 有点紧张。如果你页面里有比较多的图片或 JS 库建议把 Flash 升级到 8MB 再分配。7.3 代码层初始化顺序一个稳妥的启动初始化顺序是调用nvs_flash_init()初始化 NVS 子系统必须先于几乎所有需要持久化的模块。挂载 LittleFS 分区web_fs、data_fs。读取 NVS 中的设备配置。启动网络/业务任务。初始化代码大致长这样简化版void app_main(void) { // 初始化 NVS esp_err_t ret nvs_flash_init(); if (ret ESP_ERR_NVS_NO_FREE_PAGES || ret ESP_ERR_NVS_NEW_VERSION_FOUND) { ESP_ERROR_CHECK(nvs_flash_erase()); ret nvs_flash_init(); } ESP_ERROR_CHECK(ret); // 挂载 web_fs esp_vfs_littlefs_conf_t web_fs_conf { .base_path /web, .partition_label web_fs, .format_if_mount_failed true, .dont_mount false, }; ESP_ERROR_CHECK(esp_vfs_littlefs_register(web_fs_conf)); // 挂载 data_fs esp_vfs_littlefs_conf_t data_fs_conf { .base_path /data, .partition_label data_fs, .format_if_mount_failed true, .dont_mount false, }; ESP_ERROR_CHECK(esp_vfs_littlefs_register(data_fs_conf)); // 开启 Web 服务器、传感器采集任务等 start_web_server(); start_sensor_task(); }注意如果format_if_mount_failed设为 true那么文件系统一旦异常就会被重新格式化。作为开发调试很方便但在生产环境中如果你不希望在日志区被损坏后自动抹掉历史数据这里就要谨慎了。我的做法是业务配置分区的 autoformat 设为 false日志分区的设为 true。业务数据丢不起日志丢了可以重建。7.4 写入/读取日志时的分区边界验证在写入文件时你无法直接通过文件 API 获知剩余空间但可以用esp_littlefs_info获取分区容量和已用空间size_t total 0, used 0; esp_littlefs_info(data_fs, total, used); ESP_LOGI(LOGGER, data_fs total%d, used%d, total, used);当写入日志前检查剩余空间若剩余空间不足可以触发日志轮转将当前日志文件重命名为.old再新建写入文件。这个流程能保证 file system 不会因为写满而崩溃。日志轮转判断代码如下简化FILE *log_fp fopen(/data/current.log, a); if (log_fp NULL) { // 尝试轮转 rename(/data/current.log, /data/current.old); log_fp fopen(/data/current.log, w); }日志路径在/data下不会影响/web下的页面资源这就是隔离分区带来的直接好处。7.5 分区表变更后如何更新设备这是很多团队忽略的问题。分区表一变设备上实际 Flash 的边界就变了。如果你只是烧录新的 app 到旧分区表中是会出大乱子的。因此在线下量产/升级时若分区表变化必须做“全量刷机”也就是同时烧写 bootloader、partition-table、app甚至要擦除旧的 NVS / 文件系统。在开发阶段我通常用idf.py -p /dev/ttyUSB0 flash monitor它会根据当前配置烧写 bootloader、分区表、app。如果改了分区表执行这个命令就能保持开发板和固件一致性。在量产阶段如果你已经在设备上跑着老版本而新版本需要更换分区表唯一的稳妥流程是先备份用户可提取的数据再全盘擦除然后烧写新版本全套镜像。不要尝试做“原地扩容分区”这种操作几乎必然导致数据混乱。我有一回试图保留 NVS 数据、只扩大 app 分区结果偏移一变app 启动后读不到 NVS 的内容整整导致返工。8. 常见问题排查速查表数据串门经典案例8.1 现象一A 应用写的配置B 应用读不到或读到别人的值这大概率有两个原因。原因 1你在两个应用里使用了相同的 NVS namespace却用了相同的 key 名。即使两个模块代码不同nvs_open(app_config, NVS_READWRITE, ...)打开的是同一个空间key 自然冲突。原因 2两个应用使用了不同的 NVS namespace但你在 B 应用里调nvs_open时没有检查返回值导致 handle 无效读写全部静默失败或指向意外位置。排查方法先用 python 脚本从 Flash 镜像里 dump NVS 内容可以用nvs_tool.py之类的工具或者用nvs_dump命令在设备上把 namespace/key 打出来一目了然。我提供一个快速自测思路在 A 和 B 的启动日志里分别打印自己打开的 namespace 名称和句柄是否成功打开。确认“门牌号”对了再检查键名。基本上 70% 的问题出在 namespace 或 key 名字冲突上。8.2 现象二OTA 升级后文件系统数据丢失如果你在升级后发现自己存的日志或 Web 页面没了不要马上怀疑是升级脚本的问题。先检查分区表中 NVS / OTA 是否配置正确。如果你把 NVS 分区删了或缩得太小系统初始化 NVS 失败就会自动擦除整个 NVS进而其它数据也可能被影响。另外如果你用同一个标签partition_label同时挂载两个不同文件系统分区启动时也有可能触发格式化。还有一种很隐蔽的情况升级过程中新固件代码里的文件系统挂载标签和旧固件不一致比如从“web_fs”改成了“web”那么设备会认为这是一个全新的文件系统在format_if_mount_failed true的情况下自动格式化旧分区自然数据就没了。所以任何时候修改分区标签名都必须小心处理用户数据迁移。8.3 现象三写入文件时报错“No space left on device”有时候日志明明没满但还是报空间不足。这时你查看一下是不是把两个不同用途的数据都塞进了同一个 LittleFS 分区把空间挤爆了。比如Web 页面缓存了一个大文件日志还在继续写最终撞到一起。解决方案要么在 Web 模块做缓存清理策略要么把 Web 和日志分开成两个独立分区。这也是我在文章开头强调“先画需求地图”的原因。如果空间实在不够用 8MB 版本模块直接加大 Flash成本也就几块钱但省下大量精力和 bug。8.4 现象四烧录新固件后设备进入无限重启无限重启往往是 bootloader 或 partition table 损坏或者 app 分区与分区表不匹配。快速判断用串口工具连接开发板按复位键看打印的ESP32 ROM boot信息。如果报invalid header很可能 app0 地址处没有有效的可执行头。这时候不要慌重新烧写正确的分区表和 app 镜像即可。如果确定是 Flash 被改坏了毫不犹豫全片擦除再来一遍。这不是偷懒而是最靠谱的恢复路径。擦除命令esptool.py --chip esp32 -p /dev/ttyUSB0 erase_flash然后完整烧录 bootloader、partition-table、app、NVS 等。8.5 现象五程序跑着跑着突然访问异常甚至触发 Guru Meditation Error如果只是纯逻辑错误一般不会突然访问异常。如果出现了 Guru Meditation Error 或者 Cache disabled极有可能是有人往 Flash 的代码映射区之外乱写导致缓存状态异常。排查思路打开idf.py monitor看崩溃时 PC/EA 落在哪个地址。用addr2line把地址换算到对应源文件行号。确认该报错行有没有调用esp_flash_write或esp_partition_write时传了错误偏移。如果能定位到是某个分区写入越界把参数加上边界判断再测。这个问题的根因往往就是我在第 5.4 节说的绕过分区表用底层 API。所以排查时优先搜索项目代码里有没有直接调用esp_flash_*的地方。9. 我的一些额外建议尽量只在有需求和能力的情况下使用自定义分区表。如果你只是入门、做一个单一体积的小项目那么使用 Espressif 默认的 factory 分区表就够了——它留足了一个 app 分区和一块 NVS完全满足基本需求。但一旦你涉及多个功能模块存活、OTA 升级、Web 资源、日志保存自定义分区表就是必需品而不是可选项。针对于运维和量产我强烈建议把分区表 CSV 纳入版本管理并且每次发布固件时同时归档一份经过编译的工具生成的partition_table.bin。这样万一售后反馈问题你可以快速把设备和固件版本绑定的分区表比对排查是否有偏移漂移。另外提醒一句并非所有 Flash 都是 4KB 扇区对齐。个别外部 SPI Flash 可能有不同的页大小。分区表里写的 Offset 必须基于实际 Flash 硬件参数进行计算。如果你在用外部 QSPI Flash 扩展容量分区表的管理思路依然适用但地址映射方式会有变化请不要直接套用内部 Flash 的分区表。最后分享一个我自己常用的技巧在开发阶段我会在应用里加一个chip_info命令通过串口打印当前分区表配置esp_partition_iterator_t it esp_partition_find(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_ANY, NULL); for (; it ! NULL; it esp_partition_next(it)) { const esp_partition_t *part esp_partition_get(it); ESP_LOGI(CHIP, label%s type%d subtype%d addr0x%x size0x%x, part-label, part-type, part-subtype, part-address, part-size); }这个命令在排查数据串门问题时极其有用——眼前一黑时看一眼打印就知道当前设备上的分区边界到底是怎么画的。分区表就是数据安全的护城河护城河画好了多个小应用再怎么折腾也串不了门。希望你把这个基础打好少踩几个我踩过的坑。
网站建设高端定制企业官网