chore: archive diagnostics and project documentation

This commit is contained in:
chenjintao
2026-09-01 14:25:08 +08:00
parent 3912843869
commit c4a1da9d8f
35 changed files with 19843 additions and 3260 deletions
@@ -0,0 +1,960 @@
# Flash 与内存容量分析、风险及后续开发建议
本文记录当前固件的 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` 分区用途,并为应用与资源版本建立明确兼容机制。