Files
STM32F4-Base/tools/arch_diagram/gen_data_trust_dc_ac_svg.py
T
wandering1 163b933715 docs: 数据可信与 ADC 通道映射只留现状,清历史与悬空引用
数据可信.md 与 ADC通道映射说明.md 按「只写现状」口径重写:
删历史沿革、推理过程与已删小节的残留指针,保留现行判据。

- 数据可信:§2.7 删「当时的证据」六行表;§4.6.1 标题改「范围与
  幅度上限」、四行改现状陈述;§4.6.2 删轮次名历史映射注;§4.6.3
  段文件行去过程语;删节尾孤立排程段;§4.4 负零点段收成现状结论。
- 数据可信:把「系数合法带」从已删的 §8 风险表搬回 §4.1(代码头注释
  仍指向它,属搬回不是新发明)。
- 数据可信:行号纠错 adc_task.c:382 -> :425、protect_task.c:245 ->
  :268,逐个 grep 核对。
- 跨文档与代码:12 处指向已删 §4.6.4/§4.6.5/§8/§9/§10 的悬空引用改到
  现存小节或内联结论,涉 6 处 App 源文件头注释与 6 篇文档。
- 两个 SVG 生成器文案同步重跑,产物同笔提交。

check_doc_links --all 通过(221 篇);pre-commit 全绿。
2026-11-06 17:41:11 +08:00

324 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 点/周的正弦采样点,也无法把上下一一对齐。
用法(仓库根目录,任意 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;下排 2026-11-02 / 11-04 台架两轮跑完六轮,"
"幅值 / 波形 / 相位 / 测频 / 削顶 / 谐波全部闭合")
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 正弦", "幅度按 Vpp 设定,见底部红线"),
("注入点 R31", "同一条注入点", "外串 10 k、与板共地"),
("前端 + 转换器", "同一套前端", "STM32 五路负半周必削顶"),
("1 kHz 采样点", "TIM3 TRGO 同时触发", "50 Hz → 20 点/整周"),
("全周傅氏", "phasor_run():√2/N 预乘", "直接得有效值与相位"),
("读数出口", "meas 200 ms + 录波段文件", "幅值 + 品质 + 频率"),
]
CHIPS_TITLE = "只有正弦能证的六件事(直流在构造上就证不到)"
CHIPS = [
("整周窗对不对", "窗长差 1 点,相位误差就跟着来;直流恒 0 相位,测不出"),
("有效值是否依赖波形", "直流只有一个点:方波 / 三角 / 正弦算出的都是同一个数"),
("相位", "φ = atan2(fIm, fRe) 全靠正弦才有意义"),
("测频 freq_run", "共轭乘积取相位增量;直流相位不前进 → 恒无效"),
("削顶路由 CLIPPED", "UB/UC/IB/IC/IN 单极性 0~3.3 V,负半周必削顶 → 该报 IV 就得报 IV"),
("谐波实例 C04 / C05", "二次 / 五次相量:单注 100 / 250 Hz 已验各认各的次,叠加工况待标准源"),
]
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 = ("读数出口:串口每 10 s 的 phasor(hb) / freq(hb) / meas: 行,加上录波段文件(一次 320 点 / 8 路,PC 侧 wave_read.py 可 dump / fft / shape / plot);"
"心跳行是 ×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()