ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

ESP32存储全解析:PSRAM与Flash分工、分区表规划及实战避坑指南

2026/9/9 4:36:01 拓冰建站 浏览量
ESP32存储全解析:PSRAM与Flash分工、分区表规划及实战避坑指南 1. 为什么一块芯片要塞两种存储PSRAM和Flash的分工逻辑入手ESP32的人大概率都听过两个词PSRAM和Flash。WROOM和WROVER两个模块版本差价明显到手一看一个标着4MB Flash、一个标着8MB Flash外加8MB PSRAM很多人第一反应是Flash越大越好结果买回去发现真正让项目跑不动的不是Flash而是RAM。先把这两个存储的角色彻底分清楚后面所有选型和排错才不会跑偏。1.1 断电丢不丢数据是两者最本质的分水岭Flash属于非易失存储断电之后数据照样在承担的是硬盘角色——固件、配置、证书、甚至OTA备份固件都放在里面。PSRAM的全称是Pseudostatic RAM中文常叫伪静态随机存储器它本质上是易失的断电内容全部消失承担的是内存条角色——CPU运行时的变量、堆、栈、帧缓冲都在这类空间里临时安家。我常用一个比方Flash是仓库PSRAM是工作台。仓库里的工具和材料用多少年都不会丢但你不能站仓库门口干活工作台很宽敞但每次下班断电后东西就清空第二天上班得从仓库重新搬。ESP32内部那块比较小的SRAM相当于随身工具箱PSRAM则是外接的折叠工作台扩大干活空间用的。这个类比放到实际项目里非常直观——程序代码可以固化在Flash里掉电不丢但代码运行时要访问的全局变量、数组、帧缓冲必须放在RAM里否则处理器没法高效读写。另外要澄清一个常见误解很多人以为Flash可以当RAM用把大数组直接定义成const往Flash里塞。这在部分场景确实可行但Flash的写入寿命和写入速度跟RAM完全不是一个量级随机变更的变量放Flash会迅速磨损颗粒。反过来PSRAM也不能当Flash用关机数据清空做不了持久化存储。两者各管一摊谁也替代不了谁。1.2 ESP32里它们各自扛什么活在ESP32内部Flash承担的职责相当明确存放程序固件含Bootloader、分区表、应用镜像存放NVS非易失配置、WiFi校准数据、蓝牙白名单等存放文件系统LittleFS/SPIFFS放网页资源、证书、日志OTA升级时的备份分区和暂存分区PSRAM则用来扩展数据空间大尺寸LCD的帧缓冲LVGL等GUI库的显示缓冲区音频采样缓存、麦克风数据环形缓冲摄像头帧缓冲ESP32-CAM场景非常典型机器学习模型推理时的张量数据大数据量的JSON解析、加密运算中间结果以最经典的ESP32-WROVER为例它内部SRAM大约520KB左右但实际用户可用的堆空间通常只有300多KB。一旦跑LVGL带一块320x240分辨率的RGB屏幕光帧缓冲就要240×320×2字节约150KB再算上GUI对象的开销和任务栈内部RAM马上就见底。这时候把显示缓存挂到PSRAM上内存压力瞬间缓解这也是WROVER模块比WROOM贵出一截的核心原因。1.3 一个反直觉的细节Flash其实可以直接执行程序Flash是存储设备照理说程序要先从Flash搬到RAM才能跑但ESP32支持XIPExecute In Place机制——通过内部Cache直接把Flash映射到地址空间CPU可以像读内存一样从Flash取指令执行。所以典型的ESP32固件不需要把全部代码复制到RAM里再运行而是直接从Flash读取执行只有中断处理、高频访问的代码段才放到IRAM里。了解这个机制对排查问题很重要。很多人发现ESP32固件编译出来有几MB烧进Flash后担心RAM不够装其实完全不用担心固件是放Flash里直接跑的不占运行内存。真正占RAM的是全局变量、堆和栈。另一个容易被忽略的点是在执行Flash写入操作比如NVS写配置、OTA写入新固件时芯片会短暂暂停对Flash的读取如果此时代码正需要从Flash取指令就可能发生卡顿甚至看门狗复位。ESP-IDF对此有一套Cache禁用期间的仲裁机制实际开发中如果同一时间既写Flash又跑对实时性要求高的逻辑要注意任务的调度安排。2. 同样叫ESP32存储配置能差出好几个量级ESP32在很多人眼里就是一个芯片型号实际上它是一个庞大的家族。经典款ESP32、ESP32-S2、ESP32-S3、ESP32-C3、ESP32-C6的存储能力差异巨大有些型号原生支持PSRAM有些压根不支持选错型号后续项目都得推翻重来。2.1 常见芯片型号的存储规格速查我整理了一份常用型号的对照表方便选型时一眼看清差异芯片型号Flash支持情况模块上PSRAM支持情况内部SRAM典型适用场景经典ESP324/8/16MB部分模块支持最高8MB520KB实际可用约320KB通用物联网、LVGL界面、摄像头ESP32-S2最高16MB原生支持最高8MB320KB实际可用约200KBUSB开发、低成本联网ESP32-S3最高16MB支持Quad/Octal PSRAM最高8MB512KB实际可用约376KBAI视觉、复杂GUI、语音交互ESP32-C3最高4MB极少模块外挂更大原生不支持400KB实际可用约312KB轻量IoT、智能家居节点ESP32-C6最高4MB不支持512KB实际可用约300KBWiFi6Zigbee多协议网关需要强调的是上面这些数字只是原厂接口能力最终板上实际焊多大Flash和PSRAM由模块厂商决定。同样是S3有的模块给8MB Flash、有的给16MB Flash买到手要以丝印和实际readback为准这点下一节会详细说。2.2 硅片版本和模块型号的坑经典款ESP32有一个特别坑的细节早期ECO V1版本的硅片在PSRAM和WiFi/蓝牙同时工作时存在兼容性问题导致跑一段时间后系统不稳定或内存数据异常。ESP-IDF的部分版本里还有针对ECO V1的PSRAM工作模式限制如果你的板子是老版本芯片又想开大容量PSRAM需要特别留意。官方会不会修新版本SDK会加规避逻辑但硬件的先天缺陷改不了最稳妥的办法就是买板子时确认芯片的ECO版本或者直接用ESP32-S3这些后推出的型号。另一个判断点看模块丝印。乐鑫原厂模块的命名很直白WROOM表示不带PSRAMWROVER表示带PSRAM。比如ESP32-WROOM-32E和ESP32-WROVER-E外观尺寸几乎一样但WROVER底下多了一块PSRAM颗粒内存能力完全不同。市场上部分开发板丝印模糊甚至有拿WROOM冒充WROVER的买回来一查Flash ID发现容量对不上后面所有分区规划全部白做。2.3 存储颗粒的硬件接线与电压注意从硬件原理图看ESP32的Flash和PSRAM通常挂在同一个SPI总线上芯片内部有多个SPI控制器一个管Flash另一个管PSRAM但引脚有复用关系。ESP32-S3采用了更灵活的GPIO矩阵Flash和PSRAM可以分配到不同的引脚组合从而支持Quad SPI甚至Octal SPI接口。PSRAM从Quad升级到Octal后带宽翻倍对摄像头、LCD这类大数据吞吐场景影响非常明显。这里有个实操中容易忽略的点Flash和PSRAM的供电电压。ESP32的VDD_SDIO可以配置为3.3V或1.8V部分Flash颗粒只支持1.8V部分只支持3.3V如果板子在设计时把VDD_SDIO的电阻配置搞错Flash可能检测不到或者初始化失败。开发板用户一般不用动但如果你自己画板子或者做最小系统板一定要查看所用Flash和PSRAM颗粒的数据手册确认它们的电压范围是否匹配再决定VDD_SDIO的跳线方向。这个坑我踩过——某个自制板把开关拨到了3.3V结果那颗1.8V的Flash死活无法进入QPI模式整了两天才发现是供电配置问题。3. 读取Flash颗粒ID确认板子真实容量的第一步很多人烧录时报错flash download failed或分区表写不进去第一反应是换线、换串口芯片其实更该先确认一件事板子上那颗Flash颗粒到底多大、是什么型号、支持什么模式。市面上打着ESP32开发板旗号的板子Flash容量缩水或换杂牌颗粒的情况并不少见直接用软件工具读取是最快的验货方式。3.1 用esptool读取Flash ID乐鑫官方工具esptool提供了非常简洁的读取命令。以Windows环境为例先装好Python然后安装esptoolpip install esptool接好开发板找到串口号然后在命令行执行esptool.py --port COM3 flash_idLinux和macOS下端口名通常是/dev/ttyUSB0或/dev/cu.SLAB_USBtoUART写法一样。命令执行后工具会读取芯片信息并输出类似下面的内容Manufacturer: ef Device: 4018 Detected flash size: 4MB Flash frequency: 80MHz Flash mode: QIO这里的关键信息有几个Manufacturer是厂商编码Device是颗粒型号编码Detected flash size是软件读取到的容量。多数情况下这个容量就是板子的真实Flash大小但也不排除部分板子通过特殊方式修改了读取结果所以还要结合后续的详细ID来看。3.2 解析厂商ID和颗粒型号常见的Flash厂商编码如下0xEFWinbond华邦最常见质量稳定0xC8GigaDevice兆易创新国产板子大量使用0xC2Macronix旺宏0x9DISSI0x5EZbit0x1CEON宜扬知道了厂商编码只是第一步还要对照Device ID查具体型号。比如Winbond的W25Q32是4MB、W25Q64是8MB、W25Q128是16MBDevice ID各不相同。完整的型号对照表在各大Flash厂商官网都能下载到也可以直接用搜索引擎搜flash id查询颗粒把Device ID数字输进去就能看到颗粒型号。这个查询步骤的实际价值远超验货。确定颗粒型号后你才能确认它支不支持Quad模式QIO、支不支持1.8V低压、以及能不能开4字节地址模式。不同颗粒对QPI指令的响应有细微差异虽然ESP32的驱动层已经做了大量兼容但碰到初始化和读取异常时知道底层的颗粒型号能节省大量排查时间。3.3 从颗粒容量到分区规划读取到真实容量后紧接着要处理的就是分区表。ESP32的分区表是一张CSV格式的地址分配表定义了Flash里每个区域的起始地址、大小和用途。下面是一个4MB Flash的经典分区表示例# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 0x180000, ota_0, app, ota_0, 0x190000, 0x180000, ota_1, app, ota_1, 0x310000, 0x180000,分区表偏移量的起点从0x9000开始0x0到0x8000这段留给Bootloader和分区表本身。4MB的总空间规划下来应用分区1.5MB加OTA备份1.5MB剩余给NVS和phy_init。如果你的固件超过了1.5MB烧录时就会提示分区空间不足这时要么缩小固件要么换大容量Flash要么减少OTA分区数量。关于分区表有一个很实用的建议不要为了省空间把分区大小精确到KB级别。编译出来的固件会持续增长你今天优化到刚好1.4MB明天加个功能可能直接顶到1.6MB然后烧录失败。实际项目中我会给应用分区预留当前固件体积的1.5到2倍空间尤其是做OTA升级的项目备份分区必须能容纳完整的新固件否则升级时写入一半就报错。4. Flash空间规划分区表里藏着OTA与加密的秘密分区表是ESP32项目里最容易被忽略又最影响使用体验的部分。很多人的项目一开始用默认的factory分区做完OTA功能后才发现没有给OTA预留两个应用分区折腾半天还得重新分割Flash。4.1 为什么OTA必须规划两个应用分区OTA升级的本质是把新固件先写入Flash的另一个空闲区域写入成功后再通过引导逻辑切换启动到新固件。如果Flash里只有一个应用分区你没法在运行旧固件的同时写入新固件因为同一个区域正在被执行。所以OTA项目的分区表至少要划分出ota_0和ota_1两个应用分区外加一个otadata分区用来记录当前正在运行哪个镜像。带着OTA的4MB分区表示例如下# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x4000, otadata, data, ota, 0xd000, 0x2000, phy_init, data, phy, 0xf000, 0x1000, ota_0, app, ota_0, 0x10000, 0x1C0000, ota_1, app, ota_1, 0x1D0000, 0x1C0000,两个1.75MB的应用分区加上其他数据分区正好差不多占满4MB Flash。如果要放文件系统就需要把应用分区压缩到1MB左右空出0x700000附近的空间给LittleFS。这里有个经验文件系统分区最好放在Flash尾部且大小固定避免后续调整分区表导致文件系统数据错位。4.2 OTA升级流程中的常见坑一个完整的OTA流程是设备联网下载新固件到临时分区校验固件签名和哈希写入ota_0或ota_1根据当前运行反选更新otadata然后重启切换启动。听起来简单实际踩坑的点非常多。版本号不升很多OTA框架要求新固件的版本号高于或等于当前版本你烧了个版本号相同的固件设备会认为没有新版本升级静默失败。HTTPS证书过期设备下载固件用HTTPS时服务端证书过期会导致握手失败升级永远进行不下去。升级过程中断网如果写入一半断电ota分区里留下一个不完整镜像otadata还指着旧镜像重启还能跑旧系统。但如果此时恰好把otadata也写了就可能变砖。所以OTA写入完成前不能碰otadata。回滚机制缺失新固件启动后如果你不主动确认运行正常下次重启可能又会回滚到旧版本。这是双分区OTA的标准行为很多人不知道这一点以为是自己代码问题。关于OTA我最想强调的建议是正式量产前一定要做断电测试。用定时器让设备在OTA写入的随机时刻断电重启反复测试几十次确认无论断电在哪个阶段设备都能恢复到可用状态。这种测试在开发阶段做成本最低等到了客户现场才发现问题基本上只能返厂。4.3 Flash加密与再次烧录的连锁反应ESP32支持Flash加密和安全启动开启后固件以密文形式存储在Flash中即使有人把颗粒拆下来用编程器读也拿不到明文代码。但Flash加密开启后有一个让人抓狂的后果你没法直接用常规方式再次烧录固件。开启Flash加密后esptool烧录时需要提供加密后的固件镜像。开发阶段如果只烧了一次eFuse还没永久烧断还允许重新配置但一旦把eFuse的FLASH_CRYPT_CNT配置成永久开启状态以后再想刷不同密钥的固件就非常麻烦。网上搜esp32加密再次烧录能看到一堆求助帖基本都是量产前后没规划好密钥管理导致的。我的实际建议是开发调试阶段不开Flash加密等项目定版、固件稳定后再整体开启加密烧录。并且密钥文件一定要多处备份密钥丢了等于设备变砖没有任何办法恢复。另外开启Flash加密前先确认分区表里有足够的空间存储加密参数不然bootloader启动时可能直接报错。5. 烧录不下、跑不稳定一类报错的完整排查链路烧录报错是玩ESP32绕不开的一道坎。论坛里搜ESP32烧录失败能翻出几百页帖子报错五花八门但根因其实集中在几个层面。这里我把常见的报错按硬件、工具、配置三个维度拆开讲一套完整的排查链路。5.1 常见烧录报错与根因方向速查报错信息典型根因排查优先级Failed to connect to Espressif device: Invalid head of packet未进入下载模式、串口号错误、TXD/RXD接反高A fatal error occurred: Timed out waiting for packet header按住BOOT没到位、EN复位时序不对、USB线供电不足高error: flash download failed - target dll has been cancelled调试器与目标连接中断、目标板反复复位、目标芯片被保护中cannot load flash device description烧录工具没有对应芯片的Flash算法需手动指定芯片型号中No loader specified调试器软件没有为Flash bank配置加载算法常见于J-Link中The current flash utility is out dated烧录工具版本太旧不认识当前芯片低Error during install: net/http: request canceled环境安装时网络超时需要换源或离线包安装低需要说明一下后几条报错不是ESP32专有在Cortex-M等平台上更常见但排查思路完全可以复用。像target dll has been cancelled这种报错本质是调试器在写Flash时和目标建立不了稳定通信原因要么是目标芯片根本没进调试状态要么是供电波动导致复位要么是芯片的读保护被触发——你按这个顺序查基本不会漏。5.2 硬件层面的排查顺序别一上来就怀疑芯片烧录失败后最忌讳的做法是反复拔插USB线瞎试。正确顺序是从底层供电开始逐层检查供电ESP32的峰值电流可以到500mA甚至更高劣质USB线内阻大一进入烧录模式电压就掉到3.3V以下芯片直接进入brownout复位循环。换一根短线或者接外部3.3V电源试试能解决大量莫名其妙的连接失败。串口接线USB转TTL的TXD要接ESP32的RXDRXD要接TXD地线必须共地。很多人只接了两根信号线忘了接地上传固件时偶尔能通、经常掉线。电平匹配ESP32是3.3V逻辑如果你的USB转TTL模块是5V电平接上去可能烧坏引脚或者通信不稳。检查模块上有没有3.3V/5V跳线帽。下载模式ESP32进入下载模式需要GPIO0在复位时为低电平。多数开发板有BOOT按键按住BOOT再按一下EN复位或者在esptool提示连接时手动拉低GPIO0。串口芯片驱动CH340和CP2102是两个最常见的USB转串口芯片Windows下驱动没装好设备管理器里根本看不到串口号更谈不上烧录。5.3 软件与配置层面的坑硬件没问题但烧录还是失败就要看软件配置了。Arduino IDE里常见的配置项包括Flash Size、Flash ModeQIO/DIO/QOUT/DOUT、Partition Scheme、Upload Speed。这几个参数必须和板子实际硬件匹配选错会出现烧录成功但启动不了的诡异现象。比如你用的是只支持DIO模式的模块却在Arduino IDE里选了QIOesptool烧录时可能只报一个警告但固件写入后芯片根本起不来因为QIO模式下CPU要从两个IO口同时读数据颗粒不支持就一直在等待状态。反过来颗粒支持QIO却选了DIO只是浪费带宽功能上没问题。所以调参时要记住QIO能提升读取速度但前提是Flash颗粒和板级线路都支持不确定时就先用DIO保底。还有个非常隐蔽的问题烧录频率过高。部分颗粒标称支持80MHz实际在长走线、劣质PCB上跑到80MHz就会读错数据。如果你发现固件烧录后随机崩溃、WiFi连不上、定时器乱跳试试把Flash Frequency从80MHz降到40MHz很多时候能立竿见影。5.4 一个真实排查案例固件启动不了但烧录明明成功我自己踩过最深的坑是给一块ESP32-S3开发板烧录esptool提示烧录成功、校验通过但板子上电后串口只有bootloader日志应用就是不启动。查了很久最后定位到是分区表偏移问题——我用了16MB Flash的板子但分区表里给Bootloader预留了4MB空间应用分区起始地址算错了固件被写到了芯片不认识的地方。这种问题在IDF Monitor里看日志最明显如果启动日志停在某个地址但没有app loaded的提示大概率就是分区表和应用镜像的地址错位。用esptool.py --port COM3 read_mac能确认芯片MAC用esptool.py --port COM3 image_info可以查看已烧录固件的目标地址对照分区表检查偏移是排查的关键动作。从那以后我每次新建项目都会先把分区表用文本工具打开核对一遍偏移量而不是盲目信任IDE的默认配置。6. 启用PSRAM的正确姿势与常见翻车现场最后一块硬骨头是PSRAM。很多人以为板子带PSRAM颗粒就万事大吉结果烧个例程进去发现内存没变大甚至WiFi连不上重新翻文档才知道还要在软件层面显式开启和配置。6.1 三种开发环境下如何正确打开PSRAMESP-IDF环境在menuconfig中进入Component config → ESP32-specific → Support for external, SPI-connected RAM把选项打开然后根据你的PSRAM颗粒类型选择模式。经典ESP32的WROVER一般是Quad PSRAM选择Quad PSRAMESP32-S3部分模组用的是Octal PSRAM要选Octal PSRAM。选错类型会导致内存分配失败或系统反复重启。Arduino环境在Tools菜单里把PSRAM选项设为Enabled。有部分开发板的Arduino核心封装得比较死还需要在boards.txt里配置额外的menu选项。另外Arduino环境中即使开了PSRAM默认的malloc是不会主动使用PSRAM的你需要显式调用ps_malloc或heap_caps_malloc并指定MALLOC_CAP_SPIRAM否则大数组还是会落到内部RAM。PlatformIO环境在platformio.ini里添加宏定义和构建标志[env:esp32dev] platform espressif32 board esp32dev framework arduino board_build.arduino.memory_type qio_qspi build_flags -DBOARD_HAS_PSRAM这个qio_qspi的含义是Flash用QIO模式、PSRAM用QSPI模式具体要根据板子硬件选。PlatformIO的Arduino框架在多数情况下能自动启用PSRAM并让malloc优先级从PSRAM分配但如果升级框架版本后内存行为变了还是要回到上面的代码层面确认。6.2 验证PSRAM是否生效的代码手段开没开成功用代码验证是最直接的。Arduino环境下void setup() { Serial.begin(115200); Serial.printf(Total heap: %d\n, ESP.getHeapSize()); Serial.printf(Free heap: %d\n, ESP.getFreeHeap()); Serial.printf(Total PSRAM: %d\n, ESP.getPsramSize()); Serial.printf(Free PSRAM: %d\n, ESP.getFreePsram()); void* buf heap_caps_malloc(1024 * 1024, MALLOC_CAP_SPIRAM); if (buf) { Serial.println(PSRAM allocation OK); free(buf); } }如果ESP.getPsramSize()返回0说明PSRAM没被正确初始化如果返回了非0值但heap_caps_malloc分配失败可能是你请求的大小超过了剩余PSRAM空间也可能是IDF配置里限制了大块分配。这里有一个隐藏规则外部RAM在初始分配时不是所有地址都能直接被CPU访问IDF会建立一个MMU映射表分配大块连续内存通常没问题但如果分配和释放频繁交错可能会遇到地址窗口耗尽导致分配失败。6.3 开了PSRAM后WiFi不稳大概率是共存配置没开PSRAM最经典的翻车场景是打开PSRAM后WiFi连接变得极不稳定ping丢包严重甚至连接不上路由器。这个问题在经典ESP32上尤为突出。原因是PSRAM和WiFi/蓝牙共用部分系统资源PSRAM的时序访问会干扰RF的实时性要求。ESP-IDF的解决办法是打开一个专门选项Component config → ESP32-specific → Support for external, SPI-connected RAM → Try to allocate memories of WiFi and LWIP in SPIRAM同时把SPIRAM_USE设为SPIRAM_USE_CAPS_ALLOC或SPIRAM_USE_MALLOC。打开后WiFi和LWIP的内存会尽量放到PSRAM里把宝贵的内部分配给射频栈稳定性会明显改善。Arduino环境一般会默认处理这两项但如果你用的是IDF框架这个配置是必须检查的。除了WiFi还有几个我实测过的坑brownout复位PSRAM大容量读写时电流比想象中大如果供电本来就紧张一分配几百KB PSRAM就可能触发电压跌落复位。解决方法是加大电源电容或换电源而不是调低brownout阈值了事。实时性任务别用PSRAMPSRAM的访问延迟明显高于内部SRAM对时序要求严格的任务比如音频I2S回调、高精度定时器中断不要在里面放任务栈否则会出现抖动。大量小对象用PSRAM会碎片化PSRAM空间虽然大但频繁malloc和free小对象照样会碎片化。建议大块缓冲一次性分配长时间持有用内存池管理。我在一个屏幕UI项目里的实际做法是把LVGL的绘制缓冲、图片解码缓冲和日志环形队列全部放PSRAM任务栈和中断相关数据留在内部RAM。这样同时满足了内存容量和实时性两头的需求跑起来非常稳定。6.4 一个常见误区PSRAM不是越大越好市面上有8MB PSRAM的模组看起来很有吸引力但P价差也是实打实的。实际项目里跑LVGL、摄像头、小型AI模型4MB PSRAM完全够用8MB PSRAM适合需要同时处理多路图像或大模型的场景。选型时不要被容量大更好牵着走先估算项目实际需要的RAM量再削减一半冗余最后决定买多大PSRAM。毕竟PSRAM颗粒是焊在模块上的买到手就没法再换还不如把预算花在Flash容量上——Flash不够用反而更难受。最后分享一个个人经验如果你计划长期做ESP32开发手边至少要备一块带PSRAM的ESP32-S3开发板。它既是当前性价比较高的选择又能通过Octal PSRAM跑出更大的内存带宽很多设计方案从经典ESP32迁移过去性能反而有提升。拿到板子第一件事用flash_id确认Flash颗粒、用测试代码确认PSRAM容量把这两个值记在本子上后面所有项目规划都以它们为准。这两项确认完ESP32的存储版图就在你脑子里了烧录、分区、OTA这些坑都会好走很多。