# 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` 快照(BSP 错误码契约试点:RS485 改报 `bsp_err_t`, > `Code` 213,436 → **213,648**(该契约全波 +212:RS485 试点 +48、SD2506 +44、GD5F/TPAFE +36、 dhara 失败类 +84;RO 另 +208,全是该失败类名表)→ `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`(`/keil-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 `/keil-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%,曾登记在案,2026-10-10 削链后销项(详见"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 | 277,120 B (270.62 KB) | 1,048,576 B (1024 KB) | 26.4% | | RW_IRAM1 | 94,336 B (92.12 KB) | 131,072 B (128 KB) | 72.0% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | | **Flash (ROM 合计)** | **279,172 B (272.6 KB)** | 1,048,576 B (1024 KB) | **26.6%** | 各区的容量与常量来源: | 项 | 说明 | 来源 | |---|---|---| | 主 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(保护任务)由 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`(语义取数与有效性:`.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.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 迁入(§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.o` 49,152 B 尚在 SRAM1、静态合计 122.7 KB 的旧布局)见 `data/ram_report_20260911.md`,仅供回溯。 ### 2.3 任务栈(从 56KB 堆动态分配,2026-10-12 起;此前 48KB) ![任务栈预算](../images/任务栈预算.svg) `Src/freertos.c` 中 `osThreadAttr_t.stack_size` 单位为**字节**(CubeMX 生成代码,`1024 * 4` 表示 4096 字节): > ✅ **本表已于 2026-10-10 重新基线**(产品构建 `suite=NONE`,开机 + 空闲采样,静态链取 `stack_report.py`)。 > 同日**增列 `adcTask`**(ADC 采集链路:ADC 采集消费者,512 words,静态最坏链 504 B / 余 75%)。 > **2026-10-11 全周傅氏相量层接入后复测**:adcTask 静态 **832 B** / 台架实测峰值 **928 B / 2048(余 54%)**, > 增量为压环 + `phasor_run()`(`win[20]` 80 B)+ `log_phasors` 日志链;仍高于 1024 B 底线,7 任务全 PASS。 > **同日采样有效性校验后抬栈 512→640 words**:`adc_health_report`→`syslog_post` 控制台回显尾把静态 > 最坏链推到 **1080 B**(512 words 时余 968 B < 1024 B ISR 底线)→ 抬至 2.5 KB,余 **1480 B / 57%**; > 堆 `min_ever` 7152 → **6640 B**(-512 B,符合预算)。 > 此前 2026-10-04 的 V5 30 min 重采曾把 defaultTask 推到 **3664B/4096(余量 432B ≈ 10%,曾登记在案)**; > 2026-10-10 的栈整改(**未抬栈**,见 §4 阶段 0 的"2026-10-10 复测")把它压回 **2152B/4096**,该项已销项。 > **2026-10-12 栈余量收口(登记簿销项,「采集保护收尾」M1)**:保护定值持久化把 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_report` 9 任务 0 FAIL + `/serial-boot-test` PASS(开机 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**(栈余量收口时抬栈 1024→1280,2026-10-12) | | timeSyncTask | `640*4` | 2.5KB | TCP 时间同步 | 616 B(余 50%)/ 静态 **1264 B**(栈余量收口时抬栈 512→640,2026-10-12) | | adcTask | `640*4` | 2.5KB | TPAFE5160 采样消费 + 全周傅氏相量 + 有效性校验(TIM3 1 kHz 节拍,ADC 采集链路→全周傅氏相量→「采样有效性校验」 2026-10-11) | **952 / 2560**(余 1608 B / 62%,台架 `/serial-boot-test` 实测 2026-10-11);静态 **1320 B**(2026-10-12 复测,保护判据接线后) | | iec104Task | `640*4` | 2.5KB | IEC104 上行服务(默认 2404 监听 + link 状态机,IEC 104 上行 2026-10-12) | 未上板 / 静态 **1456 B**(拆链→重建监听→net_bind→SPI 故障→日志尾,日志主导;512 words 时余 592 B < ISR 底线 → 抬至 640,余 1104 B / 43%) | | protectTask | `512*4` | 2KB | 保护判据节拍(prot_elm_run 消费者 + 「自检失败闭锁」采样闭锁,保护任务 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_SIZE` 57,344 B;2026-09-21 迁 CCM、2026-10-12 扩容)分配。**"配置栈"列以 `Src/freertos.c` 为唯一来源;实测峰值/余量判定以 §4.0.6 的 `stack_report` 基线为准**(本表数值同步自该基线)。 ### 2.4 动态分配点与隐患 1. **lftpd 的 C 库 `malloc` 隐患(✅ 2026-09-10 已修,见 §4.2.1)**:`lftpd_io.c` 的 3 处 `malloc`/`free`(路径规范化/`DIR`)与 `lftpd.c` 的 7 处 `free` 已全部改用 `pvPortMalloc`/`vPortFree`(C 库堆仅 startup 的 512B,原写法长路径必炸);当前源码无裸 `malloc` 残留。 2. **lftpd 大栈局部变量(✅ 2026-09-21 已整改,见 §4.1.4)**:`lftpd.c:67-68`(message[256]+response[512])、`:109,134`(name[256]×2)、`:156,185`(**buffer[1024]×2**)、`:551`(read_buffer[512])、`:583`(arg_buf[512])以及 `lftpd_log.c` 的 `buffer[256]` 已全部改为 `static`;实测 ftpTask 峰值 3480 → 1184 B,不再需要 4KB+ 栈。 3. **DMA 缓冲位置**:CH395F(SPI2 DMA)、GD5F(SPI1 DMA)、UART DMA 缓冲必须由 DMA 访问 → **只能放主 SRAM,不能放 CCM**(CCM 仅 CPU 可达,见 4.3)。 ### 2.5 结论:紧张的三个根因(含 2026-09-11 审计量化) | 根因 | 占用 | 可回收性 | |---|---|---| | ① 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 动态」切,与上表的「可回收性」视角互补)**: 1. **静态大头是"缓冲常驻"**:`lv_mem_core_builtin.o` 16,384 + `lftpd.o` 7,724 + `ui_net_sink.o` 6,746 + `ui_port.o` 6,408 + `nand_ftl.o` 6,200 = **43,462 B,占 `RW_IRAM1` 的 58%**。要动 `RW_IRAM1` 只能动这五块。 2. **动态面只有一个**:所有任务栈 + TCB + 队列都在 `heap_4` 里,而堆在 CCM → **栈的余量问题就是 CCM 的 余量问题**。这就是红线从 SRAM1 迁到 CCM 的机制原因,也是"缩堆"必须先过 `stack_report.py` 的原因。 3. **栈深的主因是日志与配置写链,不是算法本体**:`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 池,`/serial-boot-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 死代码(原审查报告的三条裁剪建议,已销项)**:`Lib/lv_conf.h` 显式关掉 10 个 `LV_DRAW_SW_SUPPORT_*`(只留 I1)、5 个未用控件类(label / button / buttonmatrix / bar / textarea)、`LV_USE_THEME_SIMPLE`、`LV_USE_FLEX` / `LV_USE_GRID` → **`ER_IROM1` 274,724 → 210,148 B(省 64,576 B = 旧镜像的 23.5%)**,Flash 占比 26.3% → **20.1%**。 > 对象级对照(`.map` 组件表,Code+RO+RW):`blend_to_*` 40,582 → 1,338(其中 `blend_to_i1` 自身 5,272 → 1,338——它按**源**格式的分支也一并消失);控件类 13,397 → 0;`lv_theme_simple` 866 → 0;`lv_draw_sw_transform` 4,340 → 598;`lv_theme_mono` 1,400 → 1,036;镜像内对象数 227 → 212。实测比原报告估算(54,630 B)多省约 10 KB,差额正是这些"锚点消失后连带消失的分支"。 > ⚠ **配套本地补丁**:`lv_draw_sw_blend_to_i1.c` 的 `lv_color16_from_u16` 上游 9.3.0 漏了定义处守卫(唯一使用点全在 `#if LV_DRAW_SW_SUPPORT_RGB565_SWAPPED` 内)→ 关掉该格式后 ARMCC 报 `#177-D declared but never referenced`(破坏"0 警告"门禁)。已按使用点补同一守卫,升级 LVGL 时若上游已修可去掉。 > 验证:ui-verify 五步全过——**11 页内容黄金参考在裁剪前生成、裁剪后逐字节一致**,菜单 8 状态像素一致,LVGL 池水位 5,440 → 5,368 B(−72 B,样式表更小,一次性残留仍 80 B);`/keil-build` 0 错 0 警;`/serial-boot-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⁶ 例零差异)。 ![Flash 布局](../images/内存布局_FLASH.svg) > 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 读写的大缓冲 | `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-budget` 92% 门禁、`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` 增加: ```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。 #### 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,便于 `/serial-boot-test` 采集: ``` [STACK] peak_used=/ free_min= (%) [STACK] heap free= min_ever= ``` 要点: - `uxTaskGetStackHighWaterMark` 返回**任务创建以来**的最小剩余栈(单位 word=4B);`used = 配置 − 剩余` 即该任务历史峰值栈占用。 - **测量必须覆盖全功能路径**(高水位是历史最小值,只反映跑过的路径):全菜单遍历、FTP 上传/下载大文件、CH395F 断链重连、NAND 格式化、时间同步。建议 `/serial-boot-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 - 用法:`& '.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",随 `/keil-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 等第三方配置后;③ 任务职责变化时。串口日志覆盖的路径越全越准(路径覆盖要求见 §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_append` 704 B(FIL 512 + 记录 64 + 时间戳)、`StartDefaultTask` 560 B(探针 `FIL __chk`)、`cfg_store_read`/`cfg_store_write_atomic` 576 B、LVGL 绘制 624/560/400 B——与 §4.1.4 的整改项对得上。**lftpd 三项已于 2026-09-21 静态化(§4.1.4),已退出帧宽榜前 15**(原 `handle_control_channel` 1104 B、`receive_file`/`send_file` 1088 B、`send_response` 800 B)。 - **静态分析的天然盲区**:递归/互递归(LVGL widget 树、`layout_update_core`、`apply_theme` 等)与间接调用只能按 "Cycle" 近似,armlink 明确标注 `+ Unknown(Cycles, Untraceable Function Pointers)`。因此 uiTask 的 912 B 是**下限**(实测 3824 B),必须靠运行时水位。 - testTask 在产品构建里只是 `osDelay` 空循环(168 B);2048 words 是给**存储套件构建**(`TEST_SUITE_STORAGE`)预留的,测套件时应重跑本脚本看该构建的静态值。 - **测 ftpTask 的复现配方**:设备就绪后跑 `& '.venv\Scripts\python.exe' test/host/ftp_speed_test.py --size 128`(STOR 128KB + RETR + 逐字节比对),再按上面命令叠加串口日志出报告。**不要用裸 TCP 工具探测 21 端口**——CH395F 单客户端复用,探测会占掉唯一会话(见 `docs/测试说明/板级测试/FTP_Test_Guide.md` §8.9)。 #### 2026-10-10 复测:**不抬栈**把 defaultTask 从 FAIL 拉回 PASS(该项销项) 触发点: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 写者单任务属主(单写者契约)。先例 = 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`)。验证链:`/keil-build` 0/0 → `/serial-boot-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` 栈注释里写明)。 #### 4.0.7 configASSERT↔控制台幻影环:三树实测与记账口径(2026-10-26 道G task-38 出证) **机制**:`configASSERT` 在 FreeRTOS port/tasks/queue 内部可达(map 中 `refers to vApplicationAssertFailed` 共 103 行),而 `vApplicationAssertFailed` 钩子(`Src/freertos.c`, 2026-09-29 起)经 `dbg_console_write_raw` 打印 `[ERR] ASSERT FAILED`;同时控制台裸发/入环 路径一度也走 `taskENTER_CRITICAL()`(→ `vPortEnter/ExitCritical` → `configASSERT`)—— 两个方向互可达 ⇒ 调用图成**幻影环**。armlink 对环的展开策略(截断,或 `In Cycle` 单趟 记账)**随环的形状变动**:三棵树里 port.o、FreeRTOSConfig.h 与 configASSERT 宏逐字节相同 (本节按树核对),静态记账却差出最高 80 B——这就是「幻影环只是记账」的完整证明。 **三树实测**(每行钉双指纹:产物路径 + htm 内 Linker run 时刻;量数一律用**该树自己的** `scripts/stack_report.py` 从该树根跑起,重跑脚本不算新测量): | 树(提交态) | 产物指纹 | `vPortExitCritical` 子树 D | 最坏链含断言族 | defaultTask / netTask / iec104Task | |---|---|---|---|---| | bench-21(77779d49) | `_wt\bench-21-77779d49\MDK-ARM\STM32F407-Demo\STM32F407-Demo.htm`,Linker run 16:27:18 | **32**(展开 `⇒vApplicationAssertFailed⇒dbg_console_write_raw⇒con_tx_bounded`,钩子 D24) | 138 个函数条目的最坏子树经过钩子(浅链 +16~24 B,例 `net_get_generation` D72) | 3032 / 1912 / 1456 | | bench-23(1d4b6a17,task-23 整行入环) | `_wt\bench-23-1d4b6a17\…\STM32F407-Demo.htm`,Linker run 19:43:40 | 8(截断) | 0 | 3040 / 1920 / 1464(+8 = `dbg_console_drain` 局部变量,task-23 真账) | | 主干在飞(task-26 钩子 begin/end 接线,未提交) | 主干 .htm,Linker run 20:39:43 ——**该产物已被 22:52 覆盖,本行数字是会话内逐帧存档**(差分表见 task-38 卡片与 45086cf5 提交正文) | 8,但 `con_tx_release` 子树 D152(`⇒vPortExitCritical⇒vApplicationAssertFailed⇒dbg_console_tx_end (Cycle)`) | 3 条任务链各 **+80 B**;iec104Task 1544 → margin 1016 < 1024 = **FAIL** | 3120 / 2000 / 1544 | | 主干现状(b24b2b5f,PRIMASK 落地后) | `MDK-ARM\STM32F407-Demo\STM32F407-Demo.htm`,Linker run 22:52:12 | 8(钩子未在其下展开) | **9/9 条最坏链干净** | 3040 / 1920 / 1464(margin 2080/1920/1096,全 PASS) | **① 现状点名(22:52 树)**:断言→控制台这条边**仍然活着**(map refers 103 行未减,钩子 `[Called By]` 仍挂着 port/tasks/queue 全家),只是 armlink 当前不在临界区函数下展开它—— 状态是「**休眠**」不是「解耦」。模块级记账底数还在:**37 个函数、14 个模块**的最坏子树 经过 `vPortExitCritical`(每个吃 8 B 底账)——`net_socket`×12、`net_pending`×9、`tasks`×3、 `task_watchdog`×2、`port`×2、`adc_input_poll` / `tpafe5160_snapshot` / `sys_clock_get` / `osEventFlagsGet` / `xEventGroupClearBits` / `prvIsQueueEmpty` / `vListInsertEnd` / `fire_event` / `s_epoch_ms` 各 1。**不止 console_log 一家**:凡走内核临界区 API 的模块都在 这张账本上,环形状再变时先咬这些链。 **② 量化表("把环断掉"能回收多少)**:现状每链 **0 B**——断言已不在任何最坏链,没有可 回收的存量;历史兑现值即上表:浅链 +16~24 B(b21 时代)→ 全图截断(b23 起)→ 深链 +80 B×3(20:39 实证)。环形状再波动一次的潜在摆动带 ≈ **−80 ~ +144 B/链**(弹药深度 = `dbg_console_write_raw` 现测 D128 + 钩子自身帧 0)。 ⚠ **反向风险(钉死,原话引用为准):若将来 `configASSERT` 被定义成空宏或被裁剪,本表 每链会"回收"约 80~90 B——那是记账变化,不是固件变好;谁把它写成优化收益就是在骗人** (真实代价是内核误用全部静默)。要"正式断环"而不牺牲取证,正确方向是让断言钩子的 发送口不再回流临界区(task-26 的 PRIMASK 窗口已是这个方向),**不是**把宏裁空。 **③ 口径:硬线 vs 实际余量**:静态最坏链是**保守建模**——`Error_Handler`(own 152 + 环 记账 208 = D360)与 `[ERR]→NVIC_SystemReset` 这类终点路径,真实运行不会把环走满;它 也不算 ISR 上下文保存(约 100 B)与间接调用(§4.0.6 盲区条同源)。所以静态链适合当 **硬线**(可复现、无噪声、机器可判:margin ≥ max(1024 B, cfg×30%)),不适合当实际余量; 实际余量看 §4.0.2 的串口 `[STACK]` 水位。**台架栈判据已定稿(task-32,2026-10-27)——三档分权**: 静态 margin 是**唯一拦提交项**(提交时由 `scripts/stack_report.py` 守,见 §4.0.6);运行时水位只做 **趋势告警**:同镜像 ≥3 圈取最大值与基线轮最大值互比,容差 **tol = 160 B**(来源 = 实测同镜像最大摆动 80 B × 2,再向上取整到 16 B 格;登记与改数规则见 `scripts/stack_tolerance.tsv`——**只有台架重测后才能 回填,被这把尺考核的人不得当场决定尺子的松紧**);运行时唯一能升级成 FAIL 的条件是绝对底线 `free_min < 25% cfg`;**采集健康门不过 = VOID(exit 2)**,不红不绿也不进台账——按码聚合的人不会把 「端口没开」记成「又崩了一次栈」。判据与门都可执行:`scripts/stack_gate.py`(`--selftest` 14 条),它是 **台架侧门禁**(每轮台架跑完必跑;Lead 裁定 2026-10-27:不接 pre-commit——提交时既无设备也无当轮日志, 挂上去只会得到「永远 SKIP」或「拿上一轮日志判这一轮的码」,后者正是体积门禁那条假凭证的同族)。 口径正文:`docs/测试说明/专项验证/启动日志原子性复验流程.md` §5(三档表与门的细节都在那节)。 本节口径与本文的运行时水位小节、静态硬线小节互指(同文件内引用,不再另标编号,避免跨文档串号)。 **取证纪律(本节方法的可复用模板)**:凡跨树/跨构建引用数字,必须同时钉**产物路径 + Linker run 时刻**——commit hash 不是稳定身份(重写正文即换 hash),tree hash + 父提交 才是;测量脚本必须从目标树根用该树自己的 `scripts/`(`REPO_ROOT` 跟随 `__file__`, 拿主树脚本进别的树量 = 量的还是主树产物,2026-10-26 实测踩实)。 #### 阶段 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%);`/keil-build` 0 错 0 警,ZI 精确 −24,576 B → SRAM1 由 74.9% 降到 **56.1%**(余 57,504 B)。 - 验证口径(本次即用它):`simulator/tests/check_menu_rows.py`(菜单 8 状态像素 + 池基线 5,368 B)+ `simulator/tests/check_content_pages.py`(当时 11 页黄金参考逐字节 + 往返池水位 5,448 B)+ 真机 `menu build done: used=` 日志。 - **再降 8KB 的前置**:全量 UI 回归(PC 模拟器 + 真机)无 `LV_ASSERT_MALLOC`/分配失败;若 8KB 不够就停在 16KB,不勉强。 #### 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 保护任务 堆已扩至 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 不受影响)。 每改一批 → `/keil-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 不变)。 - **验证**:`/keil-build` 0/0 → 烧录 → `/serial-boot-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` + 单行注释),改动点在本台账登记。 #### 4.1.5 其他小项 - `Lib/FatFs/ffconf.h:32` `FF_FS_TINY` 0→1(每打开文件省 512B 扇区窗;FAT32 适用;`FF_FS_REENTRANT=0` 现状下安全) - `ui_net_sink`(PC 远程显示,6.4KB):加 `APP_UI_NET_SINK` 编译开关,产品固件可关;保留时缓冲随 §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 天 + 长期) 1. **RAM 预算台账**(✅ **2026-10-12 落地** `docs/内存管理/RAM预算台账.md`,模板见 §8.2):记录每个 Region/缓冲/栈的计划值、实测值、变更历史;每次改内存相关代码必须更新 2. **AGENTS.md 增补**(✅ 2026-09-21 已完成:"内存预算紧张"条目已含 `ram_report.py`/`stack_report.py` 数据源与"改栈前后各跑一次"要求) 3. **pre-commit 门禁**(✅ **2026-10-12 落地** `mem-budget` 钩子 = `scripts/ram_baseline.py --gate`):钩子解析最新 map,RW_IRAM1 使用率 >92% 或 RW_CCM 超限 → 提交拦截并附 Top-10 RW+ZI 对象表 4. **/serial-boot-test 扩展**:`verify_log.py` 增加可选 Required 标记 `"[MEM] heap free"`,把堆余量下限纳入自动化回归 5. **map diff 常态化**:每次 `/keil-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 保护任务 后堆 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. 实施顺序与验证 | 步骤 | 内容 | 验证 | 状态 | |---|---|---|---| | 步骤 1 | 0.1 + 0.2(溢出钩子 + `stack_probe_poll` 栈水位探针,`Src/freertos.c` USER CODE 区) | `/keil-build` 0错0警;`/serial-boot-test` 串口出现每任务 `[STACK]` 报告 | ✅ 2026-09-21 | | 步骤 2 | 0.3 `ram_report.py` + 基线 | 脚本输出与 map 人工核对一致 | ✅ 2026-09-21(`data/ram_report_20260921.md`) | | 步骤 3 | 全功能回归跑数(菜单/FTP/网络/格式化/浸泡 1h) | 拿到各任务 `free_min` 与堆 free 底值 | ◐ boot + FTP 路径已实测(09-21);全菜单遍历 + 1h 浸泡待补 | | 步骤 4 | §4.1.4 lftpd 静态化 + §4.2.1 malloc 修复 | FTP STOR/RETR 大文件 + 长路径回归 | ✅(§4.2.1 于 2026-09-10、§4.1.4 于 2026-09-21 完成) | | 步骤 5 | §4.1.1 LVGL 池 40K→16K | UI 全量回归(模拟器 + 实机),无断言失败 | ✅ **2026-10-15**(11 页黄金参考 + 池水位 5,448 B + `/serial-boot-test` 真机 `used=3192`;实测峰值 6,568 B = 池的 40%,§2.6.2) | | 步骤 6 | §4.1.2 CCM(scatter + section + 门禁宏) | map 显示 `RW_CCM`;关 `APP_CCM_ENABLE` 也能编译通过 | ⏸ **暂缓(2026-10-15 裁定)**;已完成部分仅 `RW_CCM` + 堆迁入(09-21),缓冲 section 迁移与门禁宏不再排期 | | 步骤 7 | §4.1.3 栈缩减(按步骤 3 数据)+ 复测 | 每任务 `free_min ≥ 新栈×30%`,无溢出钩子触发 | ⏸ **暂缓(2026-10-15 裁定)**(目标值已按 09-21 实测修正,重开时可直接用) | | 步骤 8 | §4.2.2 堆 48K→32K + 复测 | 全负载 `heap free ≥ 8KB` | ⏸ **暂缓(2026-10-15 裁定)**;⚠ 2026-10-12 保护任务 反向扩容至 56 KB,重开时须重跑 `stack_report.py` + 上板 `min_ever` | | 步骤 9 | §4.1.5 小项 + 台账/门禁/AGENTS.md | `/keil-build` + `/serial-boot-test` 全绿 | ◐ AGENTS.md 内存条目已增补(09-21);台账 / 趋势 / `mem-budget` 三件套 ✅ 2026-10-12(**常开,不受暂缓影响**) | 每步都必须 `/keil-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](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 不得再用 | | ``` > ✅ 本模板已于 **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 归属);**权威数据 = `/keil-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` 末尾自洽校验行)。