Files
STM32F4-Base/docs/内存管理/内存内容分配策略.md
T
wandering1 d64bbdc57f feat(wave): 故障录波数据面、接线与单通道回放页(软件闭环交付)
数据面(App/algo,纯逻辑,双端编译)
- wave_ring:8 路 × 320 点列优先环(1 kHz,预触发 160 + 后触发 160),adcTask
  唯一写者;触发/冻结/封存/冷却四态,冻结持续到落盘回执才解冻,滞留 2 s 自弃
  兜底——下一次真故障无波可录比丢一段更贵
- wave_store:段头编解码 + 拉模式打包游标 + 自带无表位运算 CRC-16/MODBUS(带
  锚点自检)+ 段名与整段长度等式;格式不依赖平台类型宽度
- 段头只存原始码值与封存瞬间生效系数,工程量只当单位标签、不进任何算式
- 三套主机单测(环 113 / 编解码 170 / 触发状态机 110),全部无桩、生产源原样
  纳入;新文件补 stddef 是 ARMCC 与主机差异,主机侧照不出这条分叉
- test/host/wave_read.py 是按 §6.2 的独立第二实现(dump/fft/shape/plot),92 条
  自洽断言;三份合成段互读双校验全过、哈希钉死,格式面双向闭合
- 三套新单测不加专属钩子,由覆盖率总闸全量真跑(沿用既有裁定)

接线(任务层)
- adc_cal:新增生效系数的 32 位定点原值入口(与既有浮点入口共用生效视图),
  段头按最低有效位精确记账,不经浮点回算
- adcTask:每拍压 8 路语义原始码值并推进状态机,封存那一拍同拍交棒封存瞬间事实;
  台架定周期自抓走双层编译门,产品镜像里零存在;串口新增一条 wave: 周期行
- defaultTask:卷空闲后一次心跳整段搬完(try 锁 + 256 字节分片 + 一次完整开档到
  关档),成败都回执;失败先删半成品再计错,段号只在成功时推进;持锁超 50 ms
  补一条告警并记历史峰值(喂狗余量可核)
- Lead 复核补第四道并发陷阱:采集任务优先级高于落盘道,搬段中途它可能正好把滞留
  超时的本段自弃、甚至封出下一段——数据区校验和是对「实际读到的字节」算的,
  半新半旧的段照样自洽,故关档后复检环侧身份判废:删文件、不补账、段号不推进
- Lead 复核抓到触发挂号不是原子操作(旧实现分三个字节写,消费者可在窗口里读到
  上一段的触发源与元件号):已把载荷压进一个字节,越界当场拒且不记任何计数;
  段文件字节布局零改动,三份合成段哈希逐字节不变
- protectTask:保护启动上升沿同点请求录波,一拍一次、元件号取最低位;判据本体
  零改动

人机(160×160 单色屏)
- 菜单树节点 26「手动录波」绑页面 + 记录查询组末新增「录波回放」叶,重跑生成器
  得 43 节点 / 34 叶 / 30 已绑,页面映射 28 → 30
- 回放页一屏一路、触发点前后各一周波共 40 点、每点 3 像素阶梯线(只用水平与
  垂直两种线段——单色屏斜线会被阈值裁掉);零点虚线、触发光标、窗口峰值、
  单位徽标与形状受限徽标;空态与坏段各一句显式文案,绝不用半截波形冒充数据
- ui_data 新增录波注入通道(六件操作表、三态返回);执行侧胶水带段头缓存、
  手动请求后连缓存一起作废、逐点走全仓唯一的码值换算入口
- 模拟器接真实环与编解码合成两段(含坏段与空态旗标),6 张新黄金参考,既有
  60 张逐字节零漂移未用更新模式;字库 218 → 224 字 / 319 字形
- 两份图形配置里图表组件仍为关;零堆分配;页面池水位不变
- 回放读路径的 uiTask 阻塞上界按实码写清:一次渲染 = 段表刷新 + 未缓存段头最坏
  22 次小读;已验段只剩一次开窗读 + 40 点解码;无实测数不编,实测归台架、
  仪器用已在跑的单轮工作耗时探针,不新加埋点

台账与文档
- 内存台账:录波环与落盘/回放静态逐条入册,三档对照 81,536 → 88,296 → 89,936
  字节(62.2% → 68.6%),累计 +8,400;预估 5.9 KB 与实测 +6,760 的差额点名;
  散列文件头注释同步;任务栈表核对后无需抬栈;体积趋势行与同码凭证由权威构建
  重打(0 错误 0 警告,四项体积与上一轮相同——本轮改动只有注释与声明位置)
- 设计说明:§5 补第四条并发陷阱;§6.3 定字段口径(两份独立实现的共同依据);
  §7 回填实测;§9 周期行写实;§10 台架判据改两段式且不比绝对相位;§12 补读路径
  风险行;抬头状态改成「代码面已交付、未做完的只有板级那一轮」
- 卡片:软件闭环一栏列证据;台架三条按用户裁定保持未完成,不做虚假销项
- 口径同步:数据可信、UI 补齐、页面补齐规划、产品需求、采集短板评测、任务规划
  索引与两份 README;陈旧现状句清扫 16 处(带日期戳的历史取数行一律不动)
- 门禁说明:nested-comment 记仓库根探测必须兼容 linked worktree 的 gitdir 文件
  形态(修前该钩子在任务树里恒报环境错、静默不可用);coding-standard 记
  「函数头注释超过 24 行就把函数功能挤出回看窗口」这条机制与本次现场
- 覆盖率基线登记两个新覆盖模块(套件 41 → 44 套);环单测重打后该文件有效行
  227 → 226、覆盖行 226 → 225,比率仍 99.6%,是分母变小不是覆盖率回退

为什么是单一提交:文档链接、双端注册、生成器幂等(菜单与字库)、内存数字与
体积事实源、编号门禁这五道整仓门禁要求代码、页面、菜单树、字库、文档与台账
同处一个提交才自洽,按数据面/接线/人机/台账拆成四笔必然红其中至少四道

编号门禁新增行 14 处命中已按语义改写:魔数常量按字符命名、段名样例改由格式串
生成、频谱表头改 f / 2x / 5x 并加一行图例;改写后自洽断言 92 条仍全过
2026-10-29 19:23:15 +08:00

174 lines
19 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.
# STM32F4-Base 内存内容分配策略
> 版本:v1.0 日期:2026-09-21
> 适用工程:STM32F4-Base(STM32F407ZGTx @168MHz,FreeRTOS V10.3.1 + CMSIS-RTOS2,Keil MDK v5 / ARMCC)
> 状态:**当前有效策略**(台架验证:产品构建台架日志代次 68 干净启动 61 行;存储套件 20/20 PASSED;恢复跑 3/3 PASSED)
> 关联文档:`RAM使用分析与优化方案.md`(**内存管理主文档**:容量审计 + 优化方案 + 观测工具 + 基线数据)
> 布局实现:`MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct`
## 1. 本文档目的
定义"**哪类内存内容放进哪个 RAM 区域**"的分配规则,并记录 2026-09-21 "CCM 驻留者:FreeRTOS 堆 vs LVGL" 的裁决过程与理由。任何 scatter 布局调整、新增大缓冲、换料(芯片批次变化)前必须先查本文档。
## 2. 硬件区域事实(本机实测)
| 区域 | 地址 | 大小 | CPU 访问 | DMA1/DMA2 |
|---|---|---|---|---|
| RW_IRAM1(SRAM1+SRAM2) | 0x20000000 | 128KB | 168MHz 零等待 | **可达** |
| RW_CCM(SRAM3/CCM) | 0x10000000 | 64KB | 168MHz 零等待(与主 SRAM **同速**) | **不可达(本机实证)** |
关键事实:
1. **CCM 对 CPU 是全速内存**。Cortex-M4 无 语义取数与有效性 cache,"CCM 是慢内存"的直觉只适用于带 cache 的核(M7/出口与记录/H7 的非 cacheable 旁路),**在本芯片上不成立**——任何"按速度分配区域"的论证对本文档无效。
2. **DMA 对 CCM 地址的传输静默不落地**。2026-09-21 首轮 CCM 实验实证:以 CCM 上栈缓冲(dhara meta 132B / is_free 64B)为 DMA 源/目的时传输不生效,引发 FTL 重格式化循环与幻影坏块。此后 DMA 阈值提至 1024B 消除。**工程规则按"DMA 不可达 CCM"执行,不论数据手册如何表述。**
3. CCM 存在性已用 `ST-LINK_CLI -r32 0x10000000` 本机实测可读写;**克隆片可能缺 SRAM3,换料必须重测并回退布局**。
4. 工程不进入 Stop/Standby 低功耗模式,两区域掉电保留行为无差异,不构成分配判据。
> 勘误(原《STM32F4-Base 内存使用分析报告》2026-09-11 快照,随主文档合并保留于此):该报告内存地图中 "SRAM3 64KB (0x2001_C000)" 为笔误——F407 的 SRAM3 即 CCM,位于 `0x10000000`(AXI);AHB2 侧只有 SRAM1(112KB)+SRAM2(16KB)。**以本文为准**(`RAM使用分析与优化方案.md` §8.4 与 `内存布局_RAM.svg` 均按此口径)。
## 3. 全板 DMA 使用方清点(DMA 缓冲必须为 SRAM 静态)
| 使用方 | 外设 | DMA 缓冲 | 位置 |
|---|---|---|---|
| GD5F2GQ5UE NAND | SPI1 | 文件作用域静态页缓冲(2048B×3,`gd5f2gq5ue.c`) | SRAM |
| CH395F 以太网 | SPI2 | `s_spi2_dma_tx_buf` / `s_spi2_dma_rx_buf`(文件静态,`ch395f.c`) | SRAM |
| RS485 / USART1 调试 | UART4 / UART1 | 纯轮询,无 DMA | — |
推论:**全板 DMA 只寻址驱动文件静态缓冲**;堆内存(`pvPortMalloc`)、任务栈、UI 缓冲均不被 DMA 寻址。UI 帧下行路径 `g_ui_fbuf →(CPU memcpy)→ g_tx →(CPU memcpy)→ s_spi2_dma_tx_buf → SPI2 DMA` 已逐跳核实为 CPU 拷贝 + 静态 DMA 缓冲。该结论在 GD5F 驱动内由 `gd5f_dma_buf_ok()` 持续强制(见 R2),并有 TC-GD5F-704/705 负路径用例实证。
## 4. 内容分配规则
- **R1(CCM 驻留)**:CCM 只放"**CPU-only 且损坏即致命、无自愈**"的内存。当前唯一驻留者 = **FreeRTOS 堆**(`heap_4.o`,56KB——2026-10-12 B5 由 48KB 扩容,`.ioc` 语义取数与有效性参数 `configTOTAL_HEAP_SIZE=57344`;全部任务栈/队列/定时器)。scatter 的 RW_CCM 节**不得**再列其他对象;任何新增必须经 §5 式裁决并走 §7 完整验证阶梯。
- **R2(DMA 缓冲)**:只允许文件作用域静态缓冲,必须落 SRAM。**三层防线缺一不可**:
- **窗口防线(编译期)**:DMA 白名单窗口 `GD5F_DMA_RAM_BASE/END`(0x20000000~0x20020000,须与 scatter `RW_IRAM1` 一致)与 CCM 窗口**永不得重叠**,另锁死阈值契约 `132 ≤ GD5F_DMA_THRESHOLD < GD5F_PAGE_SIZE`——两条都用项目惯用的**负数组 typedef 断言**(AC5 不支持 C11 `_Static_assert`),有人改窗口常量或阈值当场编译失败(`gd5f2gq5ue.h`)。
- **尺寸防线**:GD5F DMA 阈值 `GD5F_DMA_THRESHOLD=1024`(>1024B 才走 DMA;阈值以下一律 CPU 轮询)。取值须 ≥ 存储路径上可能出现在任务栈的最大缓冲(dhara meta 132B、`dhara_nand_is_free` 64B)、且 < 页大小(2048,保住页级热路径的 DMA)。
- **地址防线(运行时 + 启动期)**:`gd5f_dma_buf_ok()` 运行时校验**整段**缓冲是否落在 SRAM 区间(0x20000000~0x2001FFFF),越界者拒走 DMA、回落 CPU 轮询,并打印 `[ERR][NAND] RX/TX DMA buffer not DMA-reachable (p=… size=…) fallback poll`;`nand_ftl.c` 在 init 期对三块静态页缓冲**预先过同一守卫**,把"scatter 被改动导致静态缓冲落进 CCM"变成开机即报,而不是静默传错。
阈值只挡得住"当前已知的小栈缓冲",挡不住"将来新加的 >1024B 非 SRAM 缓冲";地址守卫把 R2/R3 从**约定**升级为**运行时强制**。守卫负路径已由 TC-GD5F-704/705 在台架实证(读/写两方向各一,探针缓冲取自 CCM 堆,数据仍正确落位)。
⚠ **缓冲自身地址无法编译期断言**:`.bss` 地址链接期才确定,AC5 的常量表达式取不到(`__builtin_*` 系在 MicroLIB 下亦不可用,实证见 `Src/main.c` `Error_Handler` 注释)→ 静态缓冲落位靠 **scatter + 启动期自检**双层保证,别再试图"加个 `_Static_assert`"。
- **R3(堆/栈隔离)**:禁止以 `pvPortMalloc` 所得内存或任务栈为 DMA 源/目的(09-21 事故类:DMA 静默不落地 = 静默数据损坏,比报错更难查)。该约束在 GD5F 驱动内已由 `gd5f_dma_buf_ok()` 运行时强制(违反即回落轮询 + `[ERR]` 日志),并有 TC-GD5F-704/705 覆盖读、写两个方向。
- **R4(UI/LVGL 留 SRAM)**:LVGL 池 + 帧缓冲为**自愈型**内容(每帧由 widget 树全量重渲染,脏帧下一帧即消失),不需要保护区;且留 SRAM 保留 DMA 门(将来帧推送若要 DMA 加速必须 SRAM 可达);其越界风险已由 32B 保护带 + canary + 溢出钩子兜住(见 §5.3)。
- **R5(新内容放置判定表)**:
| 内容性质(判定依据) | 放置 | 理由 |
|---|---|---|
| DMA 源/目的 | **SRAM(强制)**,文件作用域静态 | CCM 不可达;堆/栈禁 DMA(R2/R3) |
| CPU-only + 损坏即致命、无自愈(调度器类) | **CCM**(容量允许时) | 保护区:DMA 野地址到不了、SRAM 溢出跨不过来 |
| CPU-only + 自愈/瞬态(帧缓冲、渲染状态) | SRAM | 无需保护;DMA 门保留(R4) |
| CPU-only + 持久程序状态(配置引擎、FAT 卷状态、网络状态机) | SRAM(默认) | 损坏属中级事故(可自愈/可重格式化恢复);CCM 配额留给致命类 |
- **R6(容量台账)**:任何新增静态缓冲 >4KB、或改动 scatter/堆大小,必须先核 §6 台账余量,改后同步更新 §6 数字、`.sct` 头注释与 `AGENTS.md` 栈表。
## 5. 决策记录:2026-09-21 CCM 驻留者裁决(堆 vs LVGL)
### 5.1 议题
CCM 64KB 只能容纳 {FreeRTOS 堆 48KB} 与 {LVGL 集合 ~52.5KB} 之一(两者合计 101KB > 64KB)。候选方案:
- **方案 A(现行)**:堆 → CCM;LVGL 集合 → SRAM
- **方案 B(提议,未采纳)**:LVGL 集合 → CCM;堆 → SRAM
### 5.2 候选比较
当时提出的两条判据经核实**均为事实但不具区分力**:
- "UI 不用 DMA" ✓ 属实(§3 路径核实:DMA 只寻址驱动静态缓冲)→ 堆同样不用 DMA,两候选在 DMA 轴**等价**
- "图形不紧迫速度" ✗ 无效判据(§2:M4 无 cache,CCM 与主 SRAM 对 CPU 同速)→ 两候选在速度轴**等价**
真正可判别的轴是**"该内存损坏后的后果" × "DMA 不可达带来的隔离收益"**:
| 内存块 | 大小 | 损坏后果 | 自愈能力 | DMA 不可达的收益 |
|---|---|---|---|---|
| FreeRTOS 堆(调度器控制块 + 6 任务栈 + 队列/定时器) | 48KB | 调度静默损坏 = HardFault/复位循环/死系统,**最坏结果** | 无 | **大**:DMA 野地址传输到不了 CCM(表现为"数据没到",可检测),调度器字节免疫 DMA 类故障 |
| LVGL 集合(16KB 池 + 帧 4 份副本) | ~28.5KB | 最坏 = 屏幕花屏 | **强**(每帧全量重渲染;已实证的 1 字节越界由 32B 保护带兜住) | 无(永不用 DMA,放 CCM 只会关上 DMA 门) |
分配规则:**"损坏即致命 + 无自愈"的内存进保护区;"自愈型"内存留 SRAM。**
### 5.3 裁决:方案 A(堆 → CCM)——即当前布局
理由(按权重排序):
1. **致命类优先**:方案 B 把 48KB 调度内存放回 SRAM,与全部潜在溢出源(含 FatFS、dhara 两个有实证事故史的第三方组件)同处一域;本项目实证过的故障族正是 1 字节类越界。
2. **DMA 不可达对堆是防护**:任何 DMA 野地址/错地址传输都无法落入 CCM(静默不落地,发起方表现为可检测的"数据未到"),调度器因此免疫 DMA 类故障;方案 B 中该防护失效。
3. **LVGL 残余风险已被现有手段兜住**:32B 保护带 + canary 实证 + `configCHECK_FOR_STACK_OVERFLOW=2` + malloc 失败钩子;已发生的 1 字节越界被完整 containment。
4. **"UI bug 最多弄坏屏幕、弄不死系统"是台架既成事实**:越界发生时 CCM 中的调度器毫发无伤(台架日志代次 68 干净启动、存储套件 20/20),这正是方案 A 的直接收益。
### 5.4 方案 B 的胜出点(如实记录,不足以翻盘)
- 最坏构建(存储套件)下多腾 ~4.5KB SRAM 余量(LVGL 集合 52.5KB > 堆 48KB,CCM 装得下更大的块)
- LVGL 若再次越界且超出 32B 保护带,溅射域收敛在 LVGL 私有 CCM 域(方案 A 下溅入 SRAM 程序状态)
两者均为中级收益,不足以抵消"调度器暴露于 SRAM 全域溢出"引入的顶级风险。
### 5.5 一并否决的混合方案
- 仅 LVGL 池(16KB)进 CCM、堆回 SRAM:16+56=72KB > 64KB 无法两全,调度器失去保护区 → 劣于方案 A
- 仅帧副本进 CCM:帧副本是自愈型内容,放 CCM 只关 DMA 门、零防护收益 → 无意义
### 5.6 若未来确需改判
触发条件:测试构建 SRAM 余量不足,**且**已耗尽 `RAM使用分析与优化方案.md` 的帧副本去重(−6.4~9.6KB)与 LVGL 池缩减等低代价手段。
流程:改 scatter → 走 §7 完整验证阶梯 → 同步更新本文档、`AGENTS.md`、`.sct` 头注释。在此之前,方案 A 是唯一有效布局。
## 6. 当前容量台账(2026-10-29,产品构建 task/wave-record 分支终值;2026-09-21 最坏构建=存储套件的历史基线见 `docs/内存管理/RAM预算台账.md` 趋势)
| 区域 | 总量 | 已用 | 空闲 | 驻留内容 |
|---|---|---|---|---|
| RW_IRAM1(SRAM) | 131,072B (128KB) | 89,936B (87.83KB, 68.6%) | 41,136B (~40.2KB) | 全部静态(除堆),含录波环 `wave_ring.o` 5,300B 与 LVGL 池 16,384B(`lv_mem_core_builtin.o` 实测) |
| RW_CCM | 65,280B(64KB 减 256B `UNINIT_CCM` 尾,与 `.sct` 一致) | 57,376B (56.03KB, 87.9%) | 7,904B | **仅** `heap_4.o`(ucHeap 56KB,2026-10-12 B5 扩容);256B 尾区现用 48B |
数字身份:`RW_IRAM1` / `RW_CCM` / `ER_IROM1` 三行取自 lead 2026-10-29 权威构建(本工作树 `MDK-ARM/STM32F407-Demo/STM32F407-Demo.map`,0 错误 0 警告,`scripts/ram_baseline.py --update` 同批入 tsv)。受管快照区在 `docs/内存管理/RAM使用分析与优化方案.md`,子系统配额表在 `docs/内存管理/RAM预算台账.md`,两处由脚本渲染、本卡不手改。
LVGL 集合明细(均在 SRAM):LVGL 池 16,384B(`lv_mem_core_builtin.o`,`LV_MEM_ADR=0` 内部静态)+ `g_ui_draw_buf` 3,232B(3200+32 保护带)+ `g_ui_fbuf` 3,200B + `g_tx` 3,209B + `g_lcdmem_data` 3,204B。
产品构建比套件构建少 testTask/测试缓冲(约 40KB 级),SRAM 余量更大。数字以 map 实测为准,布局变更后必须重测并更新本表。
录波支路静态(2026-10-29 入册,R6 要求;**全部落 RW_IRAM1、无 section 属性,CCM 增量 0**):
| 归属对象 | 增量 | 构成 |
|---|---|---|
| `wave_ring.o`(`App/algo/wave_ring.c`) | 5,300 B(ZI 5,256 + RW 44) | 录波环 int16×320 点×8 路 = 5,120 B 主体 + 已封存段头副本 136 B + 状态/索引/观测计数;map 比代码侧手算多 2 B,是链接器对齐 |
| `default_task.o`(落盘道,函数内 `static`) | +1,204 B(2,736 → 3,940,同 tsv 的 RW+ZI 口径) | `FIL` 528 B(`FF_FS_TINY=0`,刻意不上栈)+ 分片搬运缓冲 256 B + 段头副本 136 B + 段路径 17 B + 打包上下文/`DIR`/`FILINFO`/状态字节约 267 B(对象级增量,未逐项拆分实测) |
| `adc_task.o` 及其余数据面 | 约 256 B | 8 路语义原码暂存与封存瞬间交棒结构(`App/task/adc_task.c`);与上两行相加 = 数据面实测 +6,760 B |
| UI 道:`ui_page_wave_play.o` 92 B、`ui_page_wave_manual.o` 28 B、`ui_task.o`、`ui_data.o` | 区级合计 +1,640 B | 两个新页面模块按 map 计 120 B;`ui_task.o`/`ui_data.o` 的增量拆分需要本波之前的 per-file 快照,本卡没有 → **只记区级增量,不猜分项** |
- **三档对照(SRAM1)**:81,536 B / 62.2 %(`scripts/ram_baseline.tsv` 2026-10-28 行)→ 88,296 B(录波数据面终值)→ **89,936 B / 68.6 %**(含 UI 道终值),累计 **+8,400 B**;`RW_CCM` 三档恒 57,376 B —— 设计文档 `docs/子系统设计/故障录波设计说明.md` §7 的「不进 CCM」落位达成,也没有挤动 CCM。
- **预估 vs 实测都要留**:设计文档 §7 原估静态增量约 5.9 KB(环 5,120 + 索引状态 32 + 双缓冲 512 + 读窗 256),实测数据面 +6,760 B。差额构成:`FIL` 528 B、`DIR`/`FILINFO`、段头副本 136 B、打包上下文(内含一份 136 B 段头)原估未记账;反向一项——落盘实际是**单缓冲 256 B**,不是预估的 512 B 双缓冲。只写实测会让下一次估算照着 6.7 KB 走、看不到原估漏了哪几项。
- **Flash**:`ER_IROM1` 264,580 → 270,248(数据面 +5,668 B)→ 272,300(UI 道 +2,052 B)→ 272,360(落盘判废复检 +60 B)→ **272,336 B**(cppcheck 返工:把变量声明搬进最小作用域,纯搬声明 −24 B),累计 **+7,756 B**、占 26.0 %;`Total ROM` 274,308 B(26.2 %)、`Code` 243,552 B。RAM 三项(RW 1,972 / ZI 145,388 / RW_IRAM1 89,936 B / 68.6 %)与 CCM 57,376 B / 87.9 % 均未变。
- **栈:9 任务全 PASS、未抬栈**(`scripts/stack_report.py` 静态最坏链)——adcTask 1,424 B(余 1,136 B / 44 %)、defaultTask 3,040 B 不变、protectTask 880 B、uiTask 1,336 B(余 15,048 B / 91 %)。R6 三处同步里 `AGENTS.md` 栈表这一处的结论是**核对过、数值未变**。
- **为什么比上一版低**:本表 2026-10-12 版记 95,816 B / 73.1 %,高于今天的 89,936 B。原因是此后 LVGL 内部池由 40 KB 缩到 16 KB(`docs/内存管理/RAM预算台账.md` §3「缩池 ✅ 2026-10-15」行;`scripts/ram_baseline.tsv` 2026-10-12 与 2026-10-24 两行的 `top_rwzi` 列可见 `lv_mem_core_builtin.o` 40960 → 16384)——**不是本波回收了什么**。
- **子系统归集的坑(本卡实测,值得记一笔)**:`scripts/ram_baseline.py` 的 `subsystem_of()` 按对象名前缀首匹配,**表里没有 `wave_` 前缀** → 录波环 `wave_ring.o` 5,300 B 落进 **「其它」(15,853 B,参考值、无配额)**,配额 8 KB 的「应用」行看不见它;落盘道 `default_task.o`(+1,204 B)与 `adc_task.o` 才计入「应用」(现值 16,888 B,本波前该行已在配额外,成因见 `docs/内存管理/RAM预算台账.md` §2「应用 ⚠ 小幅超」条)。结论:**别拿子系统表当录波预算的判据**,要按本节 per-file 台账读。要让配额表真看见录波支路,得给 `subsystem_of()` 加 `wave_` 前缀——那是脚本改动,本卡不动,已报 lead。
## 7. 布局变更验证阶梯(强制执行)
任何 scatter / CCM / 堆大小变更后,依次通过以下全部关卡方可认定生效:
1. `@build`(0 错误 0 警告)
2. 产品构建 `@test`(`.dsh/skills/serial-boot-test/scripts/run_boot_test.py`):标记校验 PASS,且**零 `[ERR]`**(默认 Forbidden 已含 `[ERR]`)
3. 存储套件(`test_config.h` → `TEST_SUITE_STORAGE`):只读模式实测 **3/3 PASSED**(破坏性关,卷内容不变);开启 `ENABLE_DESTRUCTIVE_TESTS` 后为全量集成用例(2026-09-20 实测 **20/20 PASSED**,含 TC-STORAGE-01/02/03/05/06)
4. GD5F 套件(`TEST_SUITE_GD5F`):只读模式实测 **16/16 PASSED**(含只读的守卫负路径 TC-GD5F-704);开启 `ENABLE_DESTRUCTIVE_TESTS` 后 **68/68 PASSED**,并确认日志出现 2 条守卫负路径 `[ERR]`(TC-GD5F-704/705)——破坏性模式运行须放宽 Forbidden(`--forbidden "FAIL;HardFault"`)
5. 恢复跑(`ENABLE_STORAGE_RECOVERY_TESTS`):期望 **3/3 PASSED**
6. **破坏性用例默认关闭**(见 `test_config.h` 的 `ENABLE_DESTRUCTIVE_TESTS`):写/擦 NAND、格式化、坏块注入与 FatFS 写类用例只在首次上电/产线首启或明确需要时开启——2026-09-21 实测一轮破坏性 GD5F 运行即让 dhara 多记一个坏块(容量 95248 → 95200,且该计数持久化在 dhara journal checkpoint 头里,`nand_ftl_format` 清不掉);日常回归请保持只读模式。容量满值 = **96064 pages(187 MB)**,被扣掉的容量可用 `ENABLE_STORAGE_RECOVERY_TESTS` + `STORAGE_RECOVERY_FULL_WIPE`(全片擦除 + 重建 BBT)恢复(实测 95200 → 96064 ✓)
通过全部关卡后:更新 §6 台账、`AGENTS.md` 栈表、`.sct` 头注释,并把本次裁决补入 §5。
## 8. 附:策略落点索引
| 策略要素 | 代码位置 |
|---|---|
| CCM 布局(仅 heap_4.o) | `MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct` |
| DMA 阈值 1024B | `Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h`(`GD5F_DMA_THRESHOLD`,公开供测试对齐上下界) |
| DMA 窗口/阈值**编译期**断言 | `Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h`(`gd5f_dma_window_no_ccm_check_t` 窗口不重叠 + `gd5f_dma_threshold_contract_check_t` 阈值契约,负数组 typedef) |
| DMA 可达性运行时守卫 | `Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c`(`gd5f_dma_buf_ok`,`gd5f_read_from_cache`/`gd5f_program_load` 两处接入) |
| 静态缓冲落位**启动期**自检 | `Drivers/BSP/GD5F2GQ5UE/nand_ftl.c`(init 期对三块 2KB 页缓冲过 `gd5f_dma_buf_ok`) |
| 守卫负路径用例(读/写) | `test/board/gd5f_test_task.c`(TC-GD5F-704/705) |
| 错误级日志纳入自动闭环 | `.dsh/skills/serial-boot-test/scripts/verify_log.py` + `.dsh/skills/serial-boot-test/scripts/run_boot_test.py`(默认 Forbidden 含 `[ERR]`,`[FAIL]` 仍拦截断言失败) |
| LVGL 越界保护带 32B | `Drivers/BSP/UI/ui_port.c`(`UI_DRAW_BUF_GUARD`) |
| 栈溢出/分配失败钩子 | `Inc/FreeRTOSConfig.h` + `Src/freertos.c`(`vApplicationStackOverflowHook` / `vApplicationMallocFailedHook`) |
| HardFault 现场上报 | `Src/stm32f4xx_it.c`(`HardFault_Handler` → `hard_fault_report`) |
已知未决风险:
- `.ioc` 未与 `freertos.c` 的栈值直改同步——CubeMX 重新生成会覆盖栈值(已注释标记,未修)
- CCM 存在性依赖芯片批次(克隆片可能缺 SRAM3,见 §2)