#!/usr/bin/env python3 # -*- coding: utf-8 -*- """生成 docs/images/ADC码值刻度.svg —— 解释「码」是什么:ADC 输出的整数是刻度数, 电压 = 码 × LSB,而两片转换器的 LSB 不同(纯标准库)。 为什么不用 Mermaid:这是**两条刻度尺的对照**,要让人一眼看出"同一个整数在两片转换器上 代表不同电压",靠的是刻度疏密与标注对齐,Mermaid 的流程图语义完全表达不了。 用法(仓库根目录,任意 python3): python tools/arch_diagram/gen_adc_code_scale_svg.py 输出: docs/images/ADC码值刻度.svg 改内容:码制与 LSB 由转换器决定,换芯片才重画;调坐标 / 文案编辑下方常量后重跑。 """ 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", "ADC码值刻度.svg") # ----------------------------- 画布与尺寸 ----------------------------- W = 980 H = 512 PAD = 60 RIGHT = 920 AX0, AX1 = 100, 900 FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" TITLE_FONT = 20 SUB_FONT = 13 HEAD_FONT = 14 MARK_FONT = 13 NOTE_FONT = 12 TINY_FONT = 11 INK = "#0f172a" LINE = "#334155" MUTED = "#64748b" FAINT = "#94a3b8" RULE = "#cbd5e1" WHITE = "#ffffff" C_BLUE = "#2563eb" C_TEAL = "#0d9488" # ----------------------------- 数据(改这里) ----------------------------- TITLE = "「码」是 ADC 的最小刻度 —— 两片转换器的刻度大小不同" SUB1 = "ADC 输出的是整数(码),不是电压;电压 = 码 × LSB,所以要先知道「1 码值多少伏」" SUB2 = "TPAFE5160:16 位双极性,65536 码 = 10 V 跨度 · STM32 ADC1:12 位单极性,4096 码 = 3.3 V 跨度" # (面板标题, 副标题, 左端标, 中间标, 右端标, 刻度段数, 标记码, 标记位置比, 标记文案, 颜色) PANELS = [ dict(head="A · TPAFE5160(16 位双极性)", note="码范围 −32768 ~ +32767 · 每 1 码 = 152.59 µV", left="−32768", mid="0", right="+32767", n_ticks=20, mark_frac=(11430 + 32768) / 65536.0, mark_text="11430 码 ≈ 1.744 V", color=C_BLUE), dict(head="B · STM32 ADC1(12 位单极性)", note="码范围 0 ~ 4095 · 每 1 码 = 805.66 µV", left="0", mid=None, right="4095", n_ticks=8, mark_frac=1000 / 4095.0, mark_text="1000 码 ≈ 0.806 V", color=C_TEAL), ] FOOT = ("刻度疏密仅示意位数差(16 位比 12 位细),不是逐码绘制;1 码的电压当量见各行右侧标注。", "两个换算值都来自台架实测码值:11430 与 cal: 行的 1743 mV 自洽,说明「码 × LSB」这条链是通的。") # ----------------------------- 工具函数 ----------------------------- 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 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 ('%s' % (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 ('' % (x1, y1, x2, y2, color, width, d)) def panel_head(y, s): return [txt(PAD, y, s, HEAD_FONT, INK, weight=700), line(PAD, y + 12, RIGHT, y + 12, RULE, 1.0)] # ----------------------------- 生成 ----------------------------- def build(): out = [] add = out.append avail = RIGHT - PAD for s, fs, where in ((TITLE, TITLE_FONT, "title"), (SUB1, SUB_FONT, "sub1"), (SUB2, SUB_FONT, "sub2")): check_fit(s, fs, avail, where) for i, s in enumerate(FOOT): check_fit(s, NOTE_FONT, avail, "foot %d" % (i + 1)) for i, p in enumerate(PANELS): check_fit(p["head"], HEAD_FONT, avail, "panel %d head" % (i + 1)) check_fit(p["note"], NOTE_FONT, avail, "panel %d note" % (i + 1)) check_fit(p["mark_text"], MARK_FONT, 220, "panel %d mark" % (i + 1)) if OVERFLOW: for o in OVERFLOW: print("OVERFLOW WARN " + o) add('' % (W, H, WHITE)) # ---- 标题区 ---- add(txt(PAD, 44, TITLE, TITLE_FONT, INK, weight=700)) add(txt(PAD, 72, SUB1, SUB_FONT, MUTED)) add(txt(PAD, 92, SUB2, SUB_FONT, MUTED)) ys = [(150, 238, 260), (328, 416, 438)] # (head_y, axis_y, label_y) for i, p in enumerate(PANELS): hy, ay, ly = ys[i] for e in panel_head(hy, p["head"]): add(e) add(txt(PAD, hy + 30, p["note"], NOTE_FONT, MUTED)) # 轴 add(line(AX0, ay, AX1, ay, LINE, 1.8)) # 刻度 n = p["n_ticks"] for k in range(n + 1): x = AX0 + (AX1 - AX0) * k / float(n) add(line(x, ay - 6, x, ay + 6, FAINT, 1.0)) # 端点 / 中点标 add(txt(AX0, ly, p["left"], TINY_FONT, FAINT, anchor="middle")) add(txt(AX1, ly, p["right"], TINY_FONT, FAINT, anchor="middle")) if p["mid"] is not None: add(txt((AX0 + AX1) / 2.0, ly, p["mid"], TINY_FONT, MUTED, anchor="middle")) # 实测码值标记 mx = AX0 + (AX1 - AX0) * p["mark_frac"] add(line(mx, ay - 6, mx, ay - 28, p["color"], 1.0, dash="3 3")) add('' % (mx, ay, p["color"])) add(txt(mx, ay - 36, p["mark_text"], MARK_FONT, p["color"], weight=700, anchor="middle")) # ---- 底部说明 ---- add(line(PAD, 452, RIGHT, 452, RULE, 1.0)) for i, s in enumerate(FOOT): add(txt(PAD, 470 + i * 18, s, NOTE_FONT, MUTED)) svg = ('\n%s\n\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) print("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, H, OUT)) if OVERFLOW: print("!! %d 处文字可能溢出,见上方 WARN" % len(OVERFLOW)) if __name__ == "__main__": build()