#!/usr/bin/env python3 """Generate memory layout SVG diagrams for STM32F4-Base. Usage: python tools/arch_diagram/gen_memory_svg.py [--type ram|flash|taskstack|all] Output (default --all): docs/images/内存布局_RAM.svg docs/images/内存布局_FLASH.svg docs/images/任务栈预算.svg """ import argparse import os import sys # --------------------------------------------------------------------------- # Diagram data # --------------------------------------------------------------------------- # ── RAM breakdown (2026-10-22 @build, map Grand Totals; data/ram_report_20261022.md) ── # Region 1: main SRAM RW_IRAM1 @0x20000000 (128KB = 131,072 B, DMA-reachable) SRAM1_TOTAL = 131072 SRAM1_USED = 75264 SRAM1_FREE = SRAM1_TOTAL - SRAM1_USED # Region 2: CCM RW_CCM @0x10000000 —— scatter 给的是 0x0000FF00 = 65,280 B(CPU only, # DMA 不可达);另有 0x1000FF00 起 256 B 的 UNINIT_CCM 尾区(.noinit 崩溃现场用 48 B) CCM_TOTAL = 65280 CCM_USED = 57376 CCM_FREE = CCM_TOTAL - CCM_USED RAM1_CATEGORIES = [ ("LVGL 池 16KB", 16876, "#4CAF50"), ("UI", 13546, "#FF9800"), ("lftpd", 8440, "#8BC34A"), ("FTL/BBT", 7036, "#9C27B0"), ("NET/CH395F", 5250, "#F44336"), ("CMSIS-RTOS", 1724, "#00BCD4"), ("HAL", 84, "#607D8B"), ("C stack", 1024, "#795548"), ("other", 21284, "#9E9E9E"), ("free", SRAM1_FREE, "#E0E0E0"), ] RAM2_CATEGORIES = [ ("FreeRTOS 堆", CCM_USED, "#2196F3"), ("free", CCM_FREE, "#E0E0E0"), ] # ── Flash breakdown (1MB = 1,048,576 B; 2026-10-23 @build) ────────────────── FLASH_TOTAL = 1048576 FLASH_CODE = 213676 FLASH_RO = 18860 FLASH_RW = 1188 FLASH_USED = FLASH_CODE + FLASH_RO + FLASH_RW # ROM 合计(含 RW 初始化表) FLASH_IROM = FLASH_CODE + FLASH_RO # ER_IROM1 区用量 = 232,536 B(22.2%) FLASH_FREE = FLASH_TOTAL - FLASH_USED FLASH_CATEGORIES = [ ("代码段 (Code)", FLASH_CODE, "#2196F3"), ("只读数据 (RO)", FLASH_RO, "#4CAF50"), ("初始化数据 (RW)", FLASH_RW, "#FF9800"), ("Flash 空闲", FLASH_FREE, "#E0E0E0"), ] # Flash Code+RO 分布(= ER_IROM1 232,536 B;2026-10-23 按 .map 对象表逐对象归类, # 口径同 data/ram_report_20261023.md;"其他"含链接器填充) FLASH_CODE_SUB = [ ("LVGL 库", 87706, "#4CAF50"), ("应用代码/UI", 57047, "#2196F3"), ("CH395F+FTP", 25916, "#F44336"), ("HAL 驱动", 17876, "#607D8B"), ("dhara / FTL", 11222, "#FF9800"), ("FreeRTOS 内核", 10762, "#00BCD4"), ("FatFS", 8468, "#9C27B0"), ("C 库 (microlib)", 5486, "#795548"), ("startup", 468, "#B0BEC5"), ("其他 (长尾)", 7585, "#E0E0E0"), # remainder(2026-10-23 的零散增量) ] # ── Task stack budgets (2026-10-22 @build + 同日真机 [STACK] 实测; data/stack_report_20261022.md) ── # (name, cfg, prio, worst, peak_used, margin, margin_pct, verdict, desc) # worst = max(静态最坏深度, 实测峰值); margin = cfg - worst # 9 行含 testTask(仅测试构建创建、无实测 → 取静态最坏链,峰位列填同值) TASK_STACKS = [ ("defaultTask", 5120, "Normal", 3024, 2152, 2096, 40, "PASS", "FatFS 挂载/探针 + 配置引擎 + syslog + LED"), ("netTask", 3840, "Normal", 1912, 1116, 1928, 50, "PASS", "CH395F 轮询、消息处理"), ("ftpTask", 4096, "Normal", 1456, 1404, 2640, 64, "PASS", "lftpd FTP 服务(大缓冲已静态化)"), ("uiTask", 16384, "BelowNormal", 4052, 4052, 12332, 75, "PASS", "LVGL UI 主循环 + 远程显示推流"), ("testTask", 8192, "BelowNormal", 736, 736, 7456, 91, "PASS", "测试套件(产品构建不创建,取静态最坏链)"), ("timeSyncTask", 2560, "Normal", 1264, 648, 1296, 50, "PASS", "TCP 时间同步(8888)"), ("adcTask", 2560, "AboveNormal", 1320, 952, 1240, 48, "PASS", "1 kHz 节拍消费者(L1 采集,纯取数)"), ("iec104Task", 2560, "Normal", 1456, 540, 1104, 43, "PASS", "IEC 60870-5-104 上行(2404)"), ("protectTask", 2048, "Normal", 872, 828, 1176, 57, "PASS", "20 ms 保护判据 + 闭锁 + 定值校验和巡检"), ] KERNEL_STACKS = [("idle", 512), ("timer", 1024)] # 128/256 words(未实测) HEAP_TOTAL = 57344 # configTOTAL_HEAP_SIZE 56 KB(位于 RW_CCM;段实占 57,376 B 含元数据) HEAP_FREE = 15800 # 2026-10-22 真机 [STACK](启动 + 网络/FTP/104 就绪后) HEAP_MIN_EVER = 8464 TOTAL_STACKS = sum(t[1] for t in TASK_STACKS) # 47,360(含 testTask;产品构建 8 任务 = 39,168) # --------------------------------------------------------------------------- # SVG helpers # --------------------------------------------------------------------------- def _text_width(text, char_w=7.2): """Rough monospace text width estimate (px).""" return len(text) * char_w def _wrap(text, max_chars=12): """Wrap label to multiple lines.""" words = text.split("\n") if "\n" in text else text.split() lines = [] current = "" for w in words: test = (current + " " + w).strip() if current else w if len(test) > max_chars and current: lines.append(current) current = w else: current = test if current: lines.append(current) return lines def svg_header(w, h): return ( f'\n' '\n' '\n' '\n' ) def svg_footer(): return '\n' def _draw_bar_segment(x, y, w, h, fill, label_inside, label_outside="", label_font=10, min_inside_w=40): """Return SVG fragment for a bar segment with label.""" parts = [] parts.append(f' \n') if w > min_inside_w and label_inside: # Center text inside bar lines = _wrap(label_inside, max_chars=int(w / 7)) ty = y + h / 2 - (len(lines) - 1) * 6 for line in lines: lx = x + w / 2 parts.append(f' {line}\n') ty += 12 elif label_outside: lx = x + w + 4 ty = y + h / 2 parts.append(f' {label_outside}\n') return "".join(parts) # --------------------------------------------------------------------------- # Diagram generators # --------------------------------------------------------------------------- def gen_ram_svg(): """内存布局_RAM.svg — 两条堆叠条:主 SRAM(DMA 可达)+ CCM(仅 CPU)。""" W, H = 820, 360 BAR_X, BAR_H = 60, 44 SCALE = (W - 80) / SRAM1_TOTAL # 128KB 占满宽;CCM (64KB) 占半宽 svg = svg_header(W, H) svg += f'SRAM (192KB) 使用布局 — 2026-10-22 @build\n' # Region 1: 主 SRAM y1 = 50 svg += f'主 SRAM RW_IRAM1 @0x20000000(128KB,DMA 可达)\n' x = BAR_X for label, size, color in RAM1_CATEGORIES: w = size * SCALE if w < 1: continue inside = label if w > _text_width(label) + 10 else "" svg += _draw_bar_segment(x, y1, w, BAR_H, color, inside, "") x += w svg += f'' svg += f'已用: {SRAM1_USED}B ({SRAM1_USED/SRAM1_TOTAL*100:.1f}%)' svg += f' 空闲: {SRAM1_FREE}B ({SRAM1_FREE/SRAM1_TOTAL*100:.1f}%)' svg += f' 总量: 128KB\n' # Region 2: CCM y2 = y1 + BAR_H + 50 svg += f'CCM RW_CCM @0x10000000(段 65,280 B;仅 CPU 可达、DMA 不可达 → R2/R3)\n' x = BAR_X for label, size, color in RAM2_CATEGORIES: w = size * SCALE if w < 1: continue inside = label if w > _text_width(label) + 10 else "" svg += _draw_bar_segment(x, y2, w, BAR_H, color, inside, "") x += w svg += f'' svg += f'已用: {CCM_USED}B ({CCM_USED/CCM_TOTAL*100:.1f}%)' svg += f' 空闲: {CCM_FREE}B ({CCM_FREE/CCM_TOTAL*100:.1f}%)' svg += f' 总量: 63.75KB(+256 B UNINIT_CCM 尾区)——CCM 是当前全局卡点\n' # Legend(主 SRAM 两列 + CCM 两行) ly = y2 + BAR_H + 40 lx = 60 half = (len(RAM1_CATEGORIES) + 1) // 2 for i, (label, size, color) in enumerate(RAM1_CATEGORIES): pct = size / SRAM1_TOTAL * 100 kb = size / 1024 if i >= half: lx2 = 420 else: lx2 = 60 yy = 226 + (i % half) * 16 svg += f' \n' svg += f' ' svg += f'主SRAM {label}: {kb:.1f}KB ({pct:.1f}%)\n' for i, (label, size, color) in enumerate(RAM2_CATEGORIES): pct = size / CCM_TOTAL * 100 kb = size / 1024 yy = 322 + i * 16 svg += f' \n' svg += f' ' svg += f'CCM {label}: {kb:.1f}KB ({pct:.1f}%)\n' svg += svg_footer() return svg def gen_flash_svg(): """内存布局_FLASH.svg — bar + code sub-breakdown.""" W, H = 820, 240 BAR_X, BAR_Y, BAR_H = 60, 50, 40 SCALE = (W - 80) / FLASH_TOTAL svg = svg_header(W, H) svg += f'Flash (1MB) 使用布局 — 2026-10-23 @build\n' x = BAR_X for label, size, color in FLASH_CATEGORIES: w = size * SCALE if w < 1: continue inside = f"{label} {size/1024:.1f}KB" if w > 60 else "" outside = "" if w > 60 else f"{label} {size/1024:.1f}KB" svg += _draw_bar_segment(x, BAR_Y, w, BAR_H, color, inside, outside) x += w # Summary below bar svg += f'' svg += f'已用: {FLASH_USED/1024:.1f}KB ({FLASH_USED/FLASH_TOTAL*100:.1f}%)' svg += f' 空闲: {FLASH_FREE/1024:.1f}KB ({FLASH_FREE/FLASH_TOTAL*100:.1f}%)' svg += f' ER_IROM1(Code+RO): {FLASH_IROM/1024:.1f}KB ({FLASH_IROM/FLASH_TOTAL*100:.1f}%)' svg += f' 总量: 1MB (= ROM 合计口径)' svg += f'\n' # Code sub-breakdown mini bar Y2 = 130 svg += f'Code + RO 分布(ER_IROM1 {FLASH_IROM/1024:.1f}KB,按 .map 对象归类)\n' SCALE2 = (W - 80) / FLASH_IROM x2 = BAR_X for label, size, color in FLASH_CODE_SUB: w = size * SCALE2 if w < 1: continue # 窄段(多数桶 < 40px)不放内嵌文字,统一列到下方图例,避免互相压字 inside = f"{size/1024:.1f}KB" if w > 40 else "" svg += _draw_bar_segment(x2, Y2, w, 24, color, inside, "") x2 += w # 图例:四列三行(10 个桶) for i, (label, size, color) in enumerate(FLASH_CODE_SUB): col, row = i % 4, i // 4 lx = 60 + col * 195 ly = 176 + row * 16 svg += f' \n' svg += f' {label} {size/1024:.1f}KB\n' svg += svg_footer() return svg def gen_taskstack_svg(): """任务栈预算.svg — 每任务:分配栈条 + 最坏占用叠加条 + 实测余量/判定。""" W, H = 880, 620 # 9 个任务 + 2 内核栈 + 堆汇总,行高 40 需要 ~600 高 BAR_X = 190 MAX_STACK = 16384 SCALE = (W - BAR_X - 40) / MAX_STACK Y_START = 64 ROW_PITCH = 40 BAR_H = 16 svg = svg_header(W, H) svg += f'FreeRTOS 任务栈预算(9 任务;栈全部从 56KB FreeRTOS 堆分配,堆位于 CCM)\n' svg += f'数据: 2026-10-22 @build + 同日真机 [STACK] 水位(data/stack_report_20261022.md);最坏 = max(静态最坏链, 实测峰值);判定线 = 余量 ≥ max(1024B ISR, 配置×30%)\n' svg += f'浅色条 = 分配栈 深色条 = 最坏占用(max(静态最坏, 实测峰值)) 下方文字 = 实测余量/判定\n' y = Y_START for name, cfg, prio, worst, peak, margin, pct, verdict, desc in TASK_STACKS: w = cfg * SCALE wc = worst * SCALE # 三档优先级配色:Normal 蓝 / BelowNormal 琥珀 / AboveNormal 绿(adcTask) dark = {"Normal": "#2196F3", "BelowNormal": "#FF9800", "AboveNormal": "#4CAF50"}[prio] light = {"Normal": "#BBDEFB", "BelowNormal": "#FFE0B2", "AboveNormal": "#C8E6C9"}[prio] vc = "#2E7D32" if verdict == "PASS" else "#C62828" # 任务名(左侧) svg += f' {name}\n' # 分配栈条(浅色)+ 最坏占用叠加条(深色) svg += f' \n' svg += f' \n' # 数据行 svg += f' ' svg += f'配置 {cfg}B | 实测峰值 {peak}B | 最坏 {worst}B | 余量 {margin}B ({pct}%) ' svg += f'{verdict}\n' y += ROW_PITCH # 内核任务(静态小栈,未取句柄实测) y += 4 for name, stack in KERNEL_STACKS: w = max(stack * SCALE, 2) svg += f' {name}\n' svg += f' \n' svg += f' {stack}B(未实测)\n' y += 22 # 堆汇总(实测值) y += 8 svg += f'' svg += f'堆总量: {HEAP_TOTAL}B(configTOTAL_HEAP_SIZE=56KB,位于 RW_CCM)' svg += f' 任务栈合计: {TOTAL_STACKS}B(含 testTask 8192B)\n' y += 16 svg += f'' svg += f'产品构建 8 任务栈 = 39168B + 内核栈 {512+1024}B' svg += f' 实测: free={HEAP_FREE}B / min_ever={HEAP_MIN_EVER}B(2026-10-22)\n' y += 16 svg += f'' svg += '判定: 9/9 PASS(testTask 无实测,取静态最坏链)\n' y += 24 # 颜色图例 svg += f' \n' svg += f' Normal 优先级\n' svg += f' \n' svg += f' BelowNormal 优先级\n' svg += f' \n' svg += f' AboveNormal 优先级(adcTask)\n' svg += f' \n' svg += f' 内核栈 idle/timer\n' svg += svg_footer() return svg # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- def main(): parser = argparse.ArgumentParser(description="Generate memory layout SVG diagrams") parser.add_argument("--type", choices=["ram", "flash", "taskstack", "all"], default="all") args = parser.parse_args() out_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "docs", "images") os.makedirs(out_dir, exist_ok=True) generators = { "ram": ("内存布局_RAM.svg", gen_ram_svg), "flash": ("内存布局_FLASH.svg", gen_flash_svg), "taskstack": ("任务栈预算.svg", gen_taskstack_svg), } if args.type == "all": targets = generators.values() else: targets = [generators[args.type]] for filename, gen_func in targets: path = os.path.join(out_dir, filename) svg = gen_func() with open(path, "w", encoding="utf-8", newline="\n") as f: f.write(svg) print(f"OK: {filename} ({len(svg)} bytes)") # XML self-check for filename, _ in targets: path = os.path.join(out_dir, filename) with open(path, "r", encoding="utf-8") as f: content = f.read() if not content.startswith("" not in content: print(f"FAIL: {filename} missing ", file=sys.stderr) sys.exit(1) if 'xmlns="http://www.w3.org/2000/svg"' not in content: print(f"FAIL: {filename} missing xmlns", file=sys.stderr) sys.exit(1) print("All SVG self-checks passed") if __name__ == "__main__": main()