Files
STM32F4-Base/tools/arch_diagram/gen_arch_svg.py
T
wandering1 a6cdebde62 文档体系重组与 D-7 一致性门禁落地,清理芯片手册孤儿图片
- D-6 目录规范化:docs 根目录散落文档归入 UI设计/子系统设计/架构设计/审查记录/
  环境与部署/网络 等子目录,去掉目录名空格,重命名 嵌入式C语言代码规范_V1.0.md
- D-7 三档一致性门禁:新增 scripts/check_doc_links.py(链接存在性)、
  scripts/gen_task_table.py(FreeRTOS 任务表数值)、scripts/check_mem_numbers.py
  (内存/Flash 体积),均支持 --check/--write/--diff,pre-commit 钩子同步注册
- D-5 文档索引:新建 docs/README.md,新建 docs/文档规范.md 约束目录命名与链接写法
- D-1 芯片手册瘦身:删除 1069 张无引用孤儿图片(77.06 MB),
  docs/芯片手册 120 MB → 41 MB,已引用图片零断链
- 同步 C 源码注释中的文档路径引用(D-6 重组后),构建 0 错误 0 警告
2026-09-26 00:51:53 +08:00

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_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(图形库)",
"sys_clock(墙钟基准)", "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()