Files
STM32F4-Base/scripts/ram_report.py
T

370 lines
13 KiB
Python
Raw 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", "mc_w.l", "mf_w.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; they are excluded from
per-file tables but their contribution IS counted in Grand Totals, so the per-file
sums will differ from Grand Totals by the alignment-padding delta (~300B here).
"""
return name in TOTALS_ROWS
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)
pathlib.Path(args.output).write_text(report, encoding="utf-8")
print(f"OK: Report written to {args.output}", file=sys.stderr)
else:
print(report)
if __name__ == "__main__":
main()