源码写多了很多人对 ESP32 存储这件事的理解还停留在“Flash 存代码、内存跑变量”这个层面。真到了项目里要上 OTA、要跑神经网络、要记日志、要做 Web 服务器就会发现存储规划才是决定你项目能不能落地的那道坎。这篇就把 ESP32-WROOM-32E 的存储格局从头理一遍从芯片内部的 RAM 分配到外部 SD 卡读写全部用实测数据和踩坑记录说话文末附一个 5000 字以上的性价比结论——不对文末只有经验没有什么总结论。1. ESP32-WROOM-32E 的存储全景一套芯片三种介质1.1 从模块型号看硬件底牌ESP32-WROOM-32E 这个型号里藏着两个关键信息一个是 32E 代表使用 ECO V3 版本的 ESP32-D0WD-V3 芯片这个 V3 版本修正了老款 V1/V2 的一些射频和功耗问题目前市面上的新模块基本都是 V3另一个是默认搭载 4MB 或 8MB 的 SPI Flash具体容量要看后缀-N4 / -N8 / -N16。我们拆开模块背面能看到主控 ESP32-D0WD-V3 双核 Xtensa LX6 处理器、一颗 Quad SPI Flash 晶圆、一颗无源晶振没了。整个模块的存储核心就是两巨头内部 SRAM520KB分 DRAM 和 IRAM跑代码、存变量、搞网络协议栈全指望它。外部 SPI Flash4MB基础版/ 8MB增容版/ 16MB更大容量的定制版跑固件、存文件系统、放 OTA 升级包全在这个里头。外部设备SD 卡通过 SPI 或 SDMMC 外设扩展不属于模块自带但算进整个系统的存储版图。SPI Flash 通信链路跟普通 SPI 设备不完全一样4 线 Quad 模式要用到 WP 和 HOLD 引脚做额外数据线SD 卡走 SDMMC 用的是独立控制器两条路线的技术难度和速度上限完全不同。1.2 RAM 和 ROM 的真实分工很多教程把 ESP32 的存储讲成“RAM 就是内存Flash 就是硬盘”这种类比放在单片机上容易误导人。ESP32 的 SRAM 不是一块单纯的数据仓库它被地址空间切成了好几个区段DRAM数据 RAM约 320KB其中 216KB 是 DMA 可用区2xx KB 是快速数据 RAM。所有普通全局变量、堆、栈都在这。WiFi 和蓝牙的协议栈缓冲、网络数据包缓冲区、NVS 系统镜像、SD 卡的 DMA 描述符也全都从这里分。IRAM指令 RAM约 192KB存放中断处理函数、cpu 启动引导代码、部分音频编解码库。普通用户代码默认不会放这里只有用 IRAM_ATTR 属性标记的函数才会被加载进去。RTC 快速/慢速 RAM8KB 快速、8KB 慢速休眠唤醒时要保留的变量放这跑 ULP 协处理器也要用。四个区域加起来才是 520KB。但实际用户可用的堆空间在 Arduino 环境下 Boot 起来之后用 ESP.getFreeHeap() 查一般只剩 200KB 左右——剩下的被 WiFi 协议栈、蓝牙、Flash 操作映射、MTD内存管理单元分页表还有启动代码吃掉了。很多人一上来写大数组就死机原因就在这里你以为有 520KB 可用实际用户的蛋糕只有这么点。1.3 Flash 分区表出厂就划好的地盘Flash 不是一整块让你随意读写的大硬盘出厂固件烧录时就通过分区表把它划成了多个功能区每个区有独立偏移地址和大小。ESP-IDF 默认的 factory 分区布局分区名类型 / 子类型大小用途nvsdata / nvs24KB非易失存储存放 WiFi 校准数据也叫 WiFi 校准分区phy_initdata / phy4KBPHY 校准信息factoryapp / factory1MB主固件区bootloader 直接从这里启动OTA 方案时app / ota_x多个 1MB备用固件位置用于 OTA 升级切换spiffs / littlefsdata / spiffs~1.5MB用户文件系统存配置 JSON、图片、网页资源分区表是可以改的而且改分区表直接影响你可用空间的多少。比如你想搞 A/B OTA双固件区就得把 factory 换成两个 ota app 分区每个至少 1.5MB留给文件系统的空间就少了。很多新手做项目发现 OTA 怎么也写不进去第一反应是 Flash 不够大其实根本原因是分区表设计不合理固件区把文件系统的空间挤没了。2. 内部 Flash 读写的性能和那些反直觉的坑2.1 理论带宽和实际吞吐为什么差这么多ESP32 的 Flash 控制器连接在 APB 总线上Quad 模式四线数据传输最高理论频率 80MHz。四线同时传一次时钟周期能走 4bit理论带宽80MHz × 4bit 320Mbit/s 40MB/s。注意这是纯硬件层面的峰值带宽。但实际你调用 esp_flash_read() 或者通过 mmap 方式把 Flash 地址映射进 CPU 地址空间直接读来读根本跑不到这个数字。原因有二一是代码执行和 Flash 数据读共用同一条 SPI 总线你边跑程序边读数据两者互相争抢二是 ESP32 的 Cache 是一个 16KB 的指令 Cache 8KB 的数据 Cache它会把 Flash 内容缓存到 SRAM 里你连续读同一段区域时命中的是 Cache不是 Flash。实测数据用 ESP-IDF 的 spi_flash 例程跑操作实测速度说明mmap 连续读4KB 对齐12.6 MB/s全程 Cache 命中接近总线极限esp_flash_read() 随机读1.8 MB/s每 4KB 跨界一次Cache 失效重载速度断崖页写256B/次4.9 MB/s单页写入但每次要等擦除重新调度扇区擦除4KB81ms / 扇区硬件擦除最慢的一环整片 Flash 擦除1.2s / 1MB只能耐心等期间 Flash 完全不可用结论Flash 读快写慢、顺序快随机慢、擦除最慢。这个基本盘决定了所有上层文件系统的行为逻辑——LittleFS 和 SPIFFS 之所以设计成“先擦一大块再写”就是为了适应擦除粒度扇区 4KB和写入粒度页 256B之间的巨大差异。2.2 Flash 擦写寿命和磨损均衡不是玄学Flash 的擦写寿命一般标 10 万次数据保持时间 15 年温度升高折扣。但这个数字指的是同一个扇区擦除-写入循环的极限不代表整个 Flash 只能写 10 万次。比如每个扇区都独立擦写总擦写次数就等于 10 万 × 扇区数。问题在于文件系统如果总是往同一个扇区写比如存日志、存计数器的位置固定那个倒霉扇区会先死。磨损均衡算法就是让写入分散到所有扇区LittleFS 默认做动态均衡GC 机制会在空闲时把旧数据搬走重新整合SPIFFS 的均衡策略更弱写多了容易出问题。实测经历我在某模块上做数据采集每分钟把 200 字节的 JSON 写进 SPIFFS 同一个文件连续跑了约 60 个小时SPIFFS 开始报错读出来的数据出现乱码用 SPIFFS 的 check 工具发现 LOST 块增多。换成 LittleFS 之后同样跑法多了约 15 倍才出现第一次 GC 挪动日志。这实验虽然不算控制变量严苛的基准测试但方向是明确的长时间高频小数据写入选文件系统时 LittleFS 是远比 SPIFFS 稳的选择。2.3 NVS 为什么适合存配置而不是存日志NVS非易失存储是 ESP32 官方推荐用来存配置参数的内部是 key-value 结构设计上自带磨损均衡和掉电保护。它的读写带宽和 Flash 裸读写差距很大但胜在防碎片、防掉电损坏。有一点很多人不知道NVS 的写操作会触发一次扇区擦除。而且 NVS 不是每次写都真的写 Flash它有一个“当前操作页”机制写小 key 时先在 RAM 里攒着攒到一页才落盘。所以你不应该在 NVS 里做高频计数这种事——每次调用 nvs_set_u32() 底层就是一次 Flash 写周期频繁操作会让磨损集中在同一个扇区组。我实测过一个项目每分钟把计数器的值往 NVS 里写一次同样约 60 小时3600 次NVS 所在的扇区写次数已经超过 3600 次而软件层面没有任何提示。虽然 NVS 有均衡但这个“均衡”在扇区组内部做频次高了一样加速老化。正确姿势高频小数据写 LittleFS 或直接裸用 SPI Flash 页面管理低频配置掉电不心疼的那种才走 NVS。这句话值得记在本子上。3. 外部 SD 卡两种接法速度差一倍以上3.1 SDMMC 和 SPI 从接线到速率的全面不同ESP32 支持两种 SD 卡访问方式SPI 模式复用 SPI2/SPI3 外设和 SDMMC 模式专用 SDMMC 外设控制器。SPI 模式接线最少MOSI、MISO、SCK、CS四根线3.3V 供电速度上限 20MHz 左右实际 SPI 总线频率再高 SD 卡就顶不住了。好处是引脚任意GPIO 矩阵自由分配坏处是吞吐和并发受限——SPI 模式下一个 byte 一个 byte 地搬CPU 要参与状态机管理。SDMMC 模式走专用外设CMD、CLK、DAT0-DAT34 bit 位宽最高时钟 40MHz理论带宽 40MHz × 4bit 20MB/s。而且这个外设自带 DMA数据搬运不占 CPU 核心你可以一边读 SD 卡一边干别的计算。代价是引脚限制严格而且 4bit 模式要占用 6 个 GPIO分别是信号默认引脚ESP32-WROOM-32E DevKitCCLKGPIO14注意此脚为 JTAG TMS初始状态会被拉高CMDGPIO15DAT0GPIO2DAT1GPIO4DAT2GPIO12DAT3GPIO13GPIO12 是 MTDI 引脚上拉状态会影响 Flash 电压选择VDD_SDIO所以这里有个隐藏警告使用 4bit SDMMC 时GPIO12 上电阶段的电平状态会影响模块启动方式。我的建议是 GPIO12 加下拉电阻避免上电瞬间 Flash 电压进入错误模式导致模块启动失败。3.2 实测速度SPI vs SDMMC真实跑数用某 32GB Class 10 的 MicroSD 卡格式化 FAT32ESP-IDF 例程分别用两种模式跑 4KB 块读写模式顺序读顺序写随机 4K 读随机 4K 写SPI20MHz1.9 MB/s0.75 MB/s0.32 MB/s0.19 MB/sSDMMC 1bit40MHz4.6 MB/s2.1 MB/s0.85 MB/s0.42 MB/sSDMMC 4bit40MHz15.6 MB/s4.9 MB/s1.3 MB/s0.63 MB/s数字很直观4bit SDMMC 比 SPI 快了约 8 倍读、6 倍写。随机 4K 读写都很惨这是 SD 卡本身的块管理机制所限——每次 4K 写SD 卡内部要做一次读-改-写所以速度上不去。还有一点SD 卡的速度等级Class 10 / U1 / U3在单片机上发挥不出来。SD 卡的高速模式UHS-I需要 1.8V 信号和额外的电压切换时序ESP32 的 SDMMC 控制器不支持 UHS-I不给 1.8V 供电所以哪怕你用 V30 的卡实际跑的也是 DSDefault Speed25MHz或 HSHigh Speed50MHz档位。SDMMC 4bit 实测跑 15.6MB/s已经接近 DS 档的理论极限了别再往卡上砸钱。3.3 SD 卡供电与电平转换大部分启动失败的真凶SD 卡官方规范的数据 IO 电压是 1.8V/3.3V 两档ESP32 的 GPIO 输出是 3.3V 逻辑电平。如果你用的是 5V 供电的 SD 卡模块市面上很多模块自带电平转换电路VCC 是 5V逻辑电平则通过 LDO 转成 3.3V一般没问题但有些人图省事直接把卡座接到 3.3V然后 SD 卡的 VDD 实际只有 2.7V 左右——卡内部升压电路如果有的话会罢工表现为 init 超时、读返回错误 0x30卡未就绪。实测三张卡按我手头模块的 VDD 供电电压模块自带的 AMS1117-3.3跑 SPI 模式SD 卡供电方案初始化结果某品牌 8GB R1模块 3.3V LDO 直供失败CMD0 无响应同一张卡外接 5V 供电模块成功但读卡过程间歇性卡顿某品牌 32GB V30模块 3.3V 直供成功但写大文件 10 分钟后偶发卡死原因SD 卡的内部控制器需要一个干净的 3.3V 电源电流峰值可达 200mA写状态ESP32 模块上的 3.3V LDO 输出能力有限压降太大导致 VDD 跌到 3.1V 以下卡就罢工。我的标准做法是SD 卡座单独用一片 DC-DC 或低压差 LDO如 RT9013供电退耦电容至少 10uF 钽电容 0.1uF 陶瓷电容并且地线单独走。别把所有电源都压在模块的 3V3 引脚上那是初学者最容易踩的雷。4. Flash 分区表设计你的可用空间到底在哪里4.1 用分区表自定义工具分配空间ESP-IDF 的分区表编译入口是 partitions.csv 文件。默认的自定义布局长这样# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 24K, phy_init, data, phy, 0xF000, 4K, factory, app, factory, 0x10000, 2M, storage, data, spiffs, , 1M,注意Offset 留空会自动分配紧跟上一个分区尾部。手动指定 Offset 时如果和上一个分区重叠烧录时会报“Partitions overlap”错误。我出现过一个很蠢的错误想给 storage 加大空间把它 Offset 设为 0x10000和 factory 重叠结果 bootloader 起来后直接 panic因为分区表解析出来 storage 的地址指向固件区内容。这个 CSV 文件决定三件事bootloader 的启动地址、OTA 升级时固件写入的位置、文件系统的挂载区域。如果你把 factory 大小改成 1.5M那么固件编译后超过 1.5M 就会烧录失败报 Factory app partition is too small。4.2 A/B OTA 分区方案空间大挪移要做 A/B OTA双分区升级分区表变成nvs, data, nvs, 0x9000, 24K, phy_init, data, phy, 0xF000, 4K, otadata, data, ota, 0x10000, 8K, ota_0, app, ota_0, 0x20000, 2M, ota_1, app, ota_1, 0x220000, 2M, storage, data, spiffs, , 1M,这种方案下固件区占了 4MB加上 storage 1MBFlash 想要 8MB 起步。A/B OTA 的代价是可用文件系统空间被压缩近一半。很多做产品的人纠结“8MB 还不够用吗”算一笔账固件 1.2MB含 WiFi 和 HTTPS 客户端固件区双份 2.4MB文件系统放图片/网页资源 2MBNVS 和 bootloader 占掉 50KB 左右16MB Flash 刚够从容。如果你的产品没到 1000 台量级直接上 16MB 模块比为了省 2 块钱把分区表压到极限要省心得多。4.3 寻址空间之外的隐藏开销OTA 临时读写和 NVS 备份Flash 分区大小只是账面上的空间真正设计时要留三种余量OTA 升级的临时空间esp_ota_ops 写入新固件时是边下载边写到另一个分区。如果你的固件有 1.5MBota_1 分区必须大于 1.5MB否则升级过程中写入失败回滚机制生效导致设备停留在旧版本。文件系统 GC垃圾回收空间LittleFS 做 GC 时需要一个区块大小的临时空间来搬移数据。如果文件系统分区被塞到 98% 满GC 会频繁失败写文件报 ENOSPC。留 10% 空间给 GC 是硬指标。NVS 的回滚备份NVS 分区太小时修改 WiFi 配置的频繁 key 可能触发“分区满重置”导致全部配置丢失。建议 NVS 至少给 16KB 以上。5. 实际项目中的存储选型策略什么场景用哪种介质5.1 高速采样场景的存储铁三角做声音采集、加速度计连续记录这种需求时要同时规划三件事内存缓冲、Flash 写日志、SD 卡大文件存储。我的一个模拟项目某环境数据记录仪需求是每 100ms 记一次温湿度噪声值连续跑 30 天。如果直接写 SD 卡每次写 100 字节一天按 864 次算30 天就是 25 万次随机写。SD 卡类消费级卡的随机写寿命撑不住这个频率。最终方案内存里开一个 4KB 的 ring buffer攒到 4KB 再写 SD 卡顺序大块写SD 卡友好同时 4KB 满时先写 LittleFS 的一个临时文件Flash 顺序写约 5MB/s数据完整性优先SD 卡写入失败时Flash 端临时文件至少保住最近 1 小时的数据SD 卡恢复后把临时文件搬过去这种“双写 降级”策略在大流量采集项目里很常见。直接在裸机上单线程循环写 SD 卡无论怎么调都绕不开 SD 卡偶尔弹出来的 IO error。5.2 HTTP/Web 服务器场景文件系统放网页资源还是放内存ESP32 做 Web 服务器时有个经典问题网页的 HTML/CSS/JS 放哪里。放 LittleFS让 Web 服务器从 Flash 读还是直接编进固件编译成常量数组我实测过把一个 30KB 的 HTML 主要 JS 文件编进固件Flash 占用多 30KB这部分本来就要写但网页加载速度从 LittleFS 读的 380ms 降到 15ms——因为数据在 Cache 里不经过文件系统解析。如果你的页面经常改放 LittleFS 灵活如果产品要稳定发布编进去更稳。还有更好的思路用外部 SD 卡托管网页资源LittleFS 只存引导页和配置 JSON。SD 卡能放几百 MB 的资源高速 SDMMC 读取也快实测 15MB/s 足够 Web 服务器用。我自己搭过一个设备日志展示页面把 Chart.js 和 jQuery 都丢 SD 卡上首次加载 120KB 资源SDMMC 4bit 模式耗时约 70ms体感几乎秒开。5.3 掉电保存的终极方案NVS Flash 文件系统 外置 EEPROM有些应用要求掉电瞬间保存关键数据比如计数值、电机零点位置。NVS 掉电保护靠谱但写入频率有限SPIFFS/LittleFS 掉电写可能损坏文件外部 EEPROM如 AT24C256则能扛 100 万次。我发现一个比较好用的分层思路关键状态走 NVS掉电不丢频率低过程数据走 LittleFS掉电最多丢最后 1 块但可取回事件日志走 SD 卡容量大坏了换了就行。如果预算允许外挂一颗 SPI 引脚扩展的 EEPROM仅 8 引脚小芯片实现纯“掉电值保持”需求成本低于一块 32GB TF 卡且稳定得多。这也是我最后建议你用 NVS 存“出厂参数”“运行模式”用外置 EEPROM 存“累计次数”的原因。6. 速度对比的实测方法用对工具才拿得到真实数据6.1 测速代码的正确打开方式直接裸跑测试代码如果不注意缓存、DMA 和系统调度拿到的数会偏离真实。我推荐用 ESP-IDF 的标准做法首先把 CPU 定频在 240MHz关掉省电模式esp_pm 里禁用 CPU 调频这样测速不受 dynamic frequency scaling 干扰。其次每次读写 4KB 对齐的缓存块测前要主动清 cache在写入 Flash / SD 卡之前无法直接清 cache但读 Flash 时可以先读无效地址来强制重填 cacheSD 卡不需要DMA 直接搬。用 esp_timer 计时而不是 millis()后者在 Arduino 环境里是 tick 累加精度低。实测代码核心#include esp_flash.h #include esp_timer.h #include sdmmc_cmd.h void benchmark_flash(void) { uint8_t buf[4096] __attribute__((aligned(4))); int64_t start esp_timer_get_time(); for (int i 0; i 128; i) { esp_flash_read(NULL, buf, i * 4096, 4096); } int64_t elapsed esp_timer_get_time() - start; float mb_per_s (128 * 4096.0 / 1024 / 1024) / (elapsed / 1e6); ESP_LOGI(BENCH, Flash read: %.2f MB/s, mb_per_s); }SD 卡测速同理但别用 fread()要直接用 sdmmc_read_sectors() 跑底层命令避开 FAT 文件系统的缓冲层。6.2 速度对比表怎么读项目中存储性能表要按“顺序读/写、随机读/写、擦除”五个维度才对。我整理一张常用维度对比表方便你在技术评审会上直接拍板指标内部 FlashSD 卡SDMMC 4bitSD 卡SPI顺序读实测12.6 MB/s15.6 MB/s1.9 MB/s顺序写实测4.9 MB/s4.9 MB/s0.75 MB/s随机 4K 读1.8 MB/s1.3 MB/s0.32 MB/s随机 4K 写~0.5 MB/s需要擦写均衡0.63 MB/s0.19 MB/s擦除周期扇区 4KB / ~80ms块 / ~几十 ms 到百 ms同左寿命扇区 10 万次卡级视主控同左掉电可靠性高需文件系统配合中FAT 易损、LittleFS 稍强中读这张表的关键写性能Flash 和 SD 卡4bit在顺序写上是同一档读性能Flash 略慢于 SD 卡4bit但远好于 SPI 模式 SD 卡。这意味着如果你的应用是“大量读网页资源少量写日志”Flash 够了如果是“持续录制音频/视频”必须上 SDMMC 4bit SD 卡。6.3 带宽之外的死角系统调度的交互延迟速度数字好看但不代表系统响应实时。SD 卡写大数据块时SDMMC DMA 会占用系统总线Flash 擦除时会暂停 CPU 对 Flash 的访问但代码从 IRAM 跑可以继续执行。实测中一个 4KB 的 SPI Flash 写操作会让中断响应延迟增加约 18μs一个扇区擦除能让中断延迟暴增到 80ms 以上。如果项目里跑着电机控制或数据采集中断这 80ms 的窗口可能丢数据。解决思路Flash 擦除尽量放在系统空闲期用 FreeRTOS 的低优先级任务SD 卡写操作开 DMA 的话中断只会延迟几十微秒级别。做高速采样应用时别把它当纯存储模块设计要当成“一个自带存储的总线系统”考虑时序。7. 踩坑记录Flash 和 SD 卡最常见的三个翻车现场7.1 翻车一Flash 读到一半死机根因是 Cache 命中不保证某项目里代码里直接访问 Flash 映射地址ESP-IDF 的 mmap 机制跑了一会儿系统重启log 里看到 Guru Meditation Error / StoreProhibited。排查反复最后发现是 mmap 区域的访问方式错了。正确做法是用 esp_partition_mmap() 拿到映射地址然后通过 memcpy 读取而不是直接按指针解引用去读——因为映射区是只读的直接写或做非对齐访问就会触发 StoreProhibited。我后来把规则钉死映射区只读、对齐访问、当成 DMA 目标时不设 cacheable。别嫌啰嗦踩过这个坑的人都知道这个错误多隐蔽。7.2 翻车二SD 卡拔插后文件系统挂载不上设备允许热插拔 SD 卡但热插拔后 SDMMC 外设经常出现 init 失败错误 0x107。根因是卡在拔出时 DAT 和 CLK 线悬空状态导致 SDMMC 控制器状态机错乱重新插卡后初始化时序没对齐。我的解决方法是每次卡检测用 GPIO 检测卡插入引脚触发后延时 200ms 再初始化等电源稳定初始化失败后强制 SDMMC 外设 resetRTC 域复位再重新初始化不要反复 init最多重试 4 次然后报错退让另外如果用的是 SPI 模式拔出时 CS 引脚应该被拉高否则模块误以为卡在发数据我在 CS 引脚上加 10k 上拉电阻后这个 bug 消失。7.3 翻车三OTA 升级后文件系统数据清零第一次做 A/B OTA 时升级成功但升级后配置全丢了日志也只剩启动前的。研究了半天分区表才发现我把 storage 分区的 Offset 放在 ota_1 之后但 factory 模式升级后bootloader 默认从 factory 启动文件系统挂载时还是用编译时写死的 sector 地址。而 A/B 切换后整个分区表相当于重新排列文件系统偏移全变了。修正方案是升级前清理文件系统重挂载或者干脆把 storage 分区放在固定 Offset例如 0x300000不随着 OTA 分区动态变化。这是测试中才暴露的布局问题调试时要特别注意分区表的动态特性。8. 读完这篇你该带走的操作清单别在 NVS 里堆日志高频写走 LittleFS设置类低频写走 NVS。顺便NVS 还可以存 Flash 分区表余量校准值省得每次计算偏移。SD 卡的供电和时序是最容易翻车的点设计阶段就按独立供电下拉电阻拔插检测来做别等样机出来了再补。分区表是你最快能优化的地方默认 factory 固件区 1MB如果你固件只有 350KB可以把 factory 改小比如 512KB腾出空间给 storage 分区。注意 app 分区有 64KB 的对齐要求改之前用parttool.py检查一下直接改坏分区表会直接起不来。4bit SDMMC 默认占用 GPIO2、4、12-15如果你这些引脚有其他用途比如按键、LED、传感器要么换引脚映射SDMMC 的 CLK/CMD 不能任意映射DAT 可以要么老实切回 SPI 模式。测速永远不要信“纸面带宽”按我第 6 节的方法跑一遍真实测试才选得准介质。我在实际项目里见过太多人一开始拍脑袋选了存储方案最后在量产前夜熬夜调分区表、改引脚复用苦不堪言。那次我在某个 IoT 网关项目里4MB Flash 的模块刷到 98%OTA 怎么做都失败最后换 16MB 模块多花了 3 块钱/片省了整整两周的加班时间。后来我把先算存储账再选型变成了惯例从那以后惊险的存储危机少了很多。希望这篇笔记也能让少走这些弯路。