Files
wandering1 f4b9bd57d5 docs(部署): 删任务栈副本表改指针;纠 CCM/FPU 事实并刷新配图基线
范围:AGENTS.md 一处指针、部署环境要求 §2.2/§3.1/§3.3、RAM 方案 6 处现状
数字、内存配图生成器 + 3 张 SVG、两份报告脚本的行尾与路径修正、新增两份
.map/.htm 数据快照。未改任何 .c/.h(仍跑 @build 复核:0 错误 0 警告)。

部署环境要求.md
- §3.3 删"任务栈预算"副本表、改为指针:该表已缺 adcTask/iec104Task/
  protectTask,defaultTask/timeSyncTask 两值停在旧数,注释却写"由
  gen_task_table.py 同源"——该钩子的校验面只有 README.md / AGENTS.md
  两处受管块,抄出来的副本没有门禁、改栈后必漂。指针指向 Src/freertos.c
  + 两份受管表 + RAM 方案 §2.3/§0.6,并留一条"别再抄"的成因说明。
- §2.2 链接布局补 `RW_CCM 0x10000000`(scatter 给 0x0000FF00 = 65,280 B,
  2026-10-11 起承载 FreeRTOS 堆;另有 256 B UNINIT_CCM 尾区),写明 CCM
  不可 DMA。原文"SRAM3/CCM 故意未映射"与 STM32F407-Demo.sct 直接矛盾。
- §3.1 MCU 行更正为**单精度 FPU(VFPv4-SP,无硬件双精度)、无 TrustZone**
  (原写"双精度 FPU / TrustZone"是芯片事实错误,也正是报告 F-6 双精度软
  浮点被长期误判的土壤);SRAM 行改为"两块都在用,仅 SRAM3 未映射"。
- §3.3 资源预算表补 CCM 行,并把"主 SRAM 125,104 B / 95.4% / 极度紧张"
  更正为 75,264 B / 57.4%(余 55,808 B);红线改标在 CCM(余 7,904 B)。
- 变更记录补 2026-10-22 行(含上一提交里的资源表刷新那一笔)。

AGENTS.md:任务表注里 "RAM使用分析与优化方案.md §1.4" 是**指错小节**——§1.4 确实
存在(该文档 §4「方案」下的阶段 1 子项,讲 lftpd 大局部缓冲静态化),但栈余量
台账在 §2.3(任务栈配置与实测峰值)与 §0.6(静态最坏链与判定),故改指这两处
再加 内存内容分配策略.md §6。根因是该文档里 §1(背景)与 §4 下的 1.1~1.5 同号
撞面,人和 doc-links(自引用放过)都会看错——本会话我上一轮就被它带错过一次。

配图与数据(draw-diagram 流程:改生成器顶部数据 → 重跑 → 逐文本核越界)
- 新增 data/ram_report_20261022.md、data/stack_report_20261022.md;后者
  运行时水位取自 2026-10-22 真机 [STACK] 心跳(8 任务高水位 + heap
  free=15,800 / min_ever=8,464),静态侧来自同日 .htm(全局最深链 3,024 B)。
- gen_memory_svg.py:三组图数据从 2026-09-21 / 10-10 快照换成 2026-10-22
  实测;任务栈 6 → 9 行,堆总量 48KB → 56KB,判定 6/6 → 9/9 PASS,新增
  AboveNormal 配色;Flash 子条改为"窄段不压字 + 下方四列图例";任务栈
  画布 440 → 620 高。CCM 段总量按 scatter 用 65,280 B(与台账同口径)。
- 三张 SVG 重生成;用"CJK≈1.0em / 其余≈0.58em + 锚点"逐文本估算边界,
  修掉 5 处文字越界/压字(生成器内置 ElementTree 自检亦通过)。
- ram_report.py / stack_report.py 两处工具性修正:write_text 未指定 newline
  参数 → Windows 下把 data/ 快照写成 CRLF(与 .gitattributes 及既有快照
  不符);stack_report 的 relative_to 在传相对 -i 时抛 ValueError,且会把
  本机绝对路径写进入库快照 → 统一走 resolve+relative_to 的相对路径回退。

RAM 方案:6 处"当前状态"随 .map/[STACK] 更正——RW_IRAM1 89,168 → 75,264、
RW_CCM 49,184 → 57,376、FreeRTOS 堆 48KB → 56KB(两处)、CCM 余量 ~16KB →
7,904 B、min_ever 4,848 → 8,464 B、6 任务全过 → 9/9 PASS;S-1 历史句保留。

门禁:pre-commit 对全部改动文件 Passed(doc-links --sections-strict、
task-table、mem-numbers、mem-budget、id-check 增量拦截);字节卫生自查
BOM / CR / U+FFFD / 行尾空格全清。已提交本地,未推送。
2026-10-22 15:08:04 +08:00

378 lines
14 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
#!/usr/bin/env python3
"""Parse Keil MDK-ARM .map file and generate structured RAM/Flash usage report.
Usage:
python scripts/ram_report.py [MAP_FILE] [-o OUTPUT]
Output: Markdown tables to stdout or file, ready for inclusion in docs.
"""
import re
import sys
import os
import pathlib
import argparse
from datetime import datetime
MAP_DEFAULT = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"MDK-ARM", "STM32F407-Demo", "STM32F407-Demo.map",
)
# Rows in "Image component sizes" that re-aggregate objects (would double-count sums).
TOTALS_ROWS = {"Object Totals", "Library Totals", "User Totals"}
# ARM library aggregate rows (e.g. mc_w.l / mf_w.l / m_wm.l): the linker prints one
# aggregate line per library in addition to the individual member .o lines, so any
# "<lib>.l" row is a re-aggregation and must be dropped from per-file sums.
# (2026-10-11: sha256's 64-bit shifts pulled in m_wm.l — the hardcoded name list
# missed it and the self-consistency check went +58; match the name pattern instead.)
_LIB_AGG_RE = re.compile(r'^[a-z]{1,2}_[a-z0-9]+\.l$')
def _is_aggregate(name):
"""Aggregate rows to drop from per-file sums: library-name summaries and Totals lines.
'(incl. Padding)' / '(incl. Generated)' are linker metadata ledger rows; they ARE
kept so that per-file sums reconcile exactly with Grand Totals (self-check Δ=0).
"""
return name in TOTALS_ROWS or bool(_LIB_AGG_RE.match(name))
def parse_execution_region_header(line):
"""Parse: Execution Region RW_IRAM1 (Exec base: 0x20000000, ..., Size: 0x0001e8b0, Max: 0x00020000, ...)"""
m = re.search(
r'Execution Region (\w+) \((.*?)\)', line)
if not m:
return None
name = m.group(1)
body = m.group(2)
# Split by comma first (handles multi-word keys like "Exec base")
params = {}
for part in body.split(","):
part = part.strip()
if ":" in part:
k, v = part.split(":", 1)
params[k.strip()] = v.strip()
return {
"name": name,
"exec_base": int(params.get("Exec base", "0"), 16),
"size": int(params.get("Size", "0"), 16),
"max": int(params.get("Max", "0"), 16),
}
def parse_map(path):
text = pathlib.Path(path).read_text(encoding="utf-8", errors="replace")
lines = text.splitlines()
result = {
"regions": [],
"iram1_entries": [],
"grand_totals": None,
"totals": {},
"components": [],
}
section = None # track which section we're in
region_name = None
skip_header = 0
i = 0
while i < len(lines):
ln = lines[i].rstrip()
# Detect execution region headers
if "Execution Region" in ln and "Exec base:" in ln:
region = parse_execution_region_header(ln)
if region:
result["regions"].append(region)
region_name = region["name"]
section = "region_entries"
skip_header = 2 # skip header line + blank
i += 1
continue
# Detect "Image component sizes"
if ln.strip() == "Image component sizes":
section = "components"
skip_header = 3 # skip header + blank + column header
i += 1
continue
# Detect Grand Totals
if "Grand Totals" in ln:
parts = ln.split()
# 219976 11898 11496 528 124576 5416149 Grand Totals
nums = []
for p in parts:
try:
nums.append(int(p))
except ValueError:
if "Grand" in p:
break
if len(nums) >= 6:
result["grand_totals"] = {
"code": nums[0], "inc_data": nums[1], "ro_data": nums[2],
"rw_data": nums[3], "zi_data": nums[4], "debug": nums[5],
}
section = None
i += 1
continue
# Detect Total RO/RW/ROM lines
m_tot = re.match(r'\s+Total (RO|RW|ROM)\s+Size.*?\(\s*(\d+)\s*\(\s*([\d.]+)kB\)', ln)
if m_tot:
result["totals"][m_tot.group(1)] = {
"bytes": int(m_tot.group(2)),
"kb": float(m_tot.group(3)),
}
i += 1
continue
# Parse region entries
if section == "region_entries" and region_name == "RW_IRAM1":
if skip_header > 0:
skip_header -= 1
i += 1
continue
if ln.strip() == "" or ln.strip().startswith("Exec Addr") or ln.strip().startswith("Load Addr"):
i += 1
continue
if ln.strip().startswith("0x"):
entry = _parse_region_entry(ln)
if entry:
result["iram1_entries"].append(entry)
elif not ln.strip().startswith("0x") and ln.strip():
# Hit a new section or summary
section = None
# Parse component sizes
if section == "components":
if skip_header > 0:
skip_header -= 1
i += 1
continue
if ln.strip() == "" or "Object Name" in ln:
i += 1
continue
comp = _parse_component_line(ln)
if comp:
result["components"].append(comp)
i += 1
return result
def _parse_region_entry(ln):
"""Parse a Memory Map entry line like:
0x20000000 COMPRESSED 0x00000020 Data RW 292 .data freertos.o
0x20000039 COMPRESSED 0x00000003 PAD
"""
parts = ln.split()
if len(parts) < 4:
return None
try:
exec_addr = int(parts[0], 16)
except ValueError:
return None
load_addr = parts[1]
try:
size = int(parts[2], 16)
except ValueError:
return None
typ = parts[3]
if typ == "PAD":
return {"exec_addr": exec_addr, "load_addr": load_addr, "size": size,
"type": "PAD", "attr": "", "idx": 0, "section": "", "object": ""}
if len(parts) < 7:
return None
attr = parts[4] if parts[4] not in (".data", ".bss", ".text") else ""
# Find idx (integer) and section (.something) and object (*.o)
idx = 0
section = ""
obj_parts = []
for j in range(4, len(parts)):
p = parts[j]
if p.isdigit():
idx = int(p)
elif p.startswith("."):
section = p
elif ".o" in p or ".l(" in p:
obj_parts.append(p)
elif section and not p.startswith("."):
obj_parts.append(p)
obj = " ".join(obj_parts) if obj_parts else ""
return {"exec_addr": exec_addr, "load_addr": load_addr, "size": size,
"type": typ, "attr": attr, "idx": idx, "section": section, "object": obj}
def _parse_component_line(ln):
"""Parse: 2738 536 262 1 4104 41449 ch395f.o"""
parts = ln.split()
if len(parts) < 7:
return None
try:
code = int(parts[0])
inc_data = int(parts[1])
ro_data = int(parts[2])
rw_data = int(parts[3])
zi_data = int(parts[4])
debug = int(parts[5])
except ValueError:
return None
obj_name = " ".join(parts[6:])
return {
"code": code, "inc_data": inc_data, "ro_data": ro_data,
"rw_data": rw_data, "zi_data": zi_data, "debug": debug,
"object": obj_name,
}
def fmt(n):
"""Format number with comma separator."""
return f"{n:,}"
def hexfmt(n):
return f"0x{n:08X}"
def generate_report(data, map_path):
now = datetime.now().strftime("%Y-%m-%d %H:%M")
lines = []
w = lines.append
w(f"# STM32F4-Base RAM/Flash Usage Report")
w(f"")
w(f"> Generated: {now} Map: `{os.path.basename(map_path)}`")
w("")
# Grand totals
gt = data["grand_totals"]
if gt:
total_ro = gt["code"] + gt["ro_data"]
total_rw = gt["rw_data"] + gt["zi_data"]
total_rom = total_ro + gt["rw_data"]
w("## 1. Grand Totals (全局总量)")
w("")
w("| Metric | Value |")
w("|---|---|")
w(f"| Code (inc. data) | {fmt(gt['code'])} B ({gt['code']/1024:.1f} KB) |")
w(f"| RO Data | {fmt(gt['ro_data'])} B ({gt['ro_data']/1024:.1f} KB) |")
w(f"| RW Data (initialized) | {fmt(gt['rw_data'])} B ({gt['rw_data']/1024:.2f} KB) |")
w(f"| ZI Data (zero-init) | {fmt(gt['zi_data'])} B ({gt['zi_data']/1024:.1f} KB) |")
w(f"| **Total RO (Code + RO)** | **{fmt(total_ro)} B ({total_ro/1024:.1f} KB)** |")
w(f"| **Total RW (RW + ZI) = SRAM used** | **{fmt(total_rw)} B ({total_rw/1024:.2f} KB)** |")
w(f"| **Total ROM (Code + RO + RW init)** | **{fmt(total_rom)} B ({total_rom/1024:.1f} KB)** |")
w("")
# Execution regions
w("## 2. 执行区水位 (Execution Regions)")
w("")
w("| Region | Base | Size | Max | Util% |")
w("|---|---|---|---|---|")
for r in data["regions"]:
util = r["size"] / r["max"] * 100 if r["max"] else 0
w(f"| {r['name']} | {hexfmt(r['exec_base'])} | {fmt(r['size'])} B ({r['size']/1024:.2f} KB) | {fmt(r['max'])} B ({r['max']/1024:.0f} KB) | {util:.1f}% |")
w("")
# IRAM1 entries (ZI + RW symbol detail)
zii_entries = [e for e in data["iram1_entries"] if e["type"] in ("Zero", "Data") and e["size"] > 0]
zii_entries.sort(key=lambda e: e["size"], reverse=True)
w("## 3. RW_IRAM1 逐符号明细(ZI + RW,按大小降序)")
w("")
w("| # | Address | Size (B) | Size (KB) | Type | Section | Object |")
w("|---|---|---|---|---|---|---|")
for idx, e in enumerate(zii_entries, 1):
kb = e["size"] / 1024
w(f"| {idx} | {hexfmt(e['exec_addr'])} | {fmt(e['size'])} | {kb:.2f} | {e['type']} | {e['section']} | {e['object']} |")
total_zii = sum(e["size"] for e in zii_entries)
w(f"| | | **{fmt(total_zii)}** | **{total_zii/1024:.2f}** | | | **Total** |")
w("")
# Image component sizes (sorted by RW+ZI desc) — 仅保留 *.o 个体行,求和与 Grand Totals 对齐
comps = [c for c in data["components"] if not _is_aggregate(c["object"])]
comps.sort(key=lambda c: c["rw_data"] + c["zi_data"], reverse=True)
top_n = [c for c in comps if c["rw_data"] + c["zi_data"] > 0]
w("## 4. Image Component Sizes(RW + ZI > 0,按大小降序)")
w("")
w("| # | Object | Code | RO | RW | ZI | RW+ZI |")
w("|---|---|---|---|---|---|---|")
for idx, c in enumerate(top_n, 1):
rw_zi = c["rw_data"] + c["zi_data"]
w(f"| {idx} | {c['object']} | {fmt(c['code'])} | {fmt(c['ro_data'])} | {fmt(c['rw_data'])} | {fmt(c['zi_data'])} | {fmt(rw_zi)} |")
total_comp_rw = sum(c["rw_data"] for c in comps)
total_comp_zi = sum(c["zi_data"] for c in comps)
total_comp_code = sum(c["code"] for c in comps)
total_comp_ro = sum(c["ro_data"] for c in comps)
w(f"| | **All .o files** | **{fmt(total_comp_code)}** | **{fmt(total_comp_ro)}** | **{fmt(total_comp_rw)}** | **{fmt(total_comp_zi)}** | **{fmt(total_comp_rw+total_comp_zi)}** |")
w("")
# Flash breakdown (sorted by Code+RO desc)
flash_comps = sorted(comps, key=lambda c: c["code"] + c["ro_data"], reverse=True)
flash_top = [c for c in flash_comps if c["code"] + c["ro_data"] > 100]
w("## 5. Flash (Code + RO Data) Top-30(按大小降序)")
w("")
w("| # | Object | Code | RO Data | Code+RO |")
w("|---|---|---|---|---|")
for idx, c in enumerate(flash_top[:30], 1):
c_ro = c["code"] + c["ro_data"]
w(f"| {idx} | {c['object']} | {fmt(c['code'])} | {fmt(c['ro_data'])} | {fmt(c_ro)} |")
total_code = sum(c["code"] for c in comps)
total_ro_d = sum(c["ro_data"] for c in comps)
w(f"| | **All .o files** | **{fmt(total_code)}** | **{fmt(total_ro_d)}** | **{fmt(total_code+total_ro_d)}** |")
w("")
# Self-consistency check against Grand Totals
# Per-file sums exclude "(incl. Padding/Generated)" linker metadata; their bytes are
# counted in Grand Totals, so a small positive delta (~300 B here) is expected.
if gt:
tol = 8
deltas = {
"Code": total_comp_code - gt["code"],
"RO": total_comp_ro - gt["ro_data"],
"RW": total_comp_rw - gt["rw_data"],
"ZI": total_comp_zi - gt["zi_data"],
}
ok = all(abs(d) <= tol for d in deltas.values())
status = "PASS" if ok else "FAIL"
if all(d == 0 for d in deltas.values()):
note = "精确一致(含 '('incl. Generated)'-'(incl. Padding)' 台账行)"
else:
note = "差值=图像对齐填充(incl. Padding/Generated 行)"
deltas_txt = " / ".join(f"{k}Δ={d:+d}" for k, d in deltas.items())
w(f"> 自洽校验:per-file {fmt(total_comp_code)}/{fmt(total_comp_ro)}/{fmt(total_comp_rw)}/{fmt(total_comp_zi)}"
f" vs Grand Totals {fmt(gt['code'])}/{fmt(gt['ro_data'])}/{fmt(gt['rw_data'])}/{fmt(gt['zi_data'])}"
f"({deltas_txt};{note})→ **{status}**")
w("")
return "\n".join(lines)
def main():
ap = argparse.ArgumentParser(description="Parse Keil .map → Markdown RAM/Flash report")
ap.add_argument("map_file", nargs="?", default=MAP_DEFAULT, help="Path to .map file")
ap.add_argument("-o", "--output", help="Output file (default: stdout)")
args = ap.parse_args()
if not os.path.exists(args.map_file):
print(f"ERROR: Map file not found: {args.map_file}", file=sys.stderr)
sys.exit(1)
data = parse_map(args.map_file)
report = generate_report(data, args.map_file)
if args.output:
pathlib.Path(args.output).parent.mkdir(parents=True, exist_ok=True)
# newline="\n":Windows 下 write_text 默认把 \n 翻成 \r\n,会跟 .gitattributes
# (* text=auto eol=lf)与 data/ 目录既有快照的行尾不一致(2026-10-22 实测)。
pathlib.Path(args.output).write_text(report, encoding="utf-8", newline="\n")
print(f"OK: Report written to {args.output}", file=sys.stderr)
else:
print(report)
if __name__ == "__main__":
main()