Files
STM32F4-Base/tools/arch_diagram/gen_adc_health_bitmap_svg.py
T
wandering1 9f826dbb4d fix(protect): 采样陈旧时独立闭锁保护(判定层失明的兜底门)
问题:L5-4 闭锁读的 adc_health_flags() 只有 adcTask 每拍跑
adc_health_check() 才可能置位。TIM3 一停,adcTask 就永久阻塞在
osThreadFlagsWait(刻意的 fail-safe),check() 不再执行 → stall 位
永远是 0,而 protectTask 仍每 20 ms 拿冻在最后一帧的相量继续判据。
窗口最坏约 800 ms(停喂阈值)+ 2498 ms(IWDG 标称)≈ 3.3 s。

方案:保护侧建一条与消费者存活完全解耦的兜底新鲜度门。
- adc_health.c/h 新增 adc_health_age_stale():无模块状态的纯判据,
  参照系 adc_health_age_t 由调用方自存。故意不让 protectTask 去调
  adc_health_check()——那是「每拍一次、单写者」的契约,两个上下文
  同写会互相刷新差值基线,把现有判定层一起弄坏。
- 新增 ADC_HF_STALE = bit8(与判定层 bit0..bit7 位段错开)。本位
  不由判定层写、不进 sticky、不进编排层 logged 位图。
- protect_task.c 用自己的时基直接读两个**中断侧递增**的计数器
  (tpafe5160_frame_seq / adc_input_stm32_seq),并入掩码。两条车道
  各自一份 primed:共用会让先观测的那条把标志置 1,另一条首拍拿
  adv_ms = 0 算年龄 → 开机即误锁保护。
- 门限 PROT_STALE_AGE_MS = 100 + 20 = 120 ms,买的是时序阶梯:
  判定层活着时它先报 0x1/0x2(带日志与自检留痕),死了只出现
  0x100。闭锁最迟 140 ms,仍远小于最小动作延时(默认 1000 ms)。

实测:
- @build 0 错 0 警:Code 213424 → 213520(+96 B);ZI 131480 →
  131504(+24 B = 两份 age 结构);RW-data 1184 不变
- stack_report:protectTask 静态最坏链 872 B 不变(余 1176 B / 57%)、
  adcTask 1320 B 不变——新判据无日志,不进最坏链
- 主机单测 45/45(新增 TC-ADCHEALTH-012..017 共 14 条断言:首拍只建
  基线 / 超拍不判老 / 门限含等号 / 跨 tick 回绕 / 倒退按分工不判老 /
  双观察者互不污染 / 空参数不判老)
- 台架注入(一次性停 TIM3 300 ms 探针,验完已撤;探针固件与最终
  固件 Program Size 逐字节相同):
  [+5462ms] [ERR][PROT] sampling FAULT -> protection blocked (hf=0x00000100)
  [+5757ms] [PROT] sampling recovered -> protection resumed
  同次开机 [ADC_TASK] health: 出现 0 次;MISS 快照缺的正是 bit6
  (0x1F7 - 0x1B7 = 0x40,实证 adcTask 当时确实阻塞,不是"在跑但没报");
  reset cause: 仅 1 次(300 ms 被 800 ms 容忍吸收,闭锁不靠复位)
  证据 docs/测试说明/boot_log_data/20261021_probe_stale_gate_boot_log.txt
- 字节卫生:BOM 无 / CR 0 / U+FFFD 0;新增行无非法字面量

gate: only mem-budget/mem-numbers pending lead ledger backfill
pre-commit run --all-files 失败仅这两条(台账受管快照与本树 .map 的
Flash/RAM 水位漂移,即本次 +96 B Code、+24 B ZI)。按并行分支约定
本分支不跑 --write,台账由 Lead 合并后统一回填。

文档同步:设计说明新增 §15(原相关文档改 §16),§7 位表加 bit8 行、
§8 责任矩阵与「已知窗口」段改写为已闭合、§10 盲区第 3 条、§11 用例
表、§12 判读手册、§14 改前清单全部跟上;位图配图补 bit8 列,并修掉
生成器把列数硬编码成 8 列导致新列被备注框盖住的问题。
2026-10-22 00:43:32 +08:00

235 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:本图的核心信息是**位号对齐**(同一 bit 在三张位图与四个消费者掩码
里落在同一列),必须等宽网格逐列对齐;Mermaid 无法控制列位。
用法(仓库根目录,任意 python3):
python tools/arch_diagram/gen_adc_health_bitmap_svg.py
输出:
docs/images/采样有效性位图与掩码.svg
改内容时:编辑本文件顶部的 GROUPS 数据后重跑。
"""
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
ROW_LABEL_FONT = 12.5
BIT_FONT = 11
TAG_FONT = 10.5
NOTE_FONT = 11.5
HEAD_FONT = 12.5
LABEL_X, LABEL_W = PAD, 262
GRID_X = PAD + LABEL_W + 8
BIT_W, BIT_H, BIT_GAP = 94, 46, 4
NOTE_W = 320
GROUP_GAP = 26
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
INK, MUTED = "#0f172a", "#64748b"
C_LEVEL = ("#0d9488", "#f0fdfa") # 电平(可自愈)
C_LATCH = ("#e11d48", "#fff1f2") # 锁存(不自愈)
C_EVENT = ("#d97706", "#fffbeb") # 仅边沿事件
C_MASK = ("#7c3aed", "#f5f3ff") # 消费者掩码命中
C_NONE = ("#94a3b8", "#f8fafc") # 未使用 / 不适用
# ----------------------------- 数据(改这里) -----------------------------
# kind: level / latch / event / mask / none
GROUPS = [
dict(head="判定层写出的三张位图(adc_health.c)", rows=[
dict(label="flags 电平位", sub="adc_health_flags(),当前活跃",
note="protectTask 每 20 ms 读它做 L5-4 闭锁;bit8 由本任务自己判老后 OR 入(判定层不写)",
bits=[("bit0", "TPAFE_STALL", "level"), ("bit1", "STM32_STALL", "level"),
("bit2", "SEQ_BACKWARD", "latch"), ("bit3", "—", "none"),
("bit4", "—", "none"), ("bit5", "—", "none"),
("bit6", "—", "none"), ("bit7", "—", "none"),
("bit8", "STALE", "level")]),
dict(label="sticky 锁存位", sub="adc_health_sticky(),直到 init 才清",
note="只留痕不闭锁:曾停摆/曾出错必须可追溯",
bits=[("bit0", "TPAFE_STALL", "latch"), ("bit1", "STM32_STALL", "latch"),
("bit2", "SEQ_BACKWARD", "latch"), ("bit3", "FRSTDATA", "latch"),
("bit4", "STM32_ERR", "latch"), ("bit5", "HW_ERR", "latch"),
("bit6", "不入锁存", "none"), ("bit7", "不入锁存", "none"),
("bit8", "不入锁存", "none")]),
dict(label="events 事件位", sub="adc_health_check() 出参,仅当拍有效",
note="adc_health_report() 的唯一输入:按位分三级处置",
bits=[("bit0", "—", "none"), ("bit1", "—", "none"), ("bit2", "—", "none"),
("bit3", "FRSTDATA", "event"), ("bit4", "STM32_ERR", "event"),
("bit5", "HW_ERR", "event"), ("bit6", "TPAFE_GAP", "event"),
("bit7", "STM32_GAP", "event"), ("bit8", "—", "none")]),
]),
dict(head="消费者侧的掩码与记忆位图", rows=[
dict(label="PROT_HF_LOCK_MASK", sub="protect_task.c 编译期常量",
note="命中即 prot_elm_init() 复位元件 + 本拍不 eval;SOE 5/6 带位图",
bits=[("bit0", "闭锁", "mask"), ("bit1", "闭锁", "mask"),
("bit2", "闭锁", "mask"), ("bit3", "不闭锁", "none"),
("bit4", "不闭锁", "none"), ("bit5", "不闭锁", "none"),
("bit6", "—", "none"), ("bit7", "—", "none"),
("bit8", "闭锁", "mask")]),
dict(label="serious 严重事件", sub="adc_task.c 的 adc_health_report()",
note="三类计数增量恒应为 0 → 每次 DBG_ERROR + 自检留痕",
bits=[("bit0", "—", "none"), ("bit1", "—", "none"), ("bit2", "—", "none"),
("bit3", "FRSTDATA", "mask"), ("bit4", "STM32_ERR", "mask"),
("bit5", "HW_ERR", "mask"), ("bit6", "—", "none"),
("bit7", "—", "none"), ("bit8", "—", "none")]),
dict(label="gap 丢帧事件", sub="同一函数里的丢帧分支",
note="刻意不告警:超拍是常态(启动突发期即累计 23 次),只累计",
bits=[("bit0", "—", "none"), ("bit1", "—", "none"), ("bit2", "—", "none"),
("bit3", "—", "none"), ("bit4", "—", "none"), ("bit5", "—", "none"),
("bit6", "TPAFE_GAP", "mask"), ("bit7", "STM32_GAP", "mask"),
("bit8", "—", "none")]),
dict(label="s_health_logged", sub="编排层记忆位图(每类一次留痕)",
note=" |= (serious | rise):用同一位号把 SYSLOG 压成每类每次开机一条",
bits=[("bit0", "记过", "mask"), ("bit1", "记过", "mask"),
("bit2", "记过", "mask"), ("bit3", "记过", "mask"),
("bit4", "记过", "mask"), ("bit5", "记过", "mask"),
("bit6", "不记", "none"), ("bit7", "不记", "none"),
("bit8", "不记", "none")]),
]),
]
FOOT = ("bit3 / bit4 / bit5 在 events 与 sticky 中刻意用同一位号 → 消费者能把事件位直接"
"套在锁存位图上做每类一次留痕;bit8 = 保护侧兜底门专用(判定层不写它)。"
"详见 docs/算法/采样有效性校验设计说明.md")
KIND_COLOR = dict(level=C_LEVEL, latch=C_LATCH, event=C_EVENT, mask=C_MASK, none=C_NONE)
# ----------------------------- 工具函数 -----------------------------
def esc(s):
return html.escape(s, quote=True)
def text_w(s, fs):
return sum(fs * (1.0 if ord(ch) >= 0x2E80 else 0.58) for ch in s)
WARN = []
def fit(s, fs, avail, where):
if text_w(s, fs) > avail:
WARN.append("%s: %.0f > %.0f [%s]" % (where, text_w(s, fs), avail, s))
# ----------------------------- 生成 -----------------------------
def build():
# 位数列数由数据推导(曾硬编码 8 列,加 bit8 后备注框把新列盖住了)
NB = max(len(row["bits"]) for g in GROUPS for row in g["rows"])
body = []
addb = body.append
for g in GROUPS:
fit(g["head"], HEAD_FONT, W - 2 * PAD, "group head")
for r in g["rows"]:
fit(r["label"], ROW_LABEL_FONT, LABEL_W - 16, "row label")
fit(r["sub"], TAG_FONT, LABEL_W - 16, "row sub")
for ln in wrap(r["note"], NOTE_FONT, NOTE_W - 24):
fit(ln, NOTE_FONT, NOTE_W - 24, "row note")
for b, tag, kind in r["bits"]:
fit(tag, TAG_FONT, BIT_W - 12, "bit tag " + b)
fit(FOOT, NOTE_FONT, W - 2 * PAD - 32, "foot")
for w in WARN:
print("OVERFLOW WARN " + w)
row_h = BIT_H + 10
y = 178
for g in GROUPS:
addb('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="#334155">%s</text>'
% (PAD, y, HEAD_FONT, esc(g["head"])))
y += 22
for r in g["rows"]:
cy = y
addb('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="%s">%s</text>'
% (PAD, cy + 20, ROW_LABEL_FONT, INK, esc(r["label"])))
addb('<text x="%d" y="%d" font-size="%d" fill="%s">%s</text>'
% (PAD, cy + 38, TAG_FONT, MUTED, esc(r["sub"])))
for i, (b, tag, kind) in enumerate(r["bits"]):
fg, bg = KIND_COLOR[kind]
x = GRID_X + i * (BIT_W + BIT_GAP)
addb('<rect x="%d" y="%d" width="%d" height="%d" rx="8" fill="%s" '
'stroke="%s" stroke-opacity="%s"/>'
% (x, cy, BIT_W, BIT_H, bg, fg, "0.55" if kind != "none" else "0.35"))
addb('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="%s">'
'%s</text>' % (x + 10, cy + 19, BIT_FONT, fg, esc(b)))
addb('<text x="%d" y="%d" font-size="%d" fill="#334155">%s</text>'
% (x + 10, cy + 36, TAG_FONT, esc(tag)))
nx = GRID_X + NB * (BIT_W + BIT_GAP) + 12
addb('<rect x="%d" y="%d" width="%d" height="%d" rx="8" fill="#f8fafc" '
'stroke="#64748b" stroke-opacity="0.25"/>' % (nx, cy, NOTE_W, BIT_H))
lines = wrap(r["note"], NOTE_FONT, NOTE_W - 24)
ty = cy + (BIT_H - (len(lines) - 1) * 15 + 4) / 2.0
for ln in lines:
addb('<text x="%d" y="%d" font-size="%d" fill="#475569">%s</text>'
% (nx + 12, ty, NOTE_FONT, esc(ln)))
ty += 15
y += row_h
y += GROUP_GAP
# 图例
lx, ly = GRID_X, 118
for label, kind in [("电平位(可自愈)", "level"), ("锁存位(不自愈)", "latch"),
("边沿事件位(仅当拍)", "event"), ("消费者掩码命中 / 记忆位", "mask"),
("不适用", "none")]:
fg, bg = KIND_COLOR[kind]
addb('<rect x="%d" y="%d" width="16" height="16" rx="4" fill="%s" stroke="%s" '
'stroke-width="1.6"/>' % (lx, ly - 12, bg, fg))
lx += 22
addb('<text x="%d" y="%d" font-size="12" fill="#475569">%s</text>'
% (lx, ly, esc(label)))
lx += text_w(label, 12) + 30
total_h = y + 74
foot_y = total_h - 58
body.insert(0, '<rect x="0" y="0" width="%d" height="%d" fill="#ffffff"/>' % (W, total_h))
head = ['<text x="%d" y="44" font-size="%d" font-weight="700" fill="%s">%s</text>'
% (PAD, TITLE_FONT, INK, esc("采样有效性校验 · 出口位图与消费者掩码(逐位对齐)")),
'<text x="%d" y="70" font-size="%d" fill="%s">%s</text>'
% (PAD, SUB_FONT, MUTED,
esc("同一 bit 在三张位图与四个掩码里始终落在同一列:位号对齐 = 消费者可以位图代打表"))]
svg = ('<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" '
'viewBox="0 0 %d %d" font-family="%s">\n%s\n%s\n'
'<rect x="%d" y="%d" width="%d" height="44" rx="10" fill="#fffbeb" '
'stroke="#d97706" stroke-opacity="0.45"/>\n'
'<text x="%d" y="%d" font-size="%d" fill="#7c2d12">%s</text>\n</svg>\n'
% (W, total_h, W, total_h, FONT, "\n".join(head), "\n".join(body),
PAD, foot_y, W - 2 * PAD, PAD + 16, foot_y + 27, NOTE_FONT, esc(FOOT)))
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(WARN):
print("!! %d 处文字可能溢出" % len(WARN))
def wrap(s, fs, avail):
"""按估算宽度做中文换行(无空格语言:逐字累计)。"""
lines, cur = [], ""
for ch in s:
if text_w(cur + ch, fs) > avail and cur:
lines.append(cur)
cur = ""
cur += ch
if cur:
lines.append(cur)
return lines
if __name__ == "__main__":
build()