@@ -1,6 +1,8 @@
# STM32F4-Base RAM 使用分析与优化方案(内存管理主文档)
> 版本:v1.2 日期:2026-09-21
> 版本:v1.3 日期:2026-10-22( v1.2 2026-09-21)
> 变更:v1.3 §4 的阶段子项改为**层级编号**(见下"编号约定");同步 2026-10-22 现状数字与
> `data/ram_report_20261022.md` / `data/stack_report_20261022.md` 两份新快照。
> 变更:v1.2 数据现状全量同步——§2.1/§2.2/§5/§6/§8.6 等"现状"数据全部更新为 2026-09-21 lftpd 静态化后的最新 map 与实测值;清除 2026-09-11 历史口径残留(历史快照保留于 `data/`)
> 适用工程:STM32F4-Base( STM32F407ZGTx @168MHz, FreeRTOS V10.3.1 + CMSIS-RTOS2)
> 数据来源:`MDK-ARM/STM32F407-Demo/STM32F407-Demo.map`( `@build` 重建)+ `scripts/ram_report.py`(静态台账)+ `scripts/stack_report.py`(静态调用图 × 运行时栈水位)+ 源码静态分析 + 网络调研
@@ -15,7 +17,7 @@
- 主 SRAM 128KB 中静态区曾占 **122.7KB ** ,余量仅 5.3KB
- FreeRTOS 堆 48KB 里装下 ~33.5KB 任务栈,可分配余量只有 ~12KB
- F407 另有 **64KB CCM RAM ** : **已于 2026-09-21 启用**并把 FreeRTOS 堆迁入(§1.2)
- F407 另有 **64KB CCM RAM ** : **已于 2026-09-21 启用**并把 FreeRTOS 堆迁入(§4. 1.2)
- 起步时没有任何运行时测量手段:不知道每个任务实际用了多少栈,改栈全靠拍脑袋
当前状态(**以 §2.1 受管快照为准**,由 `.map` 自动生成;2026-10-22 `@build` ):RW_IRAM1 **75,264 / 131,072 B( 57.4%) ** 、RW_CCM **57,376 / 65,280 B( 87.9%) ** 。✅ **任务栈:`stack_report.py` 9/9 PASS ** ( 2026-10-22 静态最坏链 + 同日真机 `[STACK]` 水位,`defaultTask` 最坏 3024B/5120(余 40%)、`protectTask` 872B/2048(余 57%),见 `data/stack_report_20261022.md` );**卡点已从 SRAM1 移到 CCM**(余 7,904 B)。历史:`defaultTask` 曾于 2026-10-04 达 3664B/4096 ≈10%,登记为 S-1, 2026-10-10 削链后销项(详见"2026-10-10 复测"小节)。
@@ -47,7 +49,7 @@
| 项 | 说明 | 来源 |
|---|---|---|
| 主 SRAM (0x20000000) | 128 KB( RW_IRAM1 Max `0x20000` ) | scatter `STM32F407-Demo.sct:22` |
| CCM SRAM (0x10000000) | 64 KB; FreeRTOS 堆于 2026-09-21 迁入(§1.2) | scatter `STM32F407-Demo.sct:25` ; RM0090 §3.1.1 |
| CCM SRAM (0x10000000) | 64 KB; FreeRTOS 堆于 2026-09-21 迁入(§4. 1.2) | scatter `STM32F407-Demo.sct:25` ; RM0090 §3.1.1 |
| FreeRTOS 堆 (heap_4) | **56 KB( 57,344 B) ** , **位于 RW_CCM**; 2026-10-12( B5)由 48 KB 扩容(protectTask/iec104Task 栈入堆,CCM 水位 87.9% < 92% 门禁);旧 48KB 池实测 `free=8,784 / min_ever=4,848` B( 2026-10-03),新池 min_ever 待上板复测 | `Inc/FreeRTOSConfig.h:71` `configTOTAL_HEAP_SIZE` ( L1: `.ioc` `FREERTOS.configTOTAL_HEAP_SIZE` ) |
| C 库堆/栈 (startup) | 512 B / 1 KB,启动期 main() 用 | map `startup_stm32f407xx.o` HEAP/STACK 节 |
@@ -61,7 +63,7 @@
| 2 | 6,746 | `ui_net_sink.o` | 远程显示收发缓冲 `g_tx[3209]` + `g_lcdmem_data[3204]` + `g_rx[64]` |
| 3 | 6,408 | `ui_port.o` | `g_ui_fbuf[3200]` + `g_ui_draw_buf[3200]` ( 160× 160 1bpp 帧缓冲) |
| 4 | 6,200 | `nand_ftl.o` | `s_page_buf` /`s_cache_buf` /`s_copy_buf` 各 2,048 B + dhara 结构(**DMA**) |
| 5 | 4,616 | `lftpd.o` | FTP 会话静态缓冲(2026-09-21 由栈迁入,§1.4) |
| 5 | 4,616 | `lftpd.o` | FTP 会话静态缓冲(2026-09-21 由栈迁入,§4. 1.4) |
| 6 | 4,104 | `ch395f.o` | `s_spi2_dma_tx_buf[2052]` + `s_spi2_dma_rx_buf[2052]` ( **DMA**) |
| 7 | 2,576 | `gd5f2gq5ue.o` | `s_bbt[256]` + `s_bbt_best[256]` + `s_bbt_page_buf[2048]` ( **DMA**) |
| 8 | 1,720 | `cmsis_os2.o` | CMSIS-RTOS2 封装静态区 |
@@ -77,7 +79,7 @@
> (卷探针 `FIL` 528 + `default_fatfs`/`fs_work`)、`cfg_file_store_mcu.o` 1,104 B( 2 处 `FIL` = 1,056)。
> 同期 Flash **−600 B**(删旧档迁移)。台账:`data/ram_report_20261010.md`。
**RW_CCM( CCM) ** : `heap_4.o` 49,184 B( FreeRTOS 堆 49,152 B + RW 32 B)——2026-09-21 由主 SRAM 迁入(§1.2),不再计入主 SRAM。
**RW_CCM( CCM) ** : `heap_4.o` 49,184 B( FreeRTOS 堆 49,152 B + RW 32 B)——2026-09-21 由主 SRAM 迁入(§4. 1.2),不再计入主 SRAM。
**静态合计(以 §2.1 受管快照为准) ** : RW_IRAM1 **87,040 B( 85.00 KB / 128 KB, 66.4%) ** + RW_CCM **49,184 B( 48.03 KB / 64 KB, 75.0%) ** = **136,224 B( RW+ZI) ** 。
> 历史(2026-09-21):RW_IRAM1 82,048 B( 62.6%) + RW_CCM 49,184 B = 131,232 B。
@@ -121,12 +123,12 @@
| idle(内核) | `configMINIMAL_STACK_SIZE=128` | 0.5KB | 空闲 | 未观测(未取句柄) |
| timer(内核) | `configTIMER_TASK_STACK_DEPTH=256` | 1KB | 软定时器 | 未观测 |
> 任务栈全部从 FreeRTOS 堆(56KB = `configTOTAL_HEAP_SIZE` 57,344 B; 2026-09-21 迁 CCM、2026-10-12 扩容)分配。**"配置栈"列以 `Src/freertos.c` 为唯一来源;实测峰值/余量判定以 §0.6 的 `stack_report` 基线为准**(本表数值同步自该基线)。
> 任务栈全部从 FreeRTOS 堆(56KB = `configTOTAL_HEAP_SIZE` 57,344 B; 2026-09-21 迁 CCM、2026-10-12 扩容)分配。**"配置栈"列以 `Src/freertos.c` 为唯一来源;实测峰值/余量判定以 §4. 0.6 的 `stack_report` 基线为准**(本表数值同步自该基线)。
### 2.4 动态分配点与隐患
1. **lftpd 的 C 库 `malloc` 隐患(✅ 2026-09-10 已修,见阶段 2.1) ** : `lftpd_io.c` 的 3 处 `malloc` /`free` (路径规范化/`DIR` )与 `lftpd.c` 的 7 处 `free` 已全部改用 `pvPortMalloc` /`vPortFree` ( C 库堆仅 startup 的 512B,原写法长路径必炸);当前源码无裸 `malloc` 残留。
2. **lftpd 大栈局部变量(✅ 2026-09-21 已整改,见 §1.4) ** : `lftpd.c:67-68` ( message[256]+response[512])、`:109,134` ( name[256]× 2)、`:156,185` ( **buffer[1024]× 2**)、`:551` ( read_buffer[512])、`:583` ( arg_buf[512])以及 `lftpd_log.c` 的 `buffer[256]` 已全部改为 `static` ;实测 ftpTask 峰值 3480 → 1184 B,不再需要 4KB+ 栈。
1. **lftpd 的 C 库 `malloc` 隐患(✅ 2026-09-10 已修,见 §4. 2.1) ** : `lftpd_io.c` 的 3 处 `malloc` /`free` (路径规范化/`DIR` )与 `lftpd.c` 的 7 处 `free` 已全部改用 `pvPortMalloc` /`vPortFree` ( C 库堆仅 startup 的 512B,原写法长路径必炸);当前源码无裸 `malloc` 残留。
2. **lftpd 大栈局部变量(✅ 2026-09-21 已整改,见 §4. 1.4) ** : `lftpd.c:67-68` ( message[256]+response[512])、`:109,134` ( name[256]× 2)、`:156,185` ( **buffer[1024]× 2**)、`:551` ( read_buffer[512])、`:583` ( arg_buf[512])以及 `lftpd_log.c` 的 `buffer[256]` 已全部改为 `static` ;实测 ftpTask 峰值 3480 → 1184 B,不再需要 4KB+ 栈。
3. **DMA 缓冲位置 ** : CH395F( SPI2 DMA)、GD5F( SPI1 DMA)、UART DMA 缓冲必须由 DMA 访问 → **只能放主 SRAM,不能放 CCM ** ( CCM 仅 CPU 可达,见 4.3)。
### 2.5 结论:紧张的三个根因(含 2026-09-11 审计量化)
@@ -134,7 +136,7 @@
| 根因 | 占用 | 可回收性 |
|---|---|---|
| ① LVGL 内存池 40KB 对 1bpp 小屏菜单过大 | 40KB → **16KB ✅ 2026-10-15 ** | 高(目标 8~16KB; **开机实测 UI 仅用 ~3KB**,见 §2.6.2) |
| ② 任务栈"往大了给",无实测依据 | 38,912 B( 38 KB 现值,§2.6.1) | 高(§0.6 已有实测:uiTask 3.8/16KB、netTask 0.6/4KB → 阶段 1.3 缩减) |
| ② 任务栈"往大了给",无实测依据 | 38,912 B( 38 KB 现值,§2.6.1) | 高(§4. 0.6 已有实测:uiTask 3.8/16KB、netTask 0.6/4KB → §4. 1.3 缩减) |
| ③ 64KB CCM 曾完全未利用,缓冲挤在主 SRAM | — | ✅ 2026-09-21 已启用(堆迁入 CCM);2026-10-22 CCM 余量 7,904 B( **全局卡点**) |
量化补充(来源:2026-09-11 审计快照,口径与方法见 §8.4):
@@ -142,9 +144,9 @@
- 两池(FreeRTOS 48KB + LVGL 40KB)曾占主 SRAM 的 **68% ** ,是当时"SRAM 几乎耗尽(95.4%,余 5.8KB) "的直接原因;**堆迁 CCM 后该结构性压力已解除**( 2026-10-22 RW_IRAM1 57.4%,见 §2.1)
- heap_4 **不合并相邻空闲块 ** → 长期运行需盯碎片,`min_ever` ( 2026-10-22 实测 8,464 B, 56KB 池)是硬指标而非 `free`
- 未映射资源:SRAM3 64KB( scatter 未映射;clone 芯片可能无此区);CCM 已用
- 曾经的三条中风险项:新任务无处安置(已缓解)、堆碎片(仍存)、栈溢出检测缺失 → **已于阶段 0 开启 `configCHECK_FOR_STACK_OVERFLOW=2` 并实测抓到过溢出 ** (§0.1)
- 曾经的三条中风险项:新任务无处安置(已缓解)、堆碎片(仍存)、栈溢出检测缺失 → **已于阶段 0 开启 `configCHECK_FOR_STACK_OVERFLOW=2` 并实测抓到过溢出 ** (§4. 0.1)
小项:FF_FS_TINY=0(每打开文件 512B 扇区窗,阶段 1.5 可改 1)、`ui_net_sink` PC 远程显示缓冲常驻 6.4KB(产品构建可加开关关闭,阶段 1.5);lftpd 栈局部变量 ✅ 2026-09-21 已静态化(§1.4)。
小项:FF_FS_TINY=0(每打开文件 512B 扇区窗,§4. 1.5 可改 1)、`ui_net_sink` PC 远程显示缓冲常驻 6.4KB(产品构建可加开关关闭,§4. 1.5);lftpd 栈局部变量 ✅ 2026-09-21 已静态化(§4. 1.4)。
### 2.6 堆 / LVGL 池 / C 库 / Flash 明细
@@ -189,7 +191,7 @@
| 组件 | 大小 | 说明 |
|---|---|---|
| 启动栈 | 1,024 B | `startup_stm32f407xx.o` ,仅 `HAL_Init` /`app_main_init` 期间 |
| C 库堆 | 512 B | `Heap_Size EQU 0x200` → **自研代码禁用 C 库 `malloc` ** ( AGENTS.md 铁律;lftpd 的 `malloc` 隐患见 §2.4-1 与阶段 2.1) |
| C 库堆 | 512 B | `Heap_Size EQU 0x200` → **自研代码禁用 C 库 `malloc` ** ( AGENTS.md 铁律;lftpd 的 `malloc` 隐患见 §2.4-1 与 §4. 2.1) |
| C 库代码(Flash) | ~4.2 KB | printf / 浮点 / 字符串 |
#### 2.6.4 Flash 1 MB 分布
@@ -207,7 +209,7 @@
> **2026-10-11 裁剪 LVGL 死代码(原审查报告 F-1/F-2/F-3,已销项)**: `Lib/lv_conf.h` 显式关掉 10 个 `LV_DRAW_SW_SUPPORT_*`(只留 I1)、5 个未用控件类(label / button / buttonmatrix / bar / textarea)、`LV_USE_THEME_SIMPLE`、`LV_USE_FLEX` / `LV_USE_GRID` → **`ER_IROM1` 274,724 → 210,148 B(省 64,576 B = 旧镜像的 23.5%) **, Flash 占比 26.3% → **20.1%**。
> 对象级对照(`.map` 组件表,Code+RO+RW):`blend_to_*` 40,582 → 1,338(其中 `blend_to_i1` 自身 5,272 → 1,338——它按**源**格式的分支也一并消失);控件类 13,397 → 0; `lv_theme_simple` 866 → 0; `lv_draw_sw_transform` 4,340 → 598; `lv_theme_mono` 1,400 → 1,036;镜像内对象数 227 → 212。实测比原报告估算(54,630 B)多省约 10 KB,差额正是这些"锚点消失后连带消失的分支"。
> ⚠ **配套本地补丁**: `lv_draw_sw_blend_to_i1.c` 的 `lv_color16_from_u16` 上游 9.3.0 漏了定义处守卫(唯一使用点全在 `#if LV_DRAW_SW_SUPPORT_RGB565_SWAPPED` 内)→ 关掉该格式后 ARMCC 报 `#177-D declared but never referenced`(破坏"0 警告"门禁)。已按使用点补同一守卫,升级 LVGL 时若上游已修可去掉。
> 验证:ui-verify 五步全过——**11 页内容黄金参考在裁剪前生成、裁剪后逐字节一致**,菜单 8 状态像素一致,LVGL 池水位 5,440 → 5,368 B(−72 B,样式表更小,一次性残留仍 80 B);`@build` 0 错 0 警;`@test` 真机 PASS。详见 `../UI设计/UI_设计架构.md` §1.4 第 10 条。
> 验证:ui-verify 五步全过——**11 页内容黄金参考在裁剪前生成、裁剪后逐字节一致**,菜单 8 状态像素一致,LVGL 池水位 5,440 → 5,368 B(−72 B,样式表更小,一次性残留仍 80 B);`@build` 0 错 0 警;`@test` 真机 PASS。详见 `../UI设计/UI_设计架构.md` §4. 1.4 第 10 条。
> **长效防复发**:已建门禁 `scripts/check_lv_conf_sync.py`( pre-commit 钩子 `lv-conf-sync`)——比对两份 `lv_conf.h` 的**生效值**(未定义则取 `lv_conf_internal.h` 兜底默认),并抓无效宏名与重复定义;首次跑揪出两端共 26 个 9.3 不存在的选项、3 处重复定义、10 项靠默认值隐式生效的开关,已清理(本次清理对镜像体积中性)。详解见 `../代码规范/pre-commit使用说明.md` §2.15。

@@ -231,11 +233,17 @@
## 4. 方案
> **编号约定(2026-10-22 起)**:本节的子项按层级写成 **`4.<阶段>.<项>`**(如 `§4.1.4`),
> 不再用裸 `0.x / 1.x / 2.x`——旧写法与本文顶层章节 `§1 背景`、`§2 现状测量` **同号撞面**,
> 人和 `doc-links`(自引用直接放过)都会读错;实测最坑的是"缩堆"那条既像 §2.2(静态大项排行)
> 又像阶段 2 第 2 项。对照:`0.x→4.0.x`、`1.x→4.1.x`、`2.x→4.2.x`(阶段号不变、只加 `4.` 前缀)。
> **其它文档的变更记录行里的旧编号按项目纪律冻结不改写**,需要追溯时按本表换算。
> ⏸ **实施状态(用户裁定 2026-10-15) **——本节的"阶段/天数"是当初的排期设想,现在按下面口径执行:
>
> - **保持常开**:阶段 0 的测量能力 + 阶段 3 的长效机制(`mem-budget` 92% 门禁、`RAM预算台账.md` 受管区、`ram_baseline.tsv` 趋势、`[STACK]` 水位与 `stack_report.py` / `ram_report.py`)。它们已落地、零维护成本,且正是"何时该重开优化"的传感器。
> - **已落地的优化不回退**: 1.1 LVGL 池 40→16 KB(✅ 2026-10-15, SRAM1 74.9% → **56.1%**)、1.4 lftpd 大栈局部变量改静态(✅ 2026-09-21)。
> - **整簇暂缓、不再排期**:1.2 启用 CCM 承载 UI 侧缓冲、1.3 任务栈按实测缩减、2.2 缩减 `configTOTAL_HEAP_SIZE`、2.3 任务/队列全静态化,以及把池进一步试到 8 KB。
> - **已落地的优化不回退**: §4. 1.1 LVGL 池 40→16 KB(✅ 2026-10-15, SRAM1 74.9% → **56.1%**)、§4. 1.4 lftpd 大栈局部变量改静态(✅ 2026-09-21)。
> - **整簇暂缓、不再排期**: §4. 1.2 启用 CCM 承载 UI 侧缓冲、§4. 1.3 任务栈按实测缩减、§4. 2.2 缩减 `configTOTAL_HEAP_SIZE`、§4. 2.3 任务/队列全静态化,以及把池进一步试到 8 KB。
>
> **暂缓理由与重开条件** → `../问题记录/项目审查与优化建议报告.md` §5.3。一句话:**当前没有功能在等这块预算**,而搬迁要动 scatter、要逐条过 DMA 可达性清单(违例是静默传错不是报错)、缩堆还得上板复测 `min_ever`——收益不抵风险与工时。下面各节**保留作重开时的实施依据,不代表现在要做**。
@@ -243,7 +251,7 @@
> 原则:没有数据不动刀。本阶段全部是"只增不减"的改动,零功能风险。
#### 0.1 栈溢出检测(防身)
#### 4. 0.1 栈溢出检测(防身)
`Inc/FreeRTOSConfig.h` 增加:
@@ -270,7 +278,7 @@ void vApplicationMallocFailedHook(void)
成本:每次上下文切换几十周期,不占 RAM。
#### 0.2 运行时栈水位(已落地:`stack_probe_poll`, 2026-09-21)
#### 4. 0.2 运行时栈水位(已落地:`stack_probe_poll`, 2026-09-21)
实现位置:`App/task/default_task.c` 的 `stack_probe_poll` ( 2026-09-21 落地于 `Src/freertos.c` USER CODE 区,2026-09-25 随 defaultTask 任务体整体迁出至 `App/task/` 并注册 Keil 工程 App 组)。
数据源是本版 CMSIS/FreeRTOS **已有**的 API: `osThreadGetStackSpace()` (剩余字节,内部走
@@ -291,9 +299,9 @@ void vApplicationMallocFailedHook(void)
- `uxTaskGetStackHighWaterMark` 返回**任务创建以来**的最小剩余栈(单位 word=4B);`used = 配置 − 剩余` 即该任务历史峰值栈占用。
- **测量必须覆盖全功能路径**(高水位是历史最小值,只反映跑过的路径):全菜单遍历、FTP 上传/下载大文件、CH395F 断链重连、NAND 格式化、时间同步。建议 `@test` 全回归 + 1 小时浸泡。
- 未覆盖 idle/timer(内核静态小栈、纯循环,未取句柄);构建期静态侧见 §0.6。
- 未覆盖 idle/timer(内核静态小栈、纯循环,未取句柄);构建期静态侧见 §4. 0.6。
#### 0.3 map 静态占用报告脚本
#### 4. 0.3 map 静态占用报告脚本
脚本 `scripts/ram_report.py` ( Python,已随仓库提供):
@@ -304,15 +312,15 @@ void vApplicationMallocFailedHook(void)
- ✅ **2026-10-12 落地 ** : `scripts/ram_baseline.py --trend` 与基线 TSV
( `scripts/ram_baseline.tsv` )做 diff,任何对象 RW+ZI 增量 >1KB 即高亮
#### 0.4 (可选)运行时间统计
#### 4. 0.4 (可选)运行时间统计
`configGENERATE_RUN_TIME_STATS=1` + `portCONFIGURE_TIMER_FOR_RUN_TIME_STATS` 用 TIM2( HAL 时基已占 TIM7,TIM2 当前空闲,需核对 `.ioc` )。`vTaskGetRunTimeStats()` 给出每任务 CPU 占比——用于发现"CPU 大户",与内存问题正交,优先级低于 0.1/0.2。
#### 0.5 (可选)main() 启动期栈峰值
#### 4. 0.5 (可选)main() 启动期栈峰值
启动期跑在 startup 的 1KB C 栈上(CubeMX 初始化 + 各驱动 init,含 BBT 全扫描)。ARMCC 可用 `__current_stack_usage()` 在 `app_main_init` 末尾打点,确认 1KB 够用;不够则调 scatter 的 `STACK` 节(与任务栈无关,单独预算)。
#### 0.6 静态栈分析 + 汇总工具(构建期,零目标依赖;2026-09-21 落地)
#### 4. 0.6 静态栈分析 + 汇总工具(构建期,零目标依赖;2026-09-21 落地)
**UV4 每次构建已自动生成静态调用图 ** `MDK-ARM/STM32F407-Demo/STM32F407-Demo.htm`
(标题 "Static Call Graph",随 `@build` 刷新,**无需**给 armlink 加 `--callgraph` )。每个函数
@@ -340,7 +348,7 @@ void vApplicationMallocFailedHook(void)
| `--min-free-pct` | `30` | 要求的最小余量百分比 |
- **判定规则**: `余量 = 配置 − max(静态最坏深度, 实测峰值)` ,须 `≥ max(1024 B, 配置×30%)` ;任一任务不达标 → 脚本退出码 **1 ** (可挂门禁),全达标 → 0,HTM 缺失 → 2。
- **何时跑**:① 改栈或加大局部变量前后;② 改 FatFs/lftpd/LVGL 等第三方配置后;③ 任务职责变化时。串口日志覆盖的路径越全越准(路径覆盖要求见 §0.2)。
- **何时跑**:① 改栈或加大局部变量前后;② 改 FatFs/lftpd/LVGL 等第三方配置后;③ 任务职责变化时。串口日志覆盖的路径越全越准(路径覆盖要求见 §4. 0.2)。
- **报告内容**:每任务预算表(配置/静态/实测/最坏/余量/判定)、各任务最坏调用链、帧宽 Top15(找"宽度大户")、堆 free/min_ever。
首份基线(2026-09-21,产品构建 + FTP 会话实测;产物 `docs/内存管理/data/stack_report_20260921.md` ):
@@ -349,7 +357,7 @@ void vApplicationMallocFailedHook(void)
|---|---|---|---|---|---|---|
| defaultTask | 4096 | **2776 ** | 2136 | 2776 | 1320 (32%) | PASS |
| **ftpTask ** (优化前) | 4096 | 2528 | **3480 ** | **3480 ** | **616 (15%) ** | **FAIL ** |
| **ftpTask ** (§1.4 静态化后) | 4096 | **472 ** | **1184 ** | **1184 ** | **2912 (71%) ** | **PASS ** |
| **ftpTask ** (§4. 1.4 静态化后) | 4096 | **472 ** | **1184 ** | **1184 ** | **2912 (71%) ** | **PASS ** |
| netTask | 4096 | 440 | 588 | 588 | 3508 (85%) | PASS |
| uiTask | 16384 | 912 | 3824 | 3824 | 12560 (76%) | PASS |
| timeSyncTask | 2048 | 336 | 420 | 420 | 1628 (79%) | PASS |
@@ -357,12 +365,12 @@ void vApplicationMallocFailedHook(void)
实测堆:`free=8784 B / min_ever=4848 B` ( 48KB 池)。
**处置结果(2026-09-21 当日闭环,走 ②→①) ** :先做 §1.4(lftpd 大局部变量静态化)——复测 ftpTask 峰值 **3480 → 1184 B ** (余量 616 → **2912 B / 71% ** ),已满足 `≥ max(1024 B, 配置×30%)` , **①(涨栈到 1600 words)不再需要**。代价:静态 BSS +4,616 B( RW_IRAM1 77.4 → 82.0 KB / 128 KB,余量仍 ~48 KB)。
**处置结果(2026-09-21 当日闭环,走 ②→①) ** :先做 §4. 1.4(lftpd 大局部变量静态化)——复测 ftpTask 峰值 **3480 → 1184 B ** (余量 616 → **2912 B / 71% ** ),已满足 `≥ max(1024 B, 配置×30%)` , **①(涨栈到 1600 words)不再需要**。代价:静态 BSS +4,616 B( RW_IRAM1 77.4 → 82.0 KB / 128 KB,余量仍 ~48 KB)。
**静态与实测是互补的两套数据(必须都留) ** :
- defaultTask:静态 2776 > 实测 2136——静态是**上界**(最深分支未必走到),实测是**已走路径**。**判栈够不够看两者更坏者,做定栈优化看实测。**
- netTask / uiTask / testTask / timeSyncTask / **ftpTask ** :实测 > 静态——静态是**下界**(互递归、间接调用、ISR 上下文保存 ~100B 未计入)。ftpTask 最典型:优化前静态 2528 B(控制通道握手链)**低估了 950 B**,真实峰值 3480 B 只在 128KB STOR 传输时暴露 → **不跑真实业务就定栈 = 拍脑袋。 ** §1.4 静态化后静态 472 B / 实测 1184 B,两者重新收敛(差距回到 ISR/上下文量级)。
- netTask / uiTask / testTask / timeSyncTask / **ftpTask ** :实测 > 静态——静态是**下界**(互递归、间接调用、ISR 上下文保存 ~100B 未计入)。ftpTask 最典型:优化前静态 2528 B(控制通道握手链)**低估了 950 B**,真实峰值 3480 B 只在 128KB STOR 传输时暴露 → **不跑真实业务就定栈 = 拍脑袋。 ** §4. 1.4 静态化后静态 472 B / 实测 1184 B,两者重新收敛(差距回到 ISR/上下文量级)。
- 本次可观测性的第一个产出不是数字而是**缺陷**:ftpTask 因就绪位被其它等待者消费而**从未启动**(21 端口一直无监听),修好后才有了上面这组数据——详见 `docs/测试说明/FTP_Test_Guide.md` §8.8。
要点与边界:
@@ -370,7 +378,7 @@ void vApplicationMallocFailedHook(void)
- defaultTask 的静态最坏链是**配置保存/旧档迁移**路径,不是首启探针路径(**2026-09-21 当时的链;旧档迁移已于 2026-10-10 删除、日志行缓冲已按模块收紧,现链见下方"2026-10-10 复测"**):
`net_cfg_load → net_cfg_legacy_migrate → net_cfg_save → cfg_file_save → s_write_template → s_write_doc → cfg_store_write_atomic → f_rename → follow_path → dir_next → create_chain → put_fat → move_window → sync_window → disk_write → ftl_flush_cache → dhara_map_write → prepare_write → auto_gc → dhara_map_gc → ... → trace_path → dhara_journal_read_meta → dhara_nand_read → gd5f_read_from_cache → HAL_SPI_Receive → ... → SPI_WaitFlagStateUntilTimeout` 。
**2776 B > 2048 B ** → 512 words 栈必然溢出,与 2026-09-21 实测完全一致。
- 帧宽榜直接点名宽度大户(自身帧):`s_append` 704 B( FIL 512 + 记录 64 + 时间戳)、`StartDefaultTask` 560 B(探针 `FIL __chk` )、`cfg_store_read` /`cfg_store_write_atomic` 576 B、LVGL 绘制 624/560/400 B——与 §1.4 的整改项对得上。**lftpd 三项已于 2026-09-21 静态化(§1.4),已退出帧宽榜前 15**(原 `handle_control_channel` 1104 B、`receive_file` /`send_file` 1088 B、`send_response` 800 B)。
- 帧宽榜直接点名宽度大户(自身帧):`s_append` 704 B( FIL 512 + 记录 64 + 时间戳)、`StartDefaultTask` 560 B(探针 `FIL __chk` )、`cfg_store_read` /`cfg_store_write_atomic` 576 B、LVGL 绘制 624/560/400 B——与 §4. 1.4 的整改项对得上。**lftpd 三项已于 2026-09-21 静态化(§4. 1.4),已退出帧宽榜前 15**(原 `handle_control_channel` 1104 B、`receive_file` /`send_file` 1088 B、`send_response` 800 B)。
- **静态分析的天然盲区**:递归/互递归(LVGL widget 树、`layout_update_core` 、`apply_theme` 等)与间接调用只能按 "Cycle" 近似,armlink 明确标注 `+ Unknown(Cycles, Untraceable Function Pointers)` 。因此 uiTask 的 912 B 是**下限**(实测 3824 B),必须靠运行时水位。
- testTask 在产品构建里只是 `osDelay` 空循环(168 B);2048 words 是给**存储套件构建**( `TEST_SUITE_STORAGE` )预留的,测套件时应重跑本脚本看该构建的静态值。
- **测 ftpTask 的复现配方**:设备就绪后跑 `& '.venv\Scripts\python.exe' test/host/ftp_speed_test.py --size 128` ( STOR 128KB + RETR + 逐字节比对),再按上面命令叠加串口日志出报告。**不要用裸 TCP 工具探测 21 端口**——CH395F 单客户端复用,探测会占掉唯一会话(见 `docs/测试说明/FTP_Test_Guide.md` §8.9)。
@@ -433,14 +441,14 @@ FreeRTOS 堆不变(`free=17248 / min_ever=9312`)。验证链:`@build` 0/0
### 阶段 1:静态大项削减(收益最大,3~5 天)
#### 1.1 LVGL 内存池 40KB → 16KB(✅ 已执行 2026-10-15,省 24,576 B)→ 8KB(待评估)
#### 4. 1.1 LVGL 内存池 40KB → 16KB(✅ 已执行 2026-10-15,省 24,576 B)→ 8KB(待评估)
- 依据:屏幕仅 160×160×1bpp,菜单为轻量控件树;LVGL 内存池只装对象/样式/字符串缓冲,与像素缓冲无关。40KB 来自模板默认值,无适配依据。
- **已落地**: `Lib/lv_conf.h` 与 `simulator/lv_conf.h` 同步改 `(16 * 1024)` ——两端必须同值,`lv-conf-sync` 门禁按**生效值**比对。实测水位见 §2.6.2(最重页面峰值 6,568 B = 池的 40%);`@build` 0 错 0 警,ZI 精确 − 24,576 B → SRAM1 由 74.9% 降到 **56.1% ** (余 57,504 B)。
- 验证口径(本次即用它):`simulator/tests/check_menu_rows.py` (菜单 8 状态像素 + 池基线 5,368 B) + `simulator/tests/check_content_pages.py` (11 页黄金参考逐字节 + 往返池水位 5,448 B)+ 真机 `menu build done: used=` 日志。
- **再降 8KB 的前置**:全量 UI 回归(PC 模拟器 + 真机)无 `LV_ASSERT_MALLOC` /分配失败;若 8KB 不够就停在 16KB,不勉强。
#### 1.2 启用 CCM RAM( +64KB 预算空间)
#### 4. 1.2 启用 CCM RAM( +64KB 预算空间)
依据 RM0090 §3.1.1: F407 的 64KB CCM @0x10000000 仅 CPU 可访问(DMA 不可达、无 cache)。
@@ -469,7 +477,7 @@ FreeRTOS 堆不变(`free=17248 / min_ever=9312`)。验证链:`@build` 0/0
验证:map 中出现 `RW_CCM` 且 `*(CCM_DATA)` 各对象地址落在 0x10000000~0x100FFFF; `ram_report.py` 同时输出两个 Region 水位。
#### 1.3 任务栈按实测数据缩减(省 ~17KB;目标值已按 2026-09-21 实测修正)
#### 4. 1.3 任务栈按实测数据缩减(省 ~17KB;目标值已按 2026-09-21 实测修正)
定栈规则(写入台账,长期执行):
@@ -485,7 +493,7 @@ ISR 余量:该任务运行期间可能被中断打断的最深中断链(SPI/
|---|---|---|---|
| uiTask | 16KB | **8KB ** | 实测 3824B → 5736+1024=6760B → 8KB( LVGL 渲染走静态缓冲) |
| netTask | 4KB | **2KB ** | 实测 588B → 1024+1024=2048B |
| ftpTask | 4KB | **3KB ** | 实测 1184B(§1.4 静态化后)→ 1776+1024=2800B → 3KB;再降须放宽 ISR 余量并复测 |
| ftpTask | 4KB | **3KB ** | 实测 1184B(§4. 1.4 静态化后)→ 1776+1024=2800B → 3KB;再降须放宽 ISR 余量并复测 |
| testTask | 8KB | **2KB ** | 产品构建空转(实测 348B)→ 1024+1024;存储套件构建按该构建静态值另放大 |
| defaultTask | 4KB | **保持 4KB ** | 2026-10-10 削链后静态最坏 2776B / 实测 2152B(余 32%);曾达 5312B 溢出判据,见"2026-10-10 复测" |
| timeSyncTask | 2KB | **保持 2KB ** | 实测 420B → 1024+1024=2048B,无缩减空间 |
@@ -494,7 +502,7 @@ ISR 余量:该任务运行期间可能被中断打断的最深中断链(SPI/
每改一批 → `@build` + 全功能回归 + 复测高水位(`free_min` 应 ≥ 新栈×30%),不满足则回调。
#### 1.4 lftpd 大栈局部变量 → 静态缓冲(✅ 2026-09-21 完成;ftpTask 峰值 3480 → 1184 B)
#### 4. 1.4 lftpd 大栈局部变量 → 静态缓冲(✅ 2026-09-21 完成;ftpTask 峰值 3480 → 1184 B)
已把 `lftpd.c` / `lftpd_log.c` 中 **9 个大局部缓冲改为函数级 `static` ** (lftpd 单客户端,全部调用与日志都在 ftpTask 内串行执行,静态化安全;各函数**各自独立缓冲**,无嵌套/别名冲突):
@@ -514,25 +522,25 @@ ISR 余量:该任务运行期间可能被中断打断的最深中断链(SPI/
> lftpd 属第三方代码(pre-commit 排除范围),只做最小侵入改动(每处仅加 `static` + 单行注释),改动点在本台账登记。
#### 1.5 其他小项
#### 4. 1.5 其他小项
- `Lib/FatFs/ffconf.h:32` `FF_FS_TINY` 0→1(每打开文件省 512B 扇区窗;FAT32 适用;`FF_FS_REENTRANT=0` 现状下安全)
- `ui_net_sink` ( PC 远程显示,6.4KB):加 `APP_UI_NET_SINK` 编译开关,产品固件可关;保留时缓冲随 1.2 迁 CCM(⏸ ** 1.2 已暂缓,实际仍在 SRAM1** )
- `ui_net_sink` ( PC 远程显示,6.4KB):加 `APP_UI_NET_SINK` 编译开关,产品固件可关;保留时缓冲随 §4. 1.2 迁 CCM(⏸ * * §4. 1.2 已暂缓,实际仍在 SRAM1**)
- `CH395F_TCP_MSS=1024` 已是压缩值,DMA 缓冲 `MSS× 2+4` 为协议决定,不动
### 阶段 2:堆与动态分配治理(1~2 天)
#### 2.1 修复 lftpd 的 malloc 隐患(必修)
#### 4. 2.1 修复 lftpd 的 malloc 隐患(必修)
`lftpd_io.c:44,49,54` 的 `malloc` / 对应 `free` 与 `:189` 的 `malloc(sizeof(DIR))` 改为 `pvPortMalloc` / `vPortFree` (加 `#include "FreeRTOS.h"` )。消除"512B C 堆"地雷,FTP 长路径不再随机失败。
> ✅ **2026-09-10 已落地**: `lftpd_io.c`( 3 处 `malloc` + 3 处 `free`)与 `lftpd.c`( 7 处 `free`,释放 `lftpd_io_canonicalize_path` 返回的堆指针)全部改用 `pvPortMalloc`/`vPortFree`;两文件加 `#include "FreeRTOS.h"`。lftpd 不参与模拟器构建,仅 MCU 端。Keil 重建 0 错 0 警。
#### 2.2 缩减 configTOTAL_HEAP_SIZE
#### 4. 2.2 缩减 configTOTAL_HEAP_SIZE
阶段 1 完成后复测:堆内需容纳 ≈ 栈(15KB) + TCB(2.3KB) + 队列/事件组(0.5KB) + lftpd 临时分配(1KB) + 余量。`configTOTAL_HEAP_SIZE` 49152 → **32768 ** ,要求全功能负载下 `heap free ≥ 8KB` 持续成立(浸泡 1 小时验证)。
#### 2.3 (可选,进阶)任务/队列全静态化
#### 4. 2.3 (可选,进阶)任务/队列全静态化
`configSUPPORT_STATIC_ALLOCATION=1` 已开。用 `osThreadNew` 的 `attr.cb_mem` ( `StaticTask_t` + 静态栈数组)可把栈完全移出堆,预算 100% 透明、堆只剩小对象分配。改动面涉及 CubeMX 生成的 `freertos.c` 属性块,收益(~15KB 确定性)与改动成本权衡后决定,**不作为必做项**。
@@ -546,16 +554,16 @@ ISR 余量:该任务运行期间可能被中断打断的最深中断链(SPI/
## 5. 预期效果
| 项目 | 现状(2026-09-21 实测,**主 SRAM 数字已随代码更新,以 §2.1 为准**) | 剩余项全部落地后(1.1/1.3/1.5/2.2) |
| 项目 | 现状(2026-09-21 实测,**主 SRAM 数字已随代码更新,以 §2.1 为准**) | 剩余项全部落地后(§4. 1.1/§4. 1.3/§4. 1.5/§4. 2.2) |
|---|---|---|
| 主 SRAM 静态占用 | 87,040 B / 128KB( 66.4%) | * * ~27KB / 128KB**( LVGL 池 + UI 缓冲迁 CCM、`ui_net_sink` 关) |
| 主 SRAM 余量 | 44,032 B( 43.0KB) | * * ~101KB** |
| CCM 使用 | 49,184 B / 64KB( 75.0%, FreeRTOS 堆,历史当时值。2026-10-12 B5 后堆 56 KB / 87.9%) | ~56KB / 64KB(堆 32K + 池 16K + UI 缓冲 + BBT) |
| 任务栈合计 | 38,912 B( 38.0KB) | ~21.5KB(§1.3 修正目标) |
| 任务栈合计 | 38,912 B( 38.0KB) | ~21.5KB(§4. 1.3 修正目标) |
| FreeRTOS 堆 | 49,152 B( `free=8,784 / min_ever=4,848` B) | 32,768 B(余 ≥8KB,全负载验证) |
| **总余量 ** | * * ~68KB**( 47.9 + 16.0 + 4.8) | * * ~120KB** |
> 已落地项(不再计入预期):1.2 堆迁 CCM( 2026-09-21)、1.4 lftpd 静态化、2.1 lftpd `malloc`→`pvPortMalloc`( 2026-09-10)。
> 已落地项(不再计入预期):§4. 1.2 堆迁 CCM( 2026-09-21)、§4. 1.4 lftpd 静态化、§4. 2.1 lftpd `malloc`→`pvPortMalloc`( 2026-09-10)。
> 估算假设:LVGL 池缩到 16KB( 8KB 未验证)且迁 CCM; `ui_net_sink` 产品构建关闭;`ui_port` 帧缓冲 + BBT 位图迁 CCM;堆缩 48K→32K。落地顺序与每步验证见 §6。
>
> ⏸ **2026-10-15 用户裁定:本表"目标"列所依赖的搬迁与缩堆整簇暂缓**(理由与重开条件 → `../问题记录/项目审查与优化建议报告.md` §5.3)。已落地部分只有 **1.1 池 40→16 KB**( SRAM1 实测 **73,568 B / 56.1%**,余 57,504 B)与 1.4;本表保留作重开时的收益测算依据,**当前实际水位以 `RAM预算台账.md` 受管区为准**,不要拿本表"目标"列当现状。
@@ -567,12 +575,12 @@ ISR 余量:该任务运行期间可能被中断打断的最深中断链(SPI/
| S1 | 0.1 + 0.2(溢出钩子 + `stack_probe_poll` 栈水位探针,`Src/freertos.c` USER CODE 区) | `@build` 0错0警;`@test` 串口出现每任务 `[STACK]` 报告 | ✅ 2026-09-21 |
| S2 | 0.3 `ram_report.py` + 基线 | 脚本输出与 map 人工核对一致 | ✅ 2026-09-21( `data/ram_report_20260921.md` ) |
| S3 | 全功能回归跑数(菜单/FTP/网络/格式化/浸泡 1h) | 拿到各任务 `free_min` 与堆 free 底值 | ◐ boot + FTP 路径已实测(09-21);全菜单遍历 + 1h 浸泡待补 |
| S4 | 1.4 lftpd 静态化 + 2.1 malloc 修复 | FTP STOR/RETR 大文件 + 长路径回归 | ✅(2.1 于 2026-09-10、1.4 于 2026-09-21 完成) |
| S5 | 1.1 LVGL 池 40K→16K | UI 全量回归(模拟器 + 实机),无断言失败 | ✅ **2026-10-15 ** (11 页黄金参考 + 池水位 5,448 B + `@test` 真机 `used=3192` ;实测峰值 6,568 B = 池的 40%,§2.6.2) |
| S6 | 1.2 CCM( scatter + section + 门禁宏) | map 显示 `RW_CCM` ;关 `APP_CCM_ENABLE` 也能编译通过 | ⏸ **暂缓(2026-10-15 裁定) ** ;已完成部分仅 `RW_CCM` + 堆迁入(09-21),缓冲 section 迁移与门禁宏不再排期 |
| S7 | 1.3 栈缩减(按 S3 数据)+ 复测 | 每任务 `free_min ≥ 新栈×30%` ,无溢出钩子触发 | ⏸ **暂缓(2026-10-15 裁定) ** (目标值已按 09-21 实测修正,重开时可直接用) |
| S8 | 2.2 堆 48K→32K + 复测 | 全负载 `heap free ≥ 8KB` | ⏸ **暂缓(2026-10-15 裁定) ** ;⚠ 2026-10-12 B5 反向扩容至 56 KB,重开时须重跑 `stack_report.py` + 上板 `min_ever` |
| S9 | 1.5 小项 + 台账/门禁/AGENTS.md | `@build` + `@test` 全绿 | ◐ AGENTS.md 内存条目已增补(09-21);台账 / 趋势 / `mem-budget` 三件套 ✅ 2026-10-12( **常开,不受暂缓影响**) |
| S4 | §4. 1.4 lftpd 静态化 + §4. 2.1 malloc 修复 | FTP STOR/RETR 大文件 + 长路径回归 | ✅(§4. 2.1 于 2026-09-10、§4. 1.4 于 2026-09-21 完成) |
| S5 | §4. 1.1 LVGL 池 40K→16K | UI 全量回归(模拟器 + 实机),无断言失败 | ✅ **2026-10-15 ** (11 页黄金参考 + 池水位 5,448 B + `@test` 真机 `used=3192` ;实测峰值 6,568 B = 池的 40%,§2.6.2) |
| S6 | §4. 1.2 CCM( scatter + section + 门禁宏) | map 显示 `RW_CCM` ;关 `APP_CCM_ENABLE` 也能编译通过 | ⏸ **暂缓(2026-10-15 裁定) ** ;已完成部分仅 `RW_CCM` + 堆迁入(09-21),缓冲 section 迁移与门禁宏不再排期 |
| S7 | §4. 1.3 栈缩减(按 S3 数据)+ 复测 | 每任务 `free_min ≥ 新栈×30%` ,无溢出钩子触发 | ⏸ **暂缓(2026-10-15 裁定) ** (目标值已按 09-21 实测修正,重开时可直接用) |
| S8 | §4. 2.2 堆 48K→32K + 复测 | 全负载 `heap free ≥ 8KB` | ⏸ **暂缓(2026-10-15 裁定) ** ;⚠ 2026-10-12 B5 反向扩容至 56 KB,重开时须重跑 `stack_report.py` + 上板 `min_ever` |
| S9 | §4. 1.5 小项 + 台账/门禁/AGENTS.md | `@build` + `@test` 全绿 | ◐ AGENTS.md 内存条目已增补(09-21);台账 / 趋势 / `mem-budget` 三件套 ✅ 2026-10-12( **常开,不受暂缓影响**) |
每步都必须 `@build` 通过(0 错误 0 警告)后再进入下一步;涉及 UI 的步骤用 `simulator` 先过一遍再上实机。
@@ -629,7 +637,7 @@ ISR 余量:该任务运行期间可能被中断打断的最深中断链(SPI/
| NAND 页缓冲/BBT | `Drivers/BSP/GD5F2GQ5UE/nand_ftl.c:42-50` 、`gd5f2gq5ue.c:28-39` |
| FatFs | `Lib/FatFs/ffconf.h:32` ( FF_FS_TINY) |
| scatter | `MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct` |
| lftpd 缓冲/malloc | `Drivers/BSP/NET/lftpd/lftpd.c` (大缓冲已静态化,§1.4)、`lftpd_io.c` ( `malloc` →`pvPortMalloc` 已修,阶段 2.1) |
| lftpd 缓冲/malloc | `Drivers/BSP/NET/lftpd/lftpd.c` (大缓冲已静态化,§4. 1.4)、`lftpd_io.c` ( `malloc` →`pvPortMalloc` 已修,阶段 2.1) |
### 8.4 分析口径与数据来源(原《内存使用分析规划》并入)