Files
STM32F4-Base/tools/arch_diagram/gen_data_trust_dc_ac_svg.py
T
wandering1 b46e44d92c docs(采集可信): 数据可信文档台阶化重构并立项故障录波
两份工作互锁(互相引用),故同一笔提交。

数据可信:全文按「五级台阶」重排(§1 全局 → §2 信度现状 → §3 M4 退出判据逐条 →
§4 设计约束 → §5 任务卡 → §6 顺序 → §7 决策 → §8 风险 → §9 边界 → §10 关系)。
恢复 §2.2 量纲错配(该节此前被瘦身删掉,test/host/prot_dim 的两处引用一直悬空);
新增 §2.7 与 §4.6:ADC 只验过直流静态标定,正弦动态波形一次也没采过——用信号发生
器在既有注入点注 ±2 V 以内的正弦即可验全周傅氏、测频、快照与削顶路由,不必往端子
加压、也不需要标准源。§6 手绘框图改 Mermaid。章节编号未重排:§2 / §4 / §7 / §10 被
App 与 test 的头注释当规范引用。

故障录波:用户裁定 2026-10-29 录波先行,三条口径——1 kHz(每周波 20 点)、8 路
电气量全录、一屏只画一路共 8 路可切换。新增设计说明(与产品需求 80 点/周波口径的
偏离显式登记;存原始码值而非工程量,绕开未标定闭锁与测频幅值门;四个所有者的跨
任务边界与三条并发陷阱;段文件字节布局;单通道回放显示契约;约 5.9 KB 静态增量
触发 R6)与卡片登记簿(数据面最小闭环 → 手动与保护启动触发 → 现场单通道回放页,
二期的 4 kHz、突变量启动、COMTRADE、104 文件传输不返工的结构约束写进设计文档)。

两张旧配图与一张新配图改由 tools/arch_diagram/ 的生成器产出,脚本与产物同笔提交。
代码一行未动:本波只交付文档与设计。
2026-10-29 13:18:28 +08:00

325 lines
14 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/ADC直流标定与交流验证.svg —— 上下两条泳道讲清"同一个针脚、两种证法":
上排 = 已做完的直流静态标定(一条水平线的高度),下排 = 本文新增的交流正弦验证
(整条时间轴的形状与节拍),底部 = 只有正弦能证的六件事 + 安全红线(纯标准库)。
为什么不用 Mermaid:本图要的是"两条流水线一一对齐 + 波形示意",Mermaid 画不出
20 点/周的正弦采样点,也无法把上下一一对齐。谁挡谁的顺序拓扑仍是 Mermaid,
见 docs/任务规划/数据可信.md §6。
用法(仓库根目录,任意 python3):
python tools/arch_diagram/gen_data_trust_dc_ac_svg.py
输出:
docs/images/ADC直流标定与交流验证.svg
改内容:链路环节 / 六件事 / 红线变了才重画;调坐标编辑下方常量后重跑。
"""
import html
import math
import os
import sys
import xml.etree.ElementTree as ET
def emit(s):
"""控制台打印按目标编码替换,绝不因 GBK 终端遇上 ²/− 之类字符而崩
(generated-fresh 钩子会把本脚本拷进临时目录重跑,崩了就是一片假红)。"""
enc = getattr(sys.stdout, "encoding", None) or "ascii"
sys.stdout.write(s.encode(enc, "replace").decode(enc, "replace") + "\n")
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
OUT = os.path.join(ROOT, "docs", "images", "ADC直流标定与交流验证.svg")
# ----------------------------- 画布 -----------------------------
W = 1060
H = 818
PAD = 46
RIGHT = W - PAD
INNER = RIGHT - PAD
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
TITLE_FONT = 21
SUB_FONT = 13
BOX_TITLE = 12.5
BOX_DESC = 10.5
CHIP_TITLE = 12.0
CHIP_DESC = 10.0
SMALL = 11.0
TINY = 10.5
INK = "#0f172a"
LINE = "#334155"
MUTED = "#475569"
FAINT = "#94a3b8"
RULE = "#cbd5e1"
WHITE = "#ffffff"
C_GREEN = "#16a34a"
BG_GREEN = "#f0fdf4"
BD_GREEN = "#bbf7d0"
C_ROSE = "#e11d48"
BG_ROSE = "#fff1f2"
BD_ROSE = "#fecdd3"
C_BLUE = "#2563eb"
BG_BLUE = "#eff6ff"
C_AMBER = "#d97706"
BG_AMBER = "#fffbeb"
# ----------------------------- 数据(改这里)-----------------------------
TITLE = "同一个针脚,两种证法 —— 直流只证「高度」,正弦才证「形状与节拍」"
SUB1 = ("上排已做完(2026-10-27 静态注入六项判据全 PASS);下排一次也没做过:正弦波从没进过这条链,"
"所以后面的相量、测频、削顶、谐波对不对,现在无人能答")
SUB2 = ("两排共用同一个注入点、同一套前端、同一份折算代码 —— 差别只在信号源是「平的」还是「会走 20 点/周的正弦」")
LANE_A_LABEL = "上排 · 直流静态标定(已做)"
LANE_A_GLYPH = "flat"
LANE_B_LABEL = "下排 · 交流正弦验证(本文新增 · 待做)"
LANE_B_GLYPH = "sine"
# 每条泳道 6 个环节:(标题, 说明 1, 说明 2)
BOXES_A = [
("直流源", "电池或台式电源", "+ 电位器分压"),
("万用表", "同时接在注入点", "当唯一基准读数"),
("注入点 R31", "网名 AIN1 = U5.A3", "= ADC7 引脚(1:1)"),
("前端 + 转换器", "同一条电阻网络", "TPAFE 16b、ADC1 12b"),
("折算", "adc_cal_apply()", "单位仍是引脚 V"),
("三点线性拟合", "0 / 0.5 / 1.0 V", "R² = 0.9999997,斜率差 −0.14 %"),
]
BOXES_B = [
("信号发生器", "50 Hz 正弦", "幅度按峰值算,见底部红线"),
("注入点 R31", "同上排同一个点", "外串 10 k、与板共地"),
("前端 + 转换器", "同上排同一条链", "STM32 五路负半周必削顶"),
("1 kHz 采样点", "TIM3 TRGO 同时触发", "50 Hz → 20 点/整周"),
("全周傅氏", "phasor_run():√2/N 预乘", "直接得有效值与相位"),
("读数出口", "meas 200 ms → 104 YC 0x4011", "幅值 + 品质 + 频率(或 UI)"),
]
CHIPS_TITLE = "只有正弦能证的六件事(直流在构造上就证不到)"
CHIPS = [
("整周窗对不对", "窗长差 1 点,相位误差就跟着来;直流恒 0 相位,测不出"),
("有效值是否依赖波形", "直流只有一个点:方波 / 三角 / 正弦算出的都是同一个数"),
("相位", "φ = atan2(fIm, fRe) 全靠正弦才有意义"),
("测频 freq_run", "共轭乘积取相位增量;直流相位不前进 → 恒无效"),
("削顶路由 CLIPPED", "UB/UC/IB/IC/IN 单极性 0~3.3 V,负半周必削顶 → 该报 IV 就得报 IV"),
("谐波实例 C04 / C05", "二次 / 五次相量已建但无消费者:发生器给含谐波信号才能验"),
]
NOTES = [
("安全红线(照抄 ADC通道映射说明 §7)", C_ROSE, BG_ROSE, [
"TPAFE 侧只到 ±2 V:正弦看峰值不只看有效值 —— 1 Vrms 的峰值已是 1.41 V,再高就顶到 ±5 V 档边缘;",
"STM32 那五路严禁超过 3.3 V(单极性,超了是硬件级风险,不是削顶那一路好笑的验证);",
"发生器与板必须共地(不然量到的是地环差);引脚侧注入一律外串 10 k;不许拿端子侧上额定电压走捷径。"]),
("这台发生器证不到的三件事", C_AMBER, BG_AMBER, [
"④ 端子变比:注入点在子板之前,算出来的还是引脚域 —— 变比要靠原理图或万用表实测(另一张卡);",
"精度等级验收:发生器不是标准源,它的幅值误差本身就要被校,不能拿它冒充送检依据;",
"跨转换器相位:UB/UC 与 UA 分属两片转换器、无共同采样触发(V0.1 硬件缺陷),任何相位比较都不作数。"]),
]
FOOTNOTE = ("读数出口现在就有:104 YC(test/host/iec104_master.py,浮点无量化)+ 串口每 10 s 的 phasor(hb) / freq(hb) / meas: 行;"
"但板上没有「把窗里 20 个点导出来」的出口,正弦只能靠派生标量证,而 phasor 行是 ×0.1 定点(注 1 V 只剩 10 格)。")
# ----------------------------- 布局 -----------------------------
LANE_TOP_A = 108
LANE_H = 160
LANE_TOP_B = LANE_TOP_A + LANE_H + 18
BOX_W = 136
BOX_H = 88
GAP = (INNER - 6 * BOX_W) / 5.0
LANE_LAB_H = 26
BOX_Y_OFF = LANE_LAB_H + 34
CHIPS_TOP = LANE_TOP_B + LANE_H + 20
CHIP_W = (INNER - 5 * 12) / 6.0
CHIP_H = 74
NOTE_TOP = CHIPS_TOP + CHIP_H + 18
NOTE_W = (INNER - 16) / 2.0
NOTE_H = 128
# ----------------------------- 工具 -----------------------------
OVERFLOW = []
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)
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 txt(x, y, s, fs, fill, weight=400, anchor=None):
a = "" if anchor is None else ' text-anchor="%s"' % anchor
w = "" if weight == 400 else ' font-weight="%d"' % weight
return ('<text x="%.1f" y="%.1f" font-size="%g"%s%s fill="%s">%s</text>'
% (x, y, fs, a, w, fill, esc(s)))
def line(x1, y1, x2, y2, color=LINE, width=2.0, dash=None):
d = "" if dash is None else ' stroke-dasharray="%s"' % dash
return ('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="%g"%s/>'
% (x1, y1, x2, y2, color, width, d))
def polyline(pts, color, width=1.8, dash=None):
d = "" if dash is None else ' stroke-dasharray="%s"' % dash
p = " ".join("%.1f,%.1f" % (a, b) for a, b in pts)
return '<polyline points="%s" fill="none" stroke="%s" stroke-width="%g"%s stroke-linejoin="round"/>' % (
p, color, width, d)
def rect(x, y, w, h, fill, stroke=None, rx=8, sw=1.4):
s = "" if stroke is None else ' stroke="%s" stroke-width="%g"' % (stroke, sw)
return '<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="%g" fill="%s"%s/>' % (
x, y, w, h, rx, fill, s)
def arrow(x, y, color):
"""向右的短箭头,画在两个环节之间。"""
return ('<path d="M %.1f %.1f L %.1f %.1f L %.1f %.1f Z" fill="%s"/>'
% (x, y - 4, x + 9, y, x, y + 4, color))
def glyph(x, y, w, h, kind, color):
"""泳道左侧的波形示意:flat = 一条水平线加三个取样点;sine = 正弦加 20 个采样点。"""
out = [rect(x, y, w, h, WHITE, color, rx=6, sw=1.2)]
mid = y + h / 2.0
if kind == "flat":
out.append(line(x + 6, mid - 8, x + w - 6, mid - 8, color, 2.0))
for i in range(3):
cx = x + 12 + i * (w - 24) / 2.0
out.append('<circle cx="%.1f" cy="%.1f" r="2.8" fill="%s"/>' % (cx, mid - 8, color))
else:
pts = []
n = 60
for i in range(n + 1):
t = i / float(n)
px = x + 6 + t * (w - 12)
py = mid - (h / 2.0 - 8) * math.sin(t * 2.0 * math.pi)
pts.append((px, py))
out.append(line(x + 4, mid, x + w - 4, mid, RULE, 1.0, dash="3 3"))
out.append(polyline(pts, color, 1.9))
for i in range(20):
t = i / 20.0
px = x + 6 + t * (w - 12)
py = mid - (h / 2.0 - 8) * math.sin(t * 2.0 * math.pi)
out.append('<circle cx="%.1f" cy="%.1f" r="2.0" fill="%s"/>' % (px, py, color))
return "\n".join(out)
# ----------------------------- 生成 -----------------------------
def build():
out = []
add = out.append
for s, fs, where in ((TITLE, TITLE_FONT, "title"), (SUB1, SUB_FONT, "sub1"),
(SUB2, SUB_FONT, "sub2"), (CHIPS_TITLE, BOX_TITLE, "chips title")):
check_fit(s, fs, INNER, where)
add('<rect x="0" y="0" width="%d" height="%d" fill="%s"/>' % (W, H, WHITE))
# 标题
add(txt(PAD, 42, TITLE, TITLE_FONT, INK, weight=700))
for i, s in enumerate((SUB1, SUB2)):
add(txt(PAD, 68 + i * 19, s, SUB_FONT, MUTED))
# 两条泳道
for lane_i, (top, label, boxes, color, bg, bd, glyph_kind) in enumerate((
(LANE_TOP_A, LANE_A_LABEL, BOXES_A, C_GREEN, BG_GREEN, BD_GREEN, LANE_A_GLYPH),
(LANE_TOP_B, LANE_B_LABEL, BOXES_B, C_ROSE, BG_ROSE, BD_ROSE, LANE_B_GLYPH))):
add(rect(PAD - 12, top, INNER + 24, LANE_H, bg, bd, rx=10, sw=1.2))
add(txt(PAD, top + 20, label, BOX_TITLE, color, weight=700))
add(glyph(RIGHT - 92, top + 8, 88, 34, glyph_kind, color))
for i, (bt, d1, d2) in enumerate(boxes):
bx = PAD + i * (BOX_W + GAP)
by = top + BOX_Y_OFF
add(rect(bx, by, BOX_W, BOX_H, WHITE, color, rx=8, sw=1.6))
check_fit(bt, BOX_TITLE, BOX_W - 14, "lane%d box%d title" % (lane_i, i))
add(txt(bx + 7, by + 18, bt, BOX_TITLE, INK, weight=700))
yy = by + 36
for j, d in enumerate((d1, d2)):
for ln in wrap(d, BOX_DESC, BOX_W - 14, 2, "lane%d box%d desc%d" % (lane_i, i, j)):
add(txt(bx + 7, yy, ln, BOX_DESC, MUTED))
yy += 14
if i < 5:
add(arrow(bx + BOX_W + GAP / 2.0 - 5, by + BOX_H / 2.0, LINE))
add(line(bx + BOX_W + 2, by + BOX_H / 2.0, bx + BOX_W + GAP / 2.0 - 3,
by + BOX_H / 2.0, LINE, 1.6))
# 六件事
add(txt(PAD, CHIPS_TOP - 10, CHIPS_TITLE, BOX_TITLE, C_BLUE, weight=700))
for i, (t, d) in enumerate(CHIPS):
cx = PAD + i * (CHIP_W + 12)
add(rect(cx, CHIPS_TOP, CHIP_W, CHIP_H, BG_BLUE, None, rx=8))
add(line(cx, CHIPS_TOP, cx, CHIPS_TOP + CHIP_H, C_BLUE, 3.0))
check_fit(t, CHIP_TITLE, CHIP_W - 16, "chip%d title" % i)
add(txt(cx + 10, CHIPS_TOP + 20, t, CHIP_TITLE, INK, weight=700))
for j, ln in enumerate(wrap(d, CHIP_DESC, CHIP_W - 18, 4, "chip%d desc" % i)):
add(txt(cx + 10, CHIPS_TOP + 38 + j * 14, ln, CHIP_DESC, MUTED))
# 两个说明块
for i, (title, color, bg, rows) in enumerate(NOTES):
nx = PAD + i * (NOTE_W + 16)
add(rect(nx, NOTE_TOP, NOTE_W, NOTE_H, bg, color, rx=10, sw=1.6))
check_fit(title, BOX_TITLE, NOTE_W - 20, "note%d title" % i)
add(txt(nx + 14, NOTE_TOP + 24, title, BOX_TITLE, color, weight=700))
y = NOTE_TOP + 46
for r in rows:
for j, ln in enumerate(wrap(r, SMALL, NOTE_W - 28, 2, "note%d row" % i)):
add(txt(nx + 14, y, ln, SMALL, INK if j == 0 else MUTED))
y += 15
y += 4
# 脚注
fy = NOTE_TOP + NOTE_H + 26
add(line(PAD, fy - 12, RIGHT, fy - 12, RULE, 1.0))
for i, ln in enumerate(wrap(FOOTNOTE, SUB_FONT, INNER, 2, "foot")):
add(txt(PAD, fy + 10 + i * 19, ln, SUB_FONT, MUTED))
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, H, W, 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)
emit("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, H, OUT))
if OVERFLOW:
for o in OVERFLOW:
emit("OVERFLOW WARN " + o)
emit("!! %d overflow warnings" % len(OVERFLOW))
def wrap(s, fs, avail, max_lines, where=""):
"""按估算宽度折行(CJK 逐字可断);超出 max_lines 用省略号收尾。"""
if text_w(s, fs) <= avail:
return [s]
lines = []
cur = ""
for ch in s:
if text_w(cur + ch, fs) > avail:
lines.append(cur)
cur = ch
else:
cur += ch
if cur:
lines.append(cur)
if len(lines) > max_lines:
OVERFLOW.append("%s: 折行 %d > %d" % (where, len(lines), max_lines))
lines = lines[:max_lines]
lines[max_lines - 1] = lines[max_lines - 1][:-1] + "…"
return lines
if __name__ == "__main__":
build()