一块 ESP32 的 Flash 就那么大,偏偏要塞下设备配置、OTA 固件、日志、Web 页面、语音提示,甚至几个独立的“小应用”。很多朋友都问我:多个小应用共用一块 Flash,怎么保证数据不串门?经验是:不是靠“小心写代码”,而是靠一开始就把 Flash 的“地皮”划清楚,再用分区表、命名空间、加密这些机制从底层堵死串门路径。
这篇文章会把 ESP32 的 Flash 从“一块谁都能读写的裸存储”变成“每个模块都有自己产权证的独立空间”的完整过程拆开讲,包括分区表设计、NVS 隔离、双 APP 槽、Flash 加密,以及我实际排查过 N 次的串门故障。无论你是用 ESP-IDF 还是 Arduino,只要理解这套逻辑,写多应用共用 Flash 的程序就不会再提心吊胆。
1. 为什么一块 Flash 上数据会“串门”:先搞清楚病根
1.1 三种最常见的“串门”现场
我在实际项目中遇到的“串门”,基本可以归成三类。第一类是配置项互相覆盖,比如应用 A 存了一个 WiFi 密码,应用 B 也在同一个地址范围写了东西,结果两边数据交错,设备重启后行为完全不可预知。第二类是固件和文件系统打架,OTA 固件被日志或用户数据撑爆,或者反过来,日志写入把固件区冲掉,设备直接变砖。第三类是逻辑看不出问题但偶发异常,比如两个模块都调用了nvs_set_str,但对同一个 key 名写入了不同含义的数据,读出来的是另一个模块写的值——这种最坑,因为代码 review 时很难发现。
这三类问题的本质,都是“地址空间没有独立产权”造成的。ESP32 的 Flash 是挂在 SPI 总线上的,物理上所有代码、配置、日志都在同一颗芯片里,软件层面如果不做地址隔离,谁的指针写偏一点,就会踩到别人的家里。
1.2 从 Flash 的物理规律说起
做嵌入式的人都知道,Nor Flash 有几个硬约束:按扇区擦除,最小擦除单位通常是 4KB(ESP32 的 flash 最小扇区是 4KB,某些型号甚至支持更小的安全扇区);写之前必须擦,擦除会把整个扇区打成全 0xFF;写入次数有限,一般十万次级别,频繁写同一个扇区会有寿命问题。
这些物理规律决定了隔离方案必须“用空间换安全”。如果你用文件系统思维去理解 Flash——以为像 SD 卡那样可以任意字节读写、任意位置建文件——那你很容易写出“随机地址写数据”的代码,这在串口调试时可能看不出来,一旦多个应用同时跑,立刻乱套。正确的做法是:把 Flash 按分区图切成固定大小的块,每一块只允许特定类型的数据使用。ESP32 的 bootloader、应用程序、NVS、OTA 数据、文件系统各自有专属分区,地址范围由分区表一锤定音,代码层面不得越界访问。
1.3 ESP32 的两个关键概念:分区表和存储介质抽象
先说分区表。ESP32 出厂后,Flash 地址空间并不是“从头到尾给 App 用”。最前边是 bootloader,之后是分区表,再往后才是各种应用分区。分区表本身位于0x8000之后,里面定义了一个数组,每一行描述一个分区的偏移地址、大小、类型、子类型、标签。烧录时,烧录工具直接把这个表烧进去,bootloader 启动时按表查找可执行分区去加载固件。
再说 NVS。NVS 是 ESP-IDF 提供的一个轻量级键值存储服务,它不是直接裸写在某个地址上,而是自己在指定分区内部维护一套哈希索引、链表和磨损均衡逻辑。你调用nvs_get_str、nvs_set_str时,其实用的是“命名空间 + key”两层访问路径。命名空间就相当于一个一个独立的抽屉,把不同应用的数据分开,即使 key 名字相同,只要命名空间不同,就不会互相踩到。这一点是本话题最关键的基础。
2. 第一道防线:用分区表把“地盘”先画清楚
2.1 partitions.csv 长什么样
ESP-IDF 项目里的partitions.csv就是那张“地契总表”。默认的单个 App 分区表只有两三个分区,但对于多个小应用共存的场景,必须手工规划。先看一个最朴素但合理的示例:
# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, phy_init, data, phy, 0x10000, 0x1000, app0, app, ota_0, 0x20000, 0x200000, app1, app, ota_1, 0x220000, 0x200000, smallapp, data, spiffs, 0x420000, 0x100000, logs, data, fat, 0x520000, 0x80000, webdata, data, spiffs, 0x5a0000, 0x60000,这里有几个细节。offset 是从 Flash 起始地址算起的绝对偏移,单位是字节。分区大小和偏移都要对齐到 4KB,因为 Flash 的最小擦除扇区就是 4KB。nvs 分区必须在最前面,放一些系统初始化要用的数据,比如校准信息和时钟校准偏置。otadata 专门给 OTA 记录当前运行哪个 app 槽。app0 和 app1 是两份固件镜像,用于 OTA 切换。后面的 smallapp、logs、webdata 是我为了实现“多个小应用数据隔离”自定义的分区,类型可以是 data,子类型可以从 spiffs、fat、nvs 中选择,也可以直接用自定义子类型。
2.2 自定义分区实操步骤
第一步当然是改partitions.csv。第二步要让构建系统知道用这张表。在 ESP-IDF 里,打开工程根目录的sdkconfig或直接执行idf.py menuconfig,进入 “Partition Table” 菜单,选择 “Custom partition table CSV”,并把 CSV 文件名填进去。如果是 Arduino 的esp32平台,可以在Tools→Partition Scheme里选 “Custom”,然后在工程里放partitions.csv。
第三步,用分区类 API 获取分区信息,避免硬编码地址。推荐的代码是:
#include "esp_partition.h" const esp_partition_t *part = esp_partition_find_first( ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "smallapp"); if (part == NULL) { ESP_LOGE("MAIN", "partition not found"); return; } esp_partition_erase_range(part, 0, part->size);不要像我早期那样自己写#define DATA_ADDR 0x420000之类的魔法数字。一旦分区表调整,所有硬编码全部作废,轻则数据访问错位,重则越界擦除把固件干掉。用 API 拿到的part->address和part->size永远是分区表里配置的最终结果,这才是不会串门的基石。
2.3 分区槽设计的几个关键原则
我个人规划多应用共享 Flash 时有四条铁律。
第一,给每个“应用语义”一个独立分区,而不是共享一个大分区。比如一个设备同时做传感器采集和本地 Web 服务端,那么传感器数据和 Web 静态资源就应该分别建分区,即使它们都是 spiffs 或 fatfs。这样做的好处是:刷 Web 页面固件时不需要担心动到传感器历史数据,重置其中一个分区也不会殃及池鱼。
第二,分区大小要预留 OTA 升级的空间模型。如果设备支持 OTA,App 至少要分两个槽:app0 和 app1,各占相同大小,确保新固件写入 app1 后,旧固件还能在 app0 继续跑。如果只留一个 App 分区,升级中途断电就直接变砖,因为新固件会覆盖旧固件,又没有任何回退版本。
第三,数据分区和可执行分区严格隔离。可执行分区的子类型必须是app,数据分区子类型必须是data。千万别把二进制数据塞进 app 分区后缀。原因很简单:bootloader 扫描可执行分区时,如果发现某个分区“内容看起来像应用”,但其实是日志堆,启动阶段就可能加载失败,甚至触发无限重启。我亲眼见过有人把 OTA 镜像下载到数据分区,结果 bootloader 把它当成 App 去跑,整个设备反复崩溃。
第四,给日志和 OTA 缓存留有独立垃圾桶。日志分区满了可以直接擦除,不影响其他数据;OTA 下载临时文件放到专用分区,防止下载中断污染正常数据。这些分区虽然占用空间,但能极大降低“串门”引发的事故半径。
3. 第二道防线:用 NVS 命名空间做 Key 级隔离
3.1 命名空间是怎么工作的
分区表解决了“哪些地址归谁用”的大问题,但同一个分区里如果有很多小应用都要用键值存储,仍然可能互相干扰。ESP-IDF 的 NVS 在启动时会读取整个 NVS 分区,建立页表结构。每次nvs_open都会指定一个命名空间,底层会在命名空间内分配独立的迭代上下文。从官方文档和源码来看,NVS 的 entry 结构里存储的是namespace和key的组合,不同namespace下即使 key 完全相同,也能共存。
这里有一个很多人容易踩的坑:命名空间名字最长 15 个字符,key 名最长也是 15 个字符(具体限制在nvs.h里有说明)。一旦超长,nvs_open会直接返回ESP_ERR_NVS_INVALID_NAME,你还不一定能马上看出原因。我之前用过类似device_wifi_credentials_north这种名字,结果一编译一跑,NVS 报错,排查半天才发现的。所以命名要短、明确,比如wifi、mqtt、app_b,别挑战长度限制。
3.2 应用层封装与推荐用法
实际项目我建议封装一层“命名空间管理模块”,不要让每个业务直接调nvs_open。做法很简单:定义枚举或宏,规定每个模块的命名空间和 key 前缀:
typedef enum { NS_WIFI = 0, NS_MQTT, NS_SENSOR, NS_OTA, NS_APP_B } ns_id_t; static const char *ns_names[] = { "wifi", "mqtt", "sensor", "ota", "app_b" }; esp_err_t store_u32(ns_id_t ns, const char *key, uint32_t value); esp_err_t load_u32(ns_id_t ns, const char *key, uint32_t *value);在实现里,store_u32每次都做nvs_open(ns_names[ns], NVS_READWRITE, &handle),写入后立刻 commit。这个封装至少带来三个好处:一是所有命名空间集中管理,review 代码时一眼就能看出哪些模块用哪些抽屉;二是可以统一加日志,任何一次读写都有记录,出事时能追溯;三是在单元测试时可以轻松换成内存 mock,不污染真 Flash。
3.3 键名冲突和脏数据问题
就算命名空间隔离开了,可能还会遇到另一类串门:键名相同、含义不同的问题因为“同命名空间”而出现。比如传感器模块存了status表示在线标志,OTA 模块也存了status表示升级状态,如果它们被错误地分到了同一个命名空间,那就是灾难。我在项目里统一用“模块前缀 + 键名”的方式规避,例如传感器用sens_status,OTA 用ota_status。没有强制机制,只能靠规范。
脏数据问题同样值得重视。第一次写入 NVS 时命名空间不存在,nvs_get会返回ESP_ERR_NVS_NOT_FOUND。很多新手直接忽略返回值,用未初始化的栈变量继续跑逻辑,结果数据乱飞。正确做法是初次读不到时,写入一个默认值,并 commit 一次,后续读就有结果。踩过几次坑之后,我在封装里强制要求:凡是读取函数,如果返回ESP_ERR_NVS_NOT_FOUND,必须由调用方提供默认值,不允许留空。
4. 分区实战:从规划到代码,一步步搭出隔离结构
4.1 估算每个应用的空间需求
多应用共用一个 Flash 之前,最应该做的不是写代码,而是拿一张纸算容量。以一颗常见的 8MB Flash 为例,我先列一些典型分区的大小需求。固件镜像如果启用大量组件,基础编译产物可能到 1.6MB 左右,OTA 双槽至少 2×2MB。NVS 我建议至少 20KB,太小容易频繁擦写触发磨损。一个简单 Web 页面如果只放几张静态资源和几个 JS,1MB 足够;但如果要放图片、图表库,可能 2MB 起步。日志分区大小取决于保留周期和写入频率,一般 0.5MB 到 1MB 比较舒服。
我把一次实际估算过程写出来。项目包含两块业务:一个是小型 Web 配置面板,一个是传感器数据采集。固件加上 WiFi、HTTP Server 后,编译出来 1.2MB。为了 OTA 稳定,我分配 app0 和 app1 各 2MB。Web 静态资源一共 800KB,spiffs 有块管理和目录项开销,我给 1.5MB。传感器历史数据每天产生约 20KB,考虑保留 7 天,加文件系统元数据开销,我给了 0.5MB。NVS 给了 64KB,因为多应用配置项多,同时留了 3 倍余量,避免频繁擦写导致寿命下降。加起来大约 8MB 出头,最后只能精简 Web 资源,因为 Flash 只有 8MB。
4.2 规划一张“不会串门”的分区表
结合上面的场景,我建议这么分配:
| 用途 | 类型 | 子类型 | 大小 | 说明 |
|---|---|---|---|---|
| bootloader 区 | 由工具自动预留 | - | 0x10000 起 | 通常不手写 |
| partition table | 由工具自动预留 | - | 0x1000 | 固定 |
| nvs | data | nvs | 0x10000 | 系统与各应用配置 |
| otadata | data | ota | 0x2000 | OTA 槽状态 |
| 出厂校准 | data | phy | 0x1000 | 射频校准 |
| app0 | app | ota_0 | 0x200000 | 主应用槽 0 |
| app1 | app | ota_1 | 0x200000 | 主应用槽 1 |
| web | data | spiffs | 0x180000 | Web 静态资源 |
| sensordata | data | spiffs | 0x80000 | 传感器历史数据 |
| logs | data | fat | 0x80000 | 运行日志 |
分区表里我把 web 和 sensordata 分开,即便 Web 分区被写满或崩溃,传感器采集记录还能独立读写。logs 分区用 fatfs 管理成文件,方便串口工具导出,但日志分区溢出时我自己写了清理策略,因为 fatfs 在 flash 上的掉电保护不如 spiffs 友好。
4.3 烧录自定义分区表时需要避开的坑
用了自定义分区表之后,有几个烧录相关的坑特别容易踩。
第一个坑是没有把 partition table 偏移写对。idf.py flash默认会烧 bootloader、partition table 和 app,一般不会错。但如果你用第三方烧录工具手动刷,必须知道 bootloader 在0x1000,分区表在0x8000,app 在第一个 app 分区起始地址(如0x20000)。有些工具默认从0x10000开始烧 app,那就会直接烧到分区表区域,设备起不来。
第二个坑是分区表改过了,但没有重新擦除整颗 Flash。如果旧分区表里 NVS 分区偏移是0x9000,新分区表改成0x10000,而 Flash 里还残留旧分区表和新分区表同时存在的数据,bootloader 只认0x8000那张表,一般没问题,但 NVS 里原有数据如果地址重叠,可能读到旧数据。所以我每次改分区表规划后,第一件事是全片擦除再烧。
第三个坑是 Arduino 环境里选错分区方案。Arduino 的Tools -> Partition Scheme中有些选项叫 “Huge App”、“Minimal SPIFFS”,这些模式会自动生成精简分区表,可能没有你想要的 NVS 大小或 OTA 槽。如果你发现自己辛辛苦苦写的partitions.csv没生效,检查编译日志里的--partition-table参数,确认构建系统到底用了哪个文件。
5. 进阶隔离:Flash 加密、双 APP 槽与运行态保护
5.1 打开 Flash 加密防止“读写串门”和篡改
如果你做的是设备配网、支付相关或涉及用户配置的产品,只做分区隔离还不够。因为在裸 Flash 上,任何人都可以用外部编程器或串口命令直接读走整颗 Flash 内容。分区隔离只能解决程序内部的“误操作”,解决不了物理层面的“被偷看”。这时候需要开 Flash 加密。
Flash 加密不是把 Flash 上的所有物理内容都加密成人眼不可读,而是由 ESP32 硬件在总线访问层面做透明解密:Flash 里存的是密文,CPU 访问时读出密文后用硬件解密成明文。启用方式是在 menuconfig 里开Enable flash encryption on boot,然后在第一次烧录后执行idf.py flash批量烧录并把 eFuse 里的加密位烧死。烧死后这颗芯片就只认自己的加密密钥,别人拿到 Flash 颗粒也读不出有效数据。要注意,启用加密后再烧录需要通过加密烧录流程,否则 bootloader 读到的是乱码。
加密还能防止“写串门”带来的恶意篡改。比如攻击者想让应用 B 篡改应用 A 的配置,如果懂得 Flash 地址映射,理论上可以定位到 NVS 页面直接改写数据。开了 Flash 加密后,总线写操作也要按硬件规则来,非法的跨分区写入通常会被拒绝或导致校验失败。这给多应用共用 Flash 又加了一层保护罩。
5.2 双 APP 槽:多固件互为备份与“安全回滚”
分区表里我给 app0 和 app1 都画了同样的 2MB。这不是浪费,而是 OTA 的“串门保险”。启动时 bootloader 会查询 otadata 分区里的状态,决定从 app0 还是 app1 启动。默认从 app0 启动,新固件下载到 app1 后,写入 otadata 标记“app1 可启动”,然后重启。如果 app1 运行 30 秒后没有主动上报“运行成功”,bootloader 会自动回滚到 app0。这个机制天然解决了“多个应用版本共存”时的数据隔离问题:两个版本跑在两个独立槽位,各自的配置如果也按命名空间分好,就能做到互不干扰。
有一点必须注意:app0 和 app1 里的固件版本要支持相互回滚。如果你的 app 从分区表读取 NVS 配置时用了不同版本号的 key,旧版本读不到新 key 要能恢复默认值,否则回滚后系统出现“配置串门”假象。我在开发多应用设备时,特意在每个应用里写了配置兼容函数,遇到未知 key 直接跳过但不崩溃,这样回滚才能顺利。
5.3 运行态保护:用 esp_partition API 而不是裸指针
即使分区表画得再好,如果代码里仍然有人直接操作裸 Flash 地址,一切隔离都白搭。我在团队定了一条铁规:禁止直接spi_flash_read/write某个自定义地址,必须用esp_partition_*API。
理由很简单:esp_partition_*函数会带上分区描述符作为参数,内部会检查你访问的地址是否在该分区的address到address + size范围内。一旦越界,直接返回错误,而不是写穿到隔壁分区。这里有代码示例:
const esp_partition_t *part = esp_partition_find_first( ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "sensordata"); if (part == NULL) return; uint8_t buf[64]; esp_err_t err = esp_partition_read(part, 0, buf, sizeof(buf)); if (err != ESP_OK) { ESP_LOGE("MAIN", "read partition failed: %s", esp_err_to_name(err)); }有人会说,part->address我都拿到了,直接memcpy不行吗?不行。一旦你绕过 API,分区表保护就失去了意义。而且spi_flash_*低层 API 在 flash encryption 开启时对密文操作会引入额外复杂度,遇到加密还要处理明文密文转换问题,直接用esp_partition_read/write让底层处理才是正经做法。我实测过,用 API 时越界读取返回ESP_ERR_INVALID_ARG,日志打印清晰,排查省时多了。
6. 串门故障排查实录:看日志、读分区、查表定位
6.1 先从启动日志和 NVS 错误码入手
多应用共用 Flash 出了问题,第一步不是拆机,而是打开串口监视器看启动日志。ESP-IDF 默认日志会打印分区表摘要,类似:
I (332) esp_image: segment 0: paddr=0x00020020 vaddr=0x3f400020 size=0x0c4a0 I (350) esp_image: segment 1: paddr=0x0002c4c0 vaddr=0x400d0020 size=0x1b930如果 log 打印出现E (123) NVS: nvs_flash_init failed because NVS partition is too small或者E (123) NVS: NVS partition is too small,大概率是分区表里 NVS 分区被其他分区挤占,或 NVS 分区被擦除后还没重新初始化。很多情况下,不是代码逻辑错误,而是分区表偏移冲突导致 NVS 读到了别的分区内容。这时候用partitions.csv里的 nvs 偏移和大小去对照 log 中的 org 信息,能快速定位。
还有一种常见情况是启动日志里出现E (123) boot: OTA app slot size check failed。这说明分区表中 app 槽大小比固件镜像实际大小小,或者 app1 缺了。不少人是手动改了CONFIG_ESP_MAIN_TASK_STACK_SIZE等配置后发现编译多了几百 KB,然后一次 OTA 后设备循环重启。日志会告诉你是 OTA 分区问题,别急着怀疑应用代码。
6.2 用 parttool 和 esptool 直接检查分区内容
如果日志看不出问题,就用工具直接扒分区的“肚子”。esp-idf的components/partition_table/parttool.py可以列出当前烧录到芯片里的真实分区信息。命令大概是:
python parttool.py --port /dev/ttyUSB0 get_partition_info --partition-type data --partition-subtype nvs它能读出芯片上实际分区表的偏移、大小和名称,和你的partitions.csv对比一下就能发现是否烧录了旧表。如果想直接读某个分区的内容,可以用 esptool 把 Flash dump 下来,再按分区偏移切文件分析:
esptool.py --port /dev/ttyUSB0 read_flash 0x9000 0x10000 nvs_dump.bin然后把nvs_dump.bin用 hexdump 或 NVS 解析脚本打开,能直接看到命名空间和 key 列表,一眼就能判断是不是两个模块把数据写到了同一个命名空间下面。这个方法也适合排查“两个 app 是否写了同一个 key 的脏数据”。
6.3 最典型的三类串门故障速查表
| 现象 | 可能原因 | 快速排查手段 |
|---|---|---|
| 配好的 WiFi 密码重启后又丢 | NVS 分区被其他分区覆盖或 NVS 大小不够 | 检查分区表 NVS 偏移和大小;dump NVS 看 key 是否存在 |
| 设备 OTA 后启动反复重启 | app 槽划分不对,或 app1 被数据分区挤压 | 看启动日志的 OTA 校验;parttool 查 app 槽大小 |
| 模块 A 存的值,模块 B 读出来了 | 双方共用同一个命名空间或 key 名 | 检查nvs_open的命名空间;dump NVS 比对 key 归属 |
| Web 页面更新后传感器历史数据为空 | Web 和传感器数据共用了一个 spiffs 分区 | 拆分独立分区;检查分区表子类型 |
| 擦除一个分区后整机配置丢失 | 错误使用了全片擦除命令 | 擦除时只擦目标分区,不要擦 NVS 系统分区 |
我处理过一个很隐蔽的故障:NVS 分区大小是 0x5000,某个小应用频繁写入一个 1KB 的 blob,每次写入都会触发 NVS 垃圾回收,把旧 entry 标记删除,然后整理页表。由于分区太小,回收操作频繁,某次断电导致页表损坏,随后整个 NVS 初始化失败,所有模块配置消失。后来我把 NVS 分区从 0x5000 扩到 0x10000,并限制了该应用的写入频率,问题才彻底消失。多应用共用 NVS 时,不要把 NVS 区域划得抠抠搜搜,该给的空间一定要给够,否则隔离做得再好,垃圾桶满了照样倒出来串味。
6.4 烧录相关问题的常见提示语
热词里那些flash download failed cortex-m3、error flash download failed target dll has been cancelled、cannot load flash device description看着吓人,但基本都是工具链与芯片连接的问题,不是 Flash 隔离策略问题。常见原因有三个:端口选错或驱动没装、Flash 被读保护、芯片上电时序不对导致无法进入下载模式。遇到这类报错,先检查 USB 转串口芯片驱动、换根线、按住 BOOT 键重新上电再烧。烧录是一定会用到 bootloader 的,分区表烧错了才会出现启动失败,这和下载失败不是一回事。
7. 多应用共存的整体规划思路与我的经验沉淀
7.1 先定“地契”再写代码,顺序不能反
我见过太多项目,一开始只写业务功能,完全不规划 Flash,等设备都快量产了才开始想“多个应用怎么隔离”。这个顺序必须反过来。第一步,列出这个产品会有多少个独立运行的逻辑应用,比如主控程序、配置程序、采集程序,每个程序需要哪些静态资源和数据;第二步,估算每个实体的最大占用空间,并乘以 1.5 到 2 的冗余系数;第三步,把分区表画出来,再让所有开发人员对着这张表认领自己能操作的分区;第四步,写一个启动自检函数,开机时枚举所有分区,并打印每个分区的起始地址、大小和剩余容量。这样团队协作时,谁动了别人分区立刻反应出来。
7.2 用分区标签建立“权限矩阵”
如果团队里有其他同事要加新功能,他打开分区表看见一堆名字可能不知道哪些能碰。我在项目文档里维护了一个“分区权限矩阵”,每次改分区表和命名空间都会同步更新。这相当于给所有小应用立了个“谁家的地谁种”的规矩。矩阵里写清楚:main应用只能访问app0代码区和cfg_main命名空间;webapp只能访问webdata分区,并且只读web命名空间;sensor_app只能访问sensordata分区和sensor命名空间。实际开发中我还会在构建脚本里做静态检查:谁代码里出现了esp_partition_find_first(... "webdata" ...),但组件目录不是webapp,就报 warning。虽然可能有误报,但能挡掉大部分“手滑”。
7.3 最后分享一个我自己的检查流程
踩过几次坑之后,我现在每做一个 ESP32 多应用项目,都会在交付前跑一遍自检流程:先看编译输出里Partition Table的打印信息,确认各分区偏移、大小符合预期;再用parttool.py读取芯片上的实际分区表,确认烧录的与代码里编译一致;接着写一个临时测试固件,顺序读写每个数据分区的首尾扇区,越界部分故意去访问下一个分区地址,确认 API 能拦截非法访问并返回错误;最后做一次断电老化测试,在数据写入过程中随机断电,观察重启后 NVS 和各文件系统是否能恢复。这一套动作做完,我基本敢说“共享 Flash 但不串门”已经达标了,剩下的就交给时间验证。
多应用共享一块 Flash 的本质,就是把“大家共用一间房子”变成“每人一套独立公寓”。分区表是房产证,NVS 命名空间是房间里的保险柜,Flash 加密是门锁,API 接口是管家,规范流程是物业制度。四层防线一起上,ESP32 这一小块 Flash 才能被多个小应用从容地共享,数据也才能真正做到不串门。