Files
STM32F4-Base/tools/arch_diagram/gen_adc_health_flow_svg.py
T
wandering1 790e153cd6 fix(adc_input): 丢帧计数改按帧点数累计,与 TPAFE 侧 tgap 同单位
读码确认的口径缺陷:两片转换器的丢帧计数单位不一致——TPAFE 侧
adc_health_tpafe_gaps() 按 (d-1) 加权记「帧点数」,STM32 侧
adc_input_stm32_gaps() 每次事件只 +1 记「次数」。两者都被当作心跳字段
并排打印(adc1_gap / tgap),一次跨多帧时数值就分叉:同样丢 3 帧,
一边记 2、一边记 1。跨字段对比或做除法会得出错误结论。

选 A(改代码,真对称)而不是只改字段名:
- 单拍超一帧(d=2)时两者数值相同,历史上绝大多数样本本来就等价,
  换单位对既有判读习惯冲击很小;
- 只在一次跨 ≥2 帧时分叉,而那正是心跳日志突发期最常见的形态,
  留在文档里当"注意点"迟早还会读错一次。

改动:
- adc_input.c:s_stm32_gaps += (d - 1u)(d = seq - s_dma_seq_seen,
  顺带把已算过的差值提成局部变量 d,不再重复算一遍)
- adc_input.h / adc_input.c 函数头:单位写明「Σ 每次前进的超出部分」,
  并点名 2026-10-21 之前固件该字段是次数口径,跨版本日志不可直接对比
- 用例:原两档只跨 1 帧(前进 2 记 1),钉不出单位;补第二档——连投
  3 帧不消费(一次跨 2 帧)必须记 2 点、累计 1+2=3。补充断言沿用所属
  用例覆盖点,不另立新编号,也不在文档外复制用例号(编号门禁按文件内
  实例数比对台账豁免,多写一处即被判新债)
- 文档同步:ADC采样节拍设计说明 §8/§9 字段口径与稳态基线、采集与算法
  层设计说明 §3/§4、采样有效性校验设计说明 §6.2/§10/§11(并把"两车道
  单位不同"从观测盲区降级为"历史日志口径差异")、ADC输入层单元测试
  说明用例表该行;两张配图生成器文案同步

实测:
- @build 0 错 0 警;Program Size 与上一提交**逐字节相同**(Code=213520
  RW=1184 ZI=131504)——编译器早已为半区推导算过同一差值并复用,
  故本次净增 0 B,不产生新的台账漂移
- 主机单测:ADC 输入层 103/103、ADC 有效性 45/45
- stack_report:adcTask 1320 B / protectTask 872 B 均不变
- 字节卫生:BOM 无 / CR 0 / U+FFFD 0 / 文件尾换行齐
- id-check:Passed(曾因新增用例号实例触发"台账之外 +2",已按不复制
  用例号的方式消除,未动 id_known_backlog.json 台账)

gate: only mem-budget/mem-numbers pending lead ledger backfill
pre-commit run --all-files 失败仅这两条,漂移量来自上一条提交 6f90600
(+96 B Code / +24 B ZI);本条自身 0 B。按并行分支约定本分支不跑
--write,台账由 Lead 合并后统一回填。
2026-10-22 00:43:32 +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 —— L1-6「观测计数 → 快照 → 判定 → 位图 → 消费者」(纯标准库)。
为什么不用 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++:无跳变帧累计(E-44)",
"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()