Files
STM32F4-Base/docs/内存管理/RAM使用分析与优化方案.md
T

50 KiB
Raw Blame History

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)。

区域 用量 容量 占比
ER_IROM1 268,332 B (262.04 KB) 1,048,576 B (1024 KB) 25.6%
RW_IRAM1 89,200 B (87.11 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,092 B (262.8 KB) 1,048,576 B (1024 KB) 25.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
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 元数据开销约 1015%,净可用 ≈3436 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%)。

Flash 布局

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(rtel 2011-07-20 回复)
栈溢出检测 configCHECK_FOR_STACK_OVERFLOW=2:任务栈顶部预填特征值(0xEFFE...),每次上下文切换 + tick 时校验,溢出立即报告;方法 1 只查栈指针越界 论坛 Stack Overflow Detection - Method 2;本工程内核源码 Middlewares/.../include/stack_macros.h:36-122 语义一致
堆测量 xPortGetFreeHeapSize() / xPortGetTotalHeapSize() 随时可查;heap_4 支持 free 但有碎片化,需对比"理论余量 vs 实测 free"观察碎片 FreeRTOS 内核源码 heap_4.c(工程内);FreeRTOS 文档
运行时统计 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(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 增加:

#define configCHECK_FOR_STACK_OVERFLOW   2   /* 特征值法,每次上下文切换+tick 校验 */
#define configUSE_MALLOC_FAILED_HOOK     1   /* 堆分配失败立即可见 */

Src/main.c USER CODE 区实现钩子:

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] 运行时水位):

# 最小用法(只看静态:每任务最坏链 + 余量判定)
& '.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 参考链接

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 图表与台账重生成

& '.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 末尾自洽校验行)。