task/n22-flush 合入(merge 6f99dea):vol_idle_sync 空闲 flush 兜底。 Code 215,744→215,932(+188 B)、RW 1,224→1,232(+8 B)、ZI 不变; RW_IRAM1 75,792 B(57.8%)、CCM 57,376 B(87.9%)不变——lane-f 分支侧 --no-verify 欠账清。覆盖率基线登记 vol_idle_sync.c(36/36 套件)。 凭证重盖(L0c 恢复序列)。
63 KiB
STM32F4-Base RAM 使用分析与优化方案(内存管理主文档)
版本: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两份新快照。 变更:2026-10-23 补data/ram_report_20261023.md快照(S-5 试点:RS485 改报bsp_err_t,Code213,436 → 213,648(S-5 全波 +212:契约试点 +48、C3 SD2506 +44、C1 GD5F/TPAFE +36、 C2 dhara 失败类 +84;RO 另 +208,全是 C2 的失败类名表)→ER_IROM1当日 227.1 KB / 22.2%(字节值见data/ram_report_20261023.md,正文不留会过期的裸 B 值); RAM 全部字段零增量;受管表已由check_mem_numbers.py --write同步,配图常量同步)。 变更:2026-10-24 合入task/cov-host-line-coverage(覆盖率口径 §9 的 P1 全批)——RAM +8 B (net_cfg.c为 WAIT 契约修复新增s_save_busy标志)、Flash −24 B(lftpd_string改名并删掉 永不执行的尾循环、net_select去掉冗余的timeout_ms > 0守卫),CCM 零增量;受管表与本文现状数字 已按合并后的.map同步(RW_IRAM1 75,272 / ER_IROM1 232,512 / RW_CCM 57,376 / Flash 233,704)。 变更: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,现 docs/任务规划/)已提出:"所有任务的内存都是往大的给的,导致内存不够用,要仔细排查内存的预算情况"。
分析起点(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 堆迁入(§4.1.2)
- 起步时没有任何运行时测量手段:不知道每个任务实际用了多少栈,改栈全靠拍脑袋
当前状态(以 §2.1 受管快照为准,由 .map 自动生成;2026-10-24 @build):RW_IRAM1 75,632 / 131,072 B(57.7%)、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 复测"小节)。
本方案三个目标:
- 可测量:建立"每任务 RAM 消耗 + 每模块静态占用 + 堆余量"的常态化观测手段(0.1/0.2/0.3/0.6 已落地;0.4/0.5 可选项未做)
- 降消耗:通过 LVGL 内存池、任务栈、CCM RAM、驱动缓冲等手段释放 RAM
- 可持续:建立 RAM 预算台账与门禁,防止再次"往大了给"(§3 长效机制,部分为
[计划])
2. 现状测量
2.1 总预算
本表由
.map自动生成,勿手改——改代码后跑python scripts/check_mem_numbers.py --write同步; 提交时 pre-commitmem-numbers钩子会校验一致性(见docs/文档规范.md§4)。
| 区域 | 用量 | 容量 | 占比 |
|---|---|---|---|
| ER_IROM1 | 237,328 B (231.77 KB) | 1,048,576 B (1024 KB) | 22.6% |
| RW_IRAM1 | 75,792 B (74.02 KB) | 131,072 B (128 KB) | 57.8% |
| RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% |
| Flash (ROM 合计) | 238,560 B (233.0 KB) | 1,048,576 B (1024 KB) | 22.8% |
各区的容量与常量来源:
| 项 | 说明 | 来源 |
|---|---|---|
| 主 SRAM (0x20000000) | 128 KB(RW_IRAM1 Max 0x20000) |
scatter STM32F407-Demo.sct:22 |
| 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 节 |
2.2 静态大项排行(2026-10-10 当前,完整台账 data/ram_report_20261010.md)
RW_IRAM1(主 SRAM),按 ZI/RW 合计降序:
| # | 大小 (B) | 对象文件 | 内容 |
|---|---|---|---|
| 1 | 16,384 | lv_mem_core_builtin.o |
LVGL 静态内存池 work_mem_int[LV_MEM_SIZE](§2.6.2,2026-10-15 由 40,960 收到 16,384) |
| 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 由栈迁入,§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 封装静态区 |
| 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.o1,912 B(3 处FIL静态化 = 1,584 + 证据行缓冲 192)、default_task.o2,712 B (卷探针FIL528 +default_fatfs/fs_work)、cfg_file_store_mcu.o1,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 迁入(§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。
DMA 可达性:第 4/6/7 项(NAND FTL、CH395F SPI2、GD5F BBT)为 DMA 缓冲,必须留在主 SRAM、禁止迁 CCM/SRAM3——权威清点与规则见
内存内容分配策略.md(本文不重复)。 2026-09-11 历史排行(heap_4.o49,152 B 尚在 SRAM1、静态合计 122.7 KB 的旧布局)见data/ram_report_20260911.md,仅供回溯。
2.3 任务栈(从 56KB 堆动态分配,2026-10-12 起;此前 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-11 L2 相量层接入后复测:adcTask 静态 832 B / 台架实测峰值 928 B / 2048(余 54%), 增量为压环 +phasor_run()(win[20]80 B)+log_phasors日志链;仍高于 1024 B 底线,7 任务全 PASS。 同日 L1-6 有效性校验后抬栈 512→640 words:adc_health_report→syslog_post控制台回显尾把静态 最坏链推到 1080 B(512 words 时余 968 B < 1024 B ISR 底线)→ 抬至 2.5 KB,余 1480 B / 57%; 堆min_ever7152 → 6640 B(-512 B,符合预算)。 此前 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-12 N-33 栈收口(登记簿销项,「采集保护收尾」M1):B7 定值持久化把 defaultTask 静态链推到 3016 B (storage_bring_up→cfg 写→f_rename→dhara GC→控制台回显尾),1024 words 余 1080 B < 30% 线; timeSyncTask 链 1264 B(同一控制台回显尾),512 words 余 784 B < ISR 底线 → 抬栈 defaultTask 1024→1280、timeSyncTask 512→640(堆 +1536 B,台账 §6 已登记); 台架实测heap free=16400 / min_ever=8464(与预算 −1536 吻合,>3072 告警线),stack_report9 任务 0 FAIL +@testPASS(开机 641 ms)。瘦链候选(§4 ①②)刻意未做。
| 任务 | 配置栈 | 字节 | 实际职责 | 2026-10-10 实测峰值 / 静态最坏链 |
|---|---|---|---|---|
| uiTask | 4096*4 |
16KB | LVGL 1bpp 离屏渲染(渲染缓冲在静态区,栈只走调用链) | 4016 B(余 75%)/ 静态 1336 B → 可大幅缩减 |
| netTask | 960*4 |
3.75KB | CH395F 轮询(DMA 搬运,栈压力小) | 1116 B(余 70%)/ 静态 1912 B(2026-10-14 E19 force open 降级后复测,链尾经 net_listener_tick→net_relisten_locked;前值 1920 B = 同日 E17 单一所有者收口后、1944 B = SEND pending 台架复测(含 net_send_pending_tick 新增栈帧),更早 920 B 为一期 recv 挂起时实测;FTP 传输期实测峰 1164 B) |
| ftpTask | 1024*4 |
4KB | lftpd FTP 服务 | 740 B(余 81%)/ 静态 1216 B |
| testTask | 2048*4 |
8KB | 测试套件(产品构建不创建;存储套件深链需要) | 未创建 / 静态 496 B |
| defaultTask | 1280*4 |
5KB | FatFS 挂载/探针 + 配置引擎 + syslog + 栈水位探针 | 2152 B(余 41%)/ 静态 3016 B(N-33 抬栈 1024→1280,2026-10-12) |
| timeSyncTask | 640*4 |
2.5KB | TCP 时间同步 | 616 B(余 50%)/ 静态 1264 B(N-33 抬栈 512→640,2026-10-12) |
| adcTask | 640*4 |
2.5KB | TPAFE5160 采样消费 + L2 相量 + L1-6 有效性校验(TIM3 1 kHz 节拍,B1→L2→L1-6 2026-10-11) | 952 / 2560(余 1608 B / 62%,台架 @test 实测 2026-10-11);静态 1320 B(2026-10-12 复测,L3 接线后) |
| iec104Task | 640*4 |
2.5KB | IEC104 上行服务(2404 监听 + link 状态机,B4 2026-10-12) | 未上板 / 静态 1456 B(拆链→重建监听→net_bind→SPI 故障→日志尾,日志主导;512 words 时余 592 B < ISR 底线 → 抬至 640,余 1104 B / 43%) |
| protectTask | 512*4 |
2KB | 保护判据节拍(prot_elm_run 消费者 + L5-4 采样闭锁,B5 2026-10-12) | 未上板 / 静态 872 B(float 判据 + phasor 快照 + 日志尾,余 1176 B / 57%) |
| idle(内核) | configMINIMAL_STACK_SIZE=128 |
0.5KB | 空闲 | 未观测(未取句柄) |
| timer(内核) | configTIMER_TASK_STACK_DEPTH=256 |
1KB | 软定时器 | 未观测 |
任务栈全部从 FreeRTOS 堆(56KB =
configTOTAL_HEAP_SIZE57,344 B;2026-09-21 迁 CCM、2026-10-12 扩容)分配。"配置栈"列以Src/freertos.c为唯一来源;实测峰值/余量判定以 §4.0.6 的stack_report基线为准(本表数值同步自该基线)。
2.4 动态分配点与隐患
- 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残留。 - 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+ 栈。 - 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 → 16KB ✅ 2026-10-15 | 高(目标 8~16KB;开机实测 UI 仅用 ~3KB,见 §2.6.2) |
| ② 任务栈"往大了给",无实测依据 | 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):
- 两池(FreeRTOS 48KB + LVGL 40KB)曾占主 SRAM 的 68%,是当时"SRAM 几乎耗尽(95.4%,余 5.8KB)"的直接原因;堆迁 CCM 后该结构性压力已解除(2026-10-22 的数值见历史快照
data/ram_report_20261022.md;现值见 §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并实测抓到过溢出(§4.0.1)
小项: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)。
2026-10-22 复评补充(按「常驻 vs 动态」切,与上表的「可回收性」视角互补):
- 静态大头是"缓冲常驻":
lv_mem_core_builtin.o16,384 +lftpd.o7,724 +ui_net_sink.o6,746 +ui_port.o6,408 +nand_ftl.o6,200 = 43,462 B,占RW_IRAM1的 58%。要动RW_IRAM1只能动这五块。 - 动态面只有一个:所有任务栈 + TCB + 队列都在
heap_4里,而堆在 CCM → 栈的余量问题就是 CCM 的 余量问题。这就是红线从 SRAM1 迁到 CCM 的机制原因,也是"缩堆"必须先过stack_report.py的原因。 - 栈深的主因是日志与配置写链,不是算法本体:
s_append每调用点 216 B 栈 + 配置原子写链 (cfg_store_write_atomic → f_rename)把 defaultTask 推到 3,024 B。补日志要按调用点计费。
2.6 堆 / LVGL 池 / C 库 / Flash 明细
2.6.1 FreeRTOS 堆(56 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 内存池(16 KB,2026-10-15 由 40 KB 收缩)
| 参数 | 值 | 来源 |
|---|---|---|
LV_MEM_SIZE |
16,384 B (16 KB) | Lib/lv_conf.h(模拟器 simulator/lv_conf.h 同步,lv-conf-sync 门禁比对生效值) |
| 分配器 / 色深 / OS 抽象 | TLSF / 1 (1bpp) / 无 | 同上 |
池内装 UI 对象树、样式、字符串、动画对象,均为运行时分配;TLSF 元数据开销约 10~15%。实测水位(池水位门禁常量 + 开机日志):
| 测点 | used | 占 16 KB 池 | 来源 |
|---|---|---|---|
| 菜单根框建完 | 5,368 B | 33% | simulator/tests/check_menu_rows.py 的 MENU_POOL_BASELINE |
| 开页-关页往返(一次性残留 +80 B) | 5,448 B | 33% | simulator/tests/check_content_pages.py 的 POOL_AFTER_ROUNDTRIP |
| 最重页面峰值(网口设置保存态) | 6,568 B | 40% | 同上脚本的 pool after keys: used=… free=7,080 |
真机开机(2026-10-15,16 KB 池,@test PASS) |
3,192 B | 21% | 台架日志 menu build done: total=15020 free=11828 used=3192 |
| 真机开机(2026-09-21,40 KB 池时代) | 2,976 B | — | ui_menu.c 的 lv_mem_monitor 日志 menu build done: used=2976 |
⚠ 16,384 B 建池后 lv_mem_monitor 报的 total 是 15,020 B——TLSF 控制块与对齐元数据吃掉 ≈1,364 B(约 8%);核算池余量用 15,020,不是 16,384。
→ 16 KB 留 ≈2.5× 余量(模拟器最重页面 6,568 B / 真机菜单根框 3,192 B)。再降到 8 KB 需先跑全量 UI 回归(PC + 真机)确认无分配失败;池占用静态估不出来,只能靠运行时 lv_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 与 §4.2.1) |
| C 库代码(Flash) | ~4.2 KB | printf / 浮点 / 字符串 |
2.6.4 Flash 1 MB 分布
| 段 | 大小 (B) | 占比(对 1 MB) |
|---|---|---|
| Code (inc. data) | 194,276 | 18.5% |
| RO Data | 15,872 | 1.5% |
| RW 初值 | 876 | — |
| 合计 ROM | 211,024 | 20.1%(余 ~813 KB) |
本表随代码变化;权威数字与占比见 §2.1 受管快照(
ER_IROM1/ Flash (ROM 合计) 行)。 历史快照:2026-09-21 为 Code 234,976 / RO 13,824 / 合计 ROM 249,432(23.8%);2026-10-10 为 Code 258,516 / RO 16,208 / 合计 ROM 275,604(26.3%)。
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_IROM1274,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_simple866 → 0;lv_draw_sw_transform4,340 → 598;lv_theme_mono1,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);@build0 错 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 项靠默认值隐式生效的开关,已清理(本次清理对镜像体积中性)。详解见docs/门禁说明/lv-conf-sync.md(含四类判定、端专属白名单与首次落地实测)。
⚠ MicroLIB 的
_fp_digits与双精度 helper(约 1,018 B)删不掉,别再去搜%f:全库%f命中 0, 这 1 KB 是snprintf的固有依赖——printfa.o的_printf_core无条件引用_fp_digits,只要用snprintf就带得进来。能修的是根因侧:App/algo/prot_set.h的两个 Q10 宏曾写成double, 2026-10-22 改单精度后Code−332 B、5 个 helper(dflti/d2f/f2d/dfixi/fepilogue共 300 B)消失, 且数值逐位等价(穷举 42 个字面量 +int32全域 ÷1024 往返 4.49×10⁶ 例零差异)。
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 读写的大缓冲 | docs/芯片手册/STM32F4xx参考手册_V18英文版_1-1000.md §3.1(RM0090 原文该小节为 §3.1.1 CCM,本仓摘录只到 §3.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. 方案
编号约定(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-budget92% 门禁、RAM预算台账.md受管区、ram_baseline.tsv趋势、[STACK]水位与stack_report.py/ram_report.py)。它们已落地、零维护成本,且正是"何时该重开优化"的传感器。- 已落地的优化不回退:§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。- ⚠ "UI 侧缓冲进 CCM"要分两种,别一概而论:LVGL 内存池是纯 CPU 访问 → 可以进 CCM; 而帧缓冲
g_ui_fbuf/ 绘制缓冲 /ui_net_sink推流缓冲由 DMA2 取数 → 必须留在 SRAM, 搬进 CCM(CPU 专属,DMA 不可达)的后果是 DMA 静默读到错数据,不是报错。暂缓理由与重开条件(本节是唯一权威,其它文档只留指针):
暂缓理由一句话:当前没有功能在等这块预算,而搬迁要动 scatter、要逐条过 DMA 可达性清单 (违例是静默传错不是报错)、缩堆还得上板复测
min_ever——收益不抵风险与工时。重开条件(满足任一条才谈搬迁/缩堆): ① 出现"必须新增 >8 KB RAM"的功能且产品要它; ②
min_ever跌破 4 KB(碎片或峰值失控——此时应先削峰值,不是搬家); ③ CCM 撞墙导致新任务建不起来。下面各节保留作重开时的实施依据,不代表现在要做。
阶段 0:建立测量能力(先做,1~2 天)
原则:没有数据不动刀。本阶段全部是"只增不减"的改动,零功能风险。
4.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。
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()(剩余字节,内部走
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(内核静态小栈、纯循环,未取句柄);构建期静态侧见 §4.0.6。
4.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
- RW_IRAM1 Region 的
- 用法:
& '.venv\Scripts\python.exe' scripts/ram_report.py -o docs/内存管理/data/ram_report_<日期>.md,把结果贴进改动说明 - ✅ 2026-10-12 落地:
scripts/ram_baseline.py --trend与基线 TSV (scripts/ram_baseline.tsv)做 diff,任何对象 RW+ZI 增量 >1KB 即高亮
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。
4.0.5 (可选)main() 启动期栈峰值
启动期跑在 startup 的 1KB C 栈上(CubeMX 初始化 + 各驱动 init,含 BBT 全扫描)。ARMCC 可用 __current_stack_usage() 在 app_main_init 末尾打点,确认 1KB 够用;不够则调 scatter 的 STACK 节(与任务栈无关,单独预算)。
4.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 等第三方配置后;③ 任务职责变化时。串口日志覆盖的路径越全越准(路径覆盖要求见 §4.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(§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 |
| testTask(产品构建) | 8192 | 168 | 348 | 348 | 7844 (95%) | PASS |
实测堆:free=8784 B / min_ever=4848 B(48KB 池)。
处置结果(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 传输时暴露 → 不跑真实业务就定栈 = 拍脑袋。 §4.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_append704 B(FIL 512 + 记录 64 + 时间戳)、StartDefaultTask560 B(探针FIL __chk)、cfg_store_read/cfg_store_write_atomic576 B、LVGL 绘制 624/560/400 B——与 §4.1.4 的整改项对得上。lftpd 三项已于 2026-09-21 静态化(§4.1.4),已退出帧宽榜前 15(原handle_control_channel1104 B、receive_file/send_file1088 B、send_response800 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)。
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 一行未改):
- 删旧档 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的设备 → 按"不保留旧档兼容"处置; 旧档若存在则忽略(不读、不删、不迁移),配置回落工厂默认。 - 6 处局部
FIL改函数级static(每处 −528 B):Lib/FatFs/ffconf.hFF_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 处大局部缓冲静态化。
- 日志行缓冲按模块收紧 + 时间戳缓冲并入行缓冲(每个日志调用点原内联
192 + 32 = 224 B):dbg_log.h新增DBG_LINE_BUF_MAX(默认仍 =DBG_CON_LINE_MAX192),深链模块 (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_MAX32 → 24 且与行缓冲共用一个数组。 实测链上 9 个帧各含一份行缓冲 → 仅这一项就占 defaultTask 最坏链约 1.5 KB。 ⚠ 取值须 ≥ 该模块最长日志行(各文件注释里已给出实测行长),否则该行被截断。 - 内核钩子脱离 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 天)
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%);@build0 错 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,不勉强。
4.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%;现值见 §2.1——2026-10-12 B5 堆已扩至 56 KB)。下表缓冲迁入清单为后续剩余项(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(未实现——2026-10-22 复核:App/app/app_cfg.h 里没有这个宏,重开本簇时要连它一起落地),关闭时 LV_MEM_ATTR 等定义退化为空,缓冲全部落回主 SRAM(§4.1.1 池缩到 16KB 后主 SRAM 也放得下)。真 ST 芯片默认开。
验证:map 中出现 RW_CCM 且 *(CCM_DATA) 各对象地址落在 0x10000000~0x100FFFF;ram_report.py 同时输出两个 Region 水位。
4.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(§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,无缩减空间 |
缩减后任务栈合计 38,912 → 21,504 B(省 17,408 B / ~17 KB,全部在 CCM 堆内,主 SRAM 不受影响)。
每改一批 → @build + 全功能回归 + 复测高水位(free_min 应 ≥ 新栈×30%),不满足则回调。
4.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 不变)。
- 验证:
@build0/0 → 烧录 →@testPASS →ftp_up_dl_test.py10/10 PASS +ftp_speed_test.py --size 128逐字节一致(功能与搬迁前完全一致,反证无别名/重入问题)。 - 结论:原 ① 方案(ftpTask 涨到 1600 words)不再需要,栈保持 1024 words(4096 B)。
lftpd 属第三方代码(pre-commit 排除范围),只做最小侵入改动(每处仅加
static+ 单行注释),改动点在本台账登记。
4.1.5 其他小项
Lib/FatFs/ffconf.h:32FF_FS_TINY0→1(每打开文件省 512B 扇区窗;FAT32 适用;FF_FS_REENTRANT=0现状下安全)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 天)
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 警。
4.2.2 缩减 configTOTAL_HEAP_SIZE
阶段 1 完成后复测:堆内需容纳 ≈ 栈(15KB) + TCB(2.3KB) + 队列/事件组(0.5KB) + lftpd 临时分配(1KB) + 余量。configTOTAL_HEAP_SIZE 49152 → 32768,要求全功能负载下 heap free ≥ 8KB 持续成立(浸泡 1 小时验证)。
4.2.3 (可选,进阶)任务/队列全静态化
configSUPPORT_STATIC_ALLOCATION=1 已开。用 osThreadNew 的 attr.cb_mem(StaticTask_t + 静态栈数组)可把栈完全移出堆,预算 100% 透明、堆只剩小对象分配。改动面涉及 CubeMX 生成的 freertos.c 属性块,收益(~15KB 确定性)与改动成本权衡后决定,不作为必做项。
阶段 3:长效机制(0.5 天 + 长期)
- RAM 预算台账(✅ 2026-10-12 落地
docs/内存管理/RAM预算台账.md,模板见 §8.2):记录每个 Region/缓冲/栈的计划值、实测值、变更历史;每次改内存相关代码必须更新 - AGENTS.md 增补(✅ 2026-09-21 已完成:"内存预算紧张"条目已含
ram_report.py/stack_report.py数据源与"改栈前后各跑一次"要求) - pre-commit 门禁(✅ 2026-10-12 落地
mem-budget钩子 =scripts/ram_baseline.py --gate):钩子解析最新 map,RW_IRAM1 使用率 >92% 或 RW_CCM 超限 → 提交拦截并附 Top-10 RW+ZI 对象表 - @test 扩展:
verify_log.py增加可选 Required 标记"[MEM] heap free",把堆余量下限纳入自动化回归 - map diff 常态化:每次
@build后跑ram_report.py,ZI 增量 >1KB 的改动必须在提交说明中解释
5. 预期效果
| 项目 | 现状(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(§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 |
已落地项(不再计入预期):§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 用户裁定:本表"目标"列所依赖的搬迁与缩堆整簇暂缓(理由与重开条件 → 本文档 §4)。已落地部分只有 1.1 池 40→16 KB(SRAM1 实测 73,568 B / 56.1%,余 57,504 B)与 1.4;本表保留作重开时的收益测算依据,当前实际水位以
RAM预算台账.md受管区为准,不要拿本表"目标"列当现状。
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 | §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 先过一遍再上实机。
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 区与 Inc/FreeRTOSConfig.h(App/app/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 · Stack Overflow Detection - Method 2 · checking stack usage
- FreeRTOS 文档站:https://docs.freertos.org/ (官网旧页为 JS 渲染,正文以工程内 V10.3.1 内核头文件注释为准)
- LVGL 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 不得再用 | |
✅ 本模板已于 2026-10-12 落地为
docs/内存管理/RAM预算台账.md(扩展了子系统配额口径、 CCM 分配台账与 92% 门禁说明;区域数字由受管区自动生成,不手抄)。
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(大缓冲已静态化,§4.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、test/board/test_config.h(构建口径;App/app/app_cfg.h 自 2026-10-22 起无宏,不再是口径来源)、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末尾自洽校验行)。