# Flash 与内存容量分析、风险及后续开发建议 > 历史容量快照:文中远程日志、`logfs` 和独立 `gifpack` 分区已从当前固件移除。下文的“当前”“建议”和旧构建步骤仅针对当时版本,不能作为现行操作指南。现行分区以 `16m.csv` 与最新构建报告为准,资源操作见 [应用内嵌素材使用与添加指南](../应用内嵌素材使用与添加指南.md)。 本文记录当前固件的 Flash 分区使用情况、静态 RAM 使用情况、运行时内存风险、可优化内容和后续开发约束,用于功能评审、资源接入、OTA 设计和长期容量管理。 历史 LittleFS 资源流程归档见: - [`docs/littlefs-resource-build-flash-mount.md`](littlefs-resource-build-flash-mount.md) ## 1. 分析基线与数据边界 本次分析基于: ```text 分支:feature/st7789p3-new-board 提交:d5409ea 目标:ESP32-S3 ESP-IDF:v5.5.4 Flash 配置:16 MiB 分区表:16m.csv 应用镜像:build/TQ_Cat_Code.bin 链接映射:build/TQ_Cat_Code.map 资源镜像:build/gifpack.bin ``` 当前 `build/` 目录中的应用产物生成于 2026-08-03,资源镜像生成于 2026-08-04。后续代码或资源变化后,应重新构建并更新本文数据。 需要明确区分两类数据: ### 1.1 本地构建可以准确得到的数据 - Flash 分区的地址和容量; - 应用程序二进制大小; - `gifpack` 镜像大小; - LittleFS 镜像内的文件数量、文件内容大小和已用块数; - 静态 IRAM、D/IRAM、`.data`、`.bss` 使用量; - 链接阶段放入 PSRAM 的静态 BSS 大小。 ### 1.2 必须连接设备运行后才能得到的数据 - 芯片实际检测到的 PSRAM 总容量; - 启动完成后的内部堆剩余量; - PSRAM 剩余量; - 最小历史空闲堆; - 最大连续内部内存块; - 最大连续 DMA 内存块; - 每个任务的真实栈高水位; - Wi-Fi、TLS、WebSocket、音频同时工作时的峰值占用; - `logfs`、`model`、`coredump` 在具体设备上的真实内容和占用。 因此,本文的静态分析可以判断容量趋势和结构性风险,但不能替代真机峰值内存测试。 ## 2. Flash 总体布局 设备使用 16 MiB Flash: ```text 16 MiB = 16,777,216 bytes = 0x1000000 ``` `16m.csv` 已经把全部 16 MiB 空间分配完毕,没有未划分空间。 | 区域 | 地址范围 | 分配大小 | 占 Flash 比例 | 用途 | | --- | ---: | ---: | ---: | --- | | Bootloader、分区表、NVS、OTA data、PHY | `0x000000-0x010000` | 64 KiB | 0.39% | 系统启动和持久化基础数据 | | `gifpack` | `0x010000-0xA90000` | 10.5 MiB | 65.63% | LCD 图片和提示音 LittleFS | | `logfs` | `0xA90000-0xAD0000` | 256 KiB | 1.56% | 远程日志落盘队列 | | `ota_0` | `0xAD0000-0xCA0000` | 1.8125 MiB | 11.33% | 应用 OTA 槽位 0 | | `ota_1` | `0xCA0000-0xE70000` | 1.8125 MiB | 11.33% | 应用 OTA 槽位 1 | | `model` | `0xE70000-0xFF0000` | 1.5 MiB | 9.38% | 模型数据分区 | | `coredump` | `0xFF0000-0x1000000` | 64 KiB | 0.39% | 崩溃转储 | Flash 布局可以简化为: ```text 0x000000 ├── 系统区域 64 KiB ├── gifpack 10.5 MiB ├── logfs 256 KiB ├── ota_0 1.8125 MiB ├── ota_1 1.8125 MiB ├── model 1.5 MiB └── coredump 64 KiB 0x1000000 ``` ## 3. 应用程序 Flash 使用情况 单个 OTA 应用分区大小: ```text 0x1d0000 = 1,900,544 bytes = 1.8125 MiB ``` 当前应用二进制: ```text build/TQ_Cat_Code.bin 实际文件大小:1,657,280 bytes ``` 链接工具报告: ```text Flash .text: 1,134,436 bytes Flash .rodata: 388,744 bytes 其他: 256 bytes Flash 非 RAM 合计:1,523,436 bytes 总镜像大小: 1,657,135 bytes 最终 .bin: 1,657,280 bytes ``` 单个 OTA 槽位占用: ```text 使用率:87.20% 剩余:243,264 bytes ≈ 237.56 KiB ``` 两个 OTA 槽位必须具有相同容量。`ota_1` 即使当前没有运行,也不能简单视为空闲空间,因为 OTA 升级需要把新固件完整写入另一个槽位,并保留切换和回滚能力。 ### 3.1 应用分区风险判断 风险等级:**高**。 原因: - 当前只剩约 238 KiB; - 新增网络库、编解码库、证书、字体或大型只读表可能快速消耗空间; - Debug 配置、日志字符串和完整证书包都会增加镜像; - OTA 要求每个新版本都必须装入同样大小的备用槽; - 链接成功不代表长期有充足增长空间。 建议设置容量门槛: | 使用率 | 建议处理 | | ---: | --- | | `< 80%` | 正常 | | `80%-85%` | 开始跟踪增长趋势 | | `85%-90%` | 构建警告;每个新增依赖必须解释体积影响 | | `>= 90%` | 阻止普通功能合入,先做容量治理 | 当前 87.20%,已经处于“必须持续跟踪”的区间。 ## 4. `gifpack` 资源分区使用情况 `gifpack` 分区容量: ```text 0xa80000 = 11,010,048 bytes = 10.5 MiB ``` 当前临时资源目录中: | 资源类型 | 文件数 | 文件内容大小 | | --- | ---: | ---: | | LCD RLE | 41 | 9,379,380 bytes | | Opus 提示音 | 30 | 378,918 bytes | | 合计 | 71 | 9,758,298 bytes | 读取 `gifpack.bin` 的 LittleFS 块信息后: ```text 块大小:4,096 bytes 总块数:2,688 已用块:2,426 空闲块:262 LittleFS 已用:9,936,896 bytes ≈ 9.48 MiB LittleFS 空闲:1,073,152 bytes ≈ 1.02 MiB 使用率:90.25% ``` 资源文件内容为约 9.31 MiB,LittleFS 已用空间约 9.48 MiB,两者差值来自目录、索引、元数据、块对齐和文件系统管理开销。 `build/gifpack.bin` 的文件大小始终等于整个分区大小,即 10.5 MiB。不能通过 `ls -lh build/gifpack.bin` 判断资源真实占用,因为镜像会填充到分区全尺寸。 ### 4.1 资源分区风险判断 风险等级:**高**。 原因: - 当前使用率已经超过 90%; - 理论空闲约 1.02 MiB,实际新增文件还需要 LittleFS 管理开销; - 动画通常一次增加多帧,每帧可能为 200-380 KiB; - 新增 3-5 张全屏 RLE 图片就可能耗尽剩余空间; - 资源分区没有 A/B 设计,未来直接在线更新存在断电损坏风险; - 普通应用 OTA 不会更新 `gifpack`,容易出现应用代码与资源版本不匹配。 建议容量门槛: | 使用率 | 建议处理 | | ---: | --- | | `< 80%` | 正常 | | `80%-90%` | 新增资源必须记录大小 | | `90%-95%` | 高风险;新增动画前必须优化或清理 | | `>= 95%` | 阻止资源合入 | 当前 90.25%,已经进入高风险区间。 ## 5. 当前完整烧录包含什么 当前 `build/flasher_args.json` 登记: ```text 0x000000 bootloader/bootloader.bin 0x008000 partition_table/partition-table.bin 0x00D000 ota_data_initial.bin 0x010000 gifpack.bin 0xAD0000 TQ_Cat_Code.bin ``` 常规完整烧录会写入: - Bootloader; - 分区表; - OTA 初始状态; - `gifpack` 资源镜像; - `ota_0` 应用程序。 以下分区没有预生成镜像加入当前完整烧录清单: - `logfs`:设备运行时挂载、格式化和写入; - `ota_1`:应用 OTA 时写入; - `model`:需要独立的模型写入流程; - `coredump`:设备崩溃时写入。 因此,本地构建无法判断具体设备上这几个动态分区的真实使用量。 ## 6. 静态 RAM 使用情况 链接映射报告: | RAM 区域 | 已使用 | 总容量 | 剩余 | 使用率 | | --- | ---: | ---: | ---: | ---: | | 独立 IRAM | 16,384 B | 16,384 B | 0 B | 100% | | D/IRAM | 173,387 B | 341,760 B | 168,373 B | 50.73% | | PSRAM 静态 BSS | 14,056 B | 取决于硬件 | 未知 | 未知 | D/IRAM 组成: ```text .data: 23,944 bytes .bss: 56,072 bytes .text: 93,371 bytes 合计: 173,387 bytes 剩余: 168,373 bytes ≈ 164.43 KiB ``` 其中: - `.data`:有初始值的全局和静态变量; - `.bss`:零初始化的全局和静态变量; - `.text`:必须驻留 RAM 执行的函数; - 剩余 D/IRAM 会参与形成运行时可用内部内存,但启动后还会被任务栈、DMA、驱动和堆分配继续使用。 ### 6.1 IRAM 风险 独立 IRAM 当前为: ```text .text: 15,356 bytes .vectors: 1,028 bytes 合计: 16,384 bytes 剩余: 0 bytes ``` 风险等级:**高**。 这不表示设备运行时马上耗尽普通堆,但意味着独立 IRAM 链接区域没有静态增长余量。后续新增以下内容必须谨慎: - `IRAM_ATTR` 函数; - ISR 中必须执行的代码; - Flash cache 关闭期间仍需运行的函数; - 驱动要求放入 IRAM 的回调; - 性能优化时强制进入 IRAM 的热点代码。 不要为了普通性能优化随意添加 `IRAM_ATTR`。添加后必须重新运行 size 分析并检查链接结果。 ### 6.2 D/IRAM 风险 静态使用率约 50.7%,从链接角度尚可,但运行时内部 RAM 压力预计较高,因为以下内容不能完全依赖 PSRAM: - Wi-Fi 和部分 lwIP 缓冲; - SPI/I2S DMA 描述符和 DMA 缓冲; - TLS 硬件 AES bounce buffer; - 中断相关数据; - 部分 FreeRTOS 和驱动对象; - 显式创建在内部 RAM 的任务栈; - 小于内部优先阈值的普通分配。 风险等级:**中高**,需要真机峰值数据确认。 ## 7. PSRAM 配置与使用策略 当前配置启用: ```text Octal PSRAM 80 MHz 启动初始化 加入 malloc 堆 允许 BSS 放入外部内存 允许任务栈放入外部内存 Wi-Fi/lwIP 尝试使用 PSRAM ``` 关键参数: ```text CONFIG_SPIRAM_USE_MALLOC=y CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=4096 CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=32768 ``` 设计意图: - 大型普通内存申请可以进入 PSRAM; - 较小申请倾向内部 RAM; - 为 DMA 和硬件驱动保留 32 KiB 内部 RAM; - 音频、WebSocket、HTTP 等大栈和队列优先放入 PSRAM。 链接阶段已有约 13.7 KiB BSS 放入 PSRAM。 ### 7.1 当前未知项 配置使用自动 PSRAM 类型检测,没有在项目中写死物理容量。必须查看真机启动日志或调用 `esp_psram_get_size()` 才能确认实际是 2、4 还是 8 MiB。 风险等级:**中**。 后续量产前必须把实际模组型号、PSRAM 容量和启动检测结果纳入硬件验收,避免开发板和量产板容量不同。 ## 8. 运行时主要内存消费者 ### 8.1 项目任务栈 当前较大的显式任务栈: | 任务 | 配置栈 | 默认内存策略 | | --- | ---: | --- | | 音频采集 | 32 KiB | PSRAM 优先,失败回退内部 RAM | | 音频发送 | 12 KiB | PSRAM 优先,失败回退内部 RAM | | 音频播放 | 12 KiB | PSRAM 优先,失败回退内部 RAM | | WebSocket ordered worker | 12 KiB | PSRAM 优先 | | WebSocket client | 8-32 KiB 候选 | 第三方组件内部创建,主要消耗内部 RAM | | LCD 渲染 | 12 KiB | 内部 RAM | | Wi-Fi 状态 | 12 KiB | 内部 RAM | | LAN HTTP | 8 KiB | PSRAM 优先 | | 远程日志 | 8 KiB | 内部 RAM | | 每日运势 | 6 KiB | 内部 RAM | | 多模态 watchdog | 6 KiB | 内部 RAM | | 系统事件 | 4 KiB | 内部 RAM | | 按键 | 4 KiB | 内部 RAM | | 模型命令 | 4 KiB | PSRAM 优先 | 仅项目显式任务的配置栈总量已经超过 130 KiB。除此之外,ESP-IDF 还会创建: - main task; - 两个 IDLE task; - timer task; - system event task; - Wi-Fi task; - TCP/IP task; - HTTP/WebSocket 内部任务; - 驱动相关任务。 不能把所有配置栈简单相加后与静态剩余 RAM 比较,因为: - 一部分任务不同时存在; - 一部分栈优先放入 PSRAM; - 任务控制块和队列还有额外开销; - 第三方组件可能自行创建内部任务。 但这些数据说明,任务栈是运行时内存治理的重点。 ### 8.2 音频队列 音频上行发送队列: ```text 长度:24 单项数据容量:约 1 KiB 队列数据:约 24 KiB 分配策略:PSRAM 优先 ``` 音频下行播放队列: ```text 长度:1,024 队列项保存长度和数据指针 队列描述符约 16 KiB 音频正文另外动态分配 分配策略:PSRAM 优先 ``` 下行正文占用取决于服务器下发速度、网络抖动和播放消费速度。极端积压时,真实内存占用会明显高于队列描述符本身。 ### 8.3 WebSocket 队列 ```text 消息队列上限:512 单个描述符约 20 bytes 描述符队列约 10 KiB 消息正文动态分配,单条最大值可达数 KiB ``` 描述符队列优先放入 PSRAM,但正文积压量仍可能成为峰值风险。 ### 8.4 远程日志静态队列 远程日志启用: ```text CONFIG_TQ_REMOTE_LOG_ENABLE=y 队列长度:128 单条文本容量:256 bytes ``` `remote_log_entry_t` 每项还包含时间戳、日志等级和状态字段,整个静态队列约占 33 KiB 内部 BSS。这是当前 `control_plane` 静态 BSS 约 37.8 KiB 的主要来源。 风险等级:**中高**。 这块内存在启用远程日志后始终常驻,即使没有积压日志也不会释放。 ### 8.5 LCD 缓冲 LCD 采用分行读取,不加载整张 320×240 图片: ```text 行缓冲:320 × 8 × 2 bytes = 5,120 bytes RLE chunk:128 × 4 bytes = 512 bytes ``` 这部分设计较合理。若整帧 RGB565 全部加载,将需要: ```text 320 × 240 × 2 = 153,600 bytes ``` 因此,后续不要轻易把当前流式绘制改成整帧内部 RAM 缓冲。确实需要双缓冲时,应评估 PSRAM、DMA 约束和帧率收益。 ## 9. 运行时内存风险 ### 9.1 内部连续内存不足 风险等级:**高**。 系统可能出现以下情况: ```text 总 free_heap 看起来很多 但 largest_internal 或 largest_dma 很小 导致 TLS、AES、SPI、I2S 或 Wi-Fi 分配失败 ``` 重点场景: - Wi-Fi 刚连接时进行 HTTPS Token 请求; - TLS 握手和 WebSocket 建连重叠; - 音频采集、编码、播放同时工作; - LCD 正在 SPI DMA 传输; - 远程日志同时上传 HTTPS; - LAN HTTP 服务同时处理请求。 判断内存是否健康,不能只看 `esp_get_free_heap_size()`,还必须观察: ```text free_internal largest_internal largest_dma free_spiram largest_spiram minimum_free_heap ``` ### 9.2 内存碎片 风险等级:**中高**。 TLS、JSON、WebSocket 消息、音频包和 HTTP body 都会产生不同大小、不同生命周期的动态分配。长时间连接、重连、失败重试后,可能出现总空闲量尚可但最大连续块下降的情况。 判断方法: - 多轮重连后比较 `free_internal`; - 比较 `largest_internal` 是否持续下降; - 比较 `largest_dma` 是否持续下降; - 比较连接前、握手中、交互中、断开后的数值能否恢复; - 重复语音交互 100 次以上观察最低值和趋势。 ### 9.3 PSRAM 回退导致内部 RAM 突增 很多任务和队列采用“PSRAM 优先,失败回退内部 RAM”。如果 PSRAM 初始化失败、容量不足或碎片严重,多个对象可能同时回退到内部 RAM,使内部堆瞬间恶化。 必须关注日志: ```text PSRAM allocation failed fallback to internal Task create with PSRAM stack failed ``` 量产版本不应把这些日志当成无害告警。 ### 9.4 任务栈过大或过小 风险等级:**中**。 - 栈过大:浪费内部 RAM 或 PSRAM; - 栈过小:发生栈溢出、随机崩溃; - 只在正常路径测量不足以覆盖 TLS 错误、JSON 大包、日志上传等深调用路径。 当前 `CONFIG_TQ_TASK_MONITOR_ENABLE` 关闭,缺少持续任务栈可观测性。建议在压力测试固件中启用,不一定默认用于量产版本。 ## 10. 跨 Flash 与 RAM 的组合风险 ### 10.1 资源与应用版本不一致 普通应用 OTA 只更新 `ota_0/ota_1`,不更新 `gifpack`。如果新代码引用新资源路径,而设备仍保留旧 `gifpack`,会出现: ```text open LCD asset failed Open prompt failed ``` 建议: - 应用中声明资源包版本; - `gifpack` 中保存资源 manifest; - 启动时比较应用要求版本和资源版本; - 不匹配时进入明确降级路径,不要反复尝试打开不存在的文件。 ### 10.2 单份资源分区在线更新风险 当前只有一个 `gifpack` 分区。若未来在线覆盖更新: - 更新中断电可能损坏整个资源文件系统; - 新镜像校验失败时没有旧资源可回退; - 应用和资源切换无法原子完成。 在没有完整方案前,不建议直接实现“边运行边覆盖单个 gifpack”。可选方向: 1. 资源仅随工厂完整烧录更新; 2. 下载到备用区域,校验后再切换; 3. 设计 `gifpack_0/gifpack_1` 双资源分区; 4. 使用版本化资源容器和原子状态记录; 5. 将少量新增资源并入应用,但需评估 OTA 槽容量。 ### 10.3 LittleFS 挂载依赖开机音流程 当前 `/littlefs` 主要由播放开机音前的 `voice_prompt_play_file()` 触发挂载。LCD 随后假定 `/littlefs` 已经可用。 如果开机音被关闭或音频初始化提前失败,LCD 可能在文件系统未挂载时读取资源。 建议把 `platform_mount_littlefs()` 提升为明确的启动阶段,由 `app_composition` 在音频和 LCD 之前编排,并保持调用幂等。 风险等级:**中高**。 ## 11. 优化建议与优先级 ### P0:立即建立容量门禁 在 CI 或构建检查中记录并比较: ```text 应用 .bin 大小 应用分区使用率 gifpack LittleFS 已用块数 gifpack 使用率 IRAM 使用量 D/IRAM 静态使用量 ``` 建议规则: - 应用分区达到 85% 发出警告; - 应用分区达到 90% 阻止合入; - `gifpack` 达到 90% 发出高风险警告; - `gifpack` 达到 95% 阻止合入; - IRAM 增长必须在 PR 中说明; - 新增库必须附带 size diff; - 新增动画必须附带新增资源字节数和预计剩余空间。 ### P0:采集真机峰值内存 至少在以下时间点采集: 1. Bootloader 后、应用初始化前; 2. NVS 和 LittleFS 挂载后; 3. Wi-Fi 初始化后; 4. Wi-Fi 已连接; 5. TLS Token 请求前和握手中; 6. WebSocket 已连接; 7. 音频采集和播放同时进行; 8. LCD 动画播放中; 9. 远程日志上传中; 10. 所有业务进入稳态后; 11. 多次断线重连后; 12. 长时间运行 24 小时后。 每次至少记录: ```text esp_get_free_heap_size() esp_get_minimum_free_heap_size() heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT) heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT) heap_caps_get_largest_free_block(MALLOC_CAP_DMA) heap_caps_get_free_size(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT) heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT) ``` 不要一开始就设定绝对通过阈值。先收集多台设备、多种网络条件的基线,再根据 TLS、DMA 和音频最大单次申请量设定安全阈值。 ### P1:优化远程日志静态队列 当前约 33 KiB 内部 BSS 常驻,可考虑: 1. 将队列动态分配到 PSRAM; 2. PSRAM 失败时回退到更短内部队列,而不是同等大小回退; 3. 将 `CONFIG_TQ_REMOTE_LOG_QUEUE_LEN` 从 128 调低; 4. 将单条文本上限从 256 调低; 5. 使用变长块池或分层队列; 6. 评估是否需要在所有构建类型中默认启用远程日志。 修改时要保持日志钩子并发安全,不能为了省内存破坏日志回调的锁和生命周期。 ### P1:基于高水位调整任务栈 压力测试时开启任务监控,记录所有任务最小剩余栈。 调优原则: - 不以一次正常启动数据为依据; - 覆盖错误路径、重连、超时和大 JSON; - 保留安全余量; - 优先优化长期存活且栈明显过大的任务; - 对 ISR、DMA、Flash cache 关闭相关任务,不要盲目迁移到 PSRAM; - 修改后执行长时间和异常路径测试。 ### P1:治理资源体积 当前 LCD 资源占 `gifpack` 的绝大部分。建议: - 清理白名单中已不再播放的资源; - 检查是否存在内容重复的帧; - 对相邻帧考虑差分、局部刷新或共享背景; - 检查 RLE 色彩和扫描方向是否影响压缩率; - 避免把相同首帧复制到多个动画组; - 新增资源时记录原图大小、RLE 大小、run 数和压缩率; - 建立资源 manifest,自动检查 CMake 白名单和代码映射一致性。 当前 `tools/png_to_rle.py` 生成标准 TGA RLE,而 `lcd_direct` 读取原始 `uint16_t color + uint16_t count` 数据,两者不兼容。应统一并验证资源转换工具,避免开发者误用。 ### P1:确认 `model` 分区是否仍需要 1.5 MiB 当前 `model` 分区没有进入常规烧录清单。需要确认: - 是否由工厂工具单独烧录; - 当前功能是否读取该分区; - 量产设备中是否有有效模型数据; - 1.5 MiB 是否为未来预留; - 能否缩减并重新分配给应用或资源。 如果确认未使用,可以评估重新分区。但调整前必须同时考虑: - 两个 OTA 分区需要同步增大; - 已出货设备变更分区表的迁移风险; - OTA 是否能够安全改变分区布局; - 模型恢复和工厂烧录流程; - Flash 擦除和回滚策略。 不要只修改 CSV 就认为升级安全。 ### P2:优化队列和积压策略 音频和 WebSocket 队列容量较大。建议根据真实峰值观测: - 最大队列深度; - 平均排队时间; - 丢包数量; - 消费速率; - 网络抖动时的峰值; - 语音播放延迟。 如果队列长期只使用很小比例,可以缩短;如果积压很深,应优先解决背压和消费速率,不要单纯继续增大队列。 ### P2:增加资源版本和完整性校验 在 `gifpack` 中增加小型 manifest,例如: ```json { "resource_version": "1.3.0", "format_version": 1, "target": "esp32s3-st7789p3", "files": { "lcd/jichubiaoqing0.rle": { "size": 21644, "run_count": 5411, "sha256": "..." } } } ``` 启动时不必校验所有大文件,但至少应检查: - manifest 可读取; - 资源版本兼容; - 关键启动资源存在; - 文件格式版本正确; - 必要时抽样或按需校验哈希。 ## 12. 后续开发准入要求 ### 12.1 新增普通代码或第三方库 PR 中应记录: - 应用 `.bin` 增量; - Flash `.text` 和 `.rodata` 增量; - IRAM 增量; - D/IRAM `.data/.bss/.text` 增量; - 是否新增长期任务; - 任务栈配置和内存区域; - 是否新增大队列或大缓冲; - 是否在 TLS、音频、LCD 并发路径中运行。 ### 12.2 新增 LCD 或音频资源 PR 中应记录: - 新增文件数; - 新增资源总字节数; - 新 `gifpack` 使用率; - 预计剩余空间; - LCD `run_count` 是否更新; - CMake 白名单是否更新; - 平台资源映射是否更新; - domain 播放顺序是否更新; - 完整 `idf.py flash` 硬件验证结果。 ### 12.3 新增长期任务 必须定义: - 明确 owner 层; - 创建、启动、停止和销毁流程; - 栈大小依据; - 内部 RAM 或 PSRAM 选择依据; - PSRAM 失败回退行为; - 有界停止时间; - 栈高水位观测方式; - 队列和动态内存上限。 ### 12.4 新增大型动态缓冲 必须回答: - 最大申请尺寸是多少; - 是否要求 DMA 能力; - 是否必须在内部 RAM; - 生命周期多长; - 是否会与 TLS、音频、LCD 同时存在; - PSRAM 失败时如何降级; - 是否存在无上限增长; - 分配失败是否能安全恢复。 ### 12.5 修改分区表 必须进行完整评审: - 总大小是否仍为 16 MiB; - 两个 OTA 槽是否等大; - 当前和历史固件是否兼容; - 已部署设备如何迁移; - 工厂烧录脚本是否更新; - OTA、资源、模型和 coredump 流程是否更新; - 地址变化是否影响任何硬编码工具; - 断电和回滚是否安全。 ## 13. 推荐测试矩阵 | 测试场景 | 重点观察 | | --- | --- | | 冷启动 | PSRAM 检测、LittleFS 挂载、初始内部堆 | | 无网络启动 | 重试期间是否泄漏、LCD 是否仍能读资源 | | Wi-Fi 弱网 | 队列积压、重连内存恢复 | | HTTPS Token | `largest_dma`、`largest_internal` | | WebSocket 重连 100 次 | 内存碎片、任务和队列是否释放 | | 连续语音交互 | 音频队列峰值、PSRAM 趋势 | | 上下行音频同时工作 | 内部 DMA 和 PSRAM 峰值 | | LCD 动画和语音同时工作 | SPI/I2S DMA 竞争 | | 远程日志上传 | TLS 与日志 body 峰值 | | LAN HTTP 并发访问 | HTTP task 和响应缓冲 | | 运行 24/72 小时 | 最小堆、碎片、任务泄漏 | | 人为断电资源更新 | 资源完整性和恢复策略 | 每轮测试都应保存: - 固件提交; - `sdkconfig`; - 应用和资源镜像大小; - 完整串口日志; - 各阶段堆快照; - 任务栈高水位; - 崩溃转储; - 是否出现 PSRAM 回退。 ## 14. 建议的持续观测指标 当前代码已经在认证、WebSocket、音频和远程日志路径中记录部分堆信息。建议统一成一个结构化内存快照接口,由 platform 层负责采集,control_plane 决定在何时上报。 推荐字段: ```text timestamp phase free_heap minimum_free_heap free_internal largest_internal largest_dma free_spiram largest_spiram task_count active_audio_state websocket_state lcd_state ``` 避免多个模块重复实现不同格式的 heap 日志。长期应形成单一内存观测入口,并支持: - 启动阶段快照; - 关键操作前后快照; - 低内存阈值告警; - 崩溃前最近快照保留; - Debug 构建中的任务栈报表。 ## 15. 当前风险汇总 | 风险 | 等级 | 当前证据 | 建议 | | --- | --- | --- | --- | | `gifpack` 空间不足 | 高 | 已用 90.25%,仅余约 1.02 MiB | 资源门禁、清理和压缩 | | OTA 应用槽空间不足 | 高 | 已用 87.20%,仅余约 238 KiB | 代码 size 门禁、评估重新分区 | | 独立 IRAM 无余量 | 高 | 16 KiB 已用 100% | 限制新增 `IRAM_ATTR` | | 内部连续/DMA 内存不足 | 高 | Wi-Fi、TLS、音频、LCD 并发竞争 | 真机峰值监控和压力测试 | | 应用和资源版本不一致 | 高 | 普通 OTA 不更新 `gifpack` | 资源 manifest 和兼容检查 | | 单资源分区更新断电风险 | 高 | `gifpack` 无 A/B | 不直接在线覆盖;设计安全更新机制 | | 远程日志常驻内部 BSS 较大 | 中高 | 静态队列约 33 KiB | PSRAM 化或缩小队列 | | LittleFS 挂载依赖开机音 | 中高 | 挂载由提示音路径触发 | 独立启动阶段显式挂载 | | 动态内存真实峰值未知 | 中高 | 尚无本次真机数据 | 建立基线和长稳测试 | | PSRAM 实际容量未固化 | 中 | 自动检测,项目未声明容量 | 量产硬件验收和启动校验 | | 任务栈缺少持续观测 | 中 | task monitor 默认关闭 | 压测构建启用并调优 | | RLE 转换工具格式不一致 | 中 | TGA RLE 与 raw RLE 不兼容 | 统一转换和自动校验工具 | ## 16. 推荐执行顺序 ### 第一阶段:立即完成 1. 将应用和 `gifpack` 大小检查加入构建或 CI; 2. 在真机完成一次全流程内存快照; 3. 确认实际 PSRAM 容量; 4. 统计所有任务栈高水位; 5. 清点 `gifpack` 中是否存在未使用资源; 6. 确认 `model` 分区真实用途; 7. 将 LittleFS 挂载改为明确启动阶段。 ### 第二阶段:容量治理 1. 优化或迁移远程日志静态队列; 2. 根据高水位缩减过大的任务栈; 3. 根据实测调整音频/WebSocket 队列; 4. 统一 RLE 转换工具和格式检查; 5. 增加资源 manifest; 6. 建立应用和资源版本兼容规则。 ### 第三阶段:量产和长期升级 1. 决定资源是否需要 OTA; 2. 如需要,设计资源 A/B 或安全下载切换机制; 3. 评审并冻结量产分区表; 4. 建立 24/72 小时长稳测试; 5. 建立多轮 Wi-Fi/WebSocket 重连压力测试; 6. 保存每个发布版本的 Flash/RAM 基线报告。 ## 17. 复现本分析的方法 ### 17.1 应用镜像 完整构建后检查: ```text build/TQ_Cat_Code.bin build/TQ_Cat_Code.map ``` 使用当前 ESP-IDF v5.5.4 环境运行 size 工具: ```bash idf.py size idf.py size-components ``` ### 17.2 Flash 分区 检查: ```text 16m.csv build/partition_table/partition-table.bin build/flasher_args.json ``` 不要假定空 Offset 分区的地址永久不变,应以当前构建生成结果为准。 ### 17.3 LittleFS 资源 检查临时目录文件总量: ```text build/esp-idf/platform/asset_image/ ``` 使用构建环境中的 `littlefs-python` 查看: ```bash build/littlefs_py_venv/bin/littlefs-python list \ build/gifpack.bin \ --block-size 4096 \ --name-max 64 ``` 真实空闲块应通过 LittleFS API或设备挂载后的 `esp_littlefs_info()` 获取,不能只依据资源文件内容之和。 ### 17.4 真机运行时内存 关注串口日志中的: ```text Heap before_ws_client_start free_internal largest_internal largest_dma free_spiram largest_spiram stack headroom PSRAM allocation failed fallback to internal ``` ## 18. 当前结论 目前 16 MiB Flash 的规划已经非常紧凑:`gifpack` 占整个 Flash 的 65.63%,自身使用率达到 90.25%;两个 OTA 槽位合计占 22.66%,当前应用已占单槽 87.20%。Flash 没有未划分空间,任何扩容都必须以压缩或缩减其他分区为代价。 静态 RAM 方面,D/IRAM 使用率约 50.7%,表面仍有空间,但独立 IRAM 已经 100% 使用。真正的运行风险集中在内部连续内存和 DMA 内存:Wi-Fi、TLS、音频、LCD 和远程日志同时工作时,即使 PSRAM 仍有余量,也可能因为内部连续块不足而失败。 后续开发应把容量视为正式约束,而不是等链接或运行失败后再处理。最优先的工作是建立容量门禁、收集真机峰值内存、优化远程日志静态队列、清理资源、确认 `model` 分区用途,并为应用与资源版本建立明确兼容机制。