diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index a8494ebf..4b3b655b 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -26,8 +26,8 @@ | 存储 | 7,072 B | 12 KB | OK | | UI | 36,428 B | 16 KB | ⚠ 超 19.6 KB | | RTOS 堆 | 57,376 B | 56 KB | 堆=配额 | -| 应用 | 16,888 B | 8 KB | ⚠ 超 8.5 KB | -| 其它 | 15,849 B | — | 参考 | +| 应用 | 26,333 B | 8 KB | ⚠ 超 17.7 KB | +| 其它 | 6,404 B | — | 参考 | - **门禁**:pre-commit `mem-budget` 钩子(`scripts/ram_baseline.py --gate`)—— @@ -51,8 +51,8 @@ UI 16 KB / RTOS 堆 56 KB / 应用 8 KB**。归集口径(按 `.map` 对象名 | 存储 | `gd5f*` `nand_*` `ff*` `diskio*` `vol_lock*` + dhara 裸名(`map/journal/nand/fctl/era.o`) | `cfg_*`(算应用) | | UI | `lv_*` `ui_*`(含 `ui_net_sink`、`ui_task`) | — | | RTOS 堆 | `heap_4.o`(CCM)——配额 = `configTOTAL_HEAP_SIZE` 本身 | 内核对象(`tasks/queue/timers/...` 归"其它") | -| 应用 | `App/` 其余:采集 `adc_*`、保护 `prot_*`、相量 `phasor/sample_ring`、配置 `cfg_*`、日志 `sys_/dbg_/console_log`、对时 `time_sync*`、`sys_clock`、`task_watchdog`、`sha256`、`tpafe*`、`sd2506`、`default_task` 等 | — | -| 其它 | 内核对象、HAL、CMSIS、启动文件、C 库、链接器填充 | — | +| 应用 | `App/` 其余——**前缀清单以 `scripts/ram_baseline.py` 的 `subsystem_of()` 为准**:`adc_*` `tpafe*` `sd2506` `prot_*` `protect_task` `phasor` `sample_ring` `wave_*` `meas` `freq` `run_par` `iec104*` `soe_*` `cfg_*` `sys_*` `dbg_*` `console_log` `time_sync*` `sys_clock` `task_watchdog` `sha256` `default_task` `test_task` `task_prof` `boot_stats` `vol_idle_sync` | `App/task/ui_task*`(首匹配算 UI) | +| 其它 | 内核对象、HAL、CMSIS、启动文件、C 库、链接器填充——**不含 `App/` 任何对象**:漏登记会被 `--gate` 的兜底 WARN 逐条点名(2026-10-29 加) | — | **现状判读(2026-10-12 建档)**: @@ -62,11 +62,12 @@ UI 16 KB / RTOS 堆 56 KB / 应用 8 KB**。归集口径(按 `.map` 对象名 **这条 ⚠ 会长期挂着**,判读时按已知挂账处理,别每轮当新问题追;重开条件 → `RAM使用分析与优化方案.md` §4。 - **RTOS 堆 = 配额本身**:56 KB 即 `configTOTAL_HEAP_SIZE`(2026-10-12 B5 由 48 KB 扩容,见 §6; `heap_4.o` 另含 32 B 元数据,不计超);**改堆 = 改配额**,必须先过本文件 §6 登记。 -- **应用 ⚠ 小幅超**:8 KB 为报告规划值;2026-10-10 栈整改把 3 处 `FIL` 从栈搬进 BSS +- **应用 ⚠ 超配额**(现值见 §1 受管区):8 KB 为报告规划值;2026-10-10 栈整改把 3 处 `FIL` 从栈搬进 BSS (静态 +4,128 B,见 `docs/内存管理/RAM使用分析与优化方案.md` §4"2026-10-10 复测")—— - 属已评审的"以空间换栈安全",差额在此如实体现。2026-10-29 故障录波落盘道又把 `default_task.o` - 推高 1,204 B(该行现值 16,888 B);**录波环本体 5,300 B 不计入本行**——`subsystem_of()` - 没有 `wave_` 前缀,它落在「其它」,要读全貌看 `docs/内存管理/内存内容分配策略.md` §6 的 per-file 台账。 + 属已评审的"以空间换栈安全";2026-10-29 故障录波落盘道又把 `default_task.o` 推高 1,204 B。 + **同日修正归集口径**:`subsystem_of()` 此前没有 `wave_` `iec104*` `soe_` `meas` `freq` `run_par` + 等前缀,12 个 `App/` 对象 9,445 B 被静默算进「其它」(当时「应用」行只有 16,888 B,看着离配额不远); + 补齐后本行才真含录波环 `wave_ring.o`(约 5.3 KB)。per-file 全貌 → `docs/内存管理/内存内容分配策略.md` §6。 ## 3. CCM 分配台账(谁能用 CCM) @@ -99,3 +100,4 @@ UI 16 KB / RTOS 堆 56 KB / 应用 8 KB**。归集口径(按 `.map` 对象名 | 2026-10-12 | **RTOS 堆配额 48 KB → 56 KB**(`QUOTAS` 同步) | B5 protectTask 立项:iec104Task(2.5KB)+protectTask(2KB)+TCB 入堆后 48KB 池 min_ever 预估跌破 3,072 B 告警线 → `.ioc` 语义取数与有效性参数 `configTOTAL_HEAP_SIZE` 49152→57344(CCM 水位 75.3%→87.9%,仍 <92% 门禁;CCM 空闲降至 ≈8 KB)。连带影响见 §3 迁 CCM 后半链注记 | | 2026-10-12 | **任务栈 +1536 B(堆内用量,配额 56 KB 不变)**:defaultTask 1024→1280 words(+1024 B)、timeSyncTask 512→640 words(+512 B);产品任务栈合计 37,632→39,168 B | 登记簿 N-33 栈收口:B7 定值持久化把 defaultTask 静态链推到 3016 B(余 1080 < 30% 线)、timeSyncTask 链 1264 B(余 784 < ISR 底线 1024);B6 台架背书 min_ever=10000 → 抬后实测 `heap free=16400 / min_ever=8464`(与预算 −1536 完全吻合,>3072 告警线);`stack_report` 9 任务 0 FAIL + `@test` PASS,见 `RAM使用分析与优化方案.md` §1.4 | | 2026-10-29 | **故障录波数据面入册**:新增对象 `wave_ring.o` 5,300 B(ZI 5,256 + RW 44,落 SRAM1、CCM 增量 0);`default_task.o` 静态 2,736 → 3,940 B(+1,204 B,落盘道 `FIL` 528 + 分片缓冲 256 + 段头副本 136 + 段路径 17 + 打包上下文与 `DIR`/`FILINFO`);`RW_IRAM1` 81,536 → 89,936 B(62.2 % → 68.6 %,仍远低于 92 % 门禁);`RW_CCM` 三档恒 57,376 B | 决定人 lead。实测数字出自 lead 的权威构建(本工作树 `MDK-ARM/STM32F407-Demo/STM32F407-Demo.map`,0 错误 0 警告,`scripts/ram_baseline.py --update` 同批入 tsv);预估约 5.9 KB vs 实测数据面 +6,760 B 的差额构成与三档对照见 `docs/内存管理/内存内容分配策略.md` §6;**未抬栈**(`scripts/stack_report.py` 九任务全 PASS),判据出处 `docs/子系统设计/故障录波设计说明.md` §7 | +| 2026-10-29 | **归集口径修正(非配额改动)**:`subsystem_of()` 补 `wave_` `iec104*` `soe_*` `meas` `freq` `run_par` `protect_task` `task_prof` `boot_stats` `vol_idle_sync` 等前缀——12 个 `App/` 对象 9,445 B 此前静默落「其它」;「应用」16,888 → 26,333 B、「其它」15,849 → 6,404 B,区域水位与 92% 红线均未变。同脚本加兜底:`App/**/*.c` 的对象落进「其它」时 `--gate` 逐条 WARN(不拦) | 配额表看不见录波/SOE 支路;口径依据本文件 §2「应用 = `App/` 其余」 | diff --git a/docs/内存管理/内存内容分配策略.md b/docs/内存管理/内存内容分配策略.md index 70f88ad8..5dd2f9f9 100644 --- a/docs/内存管理/内存内容分配策略.md +++ b/docs/内存管理/内存内容分配策略.md @@ -138,7 +138,7 @@ LVGL 集合明细(均在 SRAM):LVGL 池 16,384B(`lv_mem_core_builtin.o` - **栈: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。 +- **子系统归集的坑(本卡实测 → 2026-10-29 已修)**:`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 的应用条目)。**同日后续**:补齐 12 个 `App/` 对象(9,445 B)的前缀,并加兜底——`App/**/*.c` 编出的对象只要落进「其它」,`python scripts/ram_baseline.py --gate` 就逐条 WARN 点名(软提醒不拦)。结论仍然有效:**子系统配额行不是录波预算的判据**,要按本节 per-file 台账读;差别只在「应用」行现在真含录波支路,两个口径不再互相看不见。 ## 7. 布局变更验证阶梯(强制执行) 任何 scatter / CCM / 堆大小变更后,依次通过以下全部关卡方可认定生效: diff --git a/scripts/ram_baseline.py b/scripts/ram_baseline.py index c338b2af..48ca48c9 100644 --- a/scripts/ram_baseline.py +++ b/scripts/ram_baseline.py @@ -78,7 +78,11 @@ def subsystem_of(obj): return RTOS_HEAP if n in _KERNEL: return "其它" - if n.startswith(("app_", "adc_", "prot_", "phasor", "sample_ring", + # 应用 = App/ 其余(台账 §2 口径)。新增 App 目录/前缀必须同步这里, + # 漏登记不会报错,只会被静默算进「其它」——unattributed_app_objects() 是兜底信号。 + if n.startswith(("app_", "adc_", "prot_", "protect_task", "phasor", "sample_ring", + "wave_", "meas", "freq", "run_par", "iec104", "soe_", + "task_prof", "boot_stats", "vol_idle_sync", "sys_", "cfg_", "dbg_", "console_log", "task_watchdog", "sha256", "tpafe", "sd2506", "test_task", "time_sync", "default_task")): @@ -86,6 +90,56 @@ def subsystem_of(obj): return "其它" +# 兜底:源文件在 App/ 却落进「其它」= 前缀表漏登记。2026-10-29 实测这样隐身了 +# 12 个对象 / 9,445 B(wave_ring 5,296 起),配额表完全看不见录波与 SOE 支路。 +_APP_SRC_DIR = REPO_ROOT / "App" +_APP_STEMS = None + + +def _app_stems(): + """App/**/*.c 主干名集合(扫一次缓存);目录读不到就返回空集 = 兜底自动失效。""" + global _APP_STEMS + if _APP_STEMS is None: + try: + _APP_STEMS = {p.stem.lower() for p in _APP_SRC_DIR.rglob("*.c")} + except OSError: + _APP_STEMS = set() + return _APP_STEMS + + +def _obj_base(obj): + return obj.rsplit("\\", 1)[-1].rsplit("/", 1)[-1].lower() + + +def unattributed_app_objects(objs): + """[(对象, B)]:归进「其它」但源文件在 App/ 的对象,按字节降序。""" + stems = _app_stems() + if not stems: + return [] + out = [] + for obj, nbytes in objs: + if subsystem_of(obj) != "其它": + continue + base = _obj_base(obj) + if base.endswith(".o") and base[:-2] in stems: + out.append((obj, nbytes)) + return sorted(out, key=lambda t: (-t[1], t[0])) + + +def warn_unattributed(info): + """打印未归类告警(软提醒,不拦);返回条数。门禁与 --write 都调,别让人静默隐身。""" + missing = unattributed_app_objects(info["objs"]) + if not missing: + return 0 + total = sum(n for _, n in missing) + print("[WARN] %d 个 App/ 对象未登记进 subsystem_of() 前缀表,被算进「其它」而不是" + "「应用」(台账 §2:应用 = App/ 其余),合计 %d B:" % (len(missing), total)) + for obj, nbytes in missing: + print(" %-28s %6d B" % (_obj_base(obj), nbytes)) + print(" 修法:补 subsystem_of() 前缀 -> 重跑 @build -> --update / --write 回填台账") + return len(missing) + + def collect(map_path): """解析 .map → {'regions': {name: r}, 'subsys': {name: bytes}, 'top': [(obj, B)]}。""" data = ram_report.parse_map(str(map_path)) @@ -96,11 +150,11 @@ def collect(map_path): subsys = {s: 0 for s in SUBSYSTEMS} for c in comps: subsys[subsystem_of(c["object"])] += c["rw_data"] + c["zi_data"] - top = sorted(((c["object"], c["rw_data"] + c["zi_data"]) for c in comps), - key=lambda t: (-t[1], t[0])) - top = [t for t in top if t[1] > 0][:TOP_N] + objs = [(c["object"], c["rw_data"] + c["zi_data"]) for c in comps] + objs = [t for t in objs if t[1] > 0] + top = sorted(objs, key=lambda t: (-t[1], t[0]))[:TOP_N] return {"regions": regions, "region_list": data["regions"], - "subsys": subsys, "top": top} + "subsys": subsys, "top": top, "objs": objs} def region_pct(r): @@ -238,6 +292,8 @@ def cmd_gate(args): if newest and newest > map_path.stat().st_mtime: print("[WARN] .map 早于最新源码——数字来自旧构建,建议先 @build 再提交") + warn_unattributed(info) + ir, cc = info["regions"].get("RW_IRAM1"), info["regions"].get("RW_CCM") print(f"RAM 预算门禁 PASS:RW_IRAM1 {region_pct(ir):.1f}% / RW_CCM {region_pct(cc):.1f}%" f"(阈值 {THRESHOLD_PCT:.0f}%)")