d9b887c0af
B 的第一步(加规则)与第二步(修最小最值的那处)。用户裁定"B 按你说的进行"。 **为什么只能项目侧自己做**(两条实测负结果,已写进 §2.27 与 bsp_err.h 边界纪律 4): · ARMCC v5 对 __attribute__((warn_unused_result)) 不生效(凌晨在 rs485_transmit 上实测:属性 在位、调用点裸调不接返回值,构建仍 0 错 0 警告); · cppcheck 也照不出来——按本仓自己的 --enable=warning,style,performance,portability 跑 gd5f2gq5ue.c,对 14 处丢弃 HAL_SPI_* 返回值、以及"未初始化局部量直接进 P_FAIL 判定" **一条不报**(它假定 gd5f_read_status(&status) 会写出参,而失败路径根本不写)。 结论:**丢弃底层传输的返回值,今天没有任何工具会响**。 **R4 上线即照出 37 条存量丢弃**:gd5f2gq5ue.c 29(14 处 HAL + 15 处自家原语)、net_socket.c 8、 ui_net_sink.c 4(net_close)、rs485.c 2、nand_ftl.c 1。按增量拦截老规矩入基线、只减不增—— 这是覆盖面扩大新暴露的历史债,不是新增违规;基线 19 → 1 → **38**。 **R4 自己被修三次(逐条记下,都是"规则比我想的更脆")**: ① 首版漏判"裸调用行尾带注释"这种写法(正则的 \s*$ 卡死),而那恰是最常见的"带说明的丢弃" → 补可选注释分支,并做 6 条正/反例自检; ② 把 net_set_errno 八处误判成丢弃(它是 setter,返回 int 是状态回声不是调用成败)→ **按真实 反例把规则收窄**(谓词 + setter 两类豁免),而不是塞进基线洗掉假阳性; ③ must-use 名单只从**头文件**取,gd5f_set_feature 这类 static 原语整个在视野外——而下面那处 真窟窿恰恰是 static、靠人工才发现 → 名单扩到 .c 内 static 定义(同一套启发式),补完立刻 又照出 gd5f_bbt_save#1:**运行时标坏后 BBT 落盘结果被丢弃 = 坏块记账可能没持久化**,已入 基线记为待还,本步不动行为。 另修一处自己的排版误判:R3 原本要求注释块出现字面"返回值",规范 V1.0 允许列对齐写法 「返 回 值:」→ 改为忽略空格匹配(门禁不该规定排版)。 **修掉的第一处真窟窿**:bbt_rebuild 的注释写着"无论扫描成败都须恢复 ECC,否则后续 I/O 在无 ECC 下运行",而它下面 gd5f_set_feature(0xB0,…) + gd5f_wait_busy(100) **返回值全丢**:SPI 一抖 ECC 就保持关闭,且 gd5f_check_ecc() 在 ECC 关闭时恒读"无 ECC 错"——整条链再没有任何信号。 补法复用 init 路径的 E-10(写完回读 B0h 验 ECC_EN 位真生效,"写了但没生效"与写失败同罪), 并把"关 ECC 失败"改成**立即中止**(此时 ECC 仍开着,是安全侧;带着 ECC 去扫 spare 反而会把 厂标好块误判成坏块,"重建 BBT"变成制造坏块)。恢复失败时按 (void)gd5f_bbt_load() 回读盘上 副本,避免 RAM 留着"比盘上更新"的表而下次上电又退回旧表。 **一条定位纠正(差点把修复记大)**:查链接映射发现 Removing gd5f2gq5ue.o (i.gd5f2gq5ue_bbt_rebuild), (604 bytes)——该函数在产品镜像里**根本没被链接**(产品构建无调用 者),只由 ENABLE_STORAGE_RECOVERY_TESTS 的恢复构建经 testTask 调用。所以本次修的是**恢复流程 自身的安全**(跑恢复时若 ECC 没恢复,设备会带着"已恢复"的结论在无保护状态下继续跑),不是 现场日常路径;相应地镜像 Code 的 +28 B 也不是它带来的(未逐字节追,线索是 gd5f_private_get_feature 由单调用点变多调用点、内联决策可能变化)。验证因此不能只跑产品构建: 临时打开 TEST_SUITE_STORAGE + ENABLE_STORAGE_RECOVERY_TESTS 编一次(0 错 0 警告且该函数不再 出现在 Removing 列表),随即还原两处开关并重编产品构建核对(test/board 无残留差异)。 **编号门禁把我自己的自造编号拦下了**:我起初用 B-1/B2/B3/B4 标记这批缺陷,id-check 报"未登记 前缀"13 处(且 B2/B3/B4 会与真实里程碑号撞号)→ 全部改写成平实语言。这是那条"注释与文档里 不许用没人检索得到的内部阶段编号"的规则第一次在提交前拦住我,规则是对的。 验证:@build 0 错 0 警告,Code 213,648 → **213,676**、RO 18,860、ER_IROM1 **232,536 B / 22.2%** (ROM 合计 233,724 B;RAM 逐字段不变);ram_report 重生成 + ram_baseline.py --update + 配图常量 同步(mem-numbers Passed);@test **PASS**(到 net init: OK 682 ms、[ERR] 0);R4 两处**探针 实证**(临时加一条裸调用 → 立刻成为新增并 RC=1;还原 → 38/38/0);--prune 点名本次还掉的 2 条 已删;pre-commit run 全绿 15 Passed;check_doc_links --all --sections-strict 147 篇 0 问题; id-check --strict 增量拦截通过。 文档:AGENTS.md 与 .pre-commit-config.yaml 的 bsp-err 口径(四条规则、基线 38); pre-commit使用说明.md §2.27(R4 全量口径 + 两条负结果 + 名单来源与豁免形状 + 单行限度); bsp_err.h 边界纪律 4;报告 §0.4 / §5.1 / §9 / 附录 A + 变更记录;存储维护与容量恢复.md 新增 §4.3(运维判据:出现 ECC restore FAILED / ECC not enabled 即本次恢复**不算完成**,须断电重启 重走 init,不许接着 nand_ftl_format() 当作已修好;并记明该函数产品镜像不链接)——顺带修该文 结构缺陷:原 ## 5. 变更记录 误插在 §4.1 与 §4.2 之间,会让 §4.2 变成 §5 的子节。
418 lines
18 KiB
Python
418 lines
18 KiB
Python
#!/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 S-5 C3/C1/C2 的零散增量)
|
||
]
|
||
|
||
# ── 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 <tspan> 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'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" '
|
||
f'width="{w}" height="{h}" style="font-family:Consolas,\'Courier New\',monospace; '
|
||
f'font-size:12px; background:#fff">\n'
|
||
'<defs>\n'
|
||
'<style>\n'
|
||
'.title { font-size:14px; font-weight:bold; }\n'
|
||
'.label { font-size:11px; }\n'
|
||
'.legend-text { font-size:10px; }\n'
|
||
'.bar-seg { stroke:#fff; stroke-width:0.5; }\n'
|
||
'</style>\n'
|
||
'</defs>\n'
|
||
)
|
||
|
||
|
||
def svg_footer():
|
||
return '</svg>\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' <rect class="bar-seg" x="{x}" y="{y}" width="{w:.1f}" height="{h}" '
|
||
f'fill="{fill}" rx="2"/>\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' <text x="{lx:.1f}" y="{ty:.1f}" text-anchor="middle" '
|
||
f'dominant-baseline="central" class="label" fill="#000">{line}</text>\n')
|
||
ty += 12
|
||
elif label_outside:
|
||
lx = x + w + 4
|
||
ty = y + h / 2
|
||
parts.append(f' <text x="{lx:.1f}" y="{ty:.1f}" text-anchor="start" '
|
||
f'dominant-baseline="central" class="legend-text" fill="#333">{label_outside}</text>\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'<text x="{W/2}" y="24" text-anchor="middle" class="title">SRAM (192KB) 使用布局 — 2026-10-22 @build</text>\n'
|
||
|
||
# Region 1: 主 SRAM
|
||
y1 = 50
|
||
svg += f'<text x="{BAR_X}" y="{y1-8}" class="label" fill="#555">主 SRAM RW_IRAM1 @0x20000000(128KB,DMA 可达)</text>\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'<text x="{BAR_X}" y="{y1+BAR_H+18}" class="label" fill="#333">'
|
||
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</text>\n'
|
||
|
||
# Region 2: CCM
|
||
y2 = y1 + BAR_H + 50
|
||
svg += f'<text x="{BAR_X}" y="{y2-8}" class="label" fill="#555">CCM RW_CCM @0x10000000(段 65,280 B;仅 CPU 可达、DMA 不可达 → R2/R3)</text>\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'<text x="{BAR_X}" y="{y2+BAR_H+18}" class="label" fill="#333">'
|
||
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 是当前全局卡点</text>\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' <rect x="{lx2}" y="{yy}" width="12" height="12" fill="{color}" rx="2"/>\n'
|
||
svg += f' <text x="{lx2+16}" y="{yy+10}" class="legend-text" fill="#333">'
|
||
svg += f'主SRAM {label}: {kb:.1f}KB ({pct:.1f}%)</text>\n'
|
||
for i, (label, size, color) in enumerate(RAM2_CATEGORIES):
|
||
pct = size / CCM_TOTAL * 100
|
||
kb = size / 1024
|
||
yy = 322 + i * 16
|
||
svg += f' <rect x="{60}" y="{yy}" width="12" height="12" fill="{color}" rx="2"/>\n'
|
||
svg += f' <text x="{76}" y="{yy+10}" class="legend-text" fill="#333">'
|
||
svg += f'CCM {label}: {kb:.1f}KB ({pct:.1f}%)</text>\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'<text x="{W/2}" y="24" text-anchor="middle" class="title">Flash (1MB) 使用布局 — 2026-10-23 @build</text>\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'<text x="{BAR_X}" y="{BAR_Y+BAR_H+18}" class="label" fill="#333">'
|
||
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'</text>\n'
|
||
|
||
# Code sub-breakdown mini bar
|
||
Y2 = 130
|
||
svg += f'<text x="{BAR_X}" y="{Y2-6}" class="label" fill="#555">Code + RO 分布(ER_IROM1 {FLASH_IROM/1024:.1f}KB,按 .map 对象归类)</text>\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' <rect x="{lx}" y="{ly-9}" width="10" height="10" fill="{color}" rx="2"/>\n'
|
||
svg += f' <text x="{lx+14}" y="{ly}" class="legend-text" fill="#333">{label} {size/1024:.1f}KB</text>\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'<text x="{W/2}" y="22" text-anchor="middle" class="title">FreeRTOS 任务栈预算(9 任务;栈全部从 56KB FreeRTOS 堆分配,堆位于 CCM)</text>\n'
|
||
svg += f'<text x="{W/2}" y="40" text-anchor="middle" class="legend-text" fill="#777">数据: 2026-10-22 @build + 同日真机 [STACK] 水位(data/stack_report_20261022.md);最坏 = max(静态最坏链, 实测峰值);判定线 = 余量 ≥ max(1024B ISR, 配置×30%)</text>\n'
|
||
svg += f'<text x="{BAR_X}" y="{Y_START-10}" class="legend-text" fill="#555">浅色条 = 分配栈 深色条 = 最坏占用(max(静态最坏, 实测峰值)) 下方文字 = 实测余量/判定</text>\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' <text x="{BAR_X-4}" y="{y+8}" text-anchor="end" dominant-baseline="central" class="label" fill="#333">{name}</text>\n'
|
||
# 分配栈条(浅色)+ 最坏占用叠加条(深色)
|
||
svg += f' <rect class="bar-seg" x="{BAR_X}" y="{y}" width="{w:.1f}" height="{BAR_H}" fill="{light}" rx="3"/>\n'
|
||
svg += f' <rect x="{BAR_X}" y="{y}" width="{wc:.1f}" height="{BAR_H}" fill="{dark}" rx="3"/>\n'
|
||
# 数据行
|
||
svg += f' <text x="{BAR_X}" y="{y+BAR_H+11}" dominant-baseline="central" class="legend-text" fill="#333">'
|
||
svg += f'配置 {cfg}B | 实测峰值 {peak}B | 最坏 {worst}B | 余量 {margin}B ({pct}%) '
|
||
svg += f'<tspan fill="{vc}" font-weight="bold">{verdict}</tspan></text>\n'
|
||
y += ROW_PITCH
|
||
|
||
# 内核任务(静态小栈,未取句柄实测)
|
||
y += 4
|
||
for name, stack in KERNEL_STACKS:
|
||
w = max(stack * SCALE, 2)
|
||
svg += f' <text x="{BAR_X-4}" y="{y+6}" text-anchor="end" dominant-baseline="central" class="label" fill="#333">{name}</text>\n'
|
||
svg += f' <rect class="bar-seg" x="{BAR_X}" y="{y}" width="{w:.1f}" height="12" fill="#90A4AE" rx="2"/>\n'
|
||
svg += f' <text x="{BAR_X + w + 4}" y="{y+6}" dominant-baseline="central" class="legend-text" fill="#333">{stack}B(未实测)</text>\n'
|
||
y += 22
|
||
|
||
# 堆汇总(实测值)
|
||
y += 8
|
||
svg += f'<text x="{BAR_X}" y="{y}" class="label" fill="#555">'
|
||
svg += f'堆总量: {HEAP_TOTAL}B(configTOTAL_HEAP_SIZE=56KB,位于 RW_CCM)'
|
||
svg += f' 任务栈合计: {TOTAL_STACKS}B(含 testTask 8192B)</text>\n'
|
||
y += 16
|
||
svg += f'<text x="{BAR_X}" y="{y}" class="label" fill="#555">'
|
||
svg += f'产品构建 8 任务栈 = 39168B + 内核栈 {512+1024}B'
|
||
svg += f' 实测: free={HEAP_FREE}B / min_ever={HEAP_MIN_EVER}B(2026-10-22)</text>\n'
|
||
y += 16
|
||
svg += f'<text x="{BAR_X}" y="{y}" class="label" fill="#555">'
|
||
svg += '判定: 9/9 PASS(testTask 无实测,取静态最坏链)</text>\n'
|
||
y += 24
|
||
# 颜色图例
|
||
svg += f' <rect x="{BAR_X}" y="{y}" width="10" height="10" fill="#2196F3" rx="2"/>\n'
|
||
svg += f' <text x="{BAR_X+14}" y="{y+8}" class="legend-text" fill="#333">Normal 优先级</text>\n'
|
||
svg += f' <rect x="{BAR_X+130}" y="{y}" width="10" height="10" fill="#FF9800" rx="2"/>\n'
|
||
svg += f' <text x="{BAR_X+144}" y="{y+8}" class="legend-text" fill="#333">BelowNormal 优先级</text>\n'
|
||
svg += f' <rect x="{BAR_X+300}" y="{y}" width="10" height="10" fill="#4CAF50" rx="2"/>\n'
|
||
svg += f' <text x="{BAR_X+314}" y="{y+8}" class="legend-text" fill="#333">AboveNormal 优先级(adcTask)</text>\n'
|
||
svg += f' <rect x="{BAR_X+520}" y="{y}" width="10" height="10" fill="#90A4AE" rx="2"/>\n'
|
||
svg += f' <text x="{BAR_X+534}" y="{y+8}" class="legend-text" fill="#333">内核栈 idle/timer</text>\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("<svg "):
|
||
print(f"FAIL: {filename} does not start with <svg", file=sys.stderr)
|
||
sys.exit(1)
|
||
if "</svg>" not in content:
|
||
print(f"FAIL: {filename} missing </svg>", 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()
|