Files
STM32F4-Base/tools/arch_diagram/gen_net_pending_svg.py
T
wandering1 df13dec479
CI / L0 静态门禁 (pre-commit) (push) Failing after 2m14s
CI / L0.5 主机侧单测 (MSVC) (push) Successful in 22s
refactor(net): 挂起 recv 模型拆出 net_pending.c/h + 留痕探针编译门控 + E15 归因修正
- E15 归因修正:幽灵状态 0x04+旧端口 = 芯片如实报告拆除中的旧连接
  (文档化行为,非芯片故障;bit3=1 为我方设置(ver>=0x46)、
  RFC 9293 §3.3.2 Fig5 拆除顺序、WCH 官方博客方案 = close 完成后重开监听)
- 术语统一:全库"了结"→"结束"(约 45 处);阻塞模型 §4 首次出现处定义"五路"
- 注释清理:删孤儿函数头注释、考古叙事改现在时、去不可检索内部编码"L3"
  (33 处 6 文件)、banner 去重(唯一理由移到分支现场)
- 挂起超时留痕探针:编译期开关 NET_PENDING_TRIPWIRE(默认 0 = 编译剔除,
  省约 150B Flash;排查调用方挂死时置 1,只留痕绝不强制结束)
- 拆模块:net_pending.c/h(363/94 行)承载挂起 recv 模型,
  net_socket.c 4059→3661 行;tick 收控制块表参数、settle_close 双参显式传;
  net_pending_active 声明移配对头(S17 2.2);仅注册 uvprojx +
  check_registration MCU-only 白名单(同 net_listener.c 机制,
  模拟器走 net_socket_sim.c 按外部语义模拟)
- 文档:阻塞模型新增全过程小节(单主干流程图)+ §9 排查辅助;
  NET README / 函数调用关系 / 自愈架构 / BSD 指南同步;引用关系图重生成
- 台架回归:启动闭环 PASS([ERR]/Fault 零命中、回读一致)、
  :7003 重连探针 10/10、FTP 50 连接 STOR 50/50、回归窗口串口日志无异常
- RAM:ER_IROM1 230004B(探针门控后约 -150B),台账已回填
2026-10-14 09:53:38 +08:00

300 lines
13 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/Net层阻塞模型.svg —— pending 请求模型架构图(纯标准库)。
结构:左右两栏 + 主循环横带 + 五路判定带
左 = 调用方任务(net_recv → 数据已在? → 登记挂起 / 睡任务通知)
右 = netTask 立即 SPI 读出(快速路径)+ CH395F 芯片
中 = netTask 主循环(~1ms 节拍:net_poll / net_pending_tick / ...)
下 = 五路完成/取消判定 → xTaskNotify 唤醒调用方
目的:把"等数据"从执行者内的自旋改为执行者外的登记——登记后调用方在自己上下文
睡眠,netTask 零占用地继续服务整个队列。
用法(仓库根目录,任意 python3):
python tools/arch_diagram/gen_net_pending_svg.py
输出:
docs/images/Net层阻塞模型.svg
改流程时:编辑本文件顶部的 CALLER_STEPS / LOOP_STEPS / PHASES 数据后重跑。
"""
import html
import math
import os
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
OUT = os.path.join(ROOT, "docs", "images", "Net层阻塞模型.svg")
# ----------------------------- 画布与样式 -----------------------------
W = 1440
H = 1030
PAD = 40
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
TITLE_FONT = 21
SUB_FONT = 13
PANEL_TITLE_FONT = 15
STEP_TITLE_FONT = 14
STEP_NOTE_FONT = 12
LANE_FONT = 12.5
FOOT_FONT = 12
C_CALLER, BG_CALLER = "#2563eb", "#eff6ff"
C_ACTOR, BG_ACTOR = "#7c3aed", "#f5f3ff"
C_CHIP, BG_CHIP = "#0d9488", "#f0fdfa"
C_TICK, BG_TICK = "#d97706", "#fffbeb"
C_CANCEL = "#dc2626"
C_OK = "#16a34a"
ARROW_W = 2.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) >= 0x2E80 else 0.58)
return w
# ----------------------------- 数据(改这里) -----------------------------
# 左栏:调用方流程(自上而下)
CALLER_STEPS = [
("net_recv(fd, buf, len, timeout)", "公开 API 与调用契约不变(签名 / 返回值 / 超时语义)"),
("数据已在?", "readable 标志由 net_poll 每轮刷新"),
("登记 pending 到 s_net_pending[fd]", "占用执行者到此结束(微秒级)· 每 fd 至多一个,不覆盖"),
("调用方在自己上下文睡任务通知", "与 LwIP 消费者线程模型同构 · 取消必通知,永不挂死"),
]
FAST_PATH = ("立即 SPI 读出",
"GET_REMV_LEN / READ → 通知调用方(结果 = 字节数)")
LOOP_STEPS = [
("net_poll()", "刷新 readable", C_CHIP),
("net_pending_tick()", "结束挂起", C_TICK),
("net_listener_retry_tick()", "重试对账", C_ACTOR),
("排空消息队列", "服务所有消费者", "#64748b"),
]
PHASES = [
("①", "数据到达", "SPI 读出,通知调用方(结果 = 字节数)", C_CHIP),
("②", "到点 deadline", "通知 NET_ERR_TIMEDOUT(对外 -2 映射保持)", C_TICK),
("③", "EOF / DISCONNECT", "按 0(EOF) 完成 + auto-relisten + 通知", C_ACTOR),
("④", "同 fd 后续操作", "先行结算该 fd 挂起,再执行新操作", C_CALLER),
("⑤", "网络重建(芯片复位)", "取消 + 必须通知(否则调用方永久挂死)", C_CANCEL),
]
WATCHDOG = "挂起超时留痕探针(编译期选项,默认关):挂起超阈值仍未结束 → 只 DBG_ERROR 留痕(不强制结束、不伪造返回值)"
FOOT = ("关键性质:挂起期间 netTask 零额外占用(判据复用主循环既有 net_poll 的 readable,"
"不引入额外 SPI 事务);公开 API 不变,既有调用方 / 测试 / 模拟器零改动。")
# ----------------------------- 生成 -----------------------------
def build():
out = []
add = out.append
def poly(points, color, dash=False, head=True, width=ARROW_W):
pts = " ".join("%.1f,%.1f" % p for p in points)
d = ' stroke-dasharray="7 5"' if dash else ''
add('<polyline points="%s" fill="none" stroke="%s" stroke-width="%.1f" '
'stroke-linejoin="round"%s/>' % (pts, color, width, d))
if head:
(x1, y1), (x2, y2) = points[-2], points[-1]
ang = math.atan2(y2 - y1, x2 - x1)
hl, hw = 11.0, 5.5
bx, by = x2 - hl * math.cos(ang), y2 - hl * math.sin(ang)
px, py = -math.sin(ang) * hw, math.cos(ang) * hw
add('<path d="M %.1f %.1f L %.1f %.1f L %.1f %.1f z" fill="%s"/>'
% (x2, y2, bx + px, by + py, bx - px, by - py, color))
def line(x1, y1, x2, y2, color, dash=False, head=False, width=ARROW_W):
poly([(x1, y1), (x2, y2)], color, dash=dash, head=head, width=width)
def box(x, y, w, h, color, bg, title, note=None, tfs=STEP_TITLE_FONT):
add('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="10" fill="%s" '
'stroke="%s" stroke-opacity="0.55" stroke-width="1.6"/>'
% (x, y, w, h, bg, color))
add('<text x="%.1f" y="%.1f" font-size="%.1f" font-weight="700" fill="#0f172a">%s</text>'
% (x + 16, y + 32, tfs, esc(title)))
if note:
add('<text x="%.1f" y="%.1f" font-size="%.1f" fill="#64748b">%s</text>'
% (x + 16, y + 56, STEP_NOTE_FONT, esc(note)))
# ---- 画布 ----
add('<rect x="0" y="0" width="%d" height="%d" fill="#ffffff"/>' % (W, H))
# ---- 标题 ----
add('<text x="%d" y="44" font-size="%d" font-weight="700" fill="#0f172a">%s</text>'
% (PAD, TITLE_FONT, esc("Net 层阻塞模型 · pending 请求架构")))
add('<text x="%d" y="70" font-size="%d" fill="#64748b">%s</text>'
% (PAD, SUB_FONT,
esc("把「等数据」从执行者内的自旋,改为执行者外的登记 —— 调用方在自己上下文睡眠,"
"netTask 零占用地服务整个队列")))
# ---- 图例 ----
lx = PAD
for color, label in [(C_CALLER, "调用方任务"),
(C_ACTOR, "netTask(唯一执行者)"),
(C_CHIP, "CH395F 芯片"),
(C_TICK, "主循环节拍 ~1ms")]:
add('<rect x="%.1f" y="100" width="16" height="16" rx="4" fill="#ffffff" '
'stroke="%s" stroke-width="2"/>' % (lx, color))
add('<text x="%.1f" y="112" font-size="%.1f" fill="#475569">%s</text>'
% (lx + 24, LANE_FONT, esc(label)))
lx += 30 + text_w(label, LANE_FONT) + 18
# ---- 布局参数 ----
LANE_Y, LANE_H = 140, 600
LB_X, LB_W = PAD, 440
MB_X, MB_W = LB_X + LB_W + 30, W - PAD - (LB_X + LB_W + 30)
STEP_H = 74
STEP_GAP = 30
sx, sw = LB_X + 24, LB_W - 48
# 左栏背景
add('<rect x="%d" y="%d" width="%d" height="%d" rx="14" fill="#f8fafc" '
'stroke="#64748b" stroke-opacity="0.28"/>' % (LB_X, LANE_Y, LB_W, LANE_H))
add('<text x="%d" y="%d" font-size="%.1f" font-weight="700" fill="#334155">%s</text>'
% (LB_X + 18, LANE_Y + 28, PANEL_TITLE_FONT, esc("调用方任务(消费者)")))
# 左栏四个步骤
step_y = []
y = LANE_Y + 56
for i, (title, note) in enumerate(CALLER_STEPS):
color = C_ACTOR if i == 2 else C_CALLER
bg = BG_ACTOR if i == 2 else BG_CALLER
box(sx, y, sw, STEP_H, color, bg, title, note)
step_y.append(y)
if i < len(CALLER_STEPS) - 1:
line(sx + sw / 2, y + STEP_H, sx + sw / 2, y + STEP_H + STEP_GAP,
color, head=True)
y += STEP_H + STEP_GAP
# 分支标签
add('<text x="%.1f" y="%.1f" font-size="12" font-weight="700" fill="%s">%s</text>'
% (sx + sw + 12, step_y[1] + 30, C_OK, esc("是")))
add('<text x="%.1f" y="%.1f" font-size="12" font-weight="700" fill="%s">%s</text>'
% (sx + 14, step_y[1] + STEP_H + 22, C_CANCEL, esc("否,timeout > 0")))
# ---- 快速路径(中栏右上,与"数据已在?"对齐) ----
fp_x = MB_X + 30
fp_w = MB_W - 60
fp_y = step_y[1]
box(fp_x, fp_y, fp_w, STEP_H, C_CHIP, BG_CHIP, FAST_PATH[0], FAST_PATH[1])
line(sx + sw, fp_y + STEP_H / 2, fp_x - 2, fp_y + STEP_H / 2, C_OK, head=True)
# ---- 登记 → 主循环(箭头说明) ----
reg_y = step_y[2]
lw_x, lw_w = MB_X + 30, MB_W - 60
lw_h = 100
lw_y = reg_y - 12
line(sx + sw, reg_y + STEP_H / 2, lw_x - 2, reg_y + STEP_H / 2, C_ACTOR, head=True)
add('<text x="%.1f" y="%.1f" font-size="11.5" fill="#64748b" text-anchor="middle">%s</text>'
% ((sx + sw + lw_x) / 2, reg_y + 14, esc("登记即返回")))
add('<text x="%.1f" y="%.1f" font-size="11.5" fill="#64748b" text-anchor="middle">%s</text>'
% ((sx + sw + lw_x) / 2, reg_y + 28, esc("(微秒级)")))
# ---- 主循环横带 ----
add('<rect x="%d" y="%d" width="%d" height="%d" rx="12" fill="%s" '
'stroke="%s" stroke-opacity="0.5" stroke-width="1.6"/>'
% (lw_x, lw_y, lw_w, lw_h, BG_TICK, C_TICK))
add('<text x="%d" y="%d" font-size="%.1f" font-weight="700" fill="#b45309">%s</text>'
% (lw_x + 16, lw_y + 26, STEP_TITLE_FONT,
esc("netTask 主循环(~1ms 节拍)—— 挂起期间照常服务整个队列")))
n = len(LOOP_STEPS)
gap = 12
cw = (lw_w - 32 - gap * (n - 1)) / n
for i, (name, note, color) in enumerate(LOOP_STEPS):
cx = lw_x + 16 + i * (cw + gap)
cy = lw_y + 42
add('<rect x="%.1f" y="%.1f" width="%.1f" height="46" rx="8" fill="#ffffff" '
'stroke="%s" stroke-opacity="0.6"/>' % (cx, cy, cw, color))
add('<text x="%.1f" y="%.1f" font-size="12.5" font-weight="700" fill="%s">%s</text>'
% (cx + 10, cy + 20, color, esc(name)))
add('<text x="%.1f" y="%.1f" font-size="11" fill="#64748b">%s</text>'
% (cx + 10, cy + 38, esc(note)))
# ---- 五路判定带 ----
ph_y = LANE_Y + LANE_H + 26
ph_h = 234
add('<rect x="%d" y="%d" width="%d" height="%d" rx="14" fill="#f8fafc" '
'stroke="#64748b" stroke-opacity="0.28"/>'
% (PAD, ph_y, W - 2 * PAD, ph_h))
add('<text x="%d" y="%d" font-size="%.1f" font-weight="700" fill="#334155">%s</text>'
% (PAD + 18, ph_y + 28, PANEL_TITLE_FONT,
esc("net_pending_tick():五路完成 / 取消判定(每轮对每个挂起 fd 按序)")))
# 主循环 → 五路
line(lw_x + lw_w / 2, lw_y + lw_h, lw_x + lw_w / 2, ph_y - 2, C_TICK, head=True)
rows_y = ph_y + 50
rh = 27
for i, (no, cond, act, color) in enumerate(PHASES):
ry = rows_y + i * rh
add('<text x="%d" y="%.1f" font-size="14" font-weight="700" fill="%s">%s</text>'
% (PAD + 26, ry + 16, color, esc(no)))
add('<text x="%d" y="%.1f" font-size="12.5" font-weight="700" fill="#0f172a">%s</text>'
% (PAD + 56, ry + 16, esc(cond)))
add('<text x="%d" y="%.1f" font-size="12.5" fill="#475569">%s</text>'
% (PAD + 230, ry + 16, esc("→ " + act)))
wd_y = rows_y + len(PHASES) * rh + 10
add('<text x="%d" y="%d" font-size="12" font-weight="700" fill="%s">%s</text>'
% (PAD + 56, wd_y + 8, C_CANCEL, esc(WATCHDOG)))
# xTaskNotify 回环 → 调用方睡眠盒
nb_w, nb_h = 250, 62
nb_x = W - PAD - 30 - nb_w
nb_y = ph_y + 46
add('<rect x="%d" y="%d" width="%d" height="%d" rx="10" fill="%s" '
'stroke="%s" stroke-opacity="0.55" stroke-width="1.6"/>'
% (nb_x, nb_y, nb_w, nb_h, BG_CALLER, C_CALLER))
add('<text x="%d" y="%d" font-size="13.5" font-weight="700" fill="%s">%s</text>'
% (nb_x + 14, nb_y + 26, C_CALLER, esc("xTaskNotify")))
add('<text x="%d" y="%d" font-size="11.5" fill="#64748b">%s</text>'
% (nb_x + 14, nb_y + 46, esc("唤醒调用方(结果 = 字节数 / 负错误码)")))
# 五路 → notify(左向短箭头)
line(W - PAD - 30 - nb_w - 40, nb_y + nb_h / 2, nb_x - 2, nb_y + nb_h / 2,
C_CALLER, head=True)
# notify → 调用方睡眠盒(虚线长回环:沿判定带下缘走左外侧)
slp_y = step_y[3]
back_x = PAD + 10
bus_y = ph_y + ph_h - 12
poly([(nb_x + nb_w / 2, nb_y + nb_h),
(nb_x + nb_w / 2, bus_y),
(back_x, bus_y),
(back_x, slp_y + STEP_H / 2),
(sx - 8, slp_y + STEP_H / 2)],
C_CALLER, dash=True, head=True)
# ---- 页脚 ----
add('<text x="%d" y="%d" font-size="%d" fill="#64748b">%s</text>'
% (PAD, H - 24, 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">' % (W, H, W, H, FONT)]
svg.extend(out)
svg.append('</svg>')
return "\n".join(svg) + "\n"
def main():
svg = build()
os.makedirs(os.path.dirname(OUT), exist_ok=True)
with open(OUT, "w", encoding="utf-8", newline="\n") as f:
f.write(svg)
print("已生成: %s (%d bytes)" % (OUT, len(svg.encode("utf-8"))))
if __name__ == "__main__":
main()