Files
STM32F4-Base/tools/arch_diagram/gen_adc_health_flow_svg.py
T
wandering1 4c74cfa825 refactor(编号·批次二): 文档侧 194 处保护功能号改成功能名,定义表 7 张首格改语义标识
批次二文档半(道 gate-docs 执行)。先纠三个数的口径账,别再拿任何旧数当准:**234 处是准数** —— 卡片写
52(Lead 用了门禁 Checker 的引用集合,它**刻意排除属主文档内部的定义位**)、道里先报 265(裸正则把
`SDL2-2.32.10` 里的 `L2-2` 当成功能号,误报 10 处)。最终边界口径 = 带左边界 + 跳过反引号代码段。

· 19 只文件 HEAD 共 234 处 → 现 40 处,**40 处 100% 落在变更记录行**(冻结历史,按 Q3 一条没动),
  非变更记录残留 **0**;改写 163 行 / 16 只文档。站点最多:规划文档 110→37、相量层单测说明 25→1、
  采集与数据处理短板评测 21→0、App/algo/README 26→0。
  (`App/algo/README.md` 由上一笔代码提交带上车,路径归属已在该笔正文注明,内容是本批改写)
· **定义表首列由编号改成语义标识**(编号规范 §3.3「表首格就是短语」的完全形态):首列原为编号的表
  共 **7 张**(不是 5 张),删掉与之重复的「语义名」列;表头标签按域词区分——三张 L3 保护**元件**表
  保留 `| 元件 |`,四张 L1/L2 表用 `| 功能名 |`(把「元件」统一洗成功能名会丢掉领域信息)。
  七张表逐张核:表头竖线数 == 分隔行 == 首条数据行 == 列数+1;与 HEAD 逐块对拍**净新增不一致块 0**,
  并且正是这个对拍抓到第一版「三张表数据行已删列、表头没删」的破表。
· 7 处删号留下的残词/重复解释括号人工收口(如「L1-5(通道标定)」→「通道标定(一次系数 + 零漂)」),
  复扫待收口站点 0;措辞逐字取自映射表,无第二种说法。
· tools/arch_diagram 三只生成器 4 处(3 处 docstring + 1 处图形文案)与产物同批:重跑后**只有位图那张
  变 2 个 text 节点的换行位置**,画布 1580x740 与总行数不变,另两张逐字节未变;改前先干跑对照证明三张
  svg 原本与生成器一致(不在 stale 基线里)——`check_generated_fresh --only docs-svg` PASS。

本笔另含 **F-* 半批的文档面**(同一轮 `git add docs tools/arch_diagram AGENTS.md` 一并收进
,非误伤):BSD
函数调用关系里 9 处反引号紧邻指针、属主文档 §7 的停发句与导读行(**12 条定义行原样保留、不删登记族**)、
以及 `gen_net_instability_svg.py` 与它的产物 `网络不稳根因总览.svg`(「唯一读者红线」那条的框宽自适应
526 → 551,右缘 1411 未越界,画布 1560x1468 不变)——该半批的**代码注释面**在下一笔。

⚠ 因 `docs/任务规划/登记簿.md` 与 `docs/任务规划/UI补齐.md` 正被另一张卡(UI 叶数复核)改动,本笔**不含
那两只文件**,批次二的 §9 存量收尾与它们同批提交。
2026-10-27 09:05:24 +08:00

257 lines
11 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:本图要表达**三条量车道(TPAFE / STM32 / 时基)平行流过五个阶段**,
Mermaid 自动布局会把三条车道挤成交叉线、也保不住"同一拍取数"的横向对齐。
这里按车道手工排布:每车道一行,箭头全部水平,行内可直接横向读一遍判据。
用法(仓库根目录,任意 python3):
python tools/arch_diagram/gen_adc_health_flow_svg.py
输出:
docs/images/采样有效性校验数据流.svg
改内容时:只编辑本文件顶部的 STAGES / LANES 数据后重跑。
"""
import html
import os
import xml.etree.ElementTree as ET
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
OUT = os.path.join(ROOT, "docs", "images", "采样有效性校验数据流.svg")
# ----------------------------- 画布与尺寸 -----------------------------
W = 1580
PAD = 40
TITLE_FONT = 21
SUB_FONT = 13
HEAD_FONT = 13
CELL_TITLE_FONT = 12.5
CELL_NOTE_FONT = 11
LANE_FONT = 12
FOOT_FONT = 12
CELL_H = 148
LANE_GAP = 34
MAIN_Y = 214
LANE_LABEL_W = 96
COLS = [
(PAD + LANE_LABEL_W, 268), # 阶段 1:观测计数(ISR / 消费侧)
(PAD + LANE_LABEL_W + 288, 236), # 阶段 2:本拍快照
(PAD + LANE_LABEL_W + 544, 276), # 阶段 3:判定(纯逻辑)
(PAD + LANE_LABEL_W + 840, 264), # 阶段 4:出口位图
(PAD + LANE_LABEL_W + 1124, 250), # 阶段 5:消费者
]
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
C_LANE = ["#0d9488", "#2563eb", "#7c3aed"]
BG_LANE = ["#f0fdfa", "#eff6ff", "#f5f3ff"]
C_HEAD, BG_HEAD = "#475569", "#f1f5f9"
INK, MUTED = "#0f172a", "#64748b"
# ----------------------------- 数据(改这里) -----------------------------
STAGES = [
"1. 观测计数(ISR / 驱动侧)",
"2. 本拍计数快照",
"3. 判定(adc_health.c 纯逻辑)",
"4. 出口位图(32 位)",
"5. 消费者与处置",
]
# 每条车道 = 一行;cells 与 STAGES 一一对应
LANES = [
dict(
name="TPAFE",
color=0,
cells=[
dict(title="tpafe5160.c 的 BUSY EXTI",
lines=["s_frame_seq++:每读完一帧 +1",
"s_frstdata_err++:无跳变帧累计计数",
"ISR 内只加计数,不打日志"] ),
dict(title="adcTask 每拍 poll 之后取值",
lines=["tpafe_seq",
"tpafe_frst_err(uint16 放宽)",
"⚠ 必须取本拍 poll 之后的值"] ),
dict(title="序号差值 d = seq - 上次",
lines=["d < 0 → 置倒退位并重新对齐",
"d > 1 → tgap += d-1 + 当拍事件",
"d >= 1 → 刷新「最后前进时刻」"] ),
dict(title="本车道出口",
lines=["bit0 ADC_HF_TPAFE_STALL(自愈)",
"bit2 ADC_HF_SEQ_BACKWARD(锁存)",
"bit6/bit3 ADC_HE_TPAFE_GAP / FRSTDATA"] ),
dict(title="闭锁 + 留痕 + 计数",
lines=["protectTask 每 20 ms 读 bit0/bit2",
"gap / FRSTDATA 事件 → 编排层日志",
"tgap → 10 s 心跳字段"] ),
],
),
dict(
name="STM32",
color=1,
cells=[
dict(title="adc_input.c(DMA2 Stream4)",
lines=["s_dma_seq++:半区/整区完成各一次",
"s_stm32_gaps += d-1:丢帧点数",
"s_stm32_errs++:连续 >=2 拍无新帧",
"s_stm32_hw_errs++:HAL 错误回调"] ),
dict(title="同一拍的四个 STM32 计数",
lines=["stm32_seq / stm32_errs",
"stm32_gaps / stm32_hw_errs",
"gap 细账不重复取(L1 已算)"] ),
dict(title="序号差值 d + 三处计数增量",
lines=["d < 0 → 同一个 SEQ_BACKWARD 位",
"d >= 1 → 刷新最后前进时刻",
"增量 != 0 → 事件 + 锁存"] ),
dict(title="本车道出口",
lines=["bit1 ADC_HF_STM32_STALL(自愈)",
"bit2 共用(两车道同一位)",
"bit7/bit4/bit5 GAP / ERR / HW_ERR"] ),
dict(title="闭锁 + 严重事件",
lines=["bit1/bit2 → 同左闭锁路径",
"STM32_ERR / HW_ERR:恒 0 增量",
"出现即 DBG_ERROR + 自检留痕"] ),
],
),
dict(
name="时基",
color=2,
cells=[
dict(title="osKernelGetTickCount()",
lines=["HAL 时基 TIM7,tick = 1 ms",
"2^32 ms 后自然回绕"] ),
dict(title="now_ms:本拍唯一一份",
lines=["健康判定与心跳上报共用",
"不在判定层内再取一次"] ),
dict(title="差值法 (int32_t)(now - adv)",
lines=["回绕安全(取有符号差)",
">= ADC_HEALTH_STALL_MS(100)",
"可判定窗 ±2^31 ≈ ±24.85 天"] ),
dict(title="tpafe_stall_ms",
lines=["静默期内给出停滞时长",
"正常态恒为 0",
"STM32 侧未上报(观测盲区)"] ),
dict(title="上升沿日志可读性",
lines=["health: flags=… stall_ms=…",
"回答到底停了多少毫秒"] ),
],
),
]
FOOT = ("判定层零平台依赖(只 include <stdint.h>、不碰寄存器 / 不取数 / 不打日志)→ 主机侧以 #include 原样纳入、免桩全覆盖单测。"
"详见 docs/算法/采样有效性校验设计说明.md")
# ----------------------------- 工具函数 -----------------------------
def esc(s):
return html.escape(s, quote=True)
def text_w(s, fs):
"""粗略估算文本像素宽:CJK/全角 ≈ 1.0em,其余 ≈ 0.58em(宁可偏大)。"""
return sum(fs * (1.0 if ord(ch) >= 0x2E80 else 0.58) for ch in s)
OVERFLOW = []
def check_fit(s, fs, avail, where):
if text_w(s, fs) > avail:
OVERFLOW.append("%s: %.0f > %.0f [%s]" % (where, text_w(s, fs), avail, s))
# ----------------------------- 生成 -----------------------------
def build():
out = []
add = out.append
n_cells = max(len(l["cells"]) for l in LANES)
for li, lane in enumerate(LANES):
for ci, cell in enumerate(lane["cells"]):
_, cw = COLS[ci]
avail = cw - 26
check_fit(cell["title"], CELL_TITLE_FONT, avail,
"lane %s col %d title" % (lane["name"], ci + 1))
for ln in cell["lines"]:
check_fit(ln, CELL_NOTE_FONT, avail,
"lane %s col %d line" % (lane["name"], ci + 1))
for ci, st in enumerate(STAGES):
check_fit(st, HEAD_FONT, COLS[ci][1] - 20, "stage head %d" % (ci + 1))
if len(OVERFLOW):
for o in OVERFLOW:
print("OVERFLOW WARN " + o)
lane_h = CELL_H + LANE_GAP
bottom = MAIN_Y + len(LANES) * lane_h - LANE_GAP
total_h = bottom + 92
add('<rect x="0" y="0" width="%d" height="%d" fill="#ffffff"/>' % (W, total_h))
add('<text x="%d" y="44" font-size="%d" font-weight="700" fill="%s">%s</text>'
% (PAD, TITLE_FONT, INK, esc("采样有效性校验(adc_health)· 数据流与出口")))
add('<text x="%d" y="70" font-size="%d" fill="%s">%s</text>'
% (PAD, SUB_FONT, MUTED,
esc("三条量车道平行流过五个阶段;判定层每拍一次、常数次整数运算,输入只是计数快照")))
# ---- 阶段标题 ----
for ci, st in enumerate(STAGES):
x, cw = COLS[ci]
add('<rect x="%d" y="150" width="%d" height="34" rx="8" fill="%s" stroke="%s" '
'stroke-opacity="0.35"/>' % (x, cw, BG_HEAD, C_HEAD))
add('<text x="%d" y="172" font-size="%d" font-weight="700" fill="#334155">%s</text>'
% (x + 12, HEAD_FONT, esc(st)))
# ---- 车道 ----
for li, lane in enumerate(LANES):
color, bg = C_LANE[lane["color"]], BG_LANE[lane["color"]]
y0 = MAIN_Y + li * lane_h
# 车道标签
add('<rect x="%d" y="%d" width="%d" height="%d" rx="10" fill="%s" stroke="%s" '
'stroke-opacity="0.5"/>' % (PAD, y0, LANE_LABEL_W - 14, CELL_H, bg, color))
add('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="%s" '
'text-anchor="middle">%s</text>'
% (PAD + (LANE_LABEL_W - 14) / 2.0, y0 + CELL_H / 2.0 + 5, LANE_FONT,
color, esc(lane["name"])))
for ci, cell in enumerate(lane["cells"]):
x, cw = COLS[ci]
add('<rect x="%d" y="%d" width="%d" height="%d" rx="10" fill="%s" stroke="%s" '
'stroke-opacity="0.45"/>' % (x, y0, cw, CELL_H, bg, color))
add('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="%s">%s</text>'
% (x + 13, y0 + 30, CELL_TITLE_FONT, color, esc(cell["title"])))
yy = y0 + 56
for ln in cell["lines"]:
add('<text x="%d" y="%d" font-size="%d" fill="#334155">%s</text>'
% (x + 13, yy, CELL_NOTE_FONT, esc(ln)))
yy += 22
# 水平箭头到下一列
if ci + 1 < len(lane["cells"]):
nx = COLS[ci + 1][0]
cy = y0 + CELL_H / 2.0
add('<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="%s" '
'stroke-width="2.2"/>' % (x + cw, cy, nx - 9, cy, color))
add('<path d="M %.1f %.1f L %.1f %.1f L %.1f %.1f z" fill="%s"/>'
% (nx, cy, nx - 11, cy - 5.5, nx - 11, cy + 5.5, color))
# ---- 底部说明 ----
add('<rect x="%d" y="%d" width="%d" height="46" rx="10" fill="#fffbeb" '
'stroke="#d97706" stroke-opacity="0.45"/>' % (PAD, bottom + 30, W - 2 * PAD))
add('<text x="%d" y="%d" font-size="%d" fill="#7c2d12">%s</text>'
% (PAD + 16, bottom + 58, 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)))
d = os.path.dirname(OUT)
if not os.path.isdir(d):
os.makedirs(d)
with open(OUT, "w", encoding="utf-8", newline="\n") as fp:
fp.write(svg)
ET.fromstring(svg)
print("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, total_h, OUT))
if len(OVERFLOW):
print("!! %d 处文字可能溢出,见上方 WARN" % len(OVERFLOW))
if __name__ == "__main__":
build()