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

219 lines
8.9 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/BSD网络栈分层.svg —— NET 网络栈分层架构图(纯标准库,无需第三方依赖)。
由 gen_arch_svg.py 复制改数据区:BSD Socket API 六层结构
(docs/BSD_Socket_API_使用指南.md §2.1):
应用层任务 → 消息队列 → netTask → net_socket 核心 → ch395f 驱动 → CH395F 硬件。
用法(仓库根目录,任意 python3):
python tools/arch_diagram/gen_net_stack_svg.py
输出:
docs/images/BSD网络栈分层.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", "BSD网络栈分层.svg")
# ----------------------------- 画布与尺寸参数 -----------------------------
W = 1200
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_APP = dict(
title="应用层任务", sub="任意 FreeRTOS 任务可调用公共 API(线程安全)", color="#2563eb", bg="#eff6ff",
chips=["net_send() / net_recv() / net_close()(线程安全封装)",
"net_select() / net_poll_events() / net_set_event_cb()",
"消费方:lftpd(FTP :21)· time_sync(对时 :8888)· ui_net_sink(远程显示 :7003)"],
)
L_QUEUE = dict(
title="消息队列", sub="osMessageQueue · CubeMX 生成,在 MX_FREERTOS_Init() 中创建", color="#7c3aed", bg="#f5f3ff",
chips=["net_msg_t:send / recv / close / listen 四类请求",
"调用方:入队请求 → 阻塞等待结果(ulTaskNotifyTake)",
"netTask 侧:出队 → 执行 → 写结果 → xTaskNotifyGive 通知"],
)
L_NETTASK = dict(
title="netTask(网络任务)", sub="1ms 周期 · 所有 CH395F 操作在此串行执行", color="#0d9488", bg="#f0fdfa",
chips=["net_poll() — CH395F 中断轮询 + Socket 状态机 + 事件回调",
"net_process_messages() — 消费消息队列请求",
"net_send_sock() / net_recv_sock() / net_close_sock() / net_listen_locked()(内部版本)"],
)
L_CORE = dict(
title="net_socket.c(BSD Socket API 核心)", sub="Socket 状态机 + CH395F 命令封装", color="#16a34a", bg="#f0fdf4",
chips=["8 个 Socket 控制块 s_net_socks[](状态机,无锁)",
"CH395F 命令序列封装(OPEN / TCP_LISTEN / CONNECT / SEND ...)",
"网络代次号(net_get_generation / net_generation_inc,服务自愈)"],
)
L_DRV = dict(
title="ch395f.c/h(底层 SPI 驱动)", sub="SPI2 · 不可重入", color="#d97706", bg="#fffbeb",
chips=["ch395f_spi_begin() / ch395f_spi_end() — 包裹每次 SPI 事务",
"所有 SPI 事务仅能由 netTask 发起(独占,无重入)"],
)
L_HW = dict(
title="CH395F 硬件", sub="以太网协议栈芯片", color="#e11d48", bg="#fff1f2",
chips=["CH395F(SPI2:PB12 CS / PB13 SCK / PB14 SDO / PB15 SDI)",
"INT# 中断输出(低电平有效 · GPIO 轮询,无 EXTI)"],
)
# 每一行可放多个"同级并列"的层(上层只依赖下方各层)
ROWS = [
[L_APP],
[L_QUEUE],
[L_NETTASK],
[L_CORE],
[L_DRV],
[L_HW],
]
FOOT = "上层只依赖相邻下层(单向);消息队列 + netTask 串行化全部 CH395F 操作;模拟器端定义 NET_NO_OS 裁掉队列/回调(Winsock 后端)"
# ----------------------------- 工具函数 -----------------------------
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("BSD Socket API 分层架构(NET 网络栈)")))
add('<text x="%d" y="%d" font-size="%d" fill="#64748b">%s</text>'
% (PAD, 62, SUB_FONT,
esc("CH395F 以太网协议栈芯片 · 消息队列串行化全部网络操作至 netTask · FreeRTOS V10.3.1")))
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()