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

355 lines
16 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 —— IWDG + 任务心跳位图流程图(纯标准库)。
对应 docs/子系统设计/看门狗设计说明.md §3.2「采用方案:
任务置位 → 校验者判定 → 校验者喂狗」。
结构:左右两栏 + 底部时间轴
左 = 各任务循环(生产者):s_hb_bits |= (1u << MY_ID),仅置位不清零
右 = defaultTask 唯一喂狗者(消费者):100ms 抽样 → 取走清零 → 掩码判定
全中 → 喂狗;有缺 → 累计 miss_streak(容忍 8 次)
下 = 停喂 → IWDG 自然溢出 → 复位(时间轴:0 / 0.8s / 1.7s / 2.5s)
为什么不用 Mermaid:原 ASCII 图是"生产者-消费者 + 判定分支",需要精确对齐
位图行与判定框,且时间轴的"包含关系"必须按比例表达,Mermaid 做不到。
用法(仓库根目录,任意 python3):
python tools/arch_diagram/gen_watchdog_svg.py
输出:
docs/images/看门狗心跳模型.svg
改流程时:编辑本文件顶部的 TASKS / BITS / POLL_STEPS / TIMELINE 数据后重跑。
"""
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", "看门狗心跳模型.svg")
# ----------------------------- 画布与样式 -----------------------------
W = 1440
PAD = 40
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
TITLE_FONT = 21
SUB_FONT = 13
PANEL_TITLE_FONT = 15
CARD_TITLE_FONT = 14
CARD_NOTE_FONT = 11.5
LABEL_FONT = 11.5
FOOT_FONT = 12
# 调色板(docs/架构设计/图形绘制流程.md §3.1)
C_PROD, BG_PROD = "#2563eb", "#eff6ff" # 蓝:生产者(任务置位)
C_BIT, BG_BIT = "#0d9488", "#f0fdfa" # 青:心跳位图
C_CONS, BG_CONS = "#7c3aed", "#f5f3ff" # 紫:校验者(喂狗者)
C_OK = "#16a34a" # 绿:喂狗
C_WARN, BG_WARN = "#d97706", "#fffbeb" # 琥珀:累计缺失
C_FAIL, BG_FAIL = "#e11d48", "#fff1f2" # 玫红:停喂 / 复位
ARROW_W = 2.2
# ----------------------------- 数据(改这里) -----------------------------
# 任务(生产者):bit = 心跳位序号(task_watchdog.h 的枚举值)
TASKS = [
dict(name="defaultTask", bit=0, note="配置属主 / syslog 唯一写者 · 50ms 基准"),
dict(name="netTask", bit=1, note="net_poll 循环 · 1ms 节拍"),
dict(name="ftpTask", bit=2, note="lftpd 事件循环(需在 lftpd 侧埋点)"),
dict(name="uiTask", bit=4, note="LVGL 主循环 · 10ms 节拍"),
dict(name="timeSyncTask", bit=5, note="NTP 轮询 · ≤50ms"),
]
# 必需掩码(wd_required_mask):5 位全要;bit3 = testTask 不在默认掩码
REQ_BITS = [0, 1, 2, 4, 5]
# 校验者判定步骤(右栏自上而下)
POLL_STEPS = [
dict(title="① 节流:每 100ms 抽样一次",
note="now - last_tick < 100ms 则直接返回(defaultTask 50ms 循环的整数倍)",
color=C_CONS, bg=BG_CONS),
dict(title="② 取走并清零(单字原子)",
note="snapshot = s_hb_bits; s_hb_bits = 0; ← 读-改-写天然原子,免临界区",
color=C_BIT, bg=BG_BIT),
dict(title="③ 掩码判定",
note="snapshot == 0x37(bit0|1|2|4|5 全中)?",
color=C_CONS, bg=BG_CONS),
]
# 判定分支结果
BRANCH_OK = dict(title="全员到位 → 喂狗",
note="s_miss_streak = 0",
note2="IWDG->KR = 0xAAAA;",
color=C_OK, bg="#f0fdf4")
BRANCH_NG = dict(title="有缺席 → 累计,不喂狗",
note="s_miss_total++ / streak++",
note2="连续 8 次 → 打 [WD] task stall 后停喂",
color=C_WARN, bg=BG_WARN)
# 时间轴刻度(秒)与事件
T_AXIS = [0.0, 0.8, 1.7, 2.5]
TIMELINE = [
dict(seg=(0.0, 0.8), label="仍照常喂狗", sub="miss_streak 1 → 8", color=C_OK),
dict(seg=(0.8, 1.7), label="已停喂 → 等 IWDG 自然溢出", sub="worst case(LSI 47kHz)", color=C_FAIL),
dict(seg=(1.7, 2.5), label="标称溢出窗口", sub="LSI 32kHz", color=C_WARN),
]
FOOT = ("喂狗 = \"所有被监视任务都在 100ms 窗口内动过\";任一任务卡死 → 位图长期残缺 → "
"停喂 → IWDG 复位(保留 IWDGRSTF 权威归因)。对应 task_watchdog.c / 设计说明 §3.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
# ----------------------------- 生成 -----------------------------
def build():
out = []
add = out.append
def line(x1, y1, x2, y2, color, head=True, width=ARROW_W, dash=False):
d = ' stroke-dasharray="7 5"' if dash else ''
add('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" '
'stroke-width="%.1f"%s/>' % (x1, y1, x2, y2, color, width, d))
if head:
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 box(x, y, w, h, color, bg, title, note, title_fs=CARD_TITLE_FONT,
note_fs=CARD_NOTE_FONT, stroke_op=0.55, note2=None):
add('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="10" '
'fill="%s" stroke="%s" stroke-opacity="%.2f"/>'
% (x, y, w, h, bg, color, stroke_op))
add('<text x="%.1f" y="%.1f" font-size="%d" font-weight="700" fill="#0f172a">%s</text>'
% (x + 14, y + 24, title_fs, esc(title)))
add('<text x="%.1f" y="%.1f" font-size="%.1f" fill="#64748b">%s</text>'
% (x + 14, y + 24 + note_fs + 7, note_fs, esc(note)))
if note2:
add('<text x="%.1f" y="%.1f" font-size="%.1f" fill="#64748b">%s</text>'
% (x + 14, y + 24 + (note_fs + 7) * 2, note_fs, esc(note2)))
# ---- 布局参数 ----
PANEL_Y = 148
PANEL_H = 584 # 由左栏内容(5 任务 + 心跳位图带 + 位格)反推
LB_X, LB_W = PAD, 470 # 左栏:任务(生产者)
RB_X = LB_X + LB_W + 34 # 右栏:校验者(消费者)
RB_W = W - PAD - RB_X
TASK_H, TASK_GAP = 62, 18
sx = LB_X + 22
sw = LB_W - 44
# ---- 标题 ----
add('<text x="%d" y="44" font-size="%d" font-weight="700" fill="#0f172a">%s</text>'
% (PAD, TITLE_FONT, esc("看门狗心跳模型 · 任务置位 → 校验者判定 → 校验者喂狗")))
add('<text x="%d" y="70" font-size="%d" fill="#64748b">%s</text>'
% (PAD, SUB_FONT,
esc("IWDG + 单字心跳位图 · 由 defaultTask 独占喂狗权(唯一喂狗者)")))
# ---- 图例 ----
lx, ly = PAD, 110
for color, label in [(C_PROD, "生产者(任务置位)"), (C_BIT, "心跳位图"),
(C_CONS, "校验者(喂狗者)"), (C_OK, "喂狗"),
(C_FAIL, "停喂 / 复位")]:
add('<rect x="%d" y="%d" width="16" height="16" rx="4" fill="#ffffff" '
'stroke="%s" stroke-width="2"/>' % (lx, ly - 12, color))
lx += 22
add('<text x="%d" y="%d" font-size="12" fill="#475569">%s</text>' % (lx, ly, esc(label)))
lx += text_w(label, 12) + 30
# ---- 左栏:任务(生产者) ----
add('<rect x="%d" y="%d" width="%d" height="%d" rx="14" fill="#f8fafc" '
'stroke="#64748b" stroke-opacity="0.28"/>' % (LB_X, PANEL_Y, LB_W, PANEL_H))
add('<text x="%d" y="%d" font-size="%.1f" font-weight="700" fill="#334155">%s</text>'
% (LB_X + 20, PANEL_Y + 30, PANEL_TITLE_FONT,
esc("各任务主循环(生产者)—— 每轮只置位,不清零")))
add('<text x="%d" y="%d" font-size="13" font-weight="700" fill="%s">%s</text>'
% (LB_X + 20, PANEL_Y + 54, C_PROD, esc("s_hb_bits |= (1u << MY_ID)")))
task_y = []
y = PANEL_Y + 72
for t in TASKS:
add('<rect x="%.1f" y="%.1f" width="%.1f" height="%d" rx="9" fill="%s" '
'stroke="%s" stroke-opacity="0.5"/>' % (sx, y, sw, TASK_H, BG_PROD, C_PROD))
# 位号徽标
add('<rect x="%.1f" y="%.1f" width="34" height="24" rx="6" fill="%s"/>'
% (sx + 12, y + 10, C_PROD))
add('<text x="%.1f" y="%.1f" font-size="12" font-weight="700" fill="#ffffff" '
'text-anchor="middle">%s</text>'
% (sx + 29, y + 26.5, esc("b" + str(t["bit"]))))
add('<text x="%.1f" y="%.1f" font-size="%d" font-weight="700" fill="#0f172a">%s</text>'
% (sx + 56, y + 26, CARD_TITLE_FONT, esc(t["name"])))
add('<text x="%.1f" y="%.1f" font-size="%.1f" fill="#64748b">%s</text>'
% (sx + 56, y + 46, CARD_NOTE_FONT, esc(t["note"])))
task_y.append(y + TASK_H / 2.0)
y += TASK_H + TASK_GAP
# ---- 心跳位图(左栏底部横带) ----
bm_y = y + 6
bm_h = 48
add('<rect x="%.1f" y="%.1f" width="%.1f" height="%d" rx="9" fill="%s" '
'stroke="%s" stroke-opacity="0.6"/>' % (sx, bm_y, sw, bm_h, BG_BIT, C_BIT))
add('<text x="%.1f" y="%.1f" font-size="12" font-weight="700" fill="%s">%s</text>'
% (sx + 14, bm_y + 20, C_BIT, esc("心跳位图 s_hb_bits(32 位单字)")))
add('<text x="%.1f" y="%.1f" font-size="11" fill="#64748b">%s</text>'
% (sx + 14, bm_y + 38, esc("就绪掩码 0x37 = bit0|1|2|4|5(bit3=testTask 默认不在内)")))
# 各位方格
bx0 = sx + 14
bw, bgap = 34, 6
for i in range(6):
cx = bx0 + i * (bw + bgap)
cy = bm_y + bm_h + 8
on = i in REQ_BITS
add('<rect x="%.1f" y="%.1f" width="%d" height="22" rx="4" fill="%s" '
'stroke="%s" stroke-opacity="%.2f"/>'
% (cx, cy, bw, BG_BIT if on else "#f1f5f9", C_BIT if on else "#94a3b8",
0.7 if on else 0.4))
add('<text x="%.1f" y="%.1f" font-size="11" font-weight="700" fill="%s" '
'text-anchor="middle">%s</text>'
% (cx + bw / 2.0, cy + 15, C_BIT if on else "#94a3b8", esc(str(i))))
# 各任务 → 位图:右侧竖直总线汇入位图带右缘
bus_x = sx + sw - 4
for t in TASKS:
ty = task_y[TASKS.index(t)]
line(sx + sw, ty, bus_x, ty, C_PROD, head=False)
line(bus_x, task_y[0], bus_x, bm_y + bm_h / 2.0, C_PROD, head=False)
line(bus_x, bm_y + bm_h / 2.0, sx + sw, bm_y + bm_h / 2.0, C_PROD)
# ---- 右栏:校验者(defaultTask) ----
add('<rect x="%d" y="%d" width="%d" height="%d" rx="14" fill="#f8fafc" '
'stroke="#64748b" stroke-opacity="0.28"/>' % (RB_X, PANEL_Y, RB_W, PANEL_H))
add('<text x="%d" y="%d" font-size="%.1f" font-weight="700" fill="#334155">%s</text>'
% (RB_X + 20, PANEL_Y + 30, PANEL_TITLE_FONT,
esc("defaultTask 主循环(唯一喂狗者 · 消费者)")))
st_x = RB_X + 22
st_w = RB_W - 44
STEP_H, STEP_GAP2 = 62, 40
sy = PANEL_Y + 56
step_cy = []
for i, s in enumerate(POLL_STEPS):
box(st_x, sy, st_w, STEP_H, s["color"], s["bg"], s["title"], s["note"])
step_cy.append(sy + STEP_H / 2.0)
if i < len(POLL_STEPS) - 1:
line(st_x + st_w / 2, sy + STEP_H, st_x + st_w / 2, sy + STEP_H + STEP_GAP2,
s["color"])
sy += STEP_H + STEP_GAP2
# 位图 → 校验者 ②(取走清零):正交走线(右出 → 上折 → 入盒)
tap_y = bm_y + bm_h / 2.0
line(sx + sw, tap_y, st_x - 22, tap_y, C_BIT, head=False)
line(st_x - 22, tap_y, st_x - 22, step_cy[1], C_BIT, head=False)
line(st_x - 22, step_cy[1], st_x, step_cy[1], C_BIT)
add('<text x="%.1f" y="%.1f" font-size="%.1f" fill="%s" text-anchor="middle">%s</text>'
% (sx + sw + 6, tap_y - 8, LABEL_FONT, C_BIT, esc("快照")))
# ---- 判定分支:喂狗 / 累计停喂 ----
br_y = sy - STEP_GAP2 + 16
br_w = (st_w - 24) / 2.0
ok_x, ng_x = st_x, st_x + br_w + 24
# 分支引线:从 ③ 下沿分叉(fork 线画在 ③ 之下 8px 处,避免压住盒底)
fork_y = step_cy[2] + STEP_H / 2.0
line(st_x + st_w / 2, fork_y, st_x + st_w / 2, fork_y + 14, C_CONS, head=False)
line(st_x + br_w / 2.0, fork_y + 14, ok_x + br_w / 2.0, fork_y + 14, C_OK, head=False)
line(st_x + st_w / 2, fork_y + 14, ng_x + br_w / 2.0, fork_y + 14, C_WARN, head=False)
line(ok_x + br_w / 2.0, fork_y + 14, ok_x + br_w / 2.0, br_y, C_OK)
line(ng_x + br_w / 2.0, fork_y + 14, ng_x + br_w / 2.0, br_y, C_WARN)
add('<text x="%.1f" y="%.1f" font-size="11.5" font-weight="700" fill="%s" '
'text-anchor="middle">%s</text>'
% (ok_x + br_w / 2.0, br_y - 6, C_OK, esc("是")))
add('<text x="%.1f" y="%.1f" font-size="11.5" font-weight="700" fill="%s" '
'text-anchor="middle">%s</text>'
% (ng_x + br_w / 2.0, br_y - 6, C_WARN, esc("否")))
br_h = 84
box(ok_x, br_y, br_w, br_h, BRANCH_OK["color"], BRANCH_OK["bg"],
BRANCH_OK["title"], BRANCH_OK["note"], title_fs=13, note_fs=10.5,
note2=BRANCH_OK["note2"])
box(ng_x, br_y, br_w, br_h, BRANCH_NG["color"], BRANCH_NG["bg"],
BRANCH_NG["title"], BRANCH_NG["note"], title_fs=13, note_fs=10.5,
note2=BRANCH_NG["note2"])
# ---- 时间轴 ----
tl_y = PANEL_Y + PANEL_H + 34
tl_h = 150
add('<rect x="%d" y="%d" width="%d" height="%d" rx="14" fill="#f8fafc" '
'stroke="#64748b" stroke-opacity="0.28"/>' % (PAD, tl_y, W - 2 * PAD, tl_h))
add('<text x="%d" y="%d" font-size="%.1f" font-weight="700" fill="#334155">%s</text>'
% (PAD + 20, tl_y + 28, PANEL_TITLE_FONT,
esc("停喂之后:时间轴上的三个时刻(0.8s 停喂 → 1.7s 最坏溢出 → 2.5s 标称)")))
ax_x0, ax_x1 = PAD + 60, W - PAD - 60
ax_y = tl_y + 62
span = T_AXIS[-1]
def ax_pos(t):
return ax_x0 + (ax_x1 - ax_x0) * (t / span)
# 分段色带
for it in TIMELINE:
s0, s1 = it["seg"]
x0, x1 = ax_pos(s0), ax_pos(s1)
add('<rect x="%.1f" y="%.1f" width="%.1f" height="14" rx="3" fill="%s" '
'opacity="0.75"/>' % (x0, ax_y, x1 - x0, it["color"], ))
add('<text x="%.1f" y="%.1f" font-size="11.5" fill="%s" text-anchor="middle">%s</text>'
% ((x0 + x1) / 2.0, ax_y + 34, it["color"], esc(it["label"])))
add('<text x="%.1f" y="%.1f" font-size="10.5" fill="#94a3b8" text-anchor="middle">%s</text>'
% ((x0 + x1) / 2.0, ax_y + 50, esc(it["sub"])))
# 刻度
for t in T_AXIS:
x = ax_pos(t)
line(x, ax_y - 6, x, ax_y + 14, "#475569", head=False, width=1.6)
add('<text x="%.1f" y="%.1f" font-size="11" font-weight="700" fill="#475569" '
'text-anchor="middle">%ss</text>' % (x, ax_y - 12, esc(("%g" % t))))
# 停喂点竖标 + 裕度
x_stop, x_worst = ax_pos(0.8), ax_pos(1.7)
add('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="1.6" '
'stroke-dasharray="5 4"/>' % (x_stop, ax_y - 4, x_stop, ax_y + 20, C_FAIL))
add('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="1.6" '
'stroke-dasharray="5 4"/>' % (x_worst, ax_y - 4, x_worst, ax_y + 20, C_WARN))
add('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="1.6" '
'stroke-dasharray="3 3"/>' % (x_stop, ax_y + 68, x_worst, ax_y + 68, C_OK))
add('<text x="%.1f" y="%.1f" font-size="11" fill="%s" text-anchor="middle">%s</text>'
% ((x_stop + x_worst) / 2.0, ax_y + 84, C_OK, esc("裕度 ≈ 900ms")))
# ---- 页脚 ----
total_h = tl_y + tl_h + 34
add('<text x="%d" y="%d" font-size="%d" fill="#64748b">%s</text>'
% (PAD, total_h - 14, 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, total_h
def main():
svg, h = build()
os.makedirs(os.path.dirname(OUT), exist_ok=True)
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()