开篇先说个我最近实战中遇到的场景一块 4MB 的 ESP32 Flash 上跑了温湿度采集、蓝牙配网、LED 控制、日志记录几个“小应用”每个应用都要存自己的配置和数据。一开始图省事所有人共用默认分区结果 A 应用刚写完数据B 应用一启动就把同一片区域覆盖了。查了半天锅不在代码而在 Flash 资源是怎么分配的。这个问题的核心其实是 ESP32 的 Flash 分区管理。多应用共用一块 Flash 本身没问题ESP32 出厂就是这么干的bootloader、app、NVS、OTA 临时分区都在同一颗 Flash 里。真正要解决的是“每个应用该用哪块区域、用什么方式隔离、读写出错怎么兜底”。这篇文章就把我踩过的坑、验证过的方案、可以直接抄的分区表配置和代码写法全部分享出来适合正在做多业务模块、OTA 升级、或者准备在一颗 Flash 上塞多个固件的朋友参考。1. 问题本质与 Flash 管理基础1.1 为什么多个应用共用 Flash 会“串门”要理解串门得先知道 ESP32 的 Flash 是什么形态。你可以把它想象成一块巨大的线性地址空间从 0x000000 到 Flash 末尾CPU 可以直接访问。CPU 眼里没有“文件”这个概念只有“某个地址上存了 XX 字节”。所以如果没有一套统一分配规则两个应用完全有可能写到同一个偏移地址上——先写的人被后写的人覆盖或者读出来的是上半截 A 数据、下半截 B 数据。这里要区分两种“多个小应用”的含义。第一种是一个固件工程里拆成多业务模块模块之间共享同一个二进制、同一个运行时只是在逻辑上各管一摊。第二种是真正的多固件比如 factory 固件和 OTA 固件、或者用 bootloader 引导不同固件运行的方案。两者虽然抽象层次不同但底层面对的是同一个问题Flash 区域归属不明数据就串门。我遇到过的典型串门现场有四种不同任务/模块往 NVS 写同一个 key后者覆盖前者。两个固件共用同一个 SPIFFS 分区固件 A 升级后格式化了分区固件 B 的配置文件全部丢失。自定义存储区域偏移写死固件 A 的 bin 体积变大后占到了 B 的数据区。OTA 升级后新固件的分区表变了但数据分区没有跟着迁移读取时拿到的是旧格式数据。1.2 ESP32 Flash 默认布局与分区表的作用ESP32 从出厂开始就不是“裸 Flash”而是带了一套默认布局。以常见的 4MB Flash 模组为例默认的关键偏移大致如下偏移地址大小用途0x000064KBbootloader0x80004KB分区表0x9000约24KBNVS非易失存储0xD0008KBOTA 数据0xE0004KBPHY 初始化数据0x10000约3MB应用区引导流程是芯片上电 → 运行 bootloader → 读取 0x8000 处的分区表 → 根据分区表找到要启动的 app 分区 → 跳过去执行。也就是说分区表是 Flash 里的“目录”每个应用能从哪读到哪都由分区表定义。很多人的数据串门问题就是从头到尾没看过这张“目录”直接把数据写在默认分区后面或者几个人各写各的地址。等代码膨胀、Flash 布局一变立刻翻车。所以正确做法是在设计阶段就把数据区、应用区、OTA 区、NVS 区规划清楚然后让所有代码通过分区表 API 去访问禁止在代码里写死 Flash 偏移。2. 方案选型分区表是第一道防火墙2.1 分区表的结构与自定义方法ESP-IDF 使用 CSV 文件描述分区表编译时通过gen_esp32part.py解析并生成二进制分区表烧录到 0x8000 位置。每个分区条目包含六列name、type、subtype、offset、size、flags。一个标准 CSV 长这样# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x6000, , phy_init, data, phy, 0xF000, 0x1000, , factory, app, factory, 0x10000, 0x200000,, app_data, data, spiffs, 0x210000, 0x100000,, custom_log, data, fat, 0x310000, 0xA0000, ,这里最关键的是 offset 和 size 两列。offset 决定分区从哪里开始size 决定它有多大。CPU 读写 Flash 时按分区表解析后的地址映射访问只要两个分区不重叠数据就不会“物理串门”。自定义分区表后需要在工程配置里指定它。ESP-IDF 使用idf.py menuconfig进入 “Partition Table” 选项选择Custom partition table CSV填上你的 CSV 文件路径。Arduino 环境则是在 board 配置里选分区方案或者直接覆盖 partitions.csv 文件。我在选偏移和大小时有三个始终遵守的原则所有 offset 必须是 4KB 的整数倍因为 Flash 最小的可擦除单位就是一个扇区通常 4KB。offset 不对齐会导致某些擦写操作越界。app 分区偏移建议从 0x10000 开始并保持 64KB 对齐这是 bootloader 的隐含假设省得后面碰到启动异常。每个分区后面预留一点空余。嵌入式产品迭代很快固件体积涨个 100KB 是常事分区大小卡太死下次升级就头痛。2.2 数据隔离的三种主流方案对比规划好分区表后还要决定“各应用数据存在哪”。我测试下来常用方案有三种各有适用场景。第一种是 NVS 隔离。适合存少量键值比如每个应用的配置参数、设备状态、上电计数。NVS 本身支持命名空间namespace不同应用打开不同的命名空间就能互不干扰nvs_handle_t handle; esp_err_t err nvs_open(app_a, NVS_READWRITE, handle); nvs_set_str(handle, ssid, my_wifi); nvs_commit(handle); nvs_close(handle); // 另一个应用 err nvs_open(app_b, NVS_READWRITE, handle); nvs_set_u32(handle, counter, 100); nvs_commit(handle); nvs_close(handle);只要每个应用锁死自己的 namespacekey 名重复也没关系因为底层 key 是namespace key组合的。这是最轻量的隔离手段但要注意 NVS 总容量是固定的不适合存大块数据。第二种是文件系统分区隔离。每个应用分配一个独立的 SPIFFS 或 LittleFS 分区应用 A 只挂载app_a_fs应用 B 只挂载app_b_fs。这样即使两个固件乱写物理上也不会互相影响esp_vfs_spiffs_conf_t conf { .base_path /app_a, .partition_label app_a_fs, .max_files 8, .format_if_mount_failed false, }; esp_vfs_spiffs_register(conf);注意format_if_mount_failed这个参数我一般设成 false。如果设成 true挂载失败时会直接格式化整个分区相当于把别的应用留下的数据全抹了。第三种是裸分区 自定义数据结构。适合存日志文件、模型参数、或者需要跨应用共享的大块数据。代码里用esp_partition_find_first找到分区再按偏移读写。隔离靠的就是分区表代码只需要保证“我只往这个分区里写”。三种方案可以混合使用。我目前最稳的组合是全局配置用 NVS 多 namespace大文件各应用独占分区跨应用共享区单独划一个分区并在每条数据前加上 magic 和版本号。2.3 OTA 场景下的特殊注意事项如果产品要做 OTA分区表设计会更复杂一点。常见做法是factory ota0 ota1 数据区两个 app 分区轮流存放新旧固件。升级时新固件写入另一个 app 分区校验通过后切换启动项。这个机制本身就是一种“多应用共用 Flash”的经典案例。OTA 场景最容易出现的数据串门不是 app 区而是数据区。因为新固件可能使用不同的数据格式或者分区表大小有调整。处理办法是给所有自定义数据结构加一个版本字段新固件启动后读取版本如果低于预期就执行迁移脚本。另一个坑是 OTA 数据分区默认otadata不要手动写否则 bootloader 不知道现在应该启动哪个 app。别问我为什么知道说多了都是泪。3. 实操从分区表到代码隔离3.1 手把手配置自定义分区表下面我用一个真实项目为例带你完整走一遍。假设 Flash 一共 4MB0x400000需要运行三个逻辑应用应用 A 负责传感器采集应用 B 负责显示和交互应用 C 负责 OTA 管理和日志上传。为了演示“多个小应用共用一块 Flash”我这里把它们分别映射到独立分区。设计如下# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x6000, , otadata, data, ota, 0xF000, 0x2000, , phy_init, data, phy, 0x11000, 0x1000, , app_a, app, factory, 0x12000, 0x180000, , # 应用 A app_b, app, ota_0, 0x192000, 0x180000, , # 应用 B 的 app app_c, app, ota_1, 0x312000, 0x180000, , # 应用 C 的 app app_a_data, data, spiffs, 0x492000, 0x90000, , app_b_data, data, spiffs, 0x522000, 0x80000, , app_share, data, fat, 0x5A2000, 0x100000, , sys_log, data, spiffs, 0x6A2000, 0x60000, ,等等上面这个表在真实 4MB Flash 上显然放不下一加起来就超了。实际设计时我确实会先把总预算列出来用计算器加一遍再写 CSV。下面给一个经过精算、能落到 4MB 芯片上的可行方案分区名类型/子类型偏移大小说明nvsdata/nvs0x90000x6000 (24KB)全局配置otadatadata/ota0xF0000x2000 (8KB)OTA 状态phy_initdata/phy0x110000x1000 (4KB)WIFI 校准数据factoryapp/factory0x120000x180000 (1.5MB)出厂固件/应用 Aota_0app/ota_00x1920000x180000 (1.5MB)应用 Bota_1app/ota_10x3120000x180000 (1.5MB)应用 Capp_a_datadata/spiffs0x4920000x40000 (256KB)应用 A 数据app_b_datadata/spiffs0x4D20000x30000 (192KB)应用 B 数据app_sharedata/fat0x5020000x70000 (448KB)共享区sys_logdata/spiffs0x5720000x40000 (256KB)日志区加起来刚好接近 4MB 可用范围。这个表格的要点是每个应用有自己的 app 区域通过 OTA 切换固件时隔离自己的数据区域以及一个显式的共享区域。共享区只放所有人都要读、但只有协调者能写的文件。CSV 第一行是注释工具会忽略以#开头的行。写完 CSV 后放进工程根目录然后在menuconfig里指定。编译时会生成partitions.bin烧录命令esptool.py --port /dev/ttyUSB0 write_flash 0x8000 build/partitions.bin3.2 代码侧的分区查找与读写策略配置好了分区表代码里就不要写死任何 Flash 地址了。ESP-IDF 提供了一套分区查找 API按名字和类型找到分区后再对分区对象做操作#include esp_partition.h const esp_partition_t* get_app_a_data_partition(void) { const esp_partition_t* part esp_partition_find_first( ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, app_a_data); if (part NULL) { ESP_LOGE(MAIN, app_a_data partition not found); } return part; } const esp_partition_t* get_share_partition(void) { const esp_partition_t* part esp_partition_find_first( ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_FAT, app_share); return part; }找到分区后可以拿到该分区的地址和大小。读写操作都基于分区对象而不是绝对偏移// 写共享区前先做好大小和格式校验 esp_err_t write_shared_record(const esp_partition_t* part, uint32_t offset, const void* data, size_t len) { if (offset len part-size) { return ESP_ERR_INVALID_SIZE; } return esp_partition_write(part, offset, data, len); }这里有个细节esp_partition_write之前必须先擦除。Flash 只能把 1 写成 0不能把 0 写成 1所以写入前要把目标扇区esp_partition_erase_range擦掉。常见错误是统一用追加写结果部分字节因为没擦除而写不进去。我的习惯是写一条记录占一个扇区写之前擦整个扇区这不浪费多少时间但能省掉很多对齐问题。NVS 部分如前所述用不同 namespace 隔离。要注意的是 NVS 键值写入前务必调nvs_commit否则数据只是缓存在 RAM 里。很多“掉电丢数据”的 bug 就是忘了 commit。文件系统方面每个应用挂载自己分区时base_path要区分开。比如 A 应用挂到/app_aB 应用挂到/app_b共享区挂到/share。挂载后代码层再按目录细化物理层已经隔离逻辑层也不会乱。3.3 数据格式设计magic version 双层保险分区表和 namespace 能挡住大多数串门但还不能完全防住“数据能读但格式不对”的坑比如 OTA 升级后固件 A 的旧文件被固件 B 的新代码打开。所以我给所有跨应用共享的数据都加了文件头魔法数和版本号。自定义结构示例typedef struct { uint32_t magic; // 固定为 0x5A5AA5A5 uint16_t version; // 当前结构体版本 uint16_t len; // 数据长度 uint32_t crc32; // 数据校验 } record_header_t; #define RECORD_MAGIC 0x5A5AA5A5读取流程如下读前 12 字节 header。检查magic不对就视为“分区被串门或从未初始化”。检查version比当前版本旧就触发迁移函数。用 CRC32 校验数据完整性防止写入中断导致数据半截。这个方案看起来多花了几十行代码但实际帮我省了无数个排查时间。尤其是两个固件交替运行时光靠分区名根本没法判断“这块数据是谁、是什么格式、能不能直接解析”有 magic 和 version 就能一眼定位。4. 常见问题与排查技巧实录4.1 我遇到的几个经典串门故障第一个故障两个模块都写 NVS 的同一个 key。现象是设备重启后 WiFi 配网信息变成另一模块的状态。我用nvs_dump查看 NVS 内容时才发现两个模块都在nvs_open(storage, ...)下用同一个 key。解决办法是强制划分 namespace并写了一个启动检查脚本把 key 名打印出来谁在用一目了然。第二个故障SPIFFS 分区被格式化。现象是设备升级后所有历史日志消失。追代码发现新固件挂载日志分区时format_if_mount_failed设成 true刚好那次升级后分区校验失败分区被整个格式化。从那以后我改成 false并额外加了日志文件名唯一前缀。第三个故障裸分区偏移重叠。现象特别诡异应用 A 写配置写在偏移 0x20000应用 B 也往偏移 0x20000 写日志两者互相覆盖而且时好时坏。查了半天后发现两边都直接用了硬编码偏移没有走esp_partition_find_first。最后把所有硬编码地址全部改成分区查找。第四个故障OTA 后版本不兼容。现象是升级后共享区的配置文件读失败系统反复重启。原因是没有版本号新固件拿到旧格式数据直接解析错误。加了 magic version 迁移逻辑之后这个问题就消失了。4.2 排查工具与方法排查 Flash 问题时用好三样工具parttool.py、esptool.py、nvs_dump.pyESP-IDF 自带或自行编译。先用 parttool 查看当前芯片里的分区表是否和编译时一致python parttool.py --port /dev/ttyUSB0 partition_table --info再用 esptool 把整个 Flash 备份出来分析esptool.py --port /dev/ttyUSB0 read_flash 0x000000 0x400000 flash_dump.bin拿到 dump 后用十六进制工具搜索你的 magic 值可以快速定位各分区实际写到了哪里。如果发现某个数据出现在两个不同分区里说明有代码越界或者偏移冲突。NVS 排查用下面命令导出内容python nvs_partition_gen.py dump build/partitions_singleapp.bin nvs_dump.csv --input 0x9000 --size 0x6000不过最直接的还是代码里加日志。我习惯在每次写入前打印[app][namespace]/[key] - offset/size把写入位置和长度都打出来上线后有问题可以直接看日志追判断是哪个模块干的。4.3 其他容易踩的坑Flash 磨损均衡。NVS 和 FAT 自带磨损均衡但如果你用裸分区直接esp_partition_write同一个扇区擦写次数太多会写坏。我的策略是日志类数据轮流写多个扇区配置类数据交给 NVS。分区大小对齐。分区表虽然允许不规整偏移但为了兼容软件和硬件所有分区偏移都按 4KB 对齐最稳。OTA 用到的 app 分区如果没按 64KB 对齐某些 bootloader 版本可能拒启。多应用并发写同一共享分区。如果一台设备上有多个任务同时写app_share需要互斥锁或用队列串行化。否则两个任务同时擦一个扇区数据必坏。同一个固件内多模块共用一个文件系统时尽量给文件名加前缀。虽然底层已经有目录隔离但有些第三方库可能会遍历根目录看到别人的文件就尝试解析导致误删误改。5. 代码仓库结构与团队约定最后说说流程层面的经验。多应用共用 Flash 不只是技术问题更是规范问题。我现在的项目里有一个partitions/目录存放所有分区 CSV 的源码版本任何修改分区布局的 PR 都必须附一张“Flash 空间占用对比图”评审时先看有没有破坏已有分区的偏移。代码仓库里还要维护一份分区声明表写明每个分区归属哪个模块、使用什么格式、能不能被其他模块读。这样新同事加入时不用翻代码看一眼表格就知道数据边界在哪。我的经验是这类规范越早定越好等到数据真串门了再补排查成本至少翻三倍。另外强烈建议在 CI 里加一步分区表校验。把编译生成的 partitions.bin 与上一版本对比如果 offset 或 size 变化就自动在 comment 里标出来。这样可以防止有人擅自偷别人的分区空间。我在实际项目中验证下来这套“分区表物理隔离 NVS namespace 逻辑隔离 文件系统目录化 magic/version 兜底”的组合是目前在 ESP32 上做多应用 Flash 共用最省心的一套。期间踩过的坑大多集中在硬编码地址、忘擦除、格式化开关这三个地方写代码时多留个心眼能少熬很多夜。
企业数字化 ERP 产品动态
相关推荐
PROFINET掉站闪断响应慢:现场排查指南与避坑清单 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/16 1:42:21
工业控制系统深度解析:30 执行器为什么让PID失效 第30章 执行器为什么让PID失效
30.1 PID控制的最后一公里
前面章节一直围绕PID如何计算控制量。完整模型:
SP → Error → PID → MV → Process → PV → Feedback理论上,PID计算出MV(例如MV=60%),控制器认为“我已经输出60%的控制作用”。但工业现场还有一个关键问题… · 2026/11/16 1:42:20
机修钳工职业技能鉴定题库,初中高级考点全解析附答案 机修钳工职业技能鉴定,考的是实打实的手上功夫和判断能力。不少考生跟我们反馈:理论题背了不少,实操也练了,可一进考场还是心里没底。问题往往不在努力程度,而在刷题方向——初、中、高三个等级考什么、怎么考、易错点… · 2026/11/11 16:17:43
酒精蒸馏工是特种工吗?多久拿证能安心上岗 酒精蒸馏工是特种工吗?多久拿证能安心上岗 怕考不过白交培训费?现在就带你搞清楚酒精蒸馏工是不是特种工,多久能拿到证,别再被培训机构忽悠了。 酒精蒸馏工属于特种作业范围吗?… · 2026/11/16 1:41:50
电工证到了怎么复审啊 在哪考最靠谱 电工证到了怎么复审啊 在哪考最靠谱 学历不高怕报不上名?电工证复审别犯愁,这几点你必须知道!很多人拿到电工证后,最大的担忧不是怎么考,而是怎么复审。尤其是那些学历不高、工作经验有限的小伙伴,担心复审流程复杂、审核不通过,甚至不知道在… · 2026/11/16 1:41:50
建筑特种工证办理怕考不过白交培训费?良心建议帮你稳过 建筑特种工证办理怕考不过白交培训费?良心建议帮你稳过 怕考不过白交培训费?这是很多建筑特种工证办理过程中最头疼的问题。尤其是像焦作市政公用工程这样的岗位,特种作业证不仅是上岗的硬门槛,更是职业发展的“敲门砖”。如果你正在准备考取建筑特种工证… · 2026/11/16 1:41:50
电工证接触器介绍值不值得考?2026河南最新政策别错过 电工证接触器介绍值不值得考?2026河南最新政策别错过 工地太忙根本没时间复习考试,但又想拿下电工证接触器介绍,这是很多郑州金水区初学者的真实写照。2026河南最新政策对特种作业操作证的报考流程、考试方式、复审周期都做了新调整,不及时了解,… · 2026/11/16 1:41:50
兰州报名电工证怎么报名?学历不高怕报不上名?良心建议来了! 兰州报名电工证怎么报名?学历不高怕报不上名?良心建议来了! 你是不是也这样,学历不高,担心兰州报名电工证根本报不上?别急,咱们今天就来聊聊这个事,从报名到考试,再到以后的就业发展,一锅端给你说清楚, 良心建议 ,别走弯路。… · 2026/11/16 1:41:50
攸县电工证在哪里考取 电工证在哪考才能上岗找工作 攸县电工证在哪里考取 电工证在哪考才能上岗找工作 你是不是正愁找不到工作?是不是还在为没有电工证发愁?现在不少岗位都要求必须有特种作业操作证才能上岗,尤其是电工证,几乎是电工、维修、安装等岗位的“入场券”。攸县电工证在哪考?这是很多初次报考… · 2026/11/16 1:40:50
长春初级电工证怎么考?通过率揭秘全在这里 长春初级电工证怎么考?通过率揭秘全在这里 之前考的证在外省能不能转过来?这几乎是每个想从事电工工作的人都会问的问题。特别是那些在外地打拼多年、手里握着一张“外地电工证”的人,到了长春想继续干这一行,第一反应就是“我的证能用吗?”。今天我们就… · 2026/11/15 0:00:50
国内电工证报考条件与考试时间 低压电工证怎么考?学历不高也能报名,官方报名入口全在这 你是不是经常听说“电工证是入行门槛”,但又担心自己学历不够、经验不足,根本报不了名?别急,今天就带你从零了解【国内电工证】的报考流程、考试内容,还有怎么在 官方报名入口… · 2026/11/15 0:00:50
电工作业时如何断电?电子证书到底能不能全国通用? 电工作业时如何断电?电子证书到底能不能全国通用? 实操考试心里没底怕挂科,特别是电工作业时如何断电这一操作,不仅关系到你能不能拿到证书,更关系到以后能不能顺利上岗。很多人觉得这不过就是“断个电”,但其实这里面的门道可不少,稍有不慎就可能出错… · 2026/11/15 0:00:50
电工证培训的通知在哪考?别再被中介骗了 电工证培训的通知在哪考?别再被中介骗了 你是不是还在为 电工证培训的通知在哪考 发愁?网上搜了一堆信息,结果要么是挂羊头卖狗肉的培训机构,要么就是打着“包过”旗号的骗子?别急,今天我来给你讲讲, 电工证培训的通知… · 2026/11/16 0:00:34
电工证去哪个部门考核值不值得考 信阳转岗人真实经历分享 电工证去哪个部门考核值不值得考 信阳转岗人真实经历分享 怕考不过白交培训费?你不是一个人,信阳的李师傅去年刚转岗做电工,就因为没搞清楚电工证去哪个部门考核,差点交了8000块的培训费。别急,下面这份实操考试全攻略,告诉你电工证到底值不值得考… · 2026/11/16 0:00:34
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/15 1:51:20
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/15 4:02:19
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/15 3:02:05