29 KiB
Flash 与内存容量分析、风险及后续开发建议
本文记录当前固件的 Flash 分区使用情况、静态 RAM 使用情况、运行时内存风险、可优化内容和后续开发约束,用于功能评审、资源接入、OTA 设计和长期容量管理。
LittleFS 资源从源码到设备的详细流程见:
docs/littlefs-resource-build-flash-mount.md
1. 分析基线与数据边界
本次分析基于:
分支: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:
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 布局可以简化为:
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 应用分区大小:
0x1d0000 = 1,900,544 bytes = 1.8125 MiB
当前应用二进制:
build/TQ_Cat_Code.bin
实际文件大小:1,657,280 bytes
链接工具报告:
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 槽位占用:
使用率: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 分区容量:
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 块信息后:
块大小: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 登记:
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 组成:
.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: 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 配置与使用策略
当前配置启用:
Octal PSRAM
80 MHz
启动初始化
加入 malloc 堆
允许 BSS 放入外部内存
允许任务栈放入外部内存
Wi-Fi/lwIP 尝试使用 PSRAM
关键参数:
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 音频队列
音频上行发送队列:
长度:24
单项数据容量:约 1 KiB
队列数据:约 24 KiB
分配策略:PSRAM 优先
音频下行播放队列:
长度:1,024
队列项保存长度和数据指针
队列描述符约 16 KiB
音频正文另外动态分配
分配策略:PSRAM 优先
下行正文占用取决于服务器下发速度、网络抖动和播放消费速度。极端积压时,真实内存占用会明显高于队列描述符本身。
8.3 WebSocket 队列
消息队列上限:512
单个描述符约 20 bytes
描述符队列约 10 KiB
消息正文动态分配,单条最大值可达数 KiB
描述符队列优先放入 PSRAM,但正文积压量仍可能成为峰值风险。
8.4 远程日志静态队列
远程日志启用:
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 图片:
行缓冲:320 × 8 × 2 bytes = 5,120 bytes
RLE chunk:128 × 4 bytes = 512 bytes
这部分设计较合理。若整帧 RGB565 全部加载,将需要:
320 × 240 × 2 = 153,600 bytes
因此,后续不要轻易把当前流式绘制改成整帧内部 RAM 缓冲。确实需要双缓冲时,应评估 PSRAM、DMA 约束和帧率收益。
9. 运行时内存风险
9.1 内部连续内存不足
风险等级:高。
系统可能出现以下情况:
总 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(),还必须观察:
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,使内部堆瞬间恶化。
必须关注日志:
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,会出现:
open LCD asset failed
Open prompt failed
建议:
- 应用中声明资源包版本;
gifpack中保存资源 manifest;- 启动时比较应用要求版本和资源版本;
- 不匹配时进入明确降级路径,不要反复尝试打开不存在的文件。
10.2 单份资源分区在线更新风险
当前只有一个 gifpack 分区。若未来在线覆盖更新:
- 更新中断电可能损坏整个资源文件系统;
- 新镜像校验失败时没有旧资源可回退;
- 应用和资源切换无法原子完成。
在没有完整方案前,不建议直接实现“边运行边覆盖单个 gifpack”。可选方向:
- 资源仅随工厂完整烧录更新;
- 下载到备用区域,校验后再切换;
- 设计
gifpack_0/gifpack_1双资源分区; - 使用版本化资源容器和原子状态记录;
- 将少量新增资源并入应用,但需评估 OTA 槽容量。
10.3 LittleFS 挂载依赖开机音流程
当前 /littlefs 主要由播放开机音前的 voice_prompt_play_file() 触发挂载。LCD 随后假定 /littlefs 已经可用。
如果开机音被关闭或音频初始化提前失败,LCD 可能在文件系统未挂载时读取资源。
建议把 platform_mount_littlefs() 提升为明确的启动阶段,由 app_composition 在音频和 LCD 之前编排,并保持调用幂等。
风险等级:中高。
11. 优化建议与优先级
P0:立即建立容量门禁
在 CI 或构建检查中记录并比较:
应用 .bin 大小
应用分区使用率
gifpack LittleFS 已用块数
gifpack 使用率
IRAM 使用量
D/IRAM 静态使用量
建议规则:
- 应用分区达到 85% 发出警告;
- 应用分区达到 90% 阻止合入;
gifpack达到 90% 发出高风险警告;gifpack达到 95% 阻止合入;- IRAM 增长必须在 PR 中说明;
- 新增库必须附带 size diff;
- 新增动画必须附带新增资源字节数和预计剩余空间。
P0:采集真机峰值内存
至少在以下时间点采集:
- Bootloader 后、应用初始化前;
- NVS 和 LittleFS 挂载后;
- Wi-Fi 初始化后;
- Wi-Fi 已连接;
- TLS Token 请求前和握手中;
- WebSocket 已连接;
- 音频采集和播放同时进行;
- LCD 动画播放中;
- 远程日志上传中;
- 所有业务进入稳态后;
- 多次断线重连后;
- 长时间运行 24 小时后。
每次至少记录:
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 常驻,可考虑:
- 将队列动态分配到 PSRAM;
- PSRAM 失败时回退到更短内部队列,而不是同等大小回退;
- 将
CONFIG_TQ_REMOTE_LOG_QUEUE_LEN从 128 调低; - 将单条文本上限从 256 调低;
- 使用变长块池或分层队列;
- 评估是否需要在所有构建类型中默认启用远程日志。
修改时要保持日志钩子并发安全,不能为了省内存破坏日志回调的锁和生命周期。
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,例如:
{
"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 决定在何时上报。
推荐字段:
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. 推荐执行顺序
第一阶段:立即完成
- 将应用和
gifpack大小检查加入构建或 CI; - 在真机完成一次全流程内存快照;
- 确认实际 PSRAM 容量;
- 统计所有任务栈高水位;
- 清点
gifpack中是否存在未使用资源; - 确认
model分区真实用途; - 将 LittleFS 挂载改为明确启动阶段。
第二阶段:容量治理
- 优化或迁移远程日志静态队列;
- 根据高水位缩减过大的任务栈;
- 根据实测调整音频/WebSocket 队列;
- 统一 RLE 转换工具和格式检查;
- 增加资源 manifest;
- 建立应用和资源版本兼容规则。
第三阶段:量产和长期升级
- 决定资源是否需要 OTA;
- 如需要,设计资源 A/B 或安全下载切换机制;
- 评审并冻结量产分区表;
- 建立 24/72 小时长稳测试;
- 建立多轮 Wi-Fi/WebSocket 重连压力测试;
- 保存每个发布版本的 Flash/RAM 基线报告。
17. 复现本分析的方法
17.1 应用镜像
完整构建后检查:
build/TQ_Cat_Code.bin
build/TQ_Cat_Code.map
使用当前 ESP-IDF v5.5.4 环境运行 size 工具:
idf.py size
idf.py size-components
17.2 Flash 分区
检查:
16m.csv
build/partition_table/partition-table.bin
build/flasher_args.json
不要假定空 Offset 分区的地址永久不变,应以当前构建生成结果为准。
17.3 LittleFS 资源
检查临时目录文件总量:
build/esp-idf/platform/asset_image/
使用构建环境中的 littlefs-python 查看:
build/littlefs_py_venv/bin/littlefs-python list \
build/gifpack.bin \
--block-size 4096 \
--name-max 64
真实空闲块应通过 LittleFS API或设备挂载后的 esp_littlefs_info() 获取,不能只依据资源文件内容之和。
17.4 真机运行时内存
关注串口日志中的:
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 分区用途,并为应用与资源版本建立明确兼容机制。