新闻详情

新闻详情

首页 / 资讯中心 / 详情

ESP32多模块Flash数据串门?分区表+NVS+LittleFS隔离指南

发布时间:2026/10/2 0:26:11来源:尧图网络
ESP32多模块Flash数据串门?分区表+NVS+LittleFS隔离指南
上个月我把一个跑了好久的ESP32环境监测项目拆成了三个独立的小模块WiFi配网、传感器数据记录、LED效果控制。三个模块都理所当然地要往Flash里写东西结果一上电WiFi密码没了温度CSV文件打不开LED配色文件更是变成了一堆乱码。排查了半天原因其实特别简单三个模块都把这片Flash当成了自己的“私有仓库”写数据时全凭感觉指定地址或者用了同一个NVS key。在板子上只有一块Flash的前提下这种“串门”几乎是必然的。这篇文章就把我当时的排查过程和最终解决方案完整讲清楚怎么通过分区表partition table划分“产权”怎么用NVS命名空间和文件系统目录做逻辑隔离以及一个可以直接抄走的“仪表盘日志用户设置”三合一分区方案。适合正在做ESP32多模块固件整合、或者准备给项目加OTA升级的开发者参考。1. 多个应用共用 Flash 时数据是怎么“串门”的——两类真实场景1.1 场景一地址越界A模块把B模块的数据直接覆盖ESP32的Flash是一块通过SPI接口挂在芯片外面的NOR Flash所有程序代码、配置文件、日志、OTA备份都住在同一片芯片里。在默认情况下ESP-IDF的启动流程会把Flash分成几个逻辑区域但很多刚开始写多模块代码的朋友并不关心这些区域写数据时直接用spi_flash_write或者干脆用esp_flash_write指定一个“看起来没人用”的地址。比如模块A为了存传感器校准值写到了0x120000这个地址模块B为了存系统日志也正好选了0x120000于是两边互相覆盖。更隐蔽的情况是模块A先写了一批数据模块B也写了一批读的时候各有各的偏移表面看着没冲突实际上其中一方的数据已经被悄悄冲掉了。这里要明白一件事物理地址本身没有“属于谁”的概念它只是一段连续的存储空间。如果没有任何分配规则那每一块区域谁都能写谁都能读。尤其在多个模块都存在“爆改Flash”习惯的项目里地址冲突几乎是午夜幽灵不知道什么时候就跳出来咬你一口。1.2 场景二逻辑名称冲突配置文件“同名不同姓”比地址越界更隐蔽的是逻辑名字撞车。多数模块不会直接碰Flash地址而是用现成库比如用NVS存储WiFi凭据、设备状态、用户偏好或者用文件系统存设定好的JSON、CSV。NVS允许你以namespacekey的形式读写数据。如果模块A开了一个叫config的命名空间模块B也开了一个叫config的命名空间那两边读写同一个key时就会互相干扰。文件系统也是一样如果两个模块都把配置文件命名为settings.json放在根目录最后落盘的结果就是谁后写谁生效另一个模块读到的永远是别人家的配置。这类串门不会像地址越界那样导致程序崩溃但比崩溃更麻烦数据看起来还在内容却是错的。WiFi密码可能变成了一段乱码摄像头配置可能变成了一串奇怪的数字。1.3 本质原因缺一张“房产证”Flash的物理特性是它可以按扇区通常4KB擦除和写入但寿命有限典型擦写次数在10万次左右。在共享Flash上所有模块看到的都是同一块地址空间如果没有一套从全局角度出发的分配方案那么“你写你的、我写我的”就必然失败。解决方式无非两种要么在应用层手工约定地址范围和使用规则要么使用ESP-IDF提供的分区表让系统在启动时为你建好一套带名字、带大小、带类型的“空间产权证”。手工约定是能跑但脆得不行。换一个人维护或者加一个新功能就可能打破约定。分区表的好处是硬性的每个分区都有起始地址和大小库函数和API在操作时会限定在分区内越界直接返回错误而不是默默改到别人的地盘。2. 一张分区表搞懂 Flash 的“空间产权”offset、size、type 与子类型2.1 分区表在系统里的位置ESP32的默认启动过程是芯片上电后ROM里的引导代码加载位于Flash地址0x1000附近的二级引导程序bootloaderbootloader会从Flash地址0x8000附近读取分区表然后根据分区表找到要运行的应用程序镜像。这个分区表不是随便放在Flash里的内容它有一个固定的头部ESP_PARTITION_TABLE_MAGIC和一组条目。你可以用ESP-IDF菜单配置里的Partition Table页面去选择“单分区App”“双OTA App”或者“自定义分区表CSV”。更直接的方式是在项目的partitions.csv文件里自己定义分区布局然后在idf.py menuconfig里指向这个文件。2.2 partitions.csv 的字段到底是什么意思一个标准的CSV行是这样的# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x6000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 0x200000,每个字段的含义Name分区名随便取但最好可读。后面用esp_partition_find时也会用到它。Typeapp或data。app存放固件镜像data存放数据。也有预定义的其他类型但多数时候只用这两个。SubType进一步细分。app类的常见子类型是factory、ota_0、ota_1data类的常见子类型是nvs、phy、otadata、spiffs、littlefs、fatfs。Offset分区在Flash上的偏移地址。它必须按4KB对齐也就是十六进制地址最后三位是000。Size分区大小也必须是4KB的整数倍。Flags加密、不可读写等属性。日常可以留空。2.3 默认分区表长什么样它埋了什么雷大部分开发板出厂时默认分区表是这样的# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x6000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 0x1F0000,看上去很干净但它给“多个小应用共用Flash”的场景埋了雷只有一个factory应用分区没有办法支持OTA因为OTA需要两个备用app区域。nvs分区只有24KB。如果多个模块都往NVS里塞数据很快就会被填满。没有独立的文件系统分区。想用LittleFS存日志或用户文件还得自己加。默认分区表适合“单一个固件占绝对主导”的Hello World项目。一旦项目里出现日志、用户配置、网页资源、OTA等多路数据流必须抛弃默认方案主动设计分区的“房产产权”。3. NVS 命名空间隔离让配置数据住进各自的“房间”3.1 NVS 是什么为什么适合存小数据NVS是ESP-IDF里的非易失存储组件它以键值对Key-Value的格式在Flash上保存内容。它的典型使用场景是存放小尺寸配置项WiFi的SSID和密码、设备唯一ID、用户偏好、开关状态。NVS最大的好处是内置了磨损均衡逻辑不需要你关心每个key到底落在哪个扇区而且它提供了类似“数据库事务”的commit机制可以在写入完整后统一生效降低掉电损坏风险。3.2 用 namespace 给每个模块一间“独立小房间”NVS里的namespace相当于一个全局前缀。用它做多模块隔离是最简单、最暴力的有效方式。比如WiFi配网模块#include nvs.h #include nvs_flash.h void save_wifi_config(void) { nvs_handle_t handle; nvs_open(wifi_cfg, NVS_READWRITE, handle); nvs_set_str(handle, ssid, MyHomeWiFi); nvs_set_str(handle, password, 12345678); nvs_commit(handle); nvs_close(handle); }另一个模块比如传感器校准模块就绝对不要再用wifi_cfg作为命名空间void save_sensor_calib(void) { nvs_handle_t handle; nvs_open(sensor_calib, NVS_READWRITE, handle); nvs_set_u32(handle, temperature_offset, 5); nvs_set_u32(handle, humidity_offset, 2); nvs_commit(handle); nvs_close(handle); }这样即使两边都用了名叫ssid或temperature_offset的key它们各自的命名空间前缀已经不同底层写入的键名实际上是wifi_cfg:ssid和sensor_calib:temperature_offset互不干扰。3.3 使用 NVS namespace 的限制与注意点Namespace不是无限多的。在默认NVS实现中命名空间最终也会作为key的条目占用Flash空间。当你频繁nvs_erase、nvs_set时会产生“幽灵碎片”所以分区不宜太小否则容易出现ESP_ERR_NVS_NO_FREE_PAGES。我个人的习惯是NVS分区至少给0x600024KB。如果项目里模块超过5个每个模块又存几十个key我建议直接给到0x1000064KB。同时命名空间名字不要超过15个字符这是ESP-IDF内部对key名称长度的硬约束命名空间如果太长会在nvs_open时直接返回错误。另外留意一点namespace只是逻辑上的划分并不是把物理空间严格切开。两个namespace的key可能会落在同一个Flash扇区。但这没有任何问题因为NVS内部已经用哈希表和索引区保证互不覆盖你要做的只是每个模块用自己唯一的namespace不要图省事全塞进main一个篮子里。4. 文件系统隔离LittleFS / SPIFFS 里的目录与独立分区4.1 为什么大数据不能塞进NVSNVS适合几百字节级别的Key-Value存储但如果要存几十KB的CSV日志、网页资源、用户上传的图片那就得上文件系统。ESP-IDF里常见的Flash文件系统有两种SPIFFS和LittleFS。SPIFFS是ESP32初期常用的文件系统但它在掉电恢复、目录操作、并发访问上不如LittleFS稳定。LittleFS是后起之秀针对嵌入式小Flash做了大量优化支持目录、重命名、掉电保护现在也已经被ESP-IDF官方作为VFS组件重点支持。如果新项目没有历史包袱优先选LittleFS。4.2 独立分区 vs 同一个分区内的目录隔离当你有多个功能模块时每个模块各挂一个文件系统分区是最“物理”的隔离方式模块A挂载到/web模块B挂载到/log模块C挂载到/cfg大家互不往来。但如果Flash空间有限或者怕维护多个分区太费事也可以让所有模块共享一个文件系统然后用目录约定来隔离。比如/wifi/config.json/sensor/record.csv/led/effects.dat这个方案在代码上非常简单因为LittleFS天然支持多级目录。但要注意两点你需要保证每个模块在创建文件时都带上自己的目录前缀不能只写文件名。ESP32的LittleFS分区不像PC文件系统那样有权限控制任何模块都能读别人的目录。如果要求严格的“只能自己读自己”还是要用独立分区甚至可以用不同挂载点来彻底阻断路径访问。4.3 怎么挂载多个LittleFS分区在partitions.csv里定义多个LittleFS分区后你需要在应用初始化时逐个注册VFS挂载点#include esp_vfs_littlefs.h void mount_littlefs_partitions(void) { esp_vfs_littlefs_conf_t conf { .partition_label web_fs, .base_path /web, .format_if_mount_failed true, }; esp_vfs_littlefs_register(conf); conf.partition_label log_fs; conf.base_path /log; esp_vfs_littlefs_register(conf); conf.partition_label cfg_fs; conf.base_path /cfg; esp_vfs_littlefs_register(conf); }之后操作文件时用/web/index.html、/log/sensor.csv、/cfg/user.json这样的完整路径。每个模块只读取自己领域内的路径数据自然不会串门。一个可以留意的小坑format_if_mount_failed true意味着如果文件系统挂载失败它会自动格式化。这虽然在开发期方便但会让数据丢失。正式产品里最好把它设成false并在格式化前做分区日志和备份策略。5. 实战一个“仪表盘 日志 用户设置”共存的完整分区方案5.1 需求梳理假设我现在做一个智能家居网关上面有三个功能模块仪表盘 Web UI需要存HTML/CSS/JS资源大概希望有1MB空间。系统日志每小时写一个CSV文件包含温度、湿度、开关状态大概需要512KB。用户设置用户自定义的设备名、房间名、主题配色大概需要256KB。此外项目还要支持OTA升级需要预留ota_0和ota_1两个应用分区外加一个otadata分区。Flash总容量我按8MB设计板子上实际是8MB SPI Flash。5.2 实际分区表 CSV# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x10000, phy_init, data, phy, 0x19000, 0x1000, otadata, data, ota, 0x1a000, 0x2000, factory, app, factory, 0x20000, 0x200000, ota_0, app, ota_0, 0x220000, 0x200000, ota_1, app, ota_1, 0x420000, 0x200000, web_fs, data, littlefs, 0x620000, 0x100000, log_fs, data, littlefs, 0x720000, 0x100000, cfg_fs, data, littlefs, 0x820000, 0x080000,逐个解释一下nvs我特意给了64KB因为多个模块都要往NVS里写配置。phy_init存放射频校准数据一般放在固定位置大小4KB。otadataOTA状态区8KB。factory出厂固件2MB用于首次烧录如果以后有OTA固件回滚需求可以保留。ota_0、ota_1两个OTA应用区各2MB最新固件会交替写入。web_fs、log_fs、cfg_fs三个LittleFS分区大小分别为1MB、1MB、512KB。这里要注意offset的连续性所有offset都按4KB对齐而且每个分区的结束地址正好是下一个分区的起始地址。写partitions.csv的时候用十六进制能减少计算错误。5.3 代码分区初始化与读写不越界在app_main里我们需要依次做几件事#include nvs_flash.h #include esp_partition.h #include esp_vfs_littlefs.h void app_main(void) { // 1. 初始化 NVS esp_err_t err nvs_flash_init(); if (err ESP_ERR_NVS_NO_FREE_PAGES || err ESP_ERR_NVS_NEW_VERSION_FOUND) { ESP_ERROR_CHECK(nvs_flash_erase()); err nvs_flash_init(); } ESP_ERROR_CHECK(err); // 2. 挂载三个 LittleFS 分区 esp_vfs_littlefs_conf_t lfs_conf[3] { {.partition_label web_fs, .base_path /web, .format_if_mount_failed true}, {.partition_label log_fs, .base_path /log, .format_if_mount_failed true}, {.partition_label cfg_fs, .base_path /cfg, .format_if_mount_failed true}, }; for (int i 0; i 3; i) { ESP_ERROR_CHECK(esp_vfs_littlefs_register(lfs_conf[i])); } // 3. 演示写数据模块A写Web资源 FILE *web fopen(/web/index.html, w); if (web) { fputs(html.../html, web); fclose(web); } // 4. 演示写数据模块B写日志 FILE *log fopen(/log/20250115.csv, w); if (log) { fprintf(log, time,temp,hum\n10:00,25.3,60\n); fclose(log); } // 5. 演示写数据模块C读用户设置 FILE *cfg fopen(/cfg/user.json, r); if (cfg) { /* 解析JSON */ fclose(cfg); } }这里没有什么魔法魔法就是一个朴实原则每个模块只用与它相头的挂载点路径。用esp_vfs_littlefs_register时传入的partition_label会自动关联到分区表里的分区底层地址由文件系统管理你不需要自己算Flash偏移。5.4 验证怎么确认数据真的没有“串门”开发阶段我最推荐用一个“破坏性测试”来验证分区隔离是否可靠往三个分区分别写入独特的标记字符串比如/web/a.html写WEB_HELLO/log/a.csv写LOG_HELLO/cfg/a.json写CFG_HELLO。执行一次idf.py erase-flash注意这会清掉整片Flash。重新烧录后再次读取三个文件确认内容仍分别存在。把其中一个模块的目录改名叫“奇怪的路径”再重启确认另外两个模块不受影响。更硬核的做法是用parttool.py把每个分区dump下来然后用xxd或者十六进制编辑器查看。如果web_fs分区里出现了log前缀的内容说明你的文件系统挂载点或路径写错了。6. 升级与磨损均衡不能忽略的连带风险6.1 OTA 升级时新固件会把数据区“吃掉”吗OTA升级的过程其实就是把新的应用镜像通过网络或本地写入到ota_0或ota_1分区。升级完成后bootloader会根据otadata分区的状态决定启动哪个应用。如果事先没有分区表或者分区表里的app分区与数据分区重叠了那么在OTA写入时新固件极可能覆盖掉NVS或文件系统里的内容造成配置和日志全部丢失。所以当你为项目加了OTA功能后必须检查分区表里的offset和size确保ota_0、ota_1与所有data分区在空间上不重叠。我见过一个案例有人把ota_0的size定义得太大吓到了web_fs分区的起始地址OTA升级后Web UI的资源文件全部损坏网关开机变成白板。后来把OTA分区缩小才解决。6.2 Flash 磨损均衡不能让一个分区“高频加班”Flash分区大小直接决定擦写寿命。例如一个日志分区如果只有8KB每天写几MB数据用不了多久就会让某些扇区提前报废。日志分区的容量要按“预计设备寿命 × 平均写入速率”来估算。一个经验值8MB的NOR Flash每个扇区典型擦写寿命是10万次如果按天记录100KB日志一年的写入量约36MB均匀分布到一个512KB的分区128个4KB扇区每个扇区擦写约288次看起来完全没问题。但如果你每天写1MB日志扇区磨损就会再加一个数量级这时就要考虑增大日志分区或者采用动态压缩、丢弃旧日志的策略。NVS和LittleFS都有磨损均衡机制但前提是分区必须有足够大的空间来“腾挪”。分区越小均衡效果越差。这也是为什么我总建议NVS不要低于24KB的原因。6.3 掉电与“半写”状态如何避免变成“两边都认不出来”Flash写入是以扇区为单位的如果写入到一半掉电数据可能处于半扇区状态。NVS靠commit标记和回滚机制来保证只有commit成功后更新才对上层可见。LittleFS也有掉电一致性设计它采用日志提交copy-on-write结构写入失败时旧数据依然可以回退。但即便如此不要在掉电后对文件系统做“随意删除”这类盲目操作。更稳妥的做法是对于重要配置定期备份到另一个分区或SD卡。在写入关键数据时用一个“版本号”或“校验和”字段读出来时先校验再决定是否采用。不要直接调用spi_flash_write去裸写地址永远用esp_partition_read/write或VFS文件API。7. 几个我用下来的隔离经验7.1 三条黄金法则先画分区图再写代码。把Flash当成一块地皮画清楚哪个区域属于哪个模块多少空间然后才能在partitions.csv里落笔。没有分区图后边写再多代码也容易出事故。每个存储用户模块/组件都要有独一无二的namespace或目录根路径。比如WiFi用wifi_cfg热使用led_cfg日志用/log网页用/web。名字越具体越好千万不要用cfg、data这种会撞车的通用名。永远不要用裸地址去操作Flash。ESP-IDF提供的esp_partition_read/write天然支持分区绑定你再怎么传参也不会跨越分区边界。裸地址操作虽然灵活但一失手就是整个系统的不稳定。7.2 常见误区清单误区1默认分区表“够用”了。实际上默认分区表没有OTA区也没有独立文件系统区多模块项目八成会翻车。误区2NVS命名空间可以随便起。它最长15字符但如果你把两个模块的namespace都取成“main”串门概率直接飙升。误区3一个文件系统分区可以无限扩。LittleFS虽然支持目录但它没有权限隔离一个模块误写路径照样能污染另一个模块的目录。误区4多个“小应用”可以同时独立运行。很多网友误解这个。准确说ESP32同一时间只能运行一个固件镜像我们可以把一个大的固件内拆成多个功能模块也可以把不同固件放在不同分区通过OTA切换但“同时跑两个独立固件”不是ESP32的默认能力。因此多模块数据隔离更依赖分区表与命名空间的协同调配。7.3 分区设计检查清单[ ]partitions.csv里所有app分区和所有data分区的地址范围互不重叠。[ ]nvs分区大小至少24KB最好给到64KB。[ ] 每个功能模块都有一个明确指定的NVS命名空间或文件系统目录。[ ] 如果支持OTA已经定义ota_0、ota_1、otadata且正确配置了app子类型。[ ] 文件系统分区使用LittleFS并设置format_if_mount_failed false生产环境。[ ] 代码里只通过esp_partition_*或VFS路径访问存储不要出现硬编码的Flash地址。这套方案我从那次丢数据之后一直用到现在两个多月没再出过配置丢失或者日志损坏的问题。最后分享一个烧录小技巧开发阶段反复改动分区表后记得执行一次idf.py erase-flash把整颗Flash清干净再烧新分区表和固件避免旧分区残留数据干扰。玩ESP32别心急先把资源共享的“房产契约”定好后面写代码能省掉你大把查Bug的时间。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

WinUI 3重构硬件检测工具箱:集成134个工具,一键单烤CPU与极限性能检测 2026/10/2 1:14:54

WinUI 3重构硬件检测工具箱:集成134个工具,一键单烤CPU与极限性能检测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
薄膜按键去抖:非阻塞状态机与计时回绕实现 2026/10/2 1:14:54

薄膜按键去抖:非阻塞状态机与计时回绕实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
AI编码黑箱危机怎么破?用“规格驱动”把代码生成变成可验收的契约 2026/10/2 1:14:54

AI编码黑箱危机怎么破?用“规格驱动”把代码生成变成可验收的契约

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Jenkins国内镜像源加速安装:清华TUNA源实操指南 2026/10/2 1:14:54

Jenkins国内镜像源加速安装:清华TUNA源实操指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
BK3296蓝牙音频SoC评测:低功耗TWS耳机设计开发实战 2026/10/2 1:14:54

BK3296蓝牙音频SoC评测:低功耗TWS耳机设计开发实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Flowable工作流动态预测下一节点:原理、代码实现与踩坑指南 2026/10/2 1:14:41

Flowable工作流动态预测下一节点:原理、代码实现与踩坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉