Files
STM32F4-Base/docs/内存管理/存储栈RAM优化调研.md
T
wandering1 4a4ad6bc5f chore(全仓): @build/@test 旧文档别名清退为技能调用名
现状口径:/keil-build 与 /serial-boot-test 是 DSH 会话的技能调用名,
不是 shell 命令;「名 + 命令行参数」的命令形态改为可执行体
run_boot_test.py 的真实调用。

- AGENTS.md:别名说明重写为「技能调用名不是 shell 命令」,两条目改
  「编译(/keil-build)」「串口回归(/serial-boot-test)」
- keil-build / serial-boot-test 两 skill 调用名与示例同步;task-worktree
  方案文档去掉 @build/@test 惯例从句;@task 保留(不在点名范围)
- 123 文件现状陈述全改;台架捕获、变更记录/时间线、rename 映射输入
  等 41 行历史证据保留旧名
- gen_memory_svg.py 6 处措辞随改,3 张 SVG 产物同批重跑
- 顺手补并行方案同行漏网计数 51→52 钩子

验证:提交期快钩子对 123 文件实跑全 Passed;check_doc_links --all
rc=0(3038 处、无法判定 0);check_mermaid --all rc=0;YAML 解析 52
钩子不变;skill-size rc=0;py_compile 全过;字节卫生零漂移。
2026-10-30 13:03:12 +08:00

141 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 存储栈 RAM 优化调研
> 版本 v1.2 日期 2026-10-23(v1.2:新增 §5 页缓冲可合并性论证,承接原报告那条待评估项) 作者 AI 辅助调研
> 数据来源:`data/ram_report_20260921.md`(.map 静态占用)+ 逐行代码核验(`nand_ftl.c` / `gd5f2gq5ue.c` / `Lib/dhara` / `Lib/FatFs/ffconf.h` / `lftpd*`)
> 定位:`RAM使用分析与优化方案.md` 的存储专项补充;优化项编号 P0/P1/P2,与 `docs/内存管理/RAM使用分析与优化方案.md` §6 的 S 序号在"规划"一节对齐
> v1.1 清理(2026-09-21 同日):移除已落地的 BBT 页缓冲收缩(−1,776B,commit `be1a30a`)与编程回读校验整体移除的实施记录(详见 GD5F TRAP-GD5F-05),以及过期逐模块基线表;仅保留未实施项与回归要求
> ⏸ **实施状态(用户裁定 2026-10-15)**:本文**全部未实施项整簇暂缓、不再排期**——`s_copy_buf` 分块(原报告的同一评估项)、`APP_CCM_ENABLE` 的 ①② 迁 CCM、以及依赖"堆 48K→32K"的第三步。当前没有功能在等这块预算,而搬迁要逐条过 DMA 可达性清单(违例是静默传错不是报错)。**保持常开**的是测量与门禁面(`ram_report.py` / `stack_report.py` / `mem-budget` 92% 门禁 / 台账 / 趋势)。理由与重开条件 → `RAM使用分析与优化方案.md` §4;本文保留作重开时的实施依据,**不代表现在要做**。
## 1. 现状约束(不变,是下述全部方案的依据)
- 存储栈静态 RAM 全在 **RW_IRAM1(主 SRAM 128KB)**;页缓冲(nand_ftl 3×2KB + gd5f 页缓冲/位图)走 SPI1 DMA(阈值 >1024B),**DMA1/DMA2 物理不可达 CCM** → 不能直迁 CCM(唯一途径:迁移 + `gd5f_dma_buf_ok()` 运行时守卫自动回落 CPU 轮询(2026-09-21 加固),不会静默失败,代价仅每页零点几 ms CPU)
- 3×2KB 页缓冲是结构成本不可直缩:dhara journal 要求整页 checkpoint(`journal.c:196` 整页 memset);512B 扇区落 2KB 页必须整页 RMW;可动的只有 `s_copy_buf`(见 §2 分块方案)
- CPU-only 缓冲(lftpd static ~3.6K / cfg_file 0.7K / FATFS win 0.6K / lftpd_io 0.6K)迁 CCM 零代价(见 §2 迁 CCM 方案 ①)
- 栈上 FIL 重复占深链:syslog 两处、cfg 存储两处、开机探针各一个局部 `FIL`(每个含 512B 私有 buf,≈552B),全压在 defaultTask 最坏链 2,776B 内(靶点见 §2 `FF_FS_TINY` 行)
- FatFs 配置已低 RAM 取向:`FF_MAX_SS=512`、`FF_USE_LFN=0`、`FF_VOLUMES=1`、`FF_FS_LOCK=0`;`FF_FS_REENTRANT=1` 为 defaultTask/ftpTask 并发访问必需,勿动
- 教训(2026-09-21):任何比对式校验必须在**两份独立副本**间进行(U-Boot `sf test` 双缓冲形态)——旧 GC 拷贝自比对缺陷已随回读校验整体移除而消失;若未来测试构建恢复回读诊断,勿重蹈此坑
## 2. 方案清单(未实施)
| # | 方案 | 省 RAM | 区域 | 风险 | 验证 |
|---|---|---:|---|---|---|
| `s_copy_buf` 分块 | `s_copy_buf` 2048→512:GC 拷贝改分段 read→program_load(`program_load`/`read_from_cache` 支持列地址);纯 RAM 优化(GC 拷贝暂存减半) | **−1,536B(可选,不计入当前预期)** | SRAM1 | 低(仅动拷贝机制) | 待 RAM 余量需要时再议 |
| lftpd 缓冲收紧 | lftpd 缓冲收紧:response 512→256、read_buffer 512→256、arg_buf 512→256、name 256→128 ×2(本卷仅 8.3 短名,路径深度浅);数据块 2×1024 与 message 不动 | **−1,024B** | SRAM1 | 低(FTP 客户端实测行长远低于新值) | `/keil-build` + FTP 上传/下载/LIST 闭环 + `/serial-boot-test` |
| `FF_FS_TINY` 共享窗 | `FF_FS_TINY=1`:删每 FIL 私有的 512B buf,文件数据走 `FatFs->win` 共享窗 | 静态 **−528B**;且栈上每处 FIL 峰值 **−528B**(defaultTask 最坏链 2,776→~2,250,实测峰 2,136→~1,600) | SRAM1 + CCM 栈 | 中(所有文件 I/O 经 512B 窗 bounce,FTP 吞吐 −20~40%;LIST 与文件读交错只影响速度) | 存储套件 + FTP 吞吐实测 + 日志/配置闭环;**与 RAM方案 实施步骤 7 的缩栈捆绑**(直接支撑 defaultTask 降档) |
| `APP_CCM_ENABLE` 迁 CCM | `APP_CCM_ENABLE` 门(与 RAM方案 实施步骤 6 同宏):①CPU-only 缓冲迁 CCM——lftpd static 3.6K + cfg_file 0.7K + FATFS win 0.6K + lftpd_io ≈ **−4.9K** 零代价;②DMA 页缓冲(nand_ftl 4.6K + gd5f 位图/页 0.8K)迁 CCM 走轮询 ≈ **−5.4K**,需实测吞吐 | **−10.3K** | SRAM1→CCM | 低①/中② | 分段实施;每步跑 `ram_report.py` + `/serial-boot-test` + FTP 吞吐 |
| NAND copy-back | NAND copy-back 命令(0x15→0x10)替代主机搬页:省 `s_copy_buf` 的拷贝用途 + 减半 SPI 流量 | −512B(`s_copy_buf` 分块后) | SRAM1 | 高(片上 ECC 在 copy-back 下的语义须台架实证) | 仅评估,暂缓 |
| lftpd 数据块减半 | lftpd 数据块 2×1024→512 | −1,024B | SRAM1 | 吞吐 −~30% | RAM 仍紧时才做 |
| 共享静态 FIL | syslog/cfg 共享一个静态 FIL(同任务串行) | 净≈0(静态+528/栈−528) | — | 不推荐:`FF_FS_TINY` 那条已覆盖同一收益面 | — |
**明确不做**:移除 dhara(丢掉电一致性与坏块重映射,代价不对称);journal checkpoint 分片(违反 dhara 协议);调 `gc_ratio`(无 RAM 收益);`FF_FS_REENTRANT=0`(defaultTask+ftpTask 并发访问 FatFs 必需)。
## 3. 分阶段规划(与 `RAM使用分析与优化方案.md` §6 S 序号对齐)
| 阶段 | 内容 | 收益 | 前置 |
|---|---|---:|---|
| **第一步**(独立小批) | lftpd 缓冲收紧(`s_copy_buf` 分块搁置,待 RAM 余量需要时再议) | **−1,024B** | 无 |
| **第二步**(并入 RAM方案 步骤 7 的缩栈批) | `FF_FS_TINY=1` → 复跑 `stack_report.py` → defaultTask 降档(最坏链 ~2,250 支撑 3,584B 配置,余量 37%)+ 其余任务按该步骤 7 目标 | −528B 静态 + 栈档下调释放 CCM 堆 | 第一步合入 |
| **第三步**(与 RAM方案 步骤 6/8 同批) | 先做步骤 8 的堆 48K→32K(CCM 释放 ~16K),再按迁 CCM 方案 ①→② 分批 | SRAM1 再 **−10.3K**;CCM 占用 +10.3K(由该步腾挪覆盖) | 与步骤 8 同批或同批评估 CCM 水位 |
**全部落地后存储栈预期**:SRAM1 常驻降至 **~1.9KB**(现 13.7KB,其中 BBT 页缓冲收缩已省 1,776B 不重复计;余下仅小状态量),页缓冲驻 CCM(轮询),未实施项合计换回 SRAM1 ≈ **−11.9K**(`s_copy_buf` 分块搁置,其 −1,536B 不计);叠加 RAM方案 步骤 5(LVGL 40K→16K)后 SRAM1 余量 >65KB。
## 4. 回归与基线
- **FTP 吞吐基线(2026-09-21 实测,4MB 文件,端到端含网络;瓶颈在存储侧,100M 网络非瓶颈)**:STOR **126.9 KB/s**(32.3s)/ RETR **321.4 KB/s**(12.7s),回传逐字节 MATCH(测试文件已自清理)→ 两条 FTP 相关方案前后各留一轮实测对比(`FF_FS_TINY` 那条预期吞吐降幅 20~40%)
- `FF_FS_TINY` 前后各跑一次 `stack_report.py` + `ram_report.py`(AGENTS.md 门禁)
- 迁 CCM 方案的 ②(页缓冲入 CCM 走轮询)须以实测吞吐定去留:若 NAND 读吞吐降幅 >30%,则只迁 CPU-only 侧(①)
- `s_copy_buf` 分块与 copy-back、数据块减半等小项:待 RAM 余量需要时再议
- 下次存储改动照常执行台架破坏性套件 + 掉电 resume
- 基线数字可用 `data/ram_report_20260921.md`(BBT 页缓冲收缩前快照)复核;实施后重生成基线报告并回填总方案
## 5. 页缓冲可合并性论证(2026-10-23)
> 本节的来源:`Drivers/BSP/GD5F2GQ5UE/nand_ftl.c` 原注释声称"页缓冲三块不可合并的评估见报告",
> 而原评审报告(2026-10-25 已删除)里只有一行"中 / 暂缓",**论证从来没写过**——本节补上
> (该注释已在本轮改为指向本节)。
> 结论:**三块不能合并**(最坏路径上同时存活,合并后的失败形态是**静默数据损坏**,不是报错);
> 可削减的只有 `s_copy_buf` 一块,上限 −2,048 B,而**收益落点不是红线区** →
> 原报告的该项由"待评估"改为"**已评估、判定不做**"(报告已删除,结论以本节为准),
> 与本文件 §2 那两条残余方案一起留在暂缓簇里。
### 5.1 6,216 B 的构成(`.map` 实证)
| 符号 | 尺寸 | 链接地址(2026-10-23 构建) | 角色 | 谁在写它 |
|---|---:|---|---|---|
| `s_page_buf` | 2,048 | 0x200066B4 | dhara journal 的 checkpoint 页缓冲(`dhara_map_init` 传入,`nand_ftl.c:426`) | `journal.c`:逐页累加 132 B 元数据,攒满 16 页后整页编程 |
| `s_cache_buf` | 2,048 | 0x20006EB4 | FatFS 页缓存(512 B 扇区 → 2 KB 页的 RMW) | `disk_read` / `disk_write` / `ftl_flush_cache` |
| `s_copy_buf` | 2,048 | 0x200076B4 | `dhara_nand_copy()` 的搬页暂存(GC) | `dhara_nand_copy`(`nand_ftl.c:379`/`:383`) |
| `s_map` / `s_nand` / 3 个标量 | 56 + 8 + 8 | 0x2000667C 起 | dhara 状态与缓存标志 | — |
`nand_ftl.o` 合计 **6,216 B = RW 16 + ZI 6,200**,其中三块页缓冲 6,144 B 占 **98.8%**。
原报告 §5.2 的 Top-5 表用 ZI 口径写 6,200,与本节的 6,216 差的就是那 16 B 的 RW,两处不矛盾。
### 5.2 三块同时存活的证据链(最坏路径)
触发条件是**一次脏页回写正好撞上 journal 满 → 自动 GC**,稳态写入下是常态而非边角:
1. `disk_write()` 把 512 B 扇区拼进 `s_cache_buf` 并置脏(`nand_ftl.c:565`);
2. 整页凑满或 `CTRL_SYNC` → `ftl_flush_cache()` → `dhara_map_write(&s_map, lpn, s_cache_buf, …)`
(`nand_ftl.c:195`)——**此刻 `s_cache_buf` 就是被写的数据本身**:dhara 把它当 `data` 直接交给
`dhara_nand_prog()`(`journal.c:1021`),坏块重试时还要再读一次;
3. `dhara_map_write()` 先调 `prepare_write()`(`map.c:356`),而后者**第一件事**就是 `auto_gc()`
(`map.c:327`)——回收发生在数据落盘**之前**;
4. `auto_gc → dhara_map_gc → raw_gc → dhara_journal_copy → dhara_nand_copy(src, head)`
(`map.c:247`、`journal.c:1042`),搬页需要一块 2 KB 暂存 = `s_copy_buf`;
5. 与此同时 `s_page_buf` 里攒着**当前 checkpoint 周期内已入队页的元数据**:`push_meta()` 每入队一页
就 `memcpy` 一段 132 B 进去(`journal.c:972`),攒满 16 页才整页编程(`journal.c:992`)。
这是**跨调用存活的状态**,不是暂存。
于是第 4 步执行时三块同时持有活数据:待写的缓存页、已攒的 checkpoint 元数据、搬页暂存。
任取两块合并,被覆盖的一方都会变成垃圾,而**没有一处会报错**——这与越界校验、挂载失败处置两次
"宁可报错,不可静默"的教训同族,属于必须回避的失败形态。
### 5.3 逐对否证
| 合并方案 | 看似可行的理由 | 实际后果 |
|---|---|---|
| `s_copy_buf` ⊕ `s_cache_buf` | "搬页只是中转,用完即弃" | 覆盖的正是**还没写下去的那一页**;`journal.c:1019` 的重试循环会拿被污染的数据再编程一次 → 该页永久错数据 |
| `s_copy_buf` ⊕ `s_page_buf` | "元数据才 132 B,占不了多少" | checkpoint 元数据在页内**跨 0..1,999 偏移**分布(`hdr_user_offset()`,`journal.c:89`;本卷 `log2_ppc=4` → 15 段),被覆盖 → checkpoint 内容错 → 复位后整段回退或 `E_CORRUPT_MAP` |
| `s_page_buf` ⊕ `s_cache_buf` | "都是页缓冲,都是 2 KB" | 同第二行;且 `s_page_buf` 的存活期以**16 页**为单位,远长于缓存页 |
| 条件别名(脏时用 A,否则用 B) | "只有一条路径同时用" | 需要"缓存是否脏"这个跨模块标志;将来任何从 dhara 侧新进的调用点都会静默踩中。为省 2 KB 引入一条**不可静态验证**的隐式契约,收益与风险不成比例 |
`s_page_buf` 也不能缩:虽然每页只用到 132 B 元数据,但**编程的是整页**(`journal.c:196` 整页
`memset` 0xFF + `journal.c:992` 整页 `prog`),且元数据段一直排到偏移 1,999 → 必须 2,048 B。
### 5.4 真正可动的只有 `s_copy_buf`
| 方案 | 收益 | 做法 | 代价 / 风险 |
|---|---:|---|---|
| `s_copy_buf` 分块(本文件 §2 方案清单) | **−1,536 B** | 512 B 分块:`page_read` 一次 → 4×(read_from_cache → program_load) → **一次** `program_exec` | 正确性风险低(仍是一次编程,ECC 一次算完,不是"部分页编程");但 512 B 不大于 `GD5F_DMA_THRESHOLD`(1024) → 4 次全走 CPU 轮询:SPI1 42 MHz 下 2 KB 线上时间 0.39 ms,轮询比 DMA 多约 25~50%,即每页多 ~0.1~0.2 ms 且全程占住 CPU(不 yield)。要保住 DMA 就得下调阈值,而那会破坏"阈值 ≥ 存储路径最大栈缓冲"这条编译期契约(`gd5f2gq5ue.h:105`),不建议 |
| 内部搬页(copy-back,本文件 §2 方案清单) | **−2,048 B** | `PAGE READ(13H)` src → `WRITE ENABLE` → `PROGRAM EXECUTE(10H)` dst,**主机侧零缓冲**(`../芯片手册/GD5F2GQ5UExxG.md` §9.6) | ① 手册限定"只能在**奇偶属性相同**的块之间搬",跨属性行为须台架实证;② 读侧 ECC 校正后的数据才进 cache register,语义上等价于"搬页 + 纠错",但同样要实测;③ 主机侧假芯片把 `program_exec` 建模成"必须 `program_load` 过"(`test/host/nand_port/nand_port_test.c:187`),要先补 internal-data-move 语义,否则正确实现会被判失败 |
两条都**替代不了**"三块合一":做满也只能把 6,216 B 压到 4,168 B,压不回 2 KB 级。
### 5.5 为什么省下来也没用(落点分析)
- 三块页缓冲全在 `RW_IRAM1`(0x2000_0000 窗口内,`gd5f_dma_buf_ok()` 白名单覆盖),该区
2026-10-23 实测 **75,264 / 57.4%,余 55,808 B**;
- 红线在 **`RW_CCM`:57,376 / 87.9%,余 7,904 B**(FreeRTOS 堆 56 KB 吃掉了它);
- 所以这条线即使做满(−2,048 B)也**一分不减红线**;反过来把页缓冲搬进 CCM(本文件 §2 的迁 CCM 方案 ②)
才是"对红线有害"的方向——6,144 B 会吃掉 CCM 余量的 78%;
- 把 `s_copy_buf` 挪到栈上同理:defaultTask 静态最坏链 3,024 B / 5,120 B,再塞 2 KB 直接顶到上限,
而任务栈本身就在 CCM。
结论:**页缓冲合并这条线的收益没有消费者**。这正是 2026-10-15 整簇暂缓的机制原因,本节只是把它从"判断"
补成了"论证"——所以暂缓不变,重开仍按 `RAM使用分析与优化方案.md` §4 的三条条件。
### 5.6 复现
```bash
grep -n " nand_ftl.o" MDK-ARM/STM32F407-Demo/STM32F407-Demo.map # 6,216 = RW 16 + ZI 6,200
grep -n "s_page_buf\|s_cache_buf\|s_copy_buf" MDK-ARM/STM32F407-Demo/STM32F407-Demo.map
```
### 5.7 顺带订正的两处注释
- `nand_ftl.c:300` 原写"页缓冲三块不可合并的评估见报告",而报告里此前**没有**这份评估
→ 改为指向本节。
- `nand_ftl.c:405` 原写"`s_page_buf` 现仅装 dhara meta 132 B 走轮询",**不成立**:`s_page_buf` 是
按整页编程的(`journal.c:992` → `dhara_nand_prog` → `gd5f_program_load(0, data, 2048)`),
与另两块一样走 DMA。原文这句话会误导后来做迁 CCM ②的人,已按实况改写。