790e153cd6
读码确认的口径缺陷:两片转换器的丢帧计数单位不一致——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 合并后统一回填。
257 lines
11 KiB
Python
257 lines
11 KiB
Python
#!/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()
|