merge: N32 并入 v2-on-ui 主干(解 RAM 快照冲突并重生成)
N32 的 8 个提交与主干 v2-on-ui 的 6 个新提交合并。唯一冲突是 `docs/内存管理/RAM使用分析与优化方案.md` 的 mem-numbers 快照数字表: - N32 侧基于旧基线(e4134a4)刷新 - v2-on-ui 侧含其他任务(B4/IEC104 等)的新代码 **两边都不能直接选**,故解冲突后按合并后的**实际构建**重新生成: @build(合并后代码,0 错 0 警)→ check_mem_numbers.py --write → ram_baseline.py --write(同步 RAM 台账受管区)。 合并后内存水位:ER_IROM1 223,724 / RW_IRAM1 96,144 (73.4%) / RW_CCM 57,376 (**87.9%**) / Flash 224,772;mem-budget 92% 门禁 PASS。 (RW_CCM 87.9% 已接近红线,后续加缓冲需注意。) 其余文件自动合并成功(uvprojx / check_registration.py)。
This commit is contained in:
@@ -38,7 +38,7 @@
|
||||
& '.venv\\Scripts\\python.exe' scripts/check_registration.py
|
||||
& '.venv\\Scripts\\python.exe' scripts/check_registration.py --strict
|
||||
|
||||
设计依据:docs/待处理内容.md N-23;.dsh/skills/new-source-file
|
||||
设计依据:docs/任务规划/登记簿.md N-23;.dsh/skills/new-source-file
|
||||
创建日期:2026-10-12
|
||||
"""
|
||||
|
||||
@@ -81,6 +81,7 @@ SIM_EXEMPT = [
|
||||
("App/algo/phasor.c", "L2 相量纯算法,模拟器无消费路径"),
|
||||
("App/algo/prot_elm.c", "L3 保护元件纯算法,模拟器无消费路径"),
|
||||
("App/algo/prot_set.c", "L5 保护定值纯算法,模拟器无消费路径"),
|
||||
("App/cfg/prot_set_store.c", "定值持久化 MCU 胶水(依赖 prot_set 纯算法),模拟器无消费路径"),
|
||||
("App/algo/sample_ring.c", "L2 环形缓冲纯算法,模拟器无消费路径"),
|
||||
("App/app/app_main.c", "固件启动/就绪屏障,MCU 专属"),
|
||||
("App/fs/vol_lock.c", "FreeRTOS 互斥量卷锁,MCU 专属"),
|
||||
@@ -88,9 +89,14 @@ SIM_EXEMPT = [
|
||||
("App/sync/time_sync.c", "对时任务(net+RTOS),MCU 专属"),
|
||||
("App/task/adc_task.c", "固件 FreeRTOS 任务,模拟器不跑固件任务"),
|
||||
("App/task/default_task.c", "固件 FreeRTOS 任务,模拟器不跑固件任务"),
|
||||
("App/task/iec104_serv.c", "固件 FreeRTOS 任务(104 上行,B4),模拟器不跑固件任务"),
|
||||
("App/task/protect_task.c", "固件 FreeRTOS 任务(保护判据节拍,B5),模拟器不跑固件任务"),
|
||||
("App/task/net_task.c", "固件 FreeRTOS 任务,模拟器不跑固件任务"),
|
||||
("App/task/rs485_task.c", "固件 RS-485 任务,MCU 专属(另:uvprojx 缺口见 KNOWN_GAPS)"),
|
||||
("App/task/ui_task.c", "固件 UI 任务,模拟器另有 main.c 驱动 UI"),
|
||||
("App/protocol/iec104/iec104_asdu.c", "104 ASDU 编解码纯逻辑,L0.5 主机单测覆盖,模拟器无消费路径"),
|
||||
("App/protocol/iec104/iec104_link.c", "104 链路状态机纯逻辑(I/O 注入),L0.5 覆盖,模拟器无消费路径"),
|
||||
("App/protocol/iec104/iec104_pts.c", "104 点表/数据源适配(phasor/prot 依赖),模拟器无消费路径"),
|
||||
("App/util/sha256.c", "纯工具(FTP 鉴权在 MCU 端),模拟器无消费路径"),
|
||||
("App/util/sys_clock.c", "RTC/SD2506 依赖墙钟,MCU 专属"),
|
||||
("App/util/task_prof.c", "任务性能探针,MCU 专属"),
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
& '.venv\\Scripts\\python.exe' scripts/check_test_suite.py --allow-test-build
|
||||
& '.venv\\Scripts\\python.exe' scripts/check_test_suite.py --file <路径>
|
||||
|
||||
设计依据:docs/待处理内容.md T-1;docs/测试说明/测试开发标准流程.md §3.1
|
||||
设计依据:docs/任务规划/登记簿.md T-1;docs/测试说明/测试开发标准流程.md §3.1
|
||||
创建日期:2026-10-01
|
||||
"""
|
||||
|
||||
|
||||
+10
-7
@@ -13,8 +13,8 @@
|
||||
|
||||
刻意**不含**(与 CI 同理由,见 workflow 文件头):
|
||||
- `@build`(需 Keil 授权;CI 也不跑,真编译是开发者本地红线)
|
||||
- `mem-numbers` 钩子(事实源 `.map` 是 Keil 产物——**本地若存在 .map 就照常跑**,
|
||||
与 CI 的自适应逻辑一致)
|
||||
- `mem-numbers` / `mem-budget` 钩子(事实源 `.map` 是 Keil 产物——**本地若存在 .map
|
||||
就照常跑**,与 CI 的自适应逻辑一致)
|
||||
|
||||
退出码:
|
||||
0 = 全部通过(或不适用项已按规则跳过)
|
||||
@@ -147,17 +147,20 @@ def run_l0():
|
||||
print("=" * 64)
|
||||
|
||||
env = dict(os.environ)
|
||||
# 与 CI 相同的自适应:无 .map(未做 Keil 构建)=> mem-numbers 不适用 => 跳过
|
||||
# 与 CI 相同的自适应:无 .map(未做 Keil 构建)=> mem-numbers / mem-budget
|
||||
# 不适用 => 跳过(两者事实源都是 Keil 产物 .map)
|
||||
map_path = _find_map()
|
||||
if map_path:
|
||||
print("[INFO] 找到 .map:{}".format(map_path.relative_to(REPO_ROOT)))
|
||||
print("[INFO] mem-numbers 照常运行")
|
||||
print("[INFO] mem-numbers / mem-budget 照常运行")
|
||||
else:
|
||||
print("[INFO] 未找到 .map(未做 Keil 构建)-> 跳过 mem-numbers(与 CI 一致)")
|
||||
print("[INFO] 未找到 .map(未做 Keil 构建)-> 跳过 mem-numbers / "
|
||||
"mem-budget(与 CI 一致)")
|
||||
print("[INFO] :warning: 体积门禁未执行——改代码后请本地 @build + "
|
||||
"check_mem_numbers.py --scan 兜底")
|
||||
"check_mem_numbers.py --scan + ram_baseline.py --gate 兜底")
|
||||
skip = env.get("SKIP", "")
|
||||
env["SKIP"] = (skip + "," if skip else "") + "mem-numbers"
|
||||
add = "mem-numbers,mem-budget"
|
||||
env["SKIP"] = (skip + "," if skip else "") + add
|
||||
|
||||
rc, text = _run_capture([PY, "-m", "pre_commit", "run", "--all-files"], env=env)
|
||||
# 汇总行(对齐 CI 的 ::notice::):统计钩子 Passed/Failed/Skipped
|
||||
|
||||
@@ -0,0 +1,407 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
RAM 预算门禁 + 趋势台账(登记簿 N-27 前半 / 审查报告 §5.5 三件套,2026-10-12)。
|
||||
|
||||
三件套的机械部分收在本脚本(事实源 = Keil `.map`,与 `check_mem_numbers.py` 同口径):
|
||||
|
||||
1. **92% 阈值门禁**(`--gate`,pre-commit `mem-budget` 钩子入口):
|
||||
RW_IRAM1 / RW_CCM 任一区占比 > 92% → FAIL,并附 Top-10 RW+ZI 对象表;
|
||||
无 `.map`(未做 Keil 构建)→ 自适应跳过(与 `mem-numbers` 同口径,
|
||||
CI 盲区登记在 `docs/任务规划/测试与架构专项.md` A-5 残余)。
|
||||
`--gate` 同时校验台账受管区与 `.map` 一致(防漂移,同 `mem-numbers` 思路)。
|
||||
2. **趋势基线** `scripts/ram_baseline.tsv`(`--update` 每次 @build 后追加一行;
|
||||
`--trend` 看趋势并高亮 RW+ZI 较上一行增长 >1 KB 的对象——方案 §0.3
|
||||
"ZI 增量 >1KB 必须解释"的机械化)。
|
||||
3. **台账受管区**(`--write` 同步 `docs/内存管理/RAM预算台账.md` 的
|
||||
`MEM_BUDGET:BEGIN/END` 区:区域水位 + 子系统配额对照)。
|
||||
|
||||
子系统配额(报告 §5.5 第 3 条的规划值;RTOS 堆 2026-10-12 B5 48→56 KB):
|
||||
网络 12 KB / 存储 12 KB / UI 16 KB / RTOS 堆 56 KB / 应用 8 KB
|
||||
——UI 配额是 R-9/R-2 落地后的**目标态**,现状超配 = W3 债务,台账如实展示;
|
||||
配额改动 = 先登记 `docs/内存管理/RAM预算台账.md` 变更记录,再改本脚本 `QUOTAS`。
|
||||
|
||||
用法(仓库根):
|
||||
python scripts/ram_baseline.py --gate
|
||||
python scripts/ram_baseline.py --write
|
||||
python scripts/ram_baseline.py --update
|
||||
python scripts/ram_baseline.py --trend [-n 10]
|
||||
|
||||
退出码:0 通过/跳过;1 门禁失败或受管区漂移;2 事实源解析失败。
|
||||
创建日期:2026-10-12(W1-0,登记簿 N-27 前半)
|
||||
"""
|
||||
import argparse
|
||||
import pathlib
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
|
||||
import ram_report # noqa: E402 同目录:复用 .map 解析(regions + components,口径一致)
|
||||
from check_mem_numbers import read_doc, write_doc # noqa: E402 保真读写(BOM/CRLF)
|
||||
|
||||
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
MAP_DEFAULT = REPO_ROOT / "MDK-ARM" / "STM32F407-Demo" / "STM32F407-Demo.map"
|
||||
TSV_PATH = REPO_ROOT / "scripts" / "ram_baseline.tsv"
|
||||
LEDGER = REPO_ROOT / "docs" / "内存管理" / "RAM预算台账.md"
|
||||
|
||||
BEGIN = "<!-- MEM_BUDGET:BEGIN generated by scripts/ram_baseline.py -->"
|
||||
END = "<!-- MEM_BUDGET:END -->"
|
||||
|
||||
THRESHOLD_PCT = 92.0 # 报告 §5.5 第 1 条
|
||||
GATED_REGIONS = ("RW_IRAM1", "RW_CCM") # 门禁约束区;其余区仅展示
|
||||
GROWTH_ALERT_B = 1024 # 方案 §0.3:增量 >1 KB 必须解释
|
||||
TOP_N = 10 # tsv 记录 / 失败附表的对象数
|
||||
|
||||
RTOS_HEAP = "RTOS 堆"
|
||||
SUBSYSTEMS = ("网络", "存储", "UI", RTOS_HEAP, "应用", "其它")
|
||||
QUOTAS = {"网络": 12 * 1024, "存储": 12 * 1024, "UI": 16 * 1024,
|
||||
RTOS_HEAP: 56 * 1024, "应用": 8 * 1024} # RTOS 堆 48→56 KB:2026-10-12 B5(台账 §6)
|
||||
|
||||
# dhara 是裸对象名(nand.c/map.c/...),必须精确匹配,勿用前缀
|
||||
_DHARA = {"map.o", "journal.o", "nand.o", "fctl.o", "era.o"}
|
||||
# RTOS 内核静态对象:归"其它"(RTOS 配额只约束 CCM 堆本体,见台账 §2 口径表)
|
||||
_KERNEL = {"tasks.o", "queue.o", "timers.o", "event_groups.o",
|
||||
"list.o", "port.o", "cmsis_os2.o", "freertos.o"}
|
||||
|
||||
|
||||
def subsystem_of(obj):
|
||||
"""对象名 → 子系统(前缀首匹配;口径表见 RAM预算台账.md §2)。"""
|
||||
n = obj.strip().lower()
|
||||
if n.startswith(("lv_", "ui_")):
|
||||
return "UI"
|
||||
if n.startswith(("ch395f", "net_", "lftpd", "ftp_task")):
|
||||
return "网络"
|
||||
if (n.startswith(("gd5f", "nand_", "ff", "diskio", "vol_lock"))
|
||||
or n in _DHARA):
|
||||
return "存储"
|
||||
if n == "heap_4.o":
|
||||
return RTOS_HEAP
|
||||
if n in _KERNEL:
|
||||
return "其它"
|
||||
if n.startswith(("app_", "adc_", "prot_", "phasor", "sample_ring",
|
||||
"sys_", "cfg_", "dbg_", "console_log", "task_watchdog",
|
||||
"sha256", "tpafe", "sd2506", "test_task", "time_sync",
|
||||
"default_task")):
|
||||
return "应用"
|
||||
return "其它"
|
||||
|
||||
|
||||
def collect(map_path):
|
||||
"""解析 .map → {'regions': {name: r}, 'subsys': {name: bytes}, 'top': [(obj, B)]}。"""
|
||||
data = ram_report.parse_map(str(map_path))
|
||||
regions = {r["name"]: r for r in data["regions"]}
|
||||
comps = [c for c in data["components"]
|
||||
if not ram_report._is_aggregate(c["object"])
|
||||
and not c["object"].startswith("(incl.")]
|
||||
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]
|
||||
return {"regions": regions, "region_list": data["regions"],
|
||||
"subsys": subsys, "top": top}
|
||||
|
||||
|
||||
def _util(r):
|
||||
return r["size"] / r["max"] * 100 if r["max"] else 0.0
|
||||
|
||||
|
||||
def _quota_status(name, used):
|
||||
if name not in QUOTAS:
|
||||
return "参考"
|
||||
if name == RTOS_HEAP and used - QUOTAS[name] <= 64:
|
||||
# heap_4.o = 堆本体 49,152 B(= 配额)+ 32 B RW 元数据,不算超
|
||||
return "堆=配额"
|
||||
if used <= QUOTAS[name]:
|
||||
return "OK"
|
||||
return f"⚠ 超 {(used - QUOTAS[name]) / 1024:.1f} KB"
|
||||
|
||||
|
||||
def render_budget(info):
|
||||
"""渲染台账受管区(按 .map 区出现顺序动态渲染,新增区自动入表;
|
||||
门禁列只约束 GATED_REGIONS,其余区标"参考")。"""
|
||||
L = []
|
||||
L.append("| 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) |")
|
||||
L.append("|---|---|---|---|---|")
|
||||
for r in info["region_list"]:
|
||||
gated = r["name"] in GATED_REGIONS
|
||||
label = f"{r['name']} (Flash)" if r["name"] == "ER_IROM1" else r["name"]
|
||||
u = _util(r)
|
||||
mark = ("PASS" if u <= THRESHOLD_PCT else "FAIL") if gated else "参考"
|
||||
L.append(f"| {label} | {r['size']:,} B | {r['max']:,} B | "
|
||||
f"{u:.1f}% | {mark} |")
|
||||
L.append("")
|
||||
L.append("| 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 |")
|
||||
L.append("|---|---|---|---|")
|
||||
for s in SUBSYSTEMS:
|
||||
used = info["subsys"][s]
|
||||
quota = f"{QUOTAS[s] // 1024} KB" if s in QUOTAS else "—"
|
||||
L.append(f"| {s} | {used:,} B | {quota} | {_quota_status(s, used)} |")
|
||||
return "\n".join(L)
|
||||
|
||||
|
||||
# --- 受管区读写(锚点语义同 check_mem_numbers.py) ---------------------------
|
||||
|
||||
def extract_managed(text):
|
||||
if BEGIN not in text or END not in text:
|
||||
return None
|
||||
s = text.index(BEGIN) + len(BEGIN)
|
||||
e = text.index(END)
|
||||
return text[s:e].strip()
|
||||
|
||||
|
||||
def replace_managed(text, block):
|
||||
s = text.index(BEGIN) + len(BEGIN)
|
||||
e = text.index(END)
|
||||
new = "\n" + block + "\n"
|
||||
if text[s:e] == new:
|
||||
return text, False
|
||||
return text[:s] + new + text[e:], True
|
||||
|
||||
|
||||
# --- 子命令 ------------------------------------------------------------------
|
||||
|
||||
def _map_or_skip(args):
|
||||
map_path = pathlib.Path(args.map)
|
||||
if not map_path.exists():
|
||||
return None
|
||||
return map_path
|
||||
|
||||
|
||||
def cmd_gate(args):
|
||||
map_path = _map_or_skip(args)
|
||||
if map_path is None:
|
||||
print("[SKIP] 无 .map(未做 Keil 构建)→ RAM 预算门禁不适用")
|
||||
print(" (CI 盲区已登记 docs/任务规划/测试与架构专项.md A-5 残余;"
|
||||
"本地改过代码请先 @build)")
|
||||
return 0
|
||||
info = collect(map_path)
|
||||
if not info["regions"]:
|
||||
print(f"[FAIL] 未能从 {map_path.name} 解析出执行区", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
fails = []
|
||||
for name in ("RW_IRAM1", "RW_CCM"):
|
||||
r = info["regions"].get(name)
|
||||
if not r:
|
||||
fails.append(f"缺执行区 {name}(scatter 变更?)")
|
||||
continue
|
||||
u = _util(r)
|
||||
if u > THRESHOLD_PCT:
|
||||
fails.append(f"{name} 占用 {u:.1f}% > {THRESHOLD_PCT:.0f}%"
|
||||
f"({r['size']:,}/{r['max']:,} B)")
|
||||
if fails:
|
||||
print("[FAIL] RAM 预算超阈值:", file=sys.stderr)
|
||||
for f in fails:
|
||||
print(f" - {f}", file=sys.stderr)
|
||||
print(f"\nTop-{TOP_N} 静态对象(RW+ZI,找增量元凶):", file=sys.stderr)
|
||||
for obj, b in info["top"]:
|
||||
print(f" {b:>7,} B {obj}", file=sys.stderr)
|
||||
print("\n处置:削峰(静态化回退/缓冲复用)或按 docs/内存管理/RAM预算台账.md"
|
||||
" §5 走超配额流程;勿盲目抬 scatter。", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# 台账受管区一致性(防漂移;同 mem-numbers 的"事实源唯一"思路)
|
||||
if not LEDGER.exists():
|
||||
print(f"[FAIL] 台账缺失:{LEDGER.relative_to(REPO_ROOT)}", file=sys.stderr)
|
||||
return 1
|
||||
text, newline, bom = read_doc(LEDGER)
|
||||
expected = render_budget(info)
|
||||
got = extract_managed(text)
|
||||
if got is None:
|
||||
print(f"[FAIL] 台账缺受管锚点:{LEDGER.relative_to(REPO_ROOT)}",
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
if got != expected:
|
||||
print("[FAIL] 台账受管区与 .map 不一致(区域水位或子系统配额表漂移)",
|
||||
file=sys.stderr)
|
||||
print(f"事实源:{map_path.relative_to(REPO_ROOT)}", file=sys.stderr)
|
||||
print("修复:python scripts/ram_baseline.py --write", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# 软提醒(不拦):tsv 落后 / .map 比源码旧
|
||||
if TSV_PATH.exists():
|
||||
rows = _read_tsv()
|
||||
ir = info["regions"].get("RW_IRAM1")
|
||||
if rows and ir and int(rows[-1]["iram1"]) != ir["size"]:
|
||||
print("[WARN] ram_baseline.tsv 末行落后于当前 .map —— "
|
||||
"@build 后请跑 `python scripts/ram_baseline.py --update`")
|
||||
else:
|
||||
print("[WARN] ram_baseline.tsv 不存在 —— 跑 "
|
||||
"`python scripts/ram_baseline.py --update` 建档")
|
||||
newest = _newest_source_mtime()
|
||||
if newest and newest > map_path.stat().st_mtime:
|
||||
print("[WARN] .map 早于最新源码——数字来自旧构建,建议先 @build 再提交")
|
||||
|
||||
ir, cc = info["regions"].get("RW_IRAM1"), info["regions"].get("RW_CCM")
|
||||
print(f"RAM 预算门禁 PASS:RW_IRAM1 {_util(ir):.1f}% / RW_CCM {_util(cc):.1f}%"
|
||||
f"(阈值 {THRESHOLD_PCT:.0f}%)")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_write(args):
|
||||
map_path = _map_or_skip(args)
|
||||
if map_path is None:
|
||||
print("[FAIL] 找不到 .map 事实源,先 @build", file=sys.stderr)
|
||||
return 1
|
||||
info = collect(map_path)
|
||||
if not LEDGER.exists():
|
||||
print(f"[FAIL] 台账不存在:{LEDGER.relative_to(REPO_ROOT)}", file=sys.stderr)
|
||||
return 1
|
||||
text, newline, bom = read_doc(LEDGER)
|
||||
if BEGIN not in text or END not in text:
|
||||
print("[FAIL] 台账缺受管锚点", file=sys.stderr)
|
||||
return 1
|
||||
new_text, did = replace_managed(text, render_budget(info))
|
||||
if did:
|
||||
write_doc(LEDGER, new_text, newline, bom)
|
||||
print("已同步台账受管区。")
|
||||
else:
|
||||
print("台账受管区已是最新。")
|
||||
return 0
|
||||
|
||||
|
||||
# --- tsv 趋势基线 ------------------------------------------------------------
|
||||
|
||||
TSV_HEADER = ("date\tcode\tro\trw\tzi\ter_irom1\ter_pct\tiram1\tiram1_pct"
|
||||
"\tccm\tccm_pct\ttop_rwzi")
|
||||
|
||||
|
||||
def _read_tsv():
|
||||
rows = []
|
||||
lines = TSV_PATH.read_text(encoding="utf-8").splitlines()
|
||||
if len(lines) < 2:
|
||||
return rows
|
||||
keys = lines[0].split("\t")
|
||||
for ln in lines[1:]:
|
||||
parts = ln.split("\t")
|
||||
if len(parts) == len(keys):
|
||||
rows.append(dict(zip(keys, parts)))
|
||||
return rows
|
||||
|
||||
|
||||
def _top_field(top):
|
||||
return ";".join(f"{obj}={b}" for obj, b in top)
|
||||
|
||||
|
||||
def cmd_update(args):
|
||||
map_path = _map_or_skip(args)
|
||||
if map_path is None:
|
||||
print("[FAIL] 找不到 .map 事实源,先 @build", file=sys.stderr)
|
||||
return 1
|
||||
info = collect(map_path)
|
||||
gt = ram_report.parse_map(str(map_path))["grand_totals"]
|
||||
if not gt:
|
||||
print("[FAIL] .map 缺 Grand Totals", file=sys.stderr)
|
||||
return 2
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
reg = info["regions"]
|
||||
row = [
|
||||
today, str(gt["code"]), str(gt["ro_data"]), str(gt["rw_data"]),
|
||||
str(gt["zi_data"]),
|
||||
str(reg["ER_IROM1"]["size"]), f"{_util(reg['ER_IROM1']):.1f}",
|
||||
str(reg["RW_IRAM1"]["size"]), f"{_util(reg['RW_IRAM1']):.1f}",
|
||||
str(reg["RW_CCM"]["size"]), f"{_util(reg['RW_CCM']):.1f}",
|
||||
_top_field(info["top"]),
|
||||
]
|
||||
lines = (TSV_PATH.read_text(encoding="utf-8").splitlines()
|
||||
if TSV_PATH.exists() else [])
|
||||
if not lines:
|
||||
lines = TSV_HEADER.split("\t")
|
||||
lines = [TSV_HEADER]
|
||||
body = [ln for ln in lines[1:] if ln.strip()
|
||||
and not ln.split("\t")[0] == today]
|
||||
body.append("\t".join(row))
|
||||
# newline="\n":Windows 文本模式默认 \n→\r\n,违反 .gitattributes eol=lf
|
||||
TSV_PATH.write_text("\n".join([lines[0]] + body) + "\n",
|
||||
encoding="utf-8", newline="\n")
|
||||
print(f"已记录 {today}:RW_IRAM1 {reg['RW_IRAM1']['size']:,} B "
|
||||
f"({_util(reg['RW_IRAM1']):.1f}%) / RW_CCM {reg['RW_CCM']['size']:,} B "
|
||||
f"({_util(reg['RW_CCM']):.1f}%) → {TSV_PATH.relative_to(REPO_ROOT)}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_trend(args):
|
||||
if not TSV_PATH.exists():
|
||||
print("[WARN] tsv 不存在——先跑 --update 建档")
|
||||
return 0
|
||||
rows = _read_tsv()
|
||||
if not rows:
|
||||
print("[WARN] tsv 为空")
|
||||
return 0
|
||||
print(f"趋势(最近 {min(args.n, len(rows))} 行,共 {len(rows)} 行):")
|
||||
print(f"{'日期':<12} {'RW_IRAM1':>12} {'RW_CCM':>12} {'Flash':>12}")
|
||||
for r in rows[-args.n:]:
|
||||
print(f"{r['date']:<12} {int(r['iram1']):>9,} B ({r['iram1_pct']:>4}%)"
|
||||
f" {int(r['ccm']):>7,} B ({r['ccm_pct']:>4}%)"
|
||||
f" {int(r['er_irom1']):>7,} B ({r['er_pct']:>4}%)")
|
||||
if len(rows) >= 2:
|
||||
prev = dict()
|
||||
for kv in rows[-2]["top_rwzi"].split(";"):
|
||||
if "=" in kv:
|
||||
obj, b = kv.rsplit("=", 1)
|
||||
prev[obj] = int(b)
|
||||
grew = []
|
||||
for kv in rows[-1]["top_rwzi"].split(";"):
|
||||
if "=" not in kv:
|
||||
continue
|
||||
obj, b = kv.rsplit("=", 1)
|
||||
delta = int(b) - prev.get(obj, 0)
|
||||
if delta > GROWTH_ALERT_B:
|
||||
grew.append((obj, prev.get(obj, 0), int(b), delta))
|
||||
if grew:
|
||||
print(f"\n⚠ 较上一行 RW+ZI 增长 >{GROWTH_ALERT_B} B 的对象"
|
||||
f"(提交说明须解释,方案 §0.3):")
|
||||
for obj, old, new, d in sorted(grew, key=lambda t: -t[3]):
|
||||
print(f" +{d:>6,} B {obj} ({old:,} → {new:,})")
|
||||
else:
|
||||
print("\nTop 对象无 >1 KB 的异常增长。")
|
||||
return 0
|
||||
|
||||
|
||||
# --- 工具 --------------------------------------------------------------------
|
||||
|
||||
def _newest_source_mtime():
|
||||
newest = 0
|
||||
for base in ("App", "Drivers/BSP", "Src", "Inc"):
|
||||
d = REPO_ROOT / base
|
||||
if not d.is_dir():
|
||||
continue
|
||||
for p in d.rglob("*"):
|
||||
if p.suffix in (".c", ".h") and p.is_file():
|
||||
newest = max(newest, p.stat().st_mtime)
|
||||
lv = REPO_ROOT / "Lib" / "lv_conf.h"
|
||||
if lv.is_file():
|
||||
newest = max(newest, lv.stat().st_mtime)
|
||||
return newest
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
for stream in (sys.stdout, sys.stderr):
|
||||
try:
|
||||
stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
except Exception:
|
||||
pass
|
||||
ap = argparse.ArgumentParser(
|
||||
description="RAM 预算门禁 + 趋势台账(N-27 前半 / 报告 §5.5 三件套)")
|
||||
g = ap.add_mutually_exclusive_group()
|
||||
g.add_argument("--gate", action="store_true", help="92% 阈值门禁(pre-commit 入口)")
|
||||
g.add_argument("--write", action="store_true", help="同步台账受管区")
|
||||
g.add_argument("--update", action="store_true", help="追加 tsv 趋势行(@build 后跑)")
|
||||
g.add_argument("--trend", action="store_true", help="打印趋势 + 高亮 >1 KB 增长")
|
||||
ap.add_argument("-n", type=int, default=10, help="--trend 显示行数")
|
||||
ap.add_argument("--map", default=str(MAP_DEFAULT), help=".map 路径")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
if args.write:
|
||||
return cmd_write(args)
|
||||
if args.update:
|
||||
return cmd_update(args)
|
||||
if args.trend:
|
||||
return cmd_trend(args)
|
||||
return cmd_gate(args) # 默认 = --gate
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,2 @@
|
||||
date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi
|
||||
2026-10-12 205516 18044 1036 152484 223560 21.3 96096 73.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;console_log.o=4120;ch395f.o=4106;default_task.o=2736;sys_log_mcu.o=1924
|
||||
|
@@ -46,6 +46,8 @@ ROOTS = {
|
||||
"start_test_task": "testTask",
|
||||
"time_sync_task": "timeSyncTask",
|
||||
"adc_task_func": "adcTask",
|
||||
"iec104_serv_task": "iec104Task",
|
||||
"protect_task": "protectTask",
|
||||
}
|
||||
|
||||
# Runtime lines use the short labels emitted by stack_probe_poll() in freertos.c.
|
||||
@@ -57,6 +59,8 @@ RUNTIME_ALIAS = {
|
||||
"testTask": "test",
|
||||
"timeSyncTask": "timesync",
|
||||
"adcTask": "adc",
|
||||
"iec104Task": "iec104",
|
||||
"protectTask": "protect",
|
||||
}
|
||||
|
||||
FUNC_RE = re.compile(
|
||||
|
||||
Reference in New Issue
Block a user