ESP32 Flash 数据隔离实战:多应用分区规划与防覆盖指南

发布时间:2026/11/6 1:06:20
ESP32 Flash 数据隔离实战:多应用分区规划与防覆盖指南 接手过一个不算太大的项目一块 16MB 的 ESP32-WROVER 模组要同时跑三个逻辑上完全独立的小应用——一个上报温湿度的传感器节点一个给设备做蓝牙配网的小工具还有一个通过串口跟 ROS2 小车桥接通信的中转程序。三个东西各写各的问题就出在它们全都住在一块 Flash 上。第一次联调那天传感器节点刚写进去的校准值第二天就被桥接器的旧日志盖掉了。我当时的反应是这 Flash 里的数据怎么还会串门这个问题看起来简单实际上牵扯到硬件地址、分区表、NVS 组织方式、文件系统挂载、Flash 加密策略还有烧录工具的使用习惯。很多人把多个小应用塞进同一颗 ESP32 Flash 时只关心“能不能烧进去”却忽略了数据隔离等出现互相覆盖、启动崩溃、OTA 回滚失败这些问题才回头排查。这篇文章就围绕 ESP32 的 Flash 数据隔离把原理、规划、实现和坑一次性讲透。适合正在做多应用共板、单芯片多业务、或者只是想把应用和配置数据严格分开的开发者参考。1. 问题的本质为什么一块 Flash 上的数据会“串门”1.1 先搞清楚 ESP32 的 Flash 长什么样ESP32 芯片本身不带大容量存储固件和用户数据都放在外部 NOR Flash 里通过 SPI 接口连接。这颗 Flash 物理上就是一块线性地址空间比如 16MB 的型号地址从 0x000000 到 0xFFFFFF。CPU 可以执行 Flash 里的代码也可以直接读取映射到内存地址空间的数据但所有的读、写、擦除都要通过 Flash 控制器。NOR Flash 的特点是支持按字节读但写入前必须先擦除而擦除的最小单位是一个扇区通常是 4KB。这意味着你不能像操作内存那样随便改一个地址的内容一旦擦除范围算错就会把旁边不属于你的数据一起抹掉。多个应用共用一块 Flash 时最核心的矛盾就在这Flash 本身是一整块连续存储它不知道哪些字节属于应用 A哪些属于应用 B全靠使用方自己约定边界。边界一旦失守数据必然串门。1.2 “串门”最常见的三种姿势第一种是物理地址重叠。两个应用的固件被烧到了同一段地址区间或者一个应用的固件大小超出了自己所在分区的边界擦写时把另一个应用的区域给覆盖了。这种问题通常在刷机阶段就出现表现为某个应用启动失败甚至整个板子无法启动。第二种是运行期的逻辑越界。应用代码里没有通过分区表定位自己的存储区域而是直接用了绝对地址比如有人图省事把配置数据写死在 0x210000 这个位置另一个应用也在用这个地址两个模块互相踩数据自然就乱了。就算你用的是 NVS如果多个应用共享同一个 NVS 分区某个应用调用了nvs_erase_all其他所有应用的配置也会一起被清掉。第三种是挂载错对象。多个应用使用文件系统时如果分区 label 命名重复或者代码里写错了 label就可能把应用 A 的文件系统挂载到应用 B 的数据分区上轻则读到脏数据重则触发格式化把对方的数据一次性清空。这类问题最难排查因为它不是每次都触发往往是首次上电时格式化逻辑生效等你发现已经晚了。1.3 分区表就是 ESP32 给的标准答案ESP-IDF 从底层就考虑了这件事它用分区表来管理物理 Flash。分区表是一份简单的列表记录每个分区的名称、类型、子类型、起始偏移量和大小。Bootloader 启动时读分区表找到要启动的应用运行时应用可以通过esp_partition_find_first这类 API 按条件查询自己的分区。分区表的存在等于给每个小应用划了一块专属地盘。应用代码不应该关心 Flash 底层的物理地址只需要知道自己的分区 label读写操作都通过esp_partition_read、esp_partition_write完成API 内部会限制在当前分区范围内。听起来很美好但实际使用中如果分区表规划不合理或者应用代码绕开 API 直接用绝对地址这套防护也会失效。所以要先从分区规划开始把边界划清楚后面所有操作才有基础。2. 动手规划分区布局与地址计算才是重头戏2.1 分区表 CSV 里每一列是什么ESP-IDF 的分区表用 CSV 文件描述工程里一般叫partitions.csv。每一行的格式如下# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x6000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 0x200000, app1, app, ota_0, 0x210000, 0x200000, app2, app, ota_1, 0x410000, 0x200000, storage1, data, spiffs, 0x610000, 0x100000, storage2, data, spiffs, 0x710000, 0x100000,Name 就是分区的标签代码里通过它来定位Type 分为 app 和 data 两大类SubType 进一步细分比如 data 类型下有 nvs、phy、spiffs、fat、ota 等Offset 是分区在 Flash 上的起始地址Size 是分区大小Flags 一般写加密标识比如enc表示这个分区需要 Flash 加密保护。有一点要特别注意默认分区表中nvs 分区偏移量是 0x9000phy_init 是 0xf000这两个位置是官方约定俗成的bootloader 初始化、射频校准数据读取都依赖这个布局建议保留不要为了省空间把 nvs 挪走或者跟其他分区合并。还有factory分区只能有一个如果想让多个应用都作为独立镜像存在剩下的应用要使用ota_0、ota_1这类子类型而不是给它们都标成 factory。2.2 多应用场景下我常用的两种分区方案第一种方案是多固件独立分区。每一个小应用都是一个单独的 app 分区拥有独立的代码和独立的运行入口。这种方案适合“多个业务完全割裂甚至可能由不同团队独立开发”的场景。比如前面说的温湿度节点、蓝牙配网工具、ROS2 串口桥接器三个固件互不依赖各自编译各自升级。启动时如果默认 bootloader 从 factory 启动可以在 factory 分区放一个极简的切换器通过按键、串口指令或者存储在 NVS 里的标志调用esp_ota_set_boot_partition选中要启动的 OTA 分区然后重启进入。四个应用都是 app 类型各自的地盘清清楚楚。第二种方案是单固件多模块分区。所有小应用其实编译进同一个二进制靠任务、进程或者组件的方式在一个系统里跑但每个模块的数据区单独划分。这时要隔离的不是代码区而是数据区每个模块一个独立的 data 分区分别存配置、日志、模型文件。这种方案适合“多个功能必须同时在线、随时互相调用”的场景比如温湿度上报和蓝牙配网功能可能都在同一个 main 任务里不需要真正的进程隔离但配置文件必须分开。两种方案不是互斥的。实际项目中我也见过“单个固件里塞多个模块同时又给每个模块单独划出 nvs 和 spiffs 分区”的做法效果很好。关键是要先回答一个问题这些应用之间是竞争关系还是协作关系竞争关系就选多固件协作关系就选单固件多分区。2.3 地址对齐与预留区域的几个细节分区表的 Offset 和 Size 不是随便填的必须满足 4KB 对齐因为这是 Flash 擦除扇区的最小单位。如果某个分区偏移量不是 4KB 的整数倍擦除操作就会波及相邻分区。我的习惯是所有分区一律按 64KB 对齐也就是 0x10000 的整数倍虽然会浪费一点空间但心里踏实后续如果需要调整固件大小不用反复重新计算。尺寸上还有个容易踩的坑分区大小代表的是这个分区最多能占多少空间但实际固件可能小于这个值。比如你给 app1 分了 2MB固件只有 800KB剩下 1.2MB 就闲置了。如果有人在规划时看到“16MB Flash 空间很富余”就把分区表排得密密麻麻结果某个应用迭代几次后体积增长超出边界直接覆盖到下一个分区。所以每个分区要留至少 20% 的余量尤其是 OTA 分区因为新固件要完整下载到另一个分区后才能切换大小不够就升级失败。3. 数据隔离的实现细节NVS、文件系统与加密3.1 NVS 的 namespace 能隔离但救不了一切NVS 是 ESP-IDF 自带的键值存储系统它支持 namespace 的概念。比如应用 A 的数据放在nvs_open(app1_cfg, NVS_READWRITE, h)这个命名空间里应用 B 的数据放在app2_cfg里键名即使相同也不会直接冲突。但注意这只是逻辑隔离。所有 namespace 的数据仍然在同一个物理 NVS 分区里如果某个代码路径调用了nvs_erase_all或者 NVS 分区因为空间不足触发了垃圾回收其他 namespace 的数据同样可能被清掉或覆盖。更现实的问题是默认的 nvs 分区只有 24KB多个应用把配置、状态、日志都往里塞很容易写满然后nvs_set_*就会返回ESP_ERR_NVS_NO_FREE_PAGES。我的做法是尽可能给每个独立业务单独分配一个 data/nvs 类型的物理分区。代码里用nvs_flash_init_partition(app1_nvs)初始化指定分区再通过nvs_open_from_partition(app1_nvs, cfg, ...)操作。这样每个应用都拥有一块独立的物理 NVS 区域不管别的应用怎么折腾都影响不到自己。代价是 Flash 空间被占得多一些但对隔离来说非常值得。3.2 每个应用挂载独立的文件系统分区当应用需要保存日志、图片、模型文件这类结构化数据时NVS 就不够用了要用文件系统。ESP-IDF 支持 SPIFFS、LittleFS 和 FAT 文件系统每个文件系统都要绑定一个分区。挂载时通过partition_label指定分区比如esp_vfs_spiffs_conf_t conf { .base_path /app1, .partition_label storage1, .max_files 5, .format_if_mount_failed true, }; ESP_ERROR_CHECK(esp_vfs_spiffs_register(conf));很多串门事故就发生在format_if_mount_failed这行配置上。如果两个应用错误地挂载了同一个分区第二个应用挂载失败后一看配置里写着“挂载失败就格式化”直接把第一个应用的文件系统抹掉了。我建议把这个选项默认设成false生产环境绝对不要依赖格式化来恢复文件系统。挂载前先用esp_partition_find_first确认分区存在和大小符合预期再执行挂载。分区 label 的命名也要立规矩。不要用storage、data这种通用名而要用app1_store、app2_model这种带应用前缀的名字并且在工程里统一定义成宏。这样即使有人代码写串了至少在 review 阶段容易一眼看出来。3.3 全盘 Flash 加密与数据保护的关系数据隔离除了防止“自己人”串门还要防止外部攻击者把 Flash 芯片拆下来直接读固件和数据。ESP32 支持 Flash 加密分为开发模式和生产模式开启后 Flash 中的内容以密文形式存在外部读取得到的是乱码。要注意Flash 加密是全局的不是按分区开的。你可以通过分区表的 Flags 字段标记某些分区为enc让它使用加密写但整个芯片的加密策略是统一的。当多个小应用共用一个 Flash 时一旦开始加密所有分区一视同仁谁也不能绕过。所以设计阶段就要想清楚如果某个应用需要调试器直接读取 Flash 内容不要把它和敏感业务放在同一个加密项目里。开启 Flash 加密后有些直接操作 Flash 的代码会报ESP_ERR_NOT_ALLOWED原因是代码用了普通的esp_flash_write而这种接口不能处理加密页。解决方法是改用esp_flash_write_encrypted或者干脆走esp_partition_write它内部会根据分区的 Flags 自动选择正确的写入方式。另外eFuse 一次烧写后不可逆测试阶段如果开了生产模式的 Flash 加密之后想关掉基本只能换芯片。3.4 运行期自检把边界条件焊死在代码里代码里不要相信任何人包括自己。启动时做几个检查能挡掉大部分串门问题。第一用esp_partition_find_first找到自己依赖的分区检查是否为空。如果找不到直接打印错误并进入错误状态不要继续往下执行。第二校验分区大小是否满足需求。比如一个模型文件至少需要 1MB但实际分区只有 512KB那就该报错而不是等到写入失败才发现。第三把编译期期望的分区 label 和运行期实际查询到的分区信息打出来写日志。现场问题排查时日志里如果能看到storage1 offset0x610000 size0x100000基本就能定位是不是烧错了分区表。我自己习惯写一个partition_check()函数在所有业务初始化之前调用一旦失败就蜂鸣器报警或者通过串口输出错误码。看起来有点小题大做但在多应用共板项目里这套检查救过我很多次。4. 实操过程从分区表到烧录验证4.1 创建自定义分区表并在工程里启用在 ESP-IDF 工程中先在根目录新建partitions.csv然后把分区规划写进去。需要注意 app 类型分区的子类型需要严格区分第一个可以是factory后面依次是ota_0、ota_1。data 类型分区根据自己的需要写nvs、spiffs、fat或者自定义子类型。之后在idf.py menuconfig里进入Partition Table选项选择Custom partition table CSV然后在Partition Table CSV file里填入partitions.csv的文件名。保存退出后重新编译构建系统会根据这个 CSV 在 build 目录下生成partition-table.bin烧录时会自动带上。如果你用的是 Arduino-ESP32 开发环境操作方式略有不同在Tools菜单里可以选择内置的Partition Scheme比如Huge APP、No OTA但如果要自定义多应用分区需要把 CSV 文件放到 Arduino 的tools/partitions目录下并在菜单里选中对应名称。原理和 ESP-IDF 是一致的只是入口位置不同。4.2 代码里如何按 label 拿到自己的分区在 ESP-IDF 中查询分区的 API 很直接。比如应用 A 需要访问自己的 SPIFFS 数据分区const esp_partition_t *part esp_partition_find_first( ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, storage1); if (part NULL) { ESP_LOGE(TAG, partition storage1 not found); return ESP_FAIL; } ESP_LOGI(TAG, storage1: addr0x%x size0x%x, part-address, part-size);这里esp_partition_find_first的三个参数分别是分区类型、子类型、label。子类型可以用系统自带枚举也可以传十六进制自定义值。拿到esp_partition_t结构体以后后续的读、写、擦除都通过这个结构体进行比如esp_partition_write(part, 0, data, len)API 内部会做边界检查确保你不会写到自己分区外面。NVS 指定分区的初始化对应关系也要写明白esp_err_t err nvs_flash_init_partition(app1_nvs); if (err ESP_ERR_NVS_NO_FREE_PAGES) { ESP_ERROR_CHECK(nvs_flash_erase_partition(app1_nvs)); ESP_ERROR_CHECK(nvs_flash_init_partition(app1_nvs)); } nvs_handle_t handle; err nvs_open_from_partition(app1_nvs, cfg, NVS_READWRITE, handle);注意一个细节nvs_flash_erase_partition只擦除你指定的分区不要对整个 Flash 做全片擦除。全片擦除属于那种“看起来能解决问题实际会带来更大麻烦”的操作尤其是多应用共板时等于把其他应用的所有代码和数据统统抹掉。4.3 烧录与校验命令行工具的正确姿势编译完成后可以手动用 esptool.py 烧录。以 16MB Flash、四分区为例烧录命令大致是esptool.py --chip esp32 -p /dev/ttyUSB0 -b 460800 write_flash \ --flash_mode dio --flash_size 16MB \ 0x1000 bootloader.bin \ 0x8000 partition-table.bin \ 0x10000 app.bin这里最容易出错的参数是--flash_size和烧录地址。如果芯片实际是 8MB命令里写成 16MB可能导致 Flash 控制器访问到超出物理范围的地址表现为开机重启、分区读取错误。烧录地址尤其要看清bootloader.bin固定烧到 0x1000partition-table.bin固定烧到 0x8000应用代码则根据分区表里 factory 分区的 offset 决定默认是 0x10000。如果手滑把 app.bin 写到 0x1000那就直接覆盖了 bootloader板子变砖。Windows 用户常用的 Flash Download Tool 本质上也是做同样的事界面里分别填写 bootloader、partition table、app 的地址和文件路径然后选择正确的 SPI Flash 参数。我见过不少 “flash timeout” 或者flash download failed的报错其实不是 Flash 硬件坏了而是串口接线不稳、波特率太高或者没有让模组进入下载模式。排查顺序先降到 115200 波特率检查 IO0 是否接地再确认 EN 引脚复位时序最后才考虑换个 USB 线。4.4 给分区表加一个重叠校验脚本分区表靠人眼检查终究不可靠我后来写了个简单的 Python 脚本在代码提交前自动检查分区是否重叠、是否对齐。核心逻辑不复杂就是解析 CSV把每个分区的 offset 和 size 读出来算起始和结束地址然后逐项比较。#!/usr/bin/env python3 import sys def main(csv_path): parts [] for line in open(csv_path): line line.strip() if not line or line.startswith(#): continue fields [x.strip() for x in line.split(,)] if len(fields) 5: continue name, offset, size fields[0], int(fields[3], 0), int(fields[4], 0) if offset % 0x1000 ! 0: print(f[ERROR] {name} offset 0x{offset:06x} not 4KB aligned) parts.append((name, offset, offset size)) parts.sort(keylambda x: x[1]) for i in range(1, len(parts)): prev parts[i - 1] cur parts[i] if cur[1] prev[2]: print(f[ERROR] {cur[0]} overlaps {prev[0]}) elif cur[1] prev[2]: print(f[WARN] {cur[0]} is adjacent to {prev[0]}) if __name__ __main__: main(sys.argv[1])实际项目里我会把这个脚本放进 CI 流程任何修改分区表的合并请求都要跑一次。不用追求脚本多华丽能抓住“重叠”和“未对齐”这两个致命问题就够了。5. 常见问题与排查技巧实录5.1 快速问题定位速查表现象可能原因定位方法应用 A 的配置被应用 B 覆盖两个应用共用同一个 NVS 分区检查代码中nvs_flash_init_partition和nvs_open_from_partition的 label 是否不同文件内容互相可见或丢失两个应用挂载了相同 label 的分区用esp_partition_find_first打印各分区 label 和偏移量启动报invalid partition table分区表未烧录或烧录地址错误检查 0x8000 处是否有 partition-table.bin重新烧录OTA 升级后无法启动OTA 数据分区损坏或新固件超出分区大小查看 otadata 状态确认 OTA 分区 size 远大于固件烧录时flash download failed波特率过高、Flash 参数不匹配、接线不稳降波特率到 115200检查 IO0 和 EN核对 flash_size写入返回ESP_ERR_NOT_ALLOWEDFlash 加密开启后使用了非加密写接口使用esp_partition_write或esp_flash_write_encrypted重启后数据丢失文件系统自动格式化或 NVS 被全盘擦除检查format_if_mount_failed和是否误调全片擦除这张表对应的问题我在项目里基本都遇过一遍而且每次都是踩着 deadline 才发现。所以建议把它贴到团队 wiki 里让接手的人都先看一眼。5.2 我踩过且不希望你再踩的几个坑第一个坑分区 label 太随意。早期我图省事把所有数据分区都叫data结果应用 A 和应用 B 各自挂载后读到的其实是同一块区域。后面改了命名规则所有 label 带应用前缀并在分区自检函数里把查询结果打日志这个坑才彻底堵住。第二个坑NVS 分区空间不足。有一个应用频繁记录运行状态几万个键值往里写默认 24KB 很快写满然后报错我当时以为是 Flash 坏了。后来给那个应用单独划分了 0x10000 大小的 NVS 分区很少再出问题。记住NVS 不是日志存储别拿它当数据库用。第三个坑手动烧录地址写错。有一次给 app2 升级我复制了 app1 的烧录命令忘了改地址结果 app1 的代码被 app2 覆盖板子直接起不来。从那以后凡是多应用共板项目我全部用idf.py flash或项目自己的烧录脚本绝不手动敲地址至少也要确认脚本里每个 offset 都来自分区表。5.3 两个小技巧能让排查效率明显提高第一个技巧是充分利用日志输出。在分区自检函数里把所有分区信息打印出来包括类型、子类型、偏移量、大小格式统一。现场出现问题第一件事看串口日志里这些信息对不对就能排除一半的可能。第二个技巧是每次刷机前先把当前 Flash 的分区表读出来对照一下esptool.py --chip esp32 -p /dev/ttyUSB0 read_flash 0x8000 0x1000 partition_table_dump.bin如果读出来的内容和工程里生成的不一致那说明上次烧录的就是一份旧分区表赶紧重新烧。这个操作虽然简单但能避免大量“我明明改了代码为什么行为不变”的困惑。数据隔离做得再好分区表烧错一切归零。我个人在实际操作中的体会是多应用共用一块 Flash 真正难的不是某个 API 不会用而是整个团队有没有把分区当回事。一旦有人在代码里直接写物理地址、有人图方便用通用 label、有人把格式化开关保持打开再完善的分区表也撑不住。最好的办法是写进规范里分区表是受控文件修改必须评审所有访问存储的代码只走 esp_partition 和对应的文件系统接口每个应用启动必须自检分区。做到这三条Flash 数据串门的问题至少能少八成。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询