Files
STM32F4-Base/tools/arch_diagram/gen_arch_svg.py
T

222 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
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
# -*- coding: utf-8 -*-
"""生成 docs/images/项目架构总览.svg —— 分层架构图(纯标准库,无需第三方依赖)。
为什么不用 Mermaid:自动布局不可控,多节点/跨层依赖会画成交叉的"意大利面"。
本脚本手工排布:每层一个圆角框(标题在框内顶部 + 模块芯片自动换行),
同一"行"可放多个并列层(如 应用层 与 UI 层 同级并排),行间单向下箭头。
用法(仓库根目录,任意 python3):
python tools/arch_diagram/gen_arch_svg.py
输出:
docs/images/项目架构总览.svg
改架构时:编辑本文件顶部的 LAYERS / ROWS 数据后重跑即可。
"""
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 = 1560
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"
# ----------------------------- 架构数据(改这里) -----------------------------
L_BIZ = dict(
title="① 业务层", sub="顶层共享配置 · 被 UI 与基础服务共用", color="#2563eb", bg="#eff6ff",
chips=["net_cfg(网口配置:IP/掩码/网关/MAC + Flash 持久化)"],
)
L_UI = dict(
title="② UI 层", sub="Drivers/BSP/UI · 与 PC 模拟器共用", color="#7c3aed", bg="#f5f3ff",
chips=["ui_port(显示端口)", "ui_menu / ui_menu_view(级联菜单)", "ui_menu_tab(菜单表)",
"ui_content_page(内容页框架)", "ui_page_*(5 页插件)",
"ui_net_sink(远程显示 :7003)", "ui_font_cn_12(中文字体)", "ui_deco(浅 V 装饰)"],
)
L_SVC = dict(
title="③ 基础服务层", sub="应用侧服务 / 编排", color="#0d9488", bg="#f0fdfa",
chips=["app_main(启动编排)", "sys_clock(墙钟基准)", "sys_events(就绪屏障)",
"app_cfg(编译开关)", "task/(net · ui · adc* · rs485*)",
"lftpd(FTP :21)", "time_sync(TCP 对时 :8888)"],
)
L_MW = dict(
title="④ 中间件层", sub="通用库 / 协议栈", color="#16a34a", bg="#f0fdf4",
chips=["net_socket / net_select(BSD API)",
"FatFs(文件系统)", "nand_ftl(dhara FTL)", "LVGL v9.2(图形库)",
"util(crc · ringbuf)"],
)
L_DRV = dict(
title="⑤ 驱动层", sub="Drivers/BSP · 芯片时序/中断", color="#d97706", bg="#fffbeb",
chips=["ch395f(SPI2 + DMA)", "gd5f2gq5ue(SPI1)", "tpafe5160(并行 16 位)",
"sd2506(I2C1)", "rs485(UART5 + DE)"],
)
L_KERN = dict(
title="⑥ 内核 / HAL", sub="调度 · 外设抽象", color="#475569", bg="#f1f5f9",
chips=["FreeRTOS V10.3.1(CMSIS-RTOS2 · heap_4)", "STM32 HAL(时基 TIM7)",
"CMSIS / 启动文件"],
)
L_HW = dict(
title="⑦ 硬件", sub="STM32F407ZGTx @168MHz", color="#e11d48", bg="#fff1f2",
chips=["CH395F 以太网(SPI2)", "GD5F2GQ5UE NAND(SPI1)", "TPAFE5160 ADC(并行)",
"SD2506 RTC(I2C1)", "RS-485(UART5)", "USART1 调试(115200)", "6x LED"],
)
# 每一行可放多个"同级并列"的层(上层只依赖下方各层)
ROWS = [
[L_BIZ], # 顶层:业务层(仅共享配置 net_cfg)
[L_UI, L_SVC], # UI 层与基础服务层同级并排,均使用 net_cfg
[L_MW], # 中间件层(通用库 / 协议栈)
[L_DRV],
[L_KERN],
[L_HW],
]
FOOT = "上层只依赖相邻下层(单向);UI 层与基础服务层同级并列,均使用顶层 net_cfg。跨层调用细节见 docs/项目架构总览.md §1.2 * 预留未接入调度"
# ----------------------------- 工具函数 -----------------------------
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("STM32F4-Base 项目分层架构")))
add('<text x="%d" y="%d" font-size="%d" fill="#64748b">%s</text>'
% (PAD, 62, SUB_FONT,
esc("STM32F407ZGTx @168MHz · FreeRTOS V10.3.1 (CMSIS-RTOS2) · 上层只依赖相邻下层")))
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()