# 存储栈 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 ②的人,已按实况改写。