970e574433
- CubeMX 加 TIM6(PSC=83 / ARR=999 / ARR 预装载 / NVIC 抢占优先级 6) - Src/main.c 回调加 TIM6 分支触发 CONVST(USER CODE 区) - app_main.c 用 HAL_TIM_Base_Start_IT(&htim6) 替换单次触发 - adc_task.c 退化为纯消费者(1 ms 轮询仅作消费节奏) - @build 0 错 0 警;台架 @test 验收待做 - 同步 ADC采样节拍设计说明 / 继电保护复现规划 / AGENTS / TPAFE README
636 lines
50 KiB
Markdown
636 lines
50 KiB
Markdown
# STM32F4-Base RAM 使用分析与优化方案(内存管理主文档)
|
||
|
||
> 版本:v1.2 日期:2026-09-21
|
||
> 变更: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`(静态调用图 × 运行时栈水位)+ 源码静态分析 + 网络调研
|
||
> **本文档是内存管理主文档**(2026-09-21 合并):原《STM32F4-Base 内存使用分析报告》(现状审计/快照)与《内存使用分析规划》(执行规划,已全部闭环)已并入本文;**区域放置与 DMA 可达性规范**单列于 `内存内容分配策略.md`(本文只引用、不重复)。
|
||
> 快照产物:`data/ram_report_<日期>.md`(静态台账)、`data/stack_report_<日期>.md`(栈预算)。
|
||
|
||
## 1. 背景与目标
|
||
|
||
`待处理内容.md` 已提出:"所有任务的内存都是往大的给的,导致内存不够用,要仔细排查内存的预算情况"。
|
||
|
||
分析起点(2026-09-11 快照,逐符号见 `data/ram_report_20260911.md`):
|
||
|
||
- 主 SRAM 128KB 中静态区曾占 **122.7KB**,余量仅 5.3KB
|
||
- FreeRTOS 堆 48KB 里装下 ~33.5KB 任务栈,可分配余量只有 ~12KB
|
||
- F407 另有 **64KB CCM RAM**:**已于 2026-09-21 启用**并把 FreeRTOS 堆迁入(§1.2)
|
||
- 起步时没有任何运行时测量手段:不知道每个任务实际用了多少栈,改栈全靠拍脑袋
|
||
|
||
当前状态(**以 §2.1 受管快照为准**,由 `.map` 自动生成):RW_IRAM1 **89,168 / 131,072 B(68.0%)**、RW_CCM **49,184 / 65,280 B(75.3%)**。✅ **任务栈:2026-10-10 削链后 `defaultTask` 实测峰值 2152B/4096(余 47%)、静态最坏链 2776B(余 32%)**,6 任务全过 `stack_report.py` 判据(曾于 2026-10-04 升至 3664B/4096 ≈ 10%,登记为 S-1,现已销项),详见 §2.3 与"2026-10-10 复测"小节。
|
||
|
||
本方案三个目标:
|
||
|
||
1. **可测量**:建立"每任务 RAM 消耗 + 每模块静态占用 + 堆余量"的常态化观测手段(0.1/0.2/0.3/0.6 已落地;0.4/0.5 可选项未做)
|
||
2. **降消耗**:通过 LVGL 内存池、任务栈、CCM RAM、驱动缓冲等手段释放 RAM
|
||
3. **可持续**:建立 RAM 预算台账与门禁,防止再次"往大了给"(§3 长效机制,部分为 `[计划]`)
|
||
|
||
## 2. 现状测量
|
||
|
||
### 2.1 总预算
|
||
|
||
> **本表由 `.map` 自动生成,勿手改**——改代码后跑 `python scripts/check_mem_numbers.py --write` 同步;
|
||
> 提交时 pre-commit `mem-numbers` 钩子会校验一致性(见 `docs/文档规范.md` §4)。
|
||
|
||
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
|
||
| 区域 | 用量 | 容量 | 占比 |
|
||
|---|---|---|---|
|
||
| ER_IROM1 | 268,644 B (262.35 KB) | 1,048,576 B (1024 KB) | 25.6% |
|
||
| RW_IRAM1 | 89,272 B (87.18 KB) | 131,072 B (128 KB) | 68.1% |
|
||
| RW_CCM | 49,184 B (48.03 KB) | 65,280 B (64 KB) | 75.3% |
|
||
| **Flash (ROM 合计)** | **269,404 B (263.1 KB)** | 1,048,576 B (1024 KB) | **25.7%** |
|
||
<!-- MEM_SNAPSHOT:END -->
|
||
|
||
各区的容量与常量来源:
|
||
|
||
| 项 | 说明 | 来源 |
|
||
|---|---|---|
|
||
| 主 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 |
|
||
| FreeRTOS 堆 (heap_4) | 48 KB(49,152 B),**位于 RW_CCM**;实测 `free=8,784 / min_ever=4,848` B | `Inc/FreeRTOSConfig.h:71` `configTOTAL_HEAP_SIZE` |
|
||
| C 库堆/栈 (startup) | 512 B / 1 KB,启动期 main() 用 | map `startup_stm32f407xx.o` HEAP/STACK 节 |
|
||
|
||
### 2.2 静态大项排行(2026-10-10 当前,完整台账 `data/ram_report_20261010.md`)
|
||
|
||
**RW_IRAM1(主 SRAM)**,按 ZI/RW 合计降序:
|
||
|
||
| # | 大小 (B) | 对象文件 | 内容 |
|
||
|---|---|---|---|
|
||
| 1 | 40,960 | `lv_mem_core_builtin.o` | LVGL 静态内存池 `ucMemPool[LV_MEM_SIZE]`(§2.6.2) |
|
||
| 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) |
|
||
| 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 封装静态区 |
|
||
| 9 | 1,608 | `freertos.o` | `default_fatfs`(含 512 B 扇区窗)+ `fs_work[512]` + RTOS 句柄 |
|
||
| 10 | 1,220 | `tasks.o` | FreeRTOS 内核调度静态区 |
|
||
| 11 | 1,024 | `startup_stm32f407xx.o` | C 库启动栈(仅启动期) |
|
||
| 12 | 704 | `cfg_file.o` | 配置引擎静态态(双端共享核心) |
|
||
| 13–17 | ≤560 | `spi.o` / `usart.o` / `lftpd_io.o` / `lv_init.o` / `net_socket.o` | 外设句柄、`FIL`、LVGL 全局、socket 表 |
|
||
| 其余 | ~1.7 KB | 约 25 个 .o | 小型 RW/ZI |
|
||
|
||
> **2026-10-10 栈整改带来的静态增量(+4,128 B,本质是"把 528 B 的 `FIL` 从栈搬到 BSS")**:
|
||
> `sys_log_mcu.o` 1,912 B(3 处 `FIL` 静态化 = 1,584 + 证据行缓冲 192)、`default_task.o` 2,712 B
|
||
> (卷探针 `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。
|
||
|
||
**静态合计(以 §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。
|
||
|
||
> **DMA 可达性**:第 4/6/7 项(NAND FTL、CH395F SPI2、GD5F BBT)为 DMA 缓冲,**必须留在主 SRAM**、禁止迁 CCM/SRAM3——权威清点与规则见 `内存内容分配策略.md`(本文不重复)。
|
||
> 2026-09-11 历史排行(`heap_4.o` 49,152 B 尚在 SRAM1、静态合计 122.7 KB 的旧布局)见 `data/ram_report_20260911.md`,仅供回溯。
|
||
|
||
### 2.3 任务栈(从 48KB 堆动态分配)
|
||
|
||

|
||
|
||
`Src/freertos.c` 中 `osThreadAttr_t.stack_size` 单位为**字节**(CubeMX 生成代码,`1024 * 4` 表示 4096 字节):
|
||
|
||
> ✅ **本表已于 2026-10-10 重新基线**(产品构建 `suite=NONE`,开机 + 空闲采样,静态链取 `stack_report.py`)。
|
||
> 同日**增列 `adcTask`**(B1:ADC 采集消费者,512 words,静态最坏链 504 B / 余 75%)。
|
||
> 此前 2026-10-04 的 V5 30 min 重采曾把 defaultTask 推到 **3664B/4096(余量 432B ≈ 10%,登记 S-1)**;
|
||
> 2026-10-10 的栈整改(**未抬栈**,见 §4 阶段 0 的"2026-10-10 复测")把它压回 **2152B/4096**,S-1 已销项。
|
||
|
||
| 任务 | 配置栈 | 字节 | 实际职责 | 2026-10-10 实测峰值 / 静态最坏链 |
|
||
|---|---|---|---|---|
|
||
| uiTask | `4096*4` | 16KB | LVGL 1bpp 离屏渲染(渲染缓冲在静态区,栈只走调用链) | 4016 B(余 75%)/ 静态 1336 B → 可大幅缩减 |
|
||
| netTask | `960*4` | 3.75KB | CH395F 轮询(DMA 搬运,栈压力小) | 920 B(余 76%)/ 静态 1872 B |
|
||
| ftpTask | `1024*4` | 4KB | lftpd FTP 服务 | 740 B(余 81%)/ 静态 1216 B |
|
||
| testTask | `2048*4` | 8KB | 测试套件(产品构建不创建;存储套件深链需要) | 未创建 / 静态 496 B |
|
||
| defaultTask | `1024*4` | 4KB | FatFS 挂载/探针 + 配置引擎 + syslog + 栈水位探针 | 2152 B(余 47%)/ 静态 2776 B |
|
||
| timeSyncTask | `512*4` | 2KB | TCP 时间同步 | 616 B(余 69%)/ 静态 **1024 B**(= ISR 底线,无再加深空间) |
|
||
| adcTask | `512*4` | 2KB | TPAFE5160 采样(1 ms 采样节拍触发 CONVST + 原子快照消费,B1 2026-10-10) | **496 / 2048**(余 1552 B / 75%,台架 `@test` 实测);静态 504 B |
|
||
| idle(内核) | `configMINIMAL_STACK_SIZE=128` | 0.5KB | 空闲 | 未观测(未取句柄) |
|
||
| timer(内核) | `configTIMER_TASK_STACK_DEPTH=256` | 1KB | 软定时器 | 未观测 |
|
||
|
||
> 任务栈全部从 FreeRTOS 堆(48KB,2026-09-21 已迁 CCM)分配。**"配置栈"列以 `Src/freertos.c` 为唯一来源;实测峰值/余量判定以 §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+ 栈。
|
||
3. **DMA 缓冲位置**:CH395F(SPI2 DMA)、GD5F(SPI1 DMA)、UART DMA 缓冲必须由 DMA 访问 → **只能放主 SRAM,不能放 CCM**(CCM 仅 CPU 可达,见 4.3)。
|
||
|
||
### 2.5 结论:紧张的三个根因(含 2026-09-11 审计量化)
|
||
|
||
| 根因 | 占用 | 可回收性 |
|
||
|---|---|---|
|
||
| ① LVGL 内存池 40KB 对 1bpp 小屏菜单过大 | 40KB | 高(目标 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 缩减) |
|
||
| ③ 64KB CCM 曾完全未利用,缓冲挤在主 SRAM | — | ✅ 2026-09-21 已启用(堆迁入 CCM),CCM 余量 ~16KB |
|
||
|
||
量化补充(来源:2026-09-11 审计快照,口径与方法见 §8.4):
|
||
|
||
- 两池(FreeRTOS 48KB + LVGL 40KB)曾占主 SRAM 的 **68%**,是当时"SRAM 几乎耗尽(95.4%,余 5.8KB)"的直接原因;**堆迁 CCM 后该结构性压力已解除**(现 RW_IRAM1 66.4%,见 §2.1)
|
||
- heap_4 **不合并相邻空闲块** → 长期运行需盯碎片,`min_ever`(现 4,848 B)是硬指标而非 `free`
|
||
- 未映射资源:SRAM3 64KB(scatter 未映射;clone 芯片可能无此区);CCM 已用
|
||
- 曾经的三条中风险项:新任务无处安置(已缓解)、堆碎片(仍存)、栈溢出检测缺失 → **已于阶段 0 开启 `configCHECK_FOR_STACK_OVERFLOW=2` 并实测抓到过溢出**(§0.1)
|
||
|
||
小项:FF_FS_TINY=0(每打开文件 512B 扇区窗,阶段 1.5 可改 1)、`ui_net_sink` PC 远程显示缓冲常驻 6.4KB(产品构建可加开关关闭,阶段 1.5);lftpd 栈局部变量 ✅ 2026-09-21 已静态化(§1.4)。
|
||
|
||
### 2.6 堆 / LVGL 池 / C 库 / Flash 明细
|
||
|
||
#### 2.6.1 FreeRTOS 堆(48 KB;2026-09-21 已迁 CCM `RW_CCM`)
|
||
|
||
`configTOTAL_HEAP_SIZE = 49,152`(heap_4)。内部构成:
|
||
|
||
| 组件 | 大小 | 依据 |
|
||
|---|---|---|
|
||
| 6 × 应用任务栈 | 38,912 B | 各 `stack_size` 求和(§2.3;2026-09-11 快照为 34,304 B) |
|
||
| 2 × 内核任务栈(idle/timer) | 1,536 B | `configMINIMAL_STACK_SIZE` / `configTIMER_TASK_STACK_DEPTH` |
|
||
| 8 × TCB | ~640 B | `sizeof(TCB_t) ≈ 76 B`(8 字节对齐后 80 B),逐字段见 `tasks.c:252-329` |
|
||
| netMsgQueue | ~448 B | `osMessageQueueNew(8, 48)` = `Queue_t` 56 B + 8×48 B |
|
||
| sys_evt 事件组 | ~24 B | `osEventFlagsNew` |
|
||
| **实测水位(2026-09-21,含 FTP 负载)** | `free = 8,784 B` / **`min_ever = 4,848 B`** | 串口 `[STACK] heap …` |
|
||
|
||
> lftpd 会话期间还会 `pvPortMalloc` 路径缓冲/DIR(<1 KB 瞬态)。**堆余量底线按 ≥4 KB 守住**(`min_ever` 是全负载历史最低点,比 `free` 更有约束力)。
|
||
|
||
#### 2.6.2 LVGL 内存池(40 KB)
|
||
|
||
| 参数 | 值 | 来源 |
|
||
|---|---|---|
|
||
| `LV_MEM_SIZE` | 40,960 B (40 KB) | `Lib/lv_conf.h`(模拟器 `simulator/lv_conf.h` 需同步) |
|
||
| 分配器 / 色深 / OS 抽象 | TLSF / 1 (1bpp) / 无 | 同上 |
|
||
|
||
池内装 UI 对象树、样式、字符串、动画对象,均为运行时分配;TLSF 元数据开销约 10~15%,净可用 ≈34~36 KB。**开机实测**(2026-09-21 日志):`menu build start: total=39476 free=37248` → `menu build done: used=2976`,即当前 UI 仅占约 **3 KB**——这是 §1.1 把池缩到 8~16 KB 的直接依据。静态无法估算池占用,需运行时 `LV_USE_MEM_MONITOR`(`lv_mem_monitor_t`)。
|
||
|
||
#### 2.6.3 C 库堆/栈(microlib)
|
||
|
||
| 组件 | 大小 | 说明 |
|
||
|---|---|---|
|
||
| 启动栈 | 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 库代码(Flash) | ~4.2 KB | printf / 浮点 / 字符串 |
|
||
|
||
#### 2.6.4 Flash 1 MB 分布
|
||
|
||
| 段 | 大小 (B) | 占比(对 1 MB) |
|
||
|---|---|---|
|
||
| Code (inc. data) | 246,456 | 23.5% |
|
||
| RO Data | 15,476 | 1.5% |
|
||
| RW 初值 | 728 | — |
|
||
| **合计 ROM** | **262,660** | **25.0%**(余 ~786 KB) |
|
||
|
||
> 本表随代码变化;权威数字与占比见 §2.1 受管快照(`ER_IROM1` / **Flash (ROM 合计)** 行)。
|
||
> 历史快照:2026-09-21 为 Code 234,976 / RO 13,824 / 合计 ROM 249,432(23.8%)。
|
||
|
||

|
||
|
||
> 2026-09-11 快照的模块级分解(LVGL ~72 KB / 应用+UI ~47 KB / FatFS+dhara ~11 KB / CH395F ~10 KB / HAL ~9 KB / C 库 ~4.3 KB / FreeRTOS ~4.6 KB)见 `data/ram_report_20260911.md`;重跑 `scripts/ram_report.py` 即得当前分解。
|
||
|
||
## 3. 网络调研摘要
|
||
|
||
| 主题 | 结论 | 来源 |
|
||
|---|---|---|
|
||
| 任务栈测量(高水位) | FreeRTOS 提供 stack "high water mark"(历史最小剩余栈),"you have data you can use to adjust the stack accordingly to avoid RAM being wasted" —— 官方创始人 Richard Barry 明确推荐的数据驱动定栈方法 | FreeRTOS 官方论坛 [Stack size calculation](https://forums.freertos.org/t/stack-size-calculation/4234)(rtel 2011-07-20 回复) |
|
||
| 栈溢出检测 | `configCHECK_FOR_STACK_OVERFLOW=2`:任务栈顶部预填特征值(0xEFFE...),每次上下文切换 + tick 时校验,溢出立即报告;方法 1 只查栈指针越界 | 论坛 [Stack Overflow Detection - Method 2](https://forums.freertos.org/t/stack-overflow-detection-method-2/17472);本工程内核源码 `Middlewares/.../include/stack_macros.h:36-122` 语义一致 |
|
||
| 堆测量 | `xPortGetFreeHeapSize()` / `xPortGetTotalHeapSize()` 随时可查;heap_4 支持 free 但有碎片化,需对比"理论余量 vs 实测 free"观察碎片 | FreeRTOS 内核源码 `heap_4.c`(工程内);[FreeRTOS 文档](https://docs.freertos.org/) |
|
||
| 运行时统计 | `vTaskGetRunTimeStats` 需 `configGENERATE_RUN_TIME_STATS=1` + `portCONFIGURE_TIMERS_FOR_RUN_TIME_STATS`(占用一个硬件定时器),给出每任务 CPU 占比 | 本工程内核 `task.h:1694-1756` 注释 |
|
||
| LVGL 缓冲 | 官方文档:PARTIAL 渲染模式"buffers can be smaller than the display to save RAM,**至少 1/10 屏**";单色 I1 格式缓冲 = `(W×H/8)+8`;双缓冲可并行渲染/搬运 | LVGL v9.2 文档 [Display interface](https://github.com/lvgl/lvgl/blob/release/v9.2/docs/porting/display.rst)(lvgl.io/docs/porting/display 的源文件) |
|
||
| STM32F4 CCM | F407 含 64KB CCM @0x10000000;**只能由 CPU 访问,DMA 不可达**,无 cache/ECC;适合纯 CPU 读写的大缓冲 | ST RM0090(STM32F405/407/415/417/42x 参考手册)§3.1.1 |
|
||
| FatFs | `FF_FS_TINY=1`:FIL 结构体去掉 `buf[FF_MAX_SS]` 私有扇区窗,改用卷内共享 9 字节窗口(FAT32),每打开文件省 512B | 本工程 `Lib/FatFs/ff.h` FIL 结构注释、`Lib/FatFs/ffconf.h:32` |
|
||
| 静态占用测量 | Keil map 文件 "Image component sizes" 给出每个 .o 的 RO/RW/ZI 精确字节数;`fromelf --image_info xxx.axf` 可输出各 Region 用量 | 本工程 `STM32F407-Demo.map` "Image component sizes" 表(行号随每次重建变化,勿引用行号) |
|
||
|
||
> 注:freertos.org 官网页面为 JS 动态渲染,直接抓取拿不到正文;上表结论通过官方论坛 JSON API、GitHub 文档源文件与**工程内内核源码**(V10.3.1 头文件自带完整 API 注释)三方交叉核实。
|
||
|
||
## 4. 方案
|
||
|
||
### 阶段 0:建立测量能力(先做,1~2 天)
|
||
|
||
> 原则:没有数据不动刀。本阶段全部是"只增不减"的改动,零功能风险。
|
||
|
||
#### 0.1 栈溢出检测(防身)
|
||
|
||
`Inc/FreeRTOSConfig.h` 增加:
|
||
|
||
```c
|
||
#define configCHECK_FOR_STACK_OVERFLOW 2 /* 特征值法,每次上下文切换+tick 校验 */
|
||
#define configUSE_MALLOC_FAILED_HOOK 1 /* 堆分配失败立即可见 */
|
||
```
|
||
|
||
`Src/main.c` USER CODE 区实现钩子:
|
||
|
||
```c
|
||
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName)
|
||
{
|
||
DBG_ERROR("STACK OVERFLOW: task=%s", pcTaskName);
|
||
/* 挂起或复位,按调试需要 */
|
||
for (;;);
|
||
}
|
||
void vApplicationMallocFailedHook(void)
|
||
{
|
||
DBG_ERROR("HEAP ALLOC FAILED (free=%u)", (unsigned)xPortGetFreeHeapSize());
|
||
for (;;);
|
||
}
|
||
```
|
||
|
||
成本:每次上下文切换几十周期,不占 RAM。
|
||
|
||
#### 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()`(剩余字节,内部走
|
||
`uxTaskGetStackHighWaterMark`,特征值法与 `configCHECK_FOR_STACK_OVERFLOW=2` 同源)。
|
||
本版**没有** `osThreadGetStackSize` 实现,也**没有** `ulTaskGetStackDepth` /
|
||
`xPortGetTotalHeapSize`,因此配置栈统一取各任务 `*_attributes.stack_size`(唯一定义处,不重复常量)。
|
||
|
||
行为:挂在 defaultTask 主循环——首次 5s 后报告一次(bring-up 深链 mount/mkfs/探针/cfg/syslog
|
||
均已跑过),此后每 2s 采样,**只在出现更深峰值时打印**:既不破坏串口静默窗口,也不漏掉
|
||
之后才走到的更深分支(卷修复、配置保存等)。输出纯 ASCII,便于 `@test` 采集:
|
||
|
||
```
|
||
[STACK] <task> peak_used=<B>/<cfg> free_min=<B> (<pct>%)
|
||
[STACK] heap free=<B> min_ever=<B>
|
||
```
|
||
|
||
要点:
|
||
|
||
- `uxTaskGetStackHighWaterMark` 返回**任务创建以来**的最小剩余栈(单位 word=4B);`used = 配置 − 剩余` 即该任务历史峰值栈占用。
|
||
- **测量必须覆盖全功能路径**(高水位是历史最小值,只反映跑过的路径):全菜单遍历、FTP 上传/下载大文件、CH395F 断链重连、NAND 格式化、时间同步。建议 `@test` 全回归 + 1 小时浸泡。
|
||
- 未覆盖 idle/timer(内核静态小栈、纯循环,未取句柄);构建期静态侧见 §0.6。
|
||
|
||
#### 0.3 map 静态占用报告脚本
|
||
|
||
脚本 `scripts/ram_report.py`(Python,已随仓库提供):
|
||
|
||
- 解析 `MDK-ARM/STM32F407-Demo/STM32F407-Demo.map`:
|
||
- RW_IRAM1 Region 的 `Size` / `Max` → 主 SRAM 静态水位
|
||
- "Image component sizes" 表 → 每 .o 的 ZI/RW 排序 Top20
|
||
- 用法:`& '.venv\Scripts\python.exe' scripts/ram_report.py -o docs/内存管理/data/ram_report_<日期>.md`,把结果贴进改动说明
|
||
- (待补)与基线 TSV 做 diff,任何对象 ZI 增量 >1KB 即高亮
|
||
|
||
#### 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() 启动期栈峰值
|
||
|
||
启动期跑在 startup 的 1KB C 栈上(CubeMX 初始化 + 各驱动 init,含 BBT 全扫描)。ARMCC 可用 `__current_stack_usage()` 在 `app_main_init` 末尾打点,确认 1KB 够用;不够则调 scatter 的 `STACK` 节(与任务栈无关,单独预算)。
|
||
|
||
#### 0.6 静态栈分析 + 汇总工具(构建期,零目标依赖;2026-09-21 落地)
|
||
|
||
**UV4 每次构建已自动生成静态调用图** `MDK-ARM/STM32F407-Demo/STM32F407-Demo.htm`
|
||
(标题 "Static Call Graph",随 `@build` 刷新,**无需**给 armlink 加 `--callgraph`)。每个函数
|
||
条目自带 `Stack size N bytes`(该函数自身帧)与 `Max Depth = N`(其子树最坏累计深度)
|
||
以及完整调用链——即"不烧板子也能看到最坏链有多深、多宽"。
|
||
|
||
**用法:`scripts/stack_report.py`**(纯标准库,任意 python3 均可;解析上述 HTM +
|
||
`Src/freertos.c` 的栈配置,带 `-s` 时叠加串口 `[STACK]` 运行时水位):
|
||
|
||
```powershell
|
||
# 最小用法(只看静态:每任务最坏链 + 余量判定)
|
||
& '.venv\Scripts\python.exe' scripts/stack_report.py
|
||
|
||
# 推荐用法(静态 + 实测一起判,判定取两者更坏者;报告归档到 data/)
|
||
& '.venv\Scripts\python.exe' scripts/stack_report.py -s .dsh\skills\serial-assistant\scripts\state\serial.log `
|
||
-o 'docs\内存管理\data\stack_report_<日期>.md'
|
||
```
|
||
|
||
| 参数 | 默认 | 说明 |
|
||
|---|---|---|
|
||
| `-i/--html` | `MDK-ARM/STM32F407-Demo/STM32F407-Demo.htm` | 静态调用图(Keil 构建产物) |
|
||
| `-s/--serial` | 无 | 串口日志,解析其中 `[STACK]` 行(daemon 全量历史:`.dsh/skills/serial-assistant/scripts/state/serial.log`) |
|
||
| `-o/--output` | 无(打印到 stdout) | 报告输出文件(Markdown) |
|
||
| `--isr-margin` | `1024` | ISR 嵌套预留(字节) |
|
||
| `--min-free-pct` | `30` | 要求的最小余量百分比 |
|
||
|
||
- **判定规则**:`余量 = 配置 − max(静态最坏深度, 实测峰值)`,须 `≥ max(1024 B, 配置×30%)`;任一任务不达标 → 脚本退出码 **1**(可挂门禁),全达标 → 0,HTM 缺失 → 2。
|
||
- **何时跑**:① 改栈或加大局部变量前后;② 改 FatFs/lftpd/LVGL 等第三方配置后;③ 任务职责变化时。串口日志覆盖的路径越全越准(路径覆盖要求见 §0.2)。
|
||
- **报告内容**:每任务预算表(配置/静态/实测/最坏/余量/判定)、各任务最坏调用链、帧宽 Top15(找"宽度大户")、堆 free/min_ever。
|
||
|
||
首份基线(2026-09-21,产品构建 + FTP 会话实测;产物 `docs/内存管理/data/stack_report_20260921.md`):
|
||
|
||
| 任务 | 配置 (B) | 静态最坏 (B) | 实测峰值 (B) | 最坏 (B) | 余量 | 判定 |
|
||
|---|---|---|---|---|---|---|
|
||
| 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** |
|
||
| netTask | 4096 | 440 | 588 | 588 | 3508 (85%) | PASS |
|
||
| uiTask | 16384 | 912 | 3824 | 3824 | 12560 (76%) | PASS |
|
||
| timeSyncTask | 2048 | 336 | 420 | 420 | 1628 (79%) | PASS |
|
||
| testTask(产品构建) | 8192 | 168 | 348 | 348 | 7844 (95%) | PASS |
|
||
|
||
实测堆:`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)。
|
||
|
||
**静态与实测是互补的两套数据(必须都留)**:
|
||
|
||
- 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/上下文量级)。
|
||
- 本次可观测性的第一个产出不是数字而是**缺陷**:ftpTask 因就绪位被其它等待者消费而**从未启动**(21 端口一直无监听),修好后才有了上面这组数据——详见 `docs/测试说明/FTP_Test_Guide.md` §8.8。
|
||
|
||
要点与边界:
|
||
|
||
- 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)。
|
||
- **静态分析的天然盲区**:递归/互递归(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 单客户端复用,探测会占掉唯一会话(见 FTP 指南 §8.9)。
|
||
|
||
#### 2026-10-10 复测:**不抬栈**把 defaultTask 从 FAIL 拉回 PASS(S-1 销项)
|
||
|
||
触发点:登记簿 S-1(2026-10-04 实测 defaultTask 3664B/4096,余量 10%)。叠加静态链后判据更严——
|
||
**静态最坏链已涨到 5312 B > 配置 4096 B(margin −1216 B)**。本次刻意**不动栈值**,改为削链:
|
||
|
||
| 任务 | 配置 (B) | 静态最坏 改前 → 改后 | 实测峰值 改前 → 改后 | 判定 |
|
||
|---|---|---|---|---|
|
||
| **defaultTask** | 4096 | **5312 → 2776** | 3392 → **2152** | FAIL → **PASS(余 1320 B / 32%)** |
|
||
| netTask | 3840 | 2664 → 1872 | 1008 → 920 | PASS(51%) |
|
||
| ftpTask | 4096 | 1800 → 1216 | 756 → 740 | PASS(70%) |
|
||
| uiTask | 16384 | 1536 → 1336 | 4024 → 4052 | PASS(75%) |
|
||
| **timeSyncTask** | 2048 | **1736 → 1024** | 760 → 616 | FAIL → **PASS(余 1024 B = ISR 底线,正好卡住)** |
|
||
| testTask | 8192 | 928 → 496 | —(产品构建不创建) | PASS(93%) |
|
||
|
||
四个杠杆(按收益排序,只动自研代码,第三方 dhara/FatFS/HAL 一行未改):
|
||
|
||
1. **删旧档 NCFG 迁移**(`Drivers/BSP/NETCFG/net_cfg_legacy.c/h` 整体移除,链 −600 B):
|
||
`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 → FTL`)
|
||
挂进启动期调用图。本仓库无携带 `/cfg/netcfg.bin` 的设备 → 按"不保留旧档兼容"处置;
|
||
旧档若存在则**忽略**(不读、不删、不迁移),配置回落工厂默认。
|
||
2. **6 处局部 `FIL` 改函数级 `static`**(每处 −528 B):`Lib/FatFs/ffconf.h` `FF_FS_TINY=0` →
|
||
`FIL` 内含 `buf[FF_MAX_SS]` = 512 B。落点 `App/log/sys_log_mcu.c`(3 处)、
|
||
`App/cfg/cfg_file_store_mcu.c`(2 处)、`App/task/default_task.c` 探针宏(1 处)。
|
||
安全性依据:整个 FIL 生命周期都在 `vol_lock_wait()/release()` 区间内(跨任务被卷锁串行化)
|
||
+ 配置引擎/syslog 写者单任务属主(P1-3)。先例 = 2026-09-21 lftpd 9 处大局部缓冲静态化。
|
||
3. **日志行缓冲按模块收紧 + 时间戳缓冲并入行缓冲**(每个日志调用点原内联 `192 + 32 = 224 B`):
|
||
`dbg_log.h` 新增 `DBG_LINE_BUF_MAX`(默认仍 = `DBG_CON_LINE_MAX` 192),深链模块
|
||
(`nand_ftl.c`/`gd5f2gq5ue.c`/`cfg_file.c`/`net_cfg.c`/`net_socket.c`/`ch395f.c`/`time_sync.c`/
|
||
`default_task.c`)收到 **128**;`DBG_TS_BUF_MAX` 32 → 24 且与行缓冲共用一个数组。
|
||
实测链上 9 个帧各含一份行缓冲 → **仅这一项就占 defaultTask 最坏链约 1.5 KB**。
|
||
⚠ 取值须 ≥ 该模块最长日志行(各文件注释里已给出实测行长),否则该行被截断。
|
||
4. **内核钩子脱离 HAL 发送路径**:`configASSERT` 在 FreeRTOS 内部可达 → `vApplicationAssertFailed`
|
||
挂在**每个**任务的最坏链尾;它原走 `dbg_console_write_raw → con_tx_bounded →
|
||
(DWT 不可用时)HAL_UART_Transmit → UART_WaitOnFlagUntilTimeout`。现 `con_tx_bounded` 的
|
||
DWT 回退改为**指令计数上界**(`CON_TX_GUARD_MAX`),HAL 尾巴从所有链上消失;钩子自身
|
||
`name[49]/num[16]/rev[16]` 改 static + 原地反转、去掉 `HAL_Delay`(断言现场 tick 未必可信)。
|
||
|
||
代价与复核:静态 BSS **+4,128 B**(ZI 133,524 → 137,652;RW_IRAM1 64.9% → 68.1%),Flash 反而 −36 B;
|
||
FreeRTOS 堆不变(`free=17248 / min_ever=9312`)。验证链:`@build` 0/0 → `@test` 真机 PASS(含烧录回读)
|
||
→ `stack_report.py` **6/6 PASS**。
|
||
|
||
顺带揪出并修掉的**两个静态虚高源**:① `dbg_console_drain` 故障路径调 `syslog_post` →
|
||
`write_line → con_append → drain → syslog_post → write_line` **调用图成环**(运行时有
|
||
`s_con_draining` 重入标志挡着不会真递归,但 armlink 把整条日志尾巴在每个任务上多叠一层)——
|
||
现改为置标志、由写者任务在 drain 之外补记 run.log(`dbg_console_tx_fault_log_take()`);
|
||
② 上述 HAL 发送回退。**教训:静态链异常高时先查"链尾是不是日志/钩子",再动业务代码。**
|
||
|
||
**留作候选、本轮刻意未做**:① **共享静态行缓冲池**(`dbg_console_line_claim()/release()` 发还栈上数组,改为从 2 个静态槽领取)——可再削每条日志帧的全部 152 B(全链再省 ~1.3 KB),但把"日志缓冲在调用者栈上"改成"跨任务共享 + 抢占失败即丢行",**改变日志并发语义**,需配套主机侧单测才动;② `timeSyncTask` 静态余量已正好等于 1024 B ISR 底线,**该链上再加任何调用都必须先抬栈**(已在 `Src/freertos.c` 栈注释里写明)。
|
||
|
||
#### 阶段 0 验收
|
||
|
||
- 串口可见 `[STACK]` 报告:每任务 `peak_used/free_min` + 堆 `free/min_ever`
|
||
- 人为制造栈溢出(临时把某任务栈改到 64 字节)能触发 `STACK OVERFLOW` 钩子
|
||
- `scripts/ram_report.py` 输出 .map 静态占用 Top20;`scripts/stack_report.py` 输出每任务静态余量(可叠加运行时水位)
|
||
|
||
### 阶段 1:静态大项削减(收益最大,3~5 天)
|
||
|
||
#### 1.1 LVGL 内存池 40KB → 16KB → 8KB(省 24~32KB)
|
||
|
||
- 依据:屏幕仅 160×160×1bpp,菜单为轻量控件树;LVGL 内存池只装对象/样式/字符串缓冲,与像素缓冲无关。40KB 来自模板默认值,无适配依据。
|
||
- 做法:`Lib/lv_conf.h:34` `LV_MEM_SIZE` 先改 `(16*1024)`,跑 UI 全量回归(PC 模拟器 + 实机),无 `LV_ASSERT_MALLOC`/分配失败再试 8KB。
|
||
- 同步在 `simulator/lv_conf.h:26` 保持一致(两端共用 UI 源码,配置必须同步)。
|
||
- 验证:UI 全菜单遍历 + 动画/滚动/输入框 + 中文字体渲染无缺字;LVGL 内置内存监控(`LV_USE_MEM_MONITOR`,调试构建开启)观察池占用峰值。
|
||
- 风险:个别复杂控件/长字符串可能吃紧 → 若 8KB 不够就停在 16KB,不勉强。
|
||
|
||
#### 1.2 启用 CCM RAM(+64KB 预算空间)
|
||
|
||
依据 RM0090 §3.1.1:F407 的 64KB CCM @0x10000000 仅 CPU 可访问(DMA 不可达、无 cache)。
|
||
|
||
**进度(2026-09-21)**:第一阶段已落地——scatter 新增 `RW_CCM` 区(`sct:25`,注释 "FreeRTOS heap only")并把 **FreeRTOS 堆整体迁入**(49,184 B,CCM 占用 75.3%)。下表缓冲迁入清单为**后续剩余项**(LVGL 池/UI 缓冲现仍在主 SRAM);`APP_CCM_ENABLE` 门禁宏**尚未实现**(scatter 无开关,见下"风险门禁")。
|
||
|
||
**迁入 CCM 的缓冲(全部纯 CPU 读写):**
|
||
|
||
| 缓冲 | 大小 | 方式 |
|
||
|---|---|---|
|
||
| LVGL 内存池 `ucMemPool`(`LV_MEM_ATTR` 挂载 section) | 8~16KB | `Lib/lv_conf.h` 定义 `LV_MEM_ATTR __attribute__((section("CCM_DATA")))` |
|
||
| `g_ui_fbuf` + `g_ui_draw_buf`(ui_port.c:16,18) | 6.4KB | `__attribute__((section("CCM_DATA")))` |
|
||
| `g_tx` + `g_lcdmem_data`(ui_net_sink.c:31,33) | 6.4KB | 同上 |
|
||
| `s_bbt`/`s_bbt_best`(gd5f2gq5ue.c:28,39) | 0.5KB | 同上(小,顺手) |
|
||
|
||
**严禁迁入(DMA 缓冲,必须留主 SRAM):** CH395F SPI DMA 发/收缓冲(ch395f.c:42,44)、GD5F 页缓冲(nand_ftl.c:42-50,SPI DMA 传输)、所有 UART/SPI DMA 缓冲。
|
||
|
||
**scatter 文件**(`MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct`)增加:
|
||
|
||
```
|
||
RW_CCM 0x10000000 0x00010000 { ; CCM 64KB, CPU-only(2026-09-21 实际落地区名)
|
||
*(CCM_DATA)
|
||
}
|
||
```
|
||
|
||
**风险门禁**:scatter 注释已提示"clone MCU 可能缺 SRAM3",同理 CCM 在克隆芯片上不可靠 → 加编译开关 `APP_CCM_ENABLE`(`App/app/app_cfg.h`),关闭时 `LV_MEM_ATTR` 等定义退化为空,缓冲全部落回主 SRAM(阶段 1.1 池缩到 16KB 后主 SRAM 也放得下)。真 ST 芯片默认开。
|
||
|
||
验证:map 中出现 `RW_CCM` 且 `*(CCM_DATA)` 各对象地址落在 0x10000000~0x100FFFF;`ram_report.py` 同时输出两个 Region 水位。
|
||
|
||
#### 1.3 任务栈按实测数据缩减(省 ~17KB;目标值已按 2026-09-21 实测修正)
|
||
|
||
定栈规则(写入台账,长期执行):
|
||
|
||
```
|
||
新栈 = max( 实测峰值 used × 1.5 , 1024B ) + ISR 余量
|
||
ISR 余量:该任务运行期间可能被中断打断的最深中断链(SPI/UART/CH395F EXTI),
|
||
统一按 +1KB 计;中断执行在当前任务栈上,必须计入。
|
||
```
|
||
|
||
目标值(2026-09-21 实测数据修正):
|
||
|
||
| 任务 | 现值 | 目标 | 依据(max(最坏×1.5, 1024B) + 1KB ISR 余量) |
|
||
|---|---|---|---|
|
||
| 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 余量并复测 |
|
||
| 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,无缩减空间 |
|
||
|
||
缩减后任务栈合计 38,912 → 21,504 B(**省 17,408 B / ~17 KB**,全部在 CCM 堆内,主 SRAM 不受影响)。
|
||
|
||
每改一批 → `@build` + 全功能回归 + 复测高水位(`free_min` 应 ≥ 新栈×30%),不满足则回调。
|
||
|
||
#### 1.4 lftpd 大栈局部变量 → 静态缓冲(✅ 2026-09-21 完成;ftpTask 峰值 3480 → 1184 B)
|
||
|
||
已把 `lftpd.c` / `lftpd_log.c` 中 **9 个大局部缓冲改为函数级 `static`**(lftpd 单客户端,全部调用与日志都在 ftpTask 内串行执行,静态化安全;各函数**各自独立缓冲**,无嵌套/别名冲突):
|
||
|
||
| 位置 | 原栈占用 (B) | 说明 |
|
||
|---|---|---|
|
||
| `lftpd.c:69,70` `send_response` | 256 + 512 | 每条响应都用到 |
|
||
| `lftpd.c:111,136` `send_list` / `send_nlst` | 256 × 2 | LIST/NLST 目录名 |
|
||
| `lftpd.c:158,187` `send_file` / `receive_file` | **1024 × 2** | 传输缓冲(原峰值主因) |
|
||
| `lftpd.c:553,587` `handle_control_channel` | 512 + 512 | 命令行 / 参数(`arg` 指向它,静态化后生命周期只增不减) |
|
||
| `lftpd.c:652` `lftpd_start` 的 `client` 上下文 | ~264 | `lftpd_client_t` 会话上下文 |
|
||
| `lftpd_log.c:9` `lftpd_log_internal` | 256 | 深层日志点 |
|
||
|
||
- **结果**:ftpTask 实测峰值 **3480 → 1184 B**(余量 616 → **2912 B / 71%**,判定 PASS);静态调用图 2528 → 472 B;帧宽榜中 lftpd 全部退出前 15。
|
||
- **代价(2026-09-21 实施时)**:静态 BSS **+4,616 B**(RW_IRAM1 77,432 → 82,048 B / 128 KB;CCM 与 Flash 不变)。
|
||
- **验证**:`@build` 0/0 → 烧录 → `@test` PASS → `ftp_up_dl_test.py` 10/10 PASS + `ftp_speed_test.py --size 128` 逐字节一致(功能与搬迁前完全一致,反证无别名/重入问题)。
|
||
- **结论:原 ① 方案(ftpTask 涨到 1600 words)不再需要**,栈保持 1024 words(4096 B)。
|
||
|
||
> lftpd 属第三方代码(pre-commit 排除范围),只做最小侵入改动(每处仅加 `static` + 单行注释),改动点在本台账登记。
|
||
|
||
#### 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` 编译开关,产品固件可关;保留时缓冲已迁 CCM(1.2)
|
||
- `CH395F_TCP_MSS=1024` 已是压缩值,DMA 缓冲 `MSS×2+4` 为协议决定,不动
|
||
|
||
### 阶段 2:堆与动态分配治理(1~2 天)
|
||
|
||
#### 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
|
||
|
||
阶段 1 完成后复测:堆内需容纳 ≈ 栈(15KB) + TCB(2.3KB) + 队列/事件组(0.5KB) + lftpd 临时分配(1KB) + 余量。`configTOTAL_HEAP_SIZE` 49152 → **32768**,要求全功能负载下 `heap free ≥ 8KB` 持续成立(浸泡 1 小时验证)。
|
||
|
||
#### 2.3 (可选,进阶)任务/队列全静态化
|
||
|
||
`configSUPPORT_STATIC_ALLOCATION=1` 已开。用 `osThreadNew` 的 `attr.cb_mem`(`StaticTask_t` + 静态栈数组)可把栈完全移出堆,预算 100% 透明、堆只剩小对象分配。改动面涉及 CubeMX 生成的 `freertos.c` 属性块,收益(~15KB 确定性)与改动成本权衡后决定,**不作为必做项**。
|
||
|
||
### 阶段 3:长效机制(0.5 天 + 长期)
|
||
|
||
1. **RAM 预算台账 `[计划]`**(模板见 §8.2;落地文件 `docs/内存管理/RAM预算台账.md` **尚未创建**):记录每个 Region/缓冲/栈的计划值、实测值、变更历史;每次改内存相关代码必须更新
|
||
2. **AGENTS.md 增补**(✅ 2026-09-21 已完成:"内存预算紧张"条目已含 `ram_report.py`/`stack_report.py` 数据源与"改栈前后各跑一次"要求)
|
||
3. **pre-commit 门禁**(可选):钩子解析最新 map,RW_IRAM1 使用率 >92% 或 RW_CCM 超限 → 提交拦截并提示
|
||
4. **@test 扩展**:`verify_log.py` 增加可选 Required 标记 `"[MEM] heap free"`,把堆余量下限纳入自动化回归
|
||
5. **map diff 常态化**:每次 `@build` 后跑 `ram_report.py`,ZI 增量 >1KB 的改动必须在提交说明中解释
|
||
|
||
## 5. 预期效果
|
||
|
||
| 项目 | 现状(2026-09-21 实测,**主 SRAM 数字已随代码更新,以 §2.1 为准**) | 剩余项全部落地后(1.1/1.3/1.5/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 堆) | ~56KB / 64KB(堆 32K + 池 16K + UI 缓冲 + BBT) |
|
||
| 任务栈合计 | 38,912 B(38.0KB) | ~21.5KB(§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)。
|
||
> 估算假设:LVGL 池缩到 16KB(8KB 未验证)且迁 CCM;`ui_net_sink` 产品构建关闭;`ui_port` 帧缓冲 + BBT 位图迁 CCM;堆缩 48K→32K。落地顺序与每步验证见 §6。
|
||
|
||
## 6. 实施顺序与验证
|
||
|
||
| 步骤 | 内容 | 验证 | 状态 |
|
||
|---|---|---|---|
|
||
| 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 全量回归(模拟器 + 实机),无断言失败 | 待办 |
|
||
| S6 | 1.2 CCM(scatter + section + 门禁宏) | map 显示 `RW_CCM`;关 `APP_CCM_ENABLE` 也能编译通过 | ◐ `RW_CCM` + 堆迁入已完成(09-21);缓冲 section 迁移与门禁宏待办 |
|
||
| S7 | 1.3 栈缩减(按 S3 数据)+ 复测 | 每任务 `free_min ≥ 新栈×30%`,无溢出钩子触发 | 待办(目标值已按 09-21 实测修正) |
|
||
| S8 | 2.2 堆 48K→32K + 复测 | 全负载 `heap free ≥ 8KB` | 待办 |
|
||
| S9 | 1.5 小项 + 台账/门禁/AGENTS.md | `@build` + `@test` 全绿 | ◐ AGENTS.md 内存条目已增补(09-21);台账/门禁待办(§3) |
|
||
|
||
每步都必须 `@build` 通过(0 错误 0 警告)后再进入下一步;涉及 UI 的步骤用 `simulator` 先过一遍再上实机。
|
||
|
||
## 7. 风险与注意事项
|
||
|
||
| 风险 | 说明 | 缓解 |
|
||
|---|---|---|
|
||
| 栈定小 → 溢出 | 高水位只覆盖跑过的路径;中断嵌套深度难预估 | 1.5× 系数 + 1KB ISR 余量 + 溢出钩子 + 全功能回归后才定稿 |
|
||
| CCM 依赖真芯片 | 克隆 MCU 可能无 CCM;DMA 缓冲误入 CCM → 传输全错 | `APP_CCM_ENABLE` 门禁;DMA 缓冲清单硬性排除;map 地址落点检查 |
|
||
| LVGL 池缩小 → 分配失败 | 复杂界面/长字符串吃池 | 分步降(40→16→8)+ 每步全 UI 回归 + `LV_USE_MEM_MONITOR` 观测 |
|
||
| 高水位低估 | 启动后未触发的路径(如格式化失败分支)没算进去 | 测试套件覆盖异常分支;保留 ≥30% free_min 底线 |
|
||
| 第三方代码改动 | lftpd 被 pre-commit 排除,改动不受检查 | 最小侵入、台账登记、升级 lftpd 时需重放补丁 |
|
||
| CubeMX 重生成 | `freertos.c`/`FreeRTOSConfig.h` 的 USER CODE 区受保护,但 CubeMX 改配置会覆盖部分生成区 | 所有内核宏改动集中在 USER CODE 区或 `app_cfg.h`;`.ioc` 与代码双向核对 |
|
||
| 编码安全 | 新增 .c/.h 为 UTF-8 无 BOM;`Drivers/BSP/UI/*` 为 UTF-8 带 BOM | 遵守 AGENTS.md"源码编辑与编码安全"铁律:只用 Edit 工具改,不整文件重写 |
|
||
|
||
## 8. 附录
|
||
|
||
### 8.1 参考链接
|
||
|
||
- FreeRTOS 论坛(官方):[Stack size calculation](https://forums.freertos.org/t/stack-size-calculation/4234) · [Stack Overflow Detection - Method 2](https://forums.freertos.org/t/stack-overflow-detection-method-2/17472) · [checking stack usage](https://forums.freertos.org/t/checking-stack-usage/7703)
|
||
- FreeRTOS 文档站:https://docs.freertos.org/ (官网旧页为 JS 渲染,正文以工程内 V10.3.1 内核头文件注释为准)
|
||
- LVGL v9.2 文档源:[docs/porting/display.rst](https://github.com/lvgl/lvgl/blob/release/v9.2/docs/porting/display.rst)(Draw buffers / Monochrome 章节)
|
||
- STM32F4 参考手册 RM0090 §3.1(Memory map / CCM SRAM):https://www.st.com/resource/en/reference_manual/cd00901901-stm32f40-5xx-and-stm32f42-7xx-advanced-connection-line-arm-based-32bit-mcus-st-stmicroelectronics.pdf
|
||
- FatFs(工程内 `Lib/FatFs/ff.h`、`ffconf.h` 配置注释)
|
||
|
||
### 8.2 RAM 预算台账模板
|
||
|
||
```
|
||
| Region/项目 | 计划值 | 实测值(map/串口) | 承载内容 | 变更历史(日期/人/原因) |
|
||
|---|---|---|---|---|
|
||
| RW_IRAM1 静态(除堆) | ≤75KB | | LVGL UI 缓冲、DMA 缓冲、FTL、BBT、内核 | |
|
||
| FreeRTOS 堆 | 32KB | | 6 任务栈+TCB+队列+lftpd 临时 | |
|
||
| └ 任务栈小计 | ~15KB | | 见 freertos.c 属性表 | |
|
||
| └ 堆余量底线 | ≥8KB | | 全负载 free 底值 | |
|
||
| RW_CCM(CCM) | ≤32KB | | LVGL 池、UI 帧缓冲、BBT 位图(2026-09-21 起先承载 FreeRTOS 堆) | |
|
||
| C 库堆/栈 | 512B/1KB | | 仅启动期;lftpd 不得再用 | |
|
||
```
|
||
|
||
### 8.3 关键配置位置速查
|
||
|
||
| 配置 | 位置 |
|
||
|---|---|
|
||
| 堆大小 | `Inc/FreeRTOSConfig.h:71` |
|
||
| 栈溢出检测 | `Inc/FreeRTOSConfig.h`(新增,阶段 0) |
|
||
| 任务栈 | `Src/freertos.c:61-101`(各 `osThreadAttr_t.stack_size`,字节) |
|
||
| idle/timer 栈 | `FreeRTOSConfig.h:70,94` |
|
||
| LVGL 池 | `Lib/lv_conf.h:34`(`simulator/lv_conf.h:26` 同步) |
|
||
| UI 屏参/缓冲 | `Drivers/BSP/UI/ui_port.h:21-23`、`ui_port.c:16-18` |
|
||
| 远程 UI 缓冲 | `Drivers/BSP/UI/ui_net_sink.c:28-33` |
|
||
| CH395F MSS/DMA 缓冲 | `Drivers/BSP/CH395F/ch395f.h:119,137`、`ch395f.c:42-44` |
|
||
| 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) |
|
||
|
||
### 8.4 分析口径与数据来源(原《内存使用分析规划》并入)
|
||
|
||
**芯片物理内存**(数据源 RM0090 §3.1、`STM32F407-Demo.sct`):Flash 1024 KB @`0x0800_0000`;SRAM1 112 KB + SRAM2 16 KB 构成 `RW_IRAM1` 的 128 KB @`0x2000_0000`;SRAM3 64 KB @`0x2002_0000` **未映射**(clone 芯片可能无此区);CCM 64 KB @`0x1000_0000` 仅 CPU 可达(**DMA 不可达**)。区域放置规则见 `内存内容分配策略.md`。
|
||
|
||
**口径**:
|
||
|
||
- **静态区** = `RW_IRAM1` / `RW_CCM` 内由链接器直接分配的 RW + ZI(含两个池的本体、驱动缓冲、C 库 HEAP/STACK 节)
|
||
- **运行时动态区** = 两个池**内部**的再分配(任务栈 + TCB + 队列;LVGL 对象/样式/字符串)——池本体已在静态区全额计入,**内部细分不重复加总**,只做占比说明
|
||
- **Flash** = `Total ROM Size`(Code + RO + RW 初值)
|
||
|
||
**四视角 + 交叉校验**:
|
||
|
||
```
|
||
V1 链接视图(map:Grand Totals / Image component sizes / Symbol Table)
|
||
← 静态数字的唯一权威来源
|
||
│ 交叉校验
|
||
├── V2 运行时堆视图(手算 48KB 池占用;分配点由源码遍历确认)
|
||
├── V3 LVGL 池视图(本体在静态区,只说明用途与运行时占用量级)
|
||
└── V4 源码/配置视图(给符号赋语义:用途 / 大小 / 是否 DMA 可达)
|
||
```
|
||
|
||
V4 逐项相加须与 V1 的 `Image component sizes` 逐 .o 吻合;不等则以 V1 为准并记录差异原因。
|
||
|
||
**数据来源清单**:`STM32F407-Demo.sct`(区域)、`Inc/FreeRTOSConfig.h`(堆/优先级/静态分配开关)、`cmsis_os2.c:472-476`(**栈大小单位 = 字节**:内部 `/ sizeof(StackType_t)`)、`Src/freertos.c`(任务/队列/事件组属性)、`Lib/lv_conf.h`、`Lib/FatFs/ffconf.h`、`App/app/app_cfg.h` + `test/board/test_config.h`(构建口径)、`Drivers/BSP/**` + `App/**`(缓冲语义/用途/DMA 归属);**权威数据 = `@build` 重建后的 `.map`**(历史 map 不得用于结论)。
|
||
|
||
### 8.5 图表与台账重生成
|
||
|
||
```powershell
|
||
& '.venv\Scripts\python.exe' scripts/ram_report.py -o "docs\内存管理\data\ram_report_<日期>.md" # 静态台账
|
||
& '.venv\Scripts\python.exe' scripts/stack_report.py -s .dsh\skills\serial-assistant\scripts\state\serial.log `
|
||
-o "docs\内存管理\data\stack_report_<日期>.md" # 栈预算(静态 × 实测)
|
||
& '.venv\Scripts\python.exe' tools/arch_diagram/gen_memory_svg.py # 内存布局/任务栈预算图
|
||
```
|
||
|
||
产物:`docs/images/内存布局_RAM.svg`、`内存布局_FLASH.svg`、`任务栈预算.svg`(脚本与 SVG 一并提交;PNG 本地预览不提交)。
|
||
|
||
### 8.6 自洽校验(2026-09-21 快照)
|
||
|
||
| 层 | 指标 | Per-file 求和 | Grand Totals | 判定 |
|
||
|---|---|---|---|---|
|
||
| Flash | Code | 234,976 | 234,976 | PASS |
|
||
| Flash | RO | 13,824 | 13,824 | PASS |
|
||
| SRAM | RW | 632 | 632 | PASS |
|
||
| SRAM | ZI | 130,600 | 130,600 | PASS |
|
||
|
||
> per-file 求和与 Grand Totals 的微小差异来自 linker 的 `(incl. Padding)` 行(2026-09-21:326 B code + 74 B RO + 9 B RW);`scripts/ram_report.py` 按 `'incl. Generated'−'incl. Padding'` 口径计入后与 Grand Totals 精确一致(见 `data/ram_report_20260921.md` 末尾自洽校验行)。
|