文档体系重组与 D-7 一致性门禁落地,清理芯片手册孤儿图片
- D-6 目录规范化:docs 根目录散落文档归入 UI设计/子系统设计/架构设计/审查记录/ 环境与部署/网络 等子目录,去掉目录名空格,重命名 嵌入式C语言代码规范_V1.0.md - D-7 三档一致性门禁:新增 scripts/check_doc_links.py(链接存在性)、 scripts/gen_task_table.py(FreeRTOS 任务表数值)、scripts/check_mem_numbers.py (内存/Flash 体积),均支持 --check/--write/--diff,pre-commit 钩子同步注册 - D-5 文档索引:新建 docs/README.md,新建 docs/文档规范.md 约束目录命名与链接写法 - D-1 芯片手册瘦身:删除 1069 张无引用孤儿图片(77.06 MB), docs/芯片手册 120 MB → 41 MB,已引用图片零断链 - 同步 C 源码注释中的文档路径引用(D-6 重组后),构建 0 错误 0 警告
This commit is contained in:
@@ -0,0 +1,420 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
内存/Flash 数字一致性门禁(D-7 第三档:体积)。
|
||||
|
||||
问题
|
||||
----
|
||||
文档(尤其 `docs/内存管理/*.md`)大量引用 `RW_IRAM1 82,048 B / 62.6%`、
|
||||
`合计 ROM 249,432 B / 23.8%` 这类**具体数字**。这些数字来自 `.map`,每次重建都可能变化,
|
||||
而手抄必然滞后——实测 2026-09-26 复核:文档写 `82,048 B / 62.6%`,实际 `.map` 已是
|
||||
`87,040 B / 66.4%`;Flash 写 `249,432 / 23.8%`,实际 `261,932 / 25.0%`。
|
||||
读者照文档推算内存余量会得到错误结论。
|
||||
|
||||
做法
|
||||
----
|
||||
与 `gen_task_table.py` 同构:**`.map` 是唯一事实源**,文档里放**受管生成区**:
|
||||
|
||||
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
|
||||
| 区域 | 用量 | 容量 | 占比 |
|
||||
|---|---|---|---|
|
||||
| RW_IRAM1 | 87,040 B (85.00 KB) | 131,072 B (128 KB) | 66.4% |
|
||||
...
|
||||
<!-- MEM_SNAPSHOT:END -->
|
||||
|
||||
脚本解析 `.map`,重新渲染该区并校验一致;`--write` 自动同步。
|
||||
**行文中的散句数字**(如"现 RW_IRAM1 62.6%")不自动改写——它们由作者从受管区抄写,
|
||||
`--check` 会额外做**散句数字扫描**:发现文中出现与受管区不符的"旧值"即告警(见 §扫描)。
|
||||
这样既避免全文自动改写的高风险,又能抓出漏改的散句。
|
||||
|
||||
用法
|
||||
----
|
||||
python scripts/check_mem_numbers.py # 检查(默认)
|
||||
python scripts/check_mem_numbers.py --write # 同步受管区
|
||||
python scripts/check_mem_numbers.py --diff # 只打印期望快照
|
||||
python scripts/check_mem_numbers.py --scan # 额外扫描散句数字不一致
|
||||
python scripts/check_mem_numbers.py --map <path> # 指定 .map(默认 MDK-ARM/.../*.map)
|
||||
|
||||
退出码:0 通过;1 不一致 / 找不到事实源 / 解析失败。
|
||||
"""
|
||||
import argparse
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
MAP_DEFAULT = REPO_ROOT / "MDK-ARM" / "STM32F407-Demo" / "STM32F407-Demo.map"
|
||||
|
||||
# 受管快照区锚点
|
||||
BEGIN = "<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->"
|
||||
END = "<!-- MEM_SNAPSHOT:END -->"
|
||||
|
||||
# 需要保持同步的文档
|
||||
TARGETS = [
|
||||
REPO_ROOT / "docs" / "内存管理" / "RAM使用分析与优化方案.md",
|
||||
]
|
||||
|
||||
# 散句扫描范围(只扫这些文档里的"裸数字",避免误伤历史快照 data/*.md)
|
||||
SCAN_TARGETS = [
|
||||
REPO_ROOT / "docs" / "内存管理" / "RAM使用分析与优化方案.md",
|
||||
REPO_ROOT / "docs" / "内存管理" / "内存内容分配策略.md",
|
||||
REPO_ROOT / "docs" / "内存管理" / "存储栈RAM优化调研.md",
|
||||
]
|
||||
|
||||
|
||||
# --- .map 解析(与 scripts/ram_report.py 同口径,保持数据一致)---------------
|
||||
|
||||
def parse_map(path: pathlib.Path):
|
||||
"""解析 Keil .map,返回 {'regions': [...], 'grand_totals': {...}}。
|
||||
|
||||
与 ram_report.py 使用**相同**的正则与口径,避免两处数字打架。
|
||||
"""
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
regions, grand = [], None
|
||||
for ln in text.splitlines():
|
||||
if "Execution Region" in ln and "Exec base:" in ln:
|
||||
m = re.search(r"Execution Region (\w+) \((.*?)\)", ln)
|
||||
if not m:
|
||||
continue
|
||||
params = {}
|
||||
for part in m.group(2).split(","):
|
||||
part = part.strip()
|
||||
if ":" in part:
|
||||
k, v = part.split(":", 1)
|
||||
params[k.strip()] = v.strip()
|
||||
regions.append({
|
||||
"name": m.group(1),
|
||||
"size": int(params.get("Size", "0"), 16),
|
||||
"max": int(params.get("Max", "0"), 16),
|
||||
})
|
||||
if "Grand Totals" in ln:
|
||||
nums = []
|
||||
for p in ln.split():
|
||||
try:
|
||||
nums.append(int(p))
|
||||
except ValueError:
|
||||
if "Grand" in p:
|
||||
break
|
||||
if len(nums) >= 6:
|
||||
grand = {"code": nums[0], "ro_data": nums[2],
|
||||
"rw_data": nums[3], "zi_data": nums[4]}
|
||||
return {"regions": regions, "grand_totals": grand}
|
||||
|
||||
|
||||
def compute_facts(data):
|
||||
"""从解析结果算出受管快照要展示的指标(字节 → 展示口径)。"""
|
||||
facts = {}
|
||||
for r in data["regions"]:
|
||||
util = (r["size"] / r["max"] * 100) if r["max"] else 0.0
|
||||
facts[r["name"]] = {
|
||||
"size": r["size"], "max": r["max"], "util": util,
|
||||
}
|
||||
gt = data["grand_totals"]
|
||||
if gt:
|
||||
rom = gt["code"] + gt["ro_data"] + gt["rw_data"]
|
||||
rw_total = gt["rw_data"] + gt["zi_data"]
|
||||
facts["_flash"] = {"rom": rom, "code": gt["code"], "ro": gt["ro_data"],
|
||||
"flash_max": 1048576}
|
||||
facts["_rw_total"] = rw_total
|
||||
return facts
|
||||
|
||||
|
||||
def fmt_kb(n, digits=2):
|
||||
return f"{n/1024:.{digits}f} KB"
|
||||
|
||||
|
||||
def render_snapshot(facts):
|
||||
"""渲染受管快照 Markdown。顺序/口径固定,保证多次运行结果稳定。"""
|
||||
order = ["ER_IROM1", "RW_IRAM1", "RW_CCM"]
|
||||
lines = [
|
||||
"| 区域 | 用量 | 容量 | 占比 |",
|
||||
"|---|---|---|---|",
|
||||
]
|
||||
for name in order:
|
||||
f = facts.get(name)
|
||||
if not f:
|
||||
continue
|
||||
lines.append(
|
||||
f"| {name} | {f['size']:,} B ({fmt_kb(f['size'])}) | "
|
||||
f"{f['max']:,} B ({fmt_kb(f['max'], 0)}) | {f['util']:.1f}% |"
|
||||
)
|
||||
fl = facts.get("_flash")
|
||||
if fl:
|
||||
util = fl["rom"] / fl["flash_max"] * 100
|
||||
lines.append(
|
||||
f"| **Flash (ROM 合计)** | **{fl['rom']:,} B ({fmt_kb(fl['rom'], 1)})** | "
|
||||
f"{fl['flash_max']:,} B (1024 KB) | **{util:.1f}%** |"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def snapshot_metrics(facts):
|
||||
"""返回用于散句扫描的「期望值集合」:(数字, 允许的等价写法列表)。"""
|
||||
out = []
|
||||
def add(n):
|
||||
out.append(n)
|
||||
for name in ("ER_IROM1", "RW_IRAM1", "RW_CCM"):
|
||||
f = facts.get(name)
|
||||
if f:
|
||||
add(f["size"]); add(f["max"])
|
||||
add(round(f["util"], 1))
|
||||
fl = facts.get("_flash")
|
||||
if fl:
|
||||
add(fl["rom"]); add(fl["code"]); add(fl["ro"])
|
||||
add(round(fl["rom"] / fl["flash_max"] * 100, 1))
|
||||
if "_rw_total" in facts:
|
||||
add(facts["_rw_total"])
|
||||
return out
|
||||
|
||||
|
||||
# --- 文档读写(保真换行 / BOM,同 gen_task_table.py)------------------------
|
||||
|
||||
def read_doc(path: pathlib.Path):
|
||||
raw = path.read_bytes()
|
||||
bom = raw.startswith(b"\xef\xbb\xbf")
|
||||
body = raw[3:] if bom else raw
|
||||
crlf = body.count(b"\r\n")
|
||||
lf = body.count(b"\n") - crlf
|
||||
newline = "\r\n" if crlf > lf else "\n"
|
||||
text = body.decode("utf-8", errors="replace")
|
||||
return text.replace("\r\n", "\n").replace("\r", "\n"), newline, bom
|
||||
|
||||
|
||||
def write_doc(path: pathlib.Path, text: str, newline: str, bom: bool):
|
||||
out = text if newline == "\n" else text.replace("\n", newline)
|
||||
data = out.encode("utf-8")
|
||||
path.write_bytes((b"\xef\xbb\xbf" if bom else b"") + data)
|
||||
|
||||
|
||||
def replace_managed(text: str, snapshot: str):
|
||||
if BEGIN not in text or END not in text:
|
||||
return text, False
|
||||
s = text.index(BEGIN) + len(BEGIN)
|
||||
e = text.index(END)
|
||||
new = "\n" + snapshot + "\n"
|
||||
if text[s:e] == new:
|
||||
return text, False
|
||||
return text[:s] + new + text[e:], True
|
||||
|
||||
|
||||
def extract_managed(text: str):
|
||||
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()
|
||||
|
||||
|
||||
# --- 散句扫描 ---------------------------------------------------------------
|
||||
#
|
||||
# 朴素做法(扫所有 4~7 位数字)会产生大量误报——任务栈 `4096`、堆配置 `49,152`、
|
||||
# 峰值水位 `1184` 都是合法数字(实测一次报 45 处,全是噪音,门禁会被无视)。
|
||||
#
|
||||
# 正确判据:**「上一个快照里是区域用量、但当前已不是」的数字**才是陈旧数字。
|
||||
# 例如历史 `RW_IRAM1 = 82,048 B`、当前 `87,040 B` → 正文若仍写 `82,048` 即为旧值。
|
||||
# 故扫描只针对:从 `data/ram_report_*.md` 历史快照提取的**区域用量/占比**值。
|
||||
|
||||
SNAPSHOT_DIR = REPO_ROOT / "docs" / "内存管理" / "data"
|
||||
# 历史快照里 "| RW_IRAM1 | 0x... | 82,048 B (...)" 这类行
|
||||
SNAP_REGION_RE = re.compile(
|
||||
r"\|\s*(ER_IROM1|RW_IRAM1|RW_CCM)\s*\|\s*0x[0-9A-Fa-f]+\s*\|\s*([\d,]+)\s*B"
|
||||
r"\s*\([^)]*\)\s*\|\s*([\d,]+)\s*B[^|]*\|\s*([\d.]+)%"
|
||||
)
|
||||
|
||||
# 行内出现这些词说明是在讲**别的**数字(栈配置/峰值/池大小),不是区域用量。
|
||||
# ⚠️ 只在**紧邻数字**(前后 12 字符内)时才算命中——否则会误豁免整句
|
||||
# (实测:「堆迁 CCM 后…(现 RW_IRAM1 62.6%)」因含「堆」被整句跳过,漏掉现状数字)。
|
||||
NON_REGION_HINTS = ("栈", "STACK", "stack", "堆", "heap", "pool", "池",
|
||||
"峰值", "水位", "high water", "min_ever", "free=",
|
||||
"LV_MEM_SIZE", "config", "任务", "TCB", "缓冲", "buffer")
|
||||
HINT_WINDOW = 12
|
||||
|
||||
|
||||
def _is_non_region_metric(sent: str, start: int, end: int) -> bool:
|
||||
"""数字 [start,end) 是否处于「栈/池/峰值」语境(紧邻 ≤12 字符内有提示词)。"""
|
||||
lo = max(0, start - HINT_WINDOW)
|
||||
hi = min(len(sent), end + HINT_WINDOW)
|
||||
window = sent[lo:hi]
|
||||
return any(h in window for h in NON_REGION_HINTS)
|
||||
|
||||
|
||||
def collect_stale_numbers(current_facts):
|
||||
"""从历史快照提取「曾经是区域用量、现已变化」的数字 → 陈旧值集合。
|
||||
|
||||
只收**当前值不含**的历史区域用量/占比;这样扫正文时命中的就是真正的陈旧数字。
|
||||
"""
|
||||
cur = set()
|
||||
for name in ("ER_IROM1", "RW_IRAM1", "RW_CCM"):
|
||||
f = current_facts.get(name)
|
||||
if f:
|
||||
cur.add(f["size"])
|
||||
cur.add(f["max"])
|
||||
cur.add(round(f["util"], 1))
|
||||
fl = current_facts.get("_flash")
|
||||
if fl:
|
||||
cur.add(fl["rom"])
|
||||
cur.add(fl["code"])
|
||||
cur.add(fl["ro"])
|
||||
cur.add(round(fl["rom"] / fl["flash_max"] * 100, 1))
|
||||
|
||||
stale = {} # 数字 -> 来源文件名
|
||||
if not SNAPSHOT_DIR.is_dir():
|
||||
return stale
|
||||
for snap in sorted(SNAPSHOT_DIR.glob("ram_report_*.md")):
|
||||
text = snap.read_text(encoding="utf-8", errors="replace")
|
||||
for m in SNAP_REGION_RE.finditer(text):
|
||||
size = int(m.group(2).replace(",", ""))
|
||||
util = float(m.group(4))
|
||||
for v in (size, util):
|
||||
if v not in cur:
|
||||
stale.setdefault(v, snap.name)
|
||||
# 旧快照的 Grand Totals 行(Total ROM)
|
||||
for m in re.finditer(r"\*\*Total ROM[^|]*\|\s*\*\*([\d,]+)\s*B", text):
|
||||
v = int(m.group(1).replace(",", ""))
|
||||
if v not in cur:
|
||||
stale.setdefault(v, snap.name)
|
||||
return stale
|
||||
|
||||
|
||||
def scan_prose(text: str, stale, errors, path: pathlib.Path):
|
||||
"""正文里出现「历史快照的区域用量、但当前已变化」的数字 → 报为陈旧引用。
|
||||
|
||||
只报这一类(实测 0 误报),不报任务栈/池配置等其它数字。
|
||||
|
||||
⚠️ **按句子而非按行判定**:同一行常混有「当时 X」与「现 Y」两种时态,
|
||||
按行判会把整行豁免掉、漏掉后半句的现状数字(实测踩中)。
|
||||
"""
|
||||
if not stale:
|
||||
return
|
||||
in_managed = False
|
||||
# 句读切分:中文句号/分号/感叹号/问号 + Markdown 分隔(`|`、`;`)
|
||||
SENT_SPLIT = re.compile(r"(?<=[。;;!!??])|(?<=\|)")
|
||||
for lineno, line in enumerate(text.splitlines(), 1):
|
||||
if BEGIN in line:
|
||||
in_managed = True
|
||||
continue
|
||||
if END in line:
|
||||
in_managed = False
|
||||
continue
|
||||
if in_managed:
|
||||
continue
|
||||
for sent in SENT_SPLIT.split(line):
|
||||
if not sent or not sent.strip():
|
||||
continue
|
||||
# 明确标注「历史/曾/旧/快照」或引用 data/ 的**句子** → 允许保留旧数字
|
||||
if any(h in sent for h in ("历史", "旧", "曾", "此前", "快照", "data/",
|
||||
"修复前", "优化前", "迁入前", "实施时",
|
||||
"实施前", "当时", "原值", "改动前", "改造前")):
|
||||
continue
|
||||
# 讲栈/池/峰值的**数字**(提示词紧邻该数字)语义不同,跳过;
|
||||
# 注意不能按整句跳过——同一句可能既讲堆又报区域现状。
|
||||
for m in re.finditer(r"\b(\d{1,3}(?:,\d{3})+|\d{4,7}|\d{1,3}\.\d)%?", sent):
|
||||
if _is_non_region_metric(sent, m.start(), m.end()):
|
||||
continue
|
||||
raw = m.group(1)
|
||||
try:
|
||||
v = float(raw.replace(",", ""))
|
||||
except ValueError:
|
||||
continue
|
||||
key = int(v) if v.is_integer() else v
|
||||
if key in stale:
|
||||
errors.append((path, lineno,
|
||||
f"陈旧区域数字 {raw}(历史快照 {stale[key]} 的值,"
|
||||
f"当前已变化)", "陈旧数字"))
|
||||
|
||||
|
||||
def main(argv):
|
||||
ap = argparse.ArgumentParser(description="内存/Flash 数字一致性门禁(D-7 体积档)")
|
||||
g = ap.add_mutually_exclusive_group()
|
||||
g.add_argument("--check", action="store_true", help="检查模式(默认)")
|
||||
g.add_argument("--write", action="store_true", help="同步受管快照区")
|
||||
g.add_argument("--diff", action="store_true", help="只打印期望快照")
|
||||
ap.add_argument("--scan", action="store_true", help="额外扫描散句数字")
|
||||
ap.add_argument("--map", default=str(MAP_DEFAULT), help=".map 文件路径")
|
||||
args = ap.parse_args(argv[1:])
|
||||
|
||||
map_path = pathlib.Path(args.map)
|
||||
if not map_path.exists():
|
||||
print(f"[FAIL] 找不到 .map 事实源:{map_path}", file=sys.stderr)
|
||||
print(" 先跑一次 @build(或 MDK-ARM/build.bat)生成 .map。", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
data = parse_map(map_path)
|
||||
if not data["regions"]:
|
||||
print(f"[FAIL] 未能从 {map_path.name} 解析出执行区,请检查文件格式。",
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
facts = compute_facts(data)
|
||||
snapshot = render_snapshot(facts)
|
||||
stale = collect_stale_numbers(facts)
|
||||
|
||||
if args.diff:
|
||||
print(snapshot)
|
||||
return 0
|
||||
|
||||
changed, missing, problems = [], [], []
|
||||
errors = []
|
||||
|
||||
for t in TARGETS:
|
||||
if not t.exists():
|
||||
problems.append(f"{t.relative_to(REPO_ROOT)} 不存在")
|
||||
continue
|
||||
text, newline, bom = read_doc(t)
|
||||
rel = t.relative_to(REPO_ROOT).as_posix()
|
||||
if args.write:
|
||||
new_text, did = replace_managed(text, snapshot)
|
||||
if did:
|
||||
write_doc(t, new_text, newline, bom)
|
||||
changed.append(rel)
|
||||
else:
|
||||
got = extract_managed(text)
|
||||
if got is None:
|
||||
missing.append(rel)
|
||||
elif got.replace("\r\n", "\n") != snapshot.strip():
|
||||
changed.append(rel)
|
||||
if args.scan:
|
||||
scan_prose(text, stale, errors, t)
|
||||
|
||||
if args.write:
|
||||
if missing:
|
||||
print("[WARN] 以下文档缺受管锚点,未写入:" + ", ".join(missing))
|
||||
print(f" 请在快照表上下各加一行:\n {BEGIN}\n {END}")
|
||||
if changed:
|
||||
print("已更新内存快照:" + ", ".join(changed))
|
||||
if not changed and not missing:
|
||||
print("内存快照已是最新,无需改动。")
|
||||
return 0
|
||||
|
||||
if problems:
|
||||
for p in problems:
|
||||
print(f"[FAIL] {p}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if args.scan:
|
||||
for path, lineno, msg, kind in errors:
|
||||
rel = path.relative_to(REPO_ROOT)
|
||||
print(f" {rel}:{lineno} [{kind}] {msg}")
|
||||
|
||||
if missing or changed or errors:
|
||||
if missing:
|
||||
print("[FAIL] 以下文档缺少受管锚点:")
|
||||
for n in missing:
|
||||
print(f" - {n}")
|
||||
if changed:
|
||||
print("[FAIL] 以下文档的内存快照与 .map 不一致:")
|
||||
for n in changed:
|
||||
print(f" - {n}")
|
||||
print(f"\n事实源:{map_path.relative_to(REPO_ROOT)}")
|
||||
print("\n期望快照:")
|
||||
print(snapshot)
|
||||
print("\n修复:运行 `python scripts/check_mem_numbers.py --write`")
|
||||
if errors:
|
||||
print("(散句数字告警需人工复核,--write 不会自动改写正文)")
|
||||
return 1
|
||||
|
||||
print(f"内存数字一致(事实源 {map_path.name})。")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
Reference in New Issue
Block a user