163b933715
数据可信.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 全绿。
324 lines
14 KiB
Python
324 lines
14 KiB
Python
#!/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()
|