清理存量 ASCII 框图:存储栈/分区转 SVG、CH395F 时序转 Mermaid

This commit is contained in:
2026-09-11 23:01:25 +08:00
parent e16bd882a6
commit 02a32be5ea
8 changed files with 443 additions and 106 deletions
+138
View File
@@ -0,0 +1,138 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""生成 docs/images/物理分区布局.svg —— GD5F2GQ5UE 全片分区布局图(纯标准库)。
由 gen_frame_svg.py 复制而来(build() 与数据解耦,通用):协议帧图按"字节"计量
(字段宽 ∝ 字节数),本图按"MB"计量(分区宽 ∝ MB 数),其余约定一致——
分段矩形 + 偏移标尺 + 逐段标注,连续布局无假箭头。
用法(仓库根目录,任意 python3):
python tools/arch_diagram/gen_partition_svg.py
输出:
docs/images/物理分区布局.svg
改分区时:编辑本文件顶部的 FIELDS / TITLE / SUB / FOOT 后重跑。
"""
import html
import os
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
OUT = os.path.join(ROOT, "docs", "images", "物理分区布局.svg")
# ----------------------------- 画布与尺寸参数 -----------------------------
PAD = 40 # 左右留白
PX_PER_MB = 4 # 每 MB 像素宽(分区宽 ∝ MB 数)
BAR_Y = 116 # 分区盒顶 y(标题 / 副标题 / 偏移标尺在其上方)
BAR_H = 74 # 分区盒高
NAME_DY = 30 # 分区名在盒内的 y 偏移
SIZE_DY = 54 # 尺寸标注在盒内的 y 偏移
RULER_DY = 20 # 偏移标尺线在盒顶上方
TITLE_FONT = 20
SUB_FONT = 13
NAME_FONT = 15
SIZE_FONT = 12
RULER_FONT = 12
FOOT_FONT = 12
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
# ----------------------------- 分区数据(改这里) -----------------------------
# name=分区名 mb=MB 数(None=变长段,按 DATA_UNITS 画) label=尺寸标注 color/bg=配色
DATA_UNITS = 5 # 变长段的"代表宽度"(按 N MB 画,仅示意)
FIELDS = [
dict(name="ftl_fatfs", mb=256, label="256 MB · Block 0~2047", color="#2563eb", bg="#eff6ff"),
]
TITLE = "GD5F2GQ5UE 物理分区布局(256MB)"
SUB = "单分区:全片划给 FTL(无静态分区)· 块 = 128KB(64 页 × 2KB)· 共 2048 块"
FOOT = ("ftl_fatfs:dhara FTL + FatFS 占用全部 2048 块(偏移 0 ~ 256MB)· "
"详见 docs/STM32F4-Base存储架构说明.md §1")
# ----------------------------- 工具函数 -----------------------------
def esc(s):
return html.escape(s, quote=True)
def field_w(f):
return (f["mb"] if f["mb"] is not None else DATA_UNITS) * PX_PER_MB
def offset_labels(fields):
"""按分区 MB 数累计边界偏移;偏移标注统一带 MB 单位。"""
labs, off = ["0"], 0
for f in fields:
off += f["mb"] if f["mb"] is not None else DATA_UNITS
labs.append("%d MB" % off)
return labs
# ----------------------------- 生成 -----------------------------
def build():
out = []
add = out.append
frame_w = sum(field_w(f) for f in FIELDS)
W = frame_w + 2 * PAD
# 预计算各分区 x / 宽
segs, x = [], PAD
for f in FIELDS:
w = field_w(f)
segs.append([f, x, w])
x += w
labels = offset_labels(FIELDS)
total_h = BAR_Y + BAR_H + 56
add('<rect x="0" y="0" width="%d" height="%d" fill="#ffffff"/>' % (W, total_h))
# 标题 + 副标题
add('<text x="%d" y="40" font-size="%d" font-weight="700" fill="#0f172a">%s</text>'
% (PAD, TITLE_FONT, esc(TITLE)))
add('<text x="%d" y="64" font-size="%d" fill="#64748b">%s</text>'
% (PAD, SUB_FONT, esc(SUB)))
# 偏移标尺(每个分区边界一个刻度 + 偏移标注)
ruler_y = BAR_Y - RULER_DY
add('<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#cbd5e1" stroke-width="1"/>'
% (PAD, ruler_y, PAD + frame_w, ruler_y))
bounds = [PAD] + [s[1] + s[2] for s in segs]
for i, bx in enumerate(bounds):
add('<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#94a3b8" stroke-width="1.5"/>'
% (bx, ruler_y - 4, bx, ruler_y + 4))
lab = labels[i] if i < len(labels) else "?"
add('<text x="%d" y="%d" font-size="%d" fill="#94a3b8" text-anchor="middle">%s</text>'
% (bx, ruler_y - 8, RULER_FONT, esc(lab)))
# 分区盒(名在上、尺寸在下)
for (f, bx, bw) in segs:
add('<rect x="%d" y="%d" width="%d" height="%d" fill="%s" stroke="%s" stroke-width="1.5"/>'
% (bx, BAR_Y, bw, BAR_H, f["bg"], f["color"]))
cx = bx + bw / 2.0
add('<text x="%.1f" y="%d" font-size="%d" font-weight="700" fill="#0f172a" '
'text-anchor="middle">%s</text>'
% (cx, BAR_Y + NAME_DY, NAME_FONT, esc(f["name"])))
add('<text x="%.1f" y="%d" font-size="%d" fill="%s" text-anchor="middle">%s</text>'
% (cx, BAR_Y + SIZE_DY, SIZE_FONT, f["color"], esc(f["label"])))
# 脚注
add('<text x="%d" y="%d" font-size="%d" fill="#94a3b8">%s</text>'
% (PAD, total_h - 16, FOOT_FONT, esc(FOOT)))
svg = ('<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" '
'viewBox="0 0 %d %d" font-family="%s">\n%s\n</svg>\n'
% (W, total_h, W, total_h, FONT, "\n".join(out)))
return svg, W, total_h
def main():
svg, W, h = build()
with open(OUT, "w", encoding="utf-8", newline="\n") as f:
f.write(svg)
import xml.etree.ElementTree as ET
ET.fromstring(svg)
print("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, h, OUT))
if __name__ == "__main__":
main()
+204
View File
@@ -0,0 +1,204 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""生成 docs/images/存储栈分层.svg —— GD5F2GQ5UE 存储软件栈分层图(纯标准库)。
由 gen_arch_svg.py 复制而来(build() 与数据解耦,通用):每层一个圆角框(标题 + 说明),
层间单向下箭头。本图各层均为"单层单职责",故 chips 留空(只渲染标题 + 说明行)。
用法(仓库根目录,任意 python3):
python tools/arch_diagram/gen_storage_stack_svg.py
输出:
docs/images/存储栈分层.svg
改分层时:编辑本文件顶部的 L_* / ROWS / TITLE / SUB / FOOT 数据后重跑。
"""
import html
import os
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
OUT = os.path.join(ROOT, "docs", "images", "存储栈分层.svg")
# ----------------------------- 画布与尺寸参数 -----------------------------
W = 1000
PAD = 26
TITLE_FONT = 20
SUB_FONT = 13
BAND_TITLE_FONT = 17
BAND_SUB_FONT = 12
CHIP_FONT = 13
CHIP_H = 38 # 芯片盒高
CHIP_GAP = 10 # 芯片水平间距
CHIP_ROW_GAP = 10 # 芯片行间距
CHIP_PAD = 16 # 芯片内左右留白
BAND_PAD_X = 18 # 框内左右留白
BAND_TITLE_H = 38 # 框内标题区高
BAND_PAD_BOTTOM = 16
BAND_H_GAP = 26 # 同一行内并列框的间距
BAND_V_GAP = 44 # 行间距(放箭头)
TITLE_H = 78 # 顶部标题区高
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
# ----------------------------- 架构数据(改这里) -----------------------------
TITLE = "STM32F4-Base 存储软件栈"
SUB = "GD5F2GQ5UE SPI NAND Flash(256MB)· 三层软件栈(FatFs / dhara FTL / 驱动)· 上层只依赖相邻下层"
L_APP = dict(
title="应用程序", sub="main · freertos(CubeMX 生成 + FreeRTOS 任务)",
color="#2563eb", bg="#eff6ff", chips=[],
)
L_FATFS = dict(
title="FatFs", sub="文件系统(标准 disk_* 块设备接口)",
color="#0d9488", bg="#f0fdfa", chips=[],
)
L_FTL = dict(
title="dhara FTL", sub="地址映射 · 磨损均衡 · 坏块管理 · 垃圾回收",
color="#16a34a", bg="#f0fdf4", chips=[],
)
L_DRV = dict(
title="GD5F2GQ5UE NAND Flash 驱动", sub="硬件 SPI · 页读写 · 块擦除 · BBT · 内部 ECC 使能",
color="#d97706", bg="#fffbeb", chips=[],
)
L_SPI = dict(
title="SPI1 硬件外设",
sub="PB3 SCK · PB4 MISO · PB5 MOSI · 42MHz · Mode 0 · MSB First · DMA2(S0-RX / S3-TX)· 小包轮询(≤32B)/ 页数据 DMA(>32B)",
color="#e11d48", bg="#fff1f2", chips=[],
)
ROWS = [
[L_APP],
[L_FATFS],
[L_FTL],
[L_DRV],
[L_SPI],
]
FOOT = "数据路径:应用 → FatFs → dhara FTL → gd5f2gq5ue 驱动 → SPI1 硬件外设(单向依赖)· 详见 docs/STM32F4-Base存储架构说明.md §1"
# ----------------------------- 工具函数 -----------------------------
def esc(s):
return html.escape(s, quote=True)
def text_w(s, fs):
"""粗略估算文本像素宽:CJK/全角/圈码/箭头 ≈ 1.0em,其余 ≈ 0.58em。
取偏大估计(宁可芯片略宽,不可文字溢出盒外)。
"""
w = 0.0
for ch in s:
w += fs * (1.0 if ord(ch) >= 0x2460 else 0.58)
return w
def chip_w(label):
return text_w(label, CHIP_FONT) + 2 * CHIP_PAD
def wrap_chips(chips, avail):
rows, cur, cur_w = [], [], 0.0
for c in chips:
cw = chip_w(c)
need = cw if not cur else cur_w + CHIP_GAP + cw
if cur and need > avail:
rows.append(cur)
cur, cur_w = [c], cw
else:
cur.append(c)
cur_w = need
if cur:
rows.append(cur)
return rows
# ----------------------------- 生成 -----------------------------
def build():
out = []
add = out.append
# 布局:逐行排布(行内可并列多个框,行高取该行最高框)
placements = [] # (layer, chip_rows, x, y, w, h)
y = TITLE_H
for row in ROWS:
n = len(row)
bw = (W - 2 * PAD - BAND_H_GAP * (n - 1)) // n
bands = []
for i, layer in enumerate(row):
bx = PAD + i * (bw + BAND_H_GAP)
avail = bw - 2 * BAND_PAD_X
rows = wrap_chips(layer["chips"], avail)
bh = BAND_TITLE_H + len(rows) * CHIP_H + (len(rows) - 1) * CHIP_ROW_GAP + BAND_PAD_BOTTOM
bands.append([layer, rows, bx, y, bw, bh])
row_h = max(b[5] for b in bands)
for b in bands:
b[5] = row_h # 同行等高(视觉"同级")
placements.extend(bands)
y += row_h + BAND_V_GAP
total_h = y - BAND_V_GAP + 54
add('<rect x="0" y="0" width="%d" height="%d" fill="#ffffff"/>' % (W, total_h))
add('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="#0f172a">%s</text>'
% (PAD, 40, TITLE_FONT, esc(TITLE)))
add('<text x="%d" y="%d" font-size="%d" fill="#64748b">%s</text>'
% (PAD, 62, SUB_FONT, esc(SUB)))
for (layer, rows, bx, by, bw, bh) in placements:
# 框背景 + 边框
add('<rect x="%d" y="%d" width="%d" height="%d" rx="12" fill="%s" stroke="%s" '
'stroke-opacity="0.45"/>' % (bx, by, bw, bh, layer["bg"], layer["color"]))
# 标题(层名 + 职责说明)
add('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="%s">%s</text>'
% (bx + BAND_PAD_X, by + 26, BAND_TITLE_FONT, layer["color"], esc(layer["title"])))
tw = text_w(layer["title"], BAND_TITLE_FONT)
add('<text x="%.0f" y="%d" font-size="%s" fill="#64748b">%s</text>'
% (bx + BAND_PAD_X + tw + 12, by + 26, BAND_SUB_FONT, esc(layer["sub"])))
# 芯片
cy = by + BAND_TITLE_H
for row in rows:
cx = bx + BAND_PAD_X
for c in row:
cw = chip_w(c)
add('<rect x="%d" y="%d" width="%d" height="%d" rx="8" fill="#ffffff" '
'stroke="%s" stroke-opacity="0.55"/>' % (cx, cy, cw, CHIP_H, layer["color"]))
add('<text x="%d" y="%d" font-size="%d" fill="#1e293b" text-anchor="middle">%s</text>'
% (cx + cw / 2, cy + CHIP_H / 2 + CHIP_FONT * 0.35, CHIP_FONT, esc(c)))
cx += cw + CHIP_GAP
cy += CHIP_H + CHIP_ROW_GAP
# 行间箭头:每个上层框向下引一条(顶层两框 → 两条,体现"都依赖下层")
row_first = [] # 每行首个 placement 的下标
idx = 0
for row in ROWS:
row_first.append(idx)
idx += len(row)
for r in range(len(ROWS) - 1):
y0 = placements[row_first[r]][3] + placements[row_first[r]][5] + 6
y1 = placements[row_first[r + 1]][3] - 8
for i in range(row_first[r], row_first[r] + len(ROWS[r])):
(_, _, bx, _, bw, _) = placements[i]
ax = bx + bw / 2
add('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="#94a3b8" '
'stroke-width="2.5"/>' % (ax, y0, ax, y1 - 7))
add('<path d="M %.1f %.1f l -6 -9 h 12 z" fill="#94a3b8"/>' % (ax, y1))
add('<text x="%d" y="%d" font-size="12" fill="#94a3b8">%s</text>'
% (PAD, total_h - 18, esc(FOOT)))
svg = ('<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" '
'viewBox="0 0 %d %d" font-family="%s">\n%s\n</svg>\n'
% (W, total_h, W, total_h, FONT, "\n".join(out)))
return svg, total_h
def main():
svg, h = build()
with open(OUT, "w", encoding="utf-8", newline="\n") as f:
f.write(svg)
import xml.etree.ElementTree as ET
ET.fromstring(svg)
print("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, h, OUT))
if __name__ == "__main__":
main()