Files
STM32F4-Base/tools/arch_diagram/gen_sample_ring_layout_svg.py
T
wandering1 c377fc8c15 docs(algo): 采样环缓冲补「为什么需要/设计思想」两节与两张配图
- 采集与算法层设计说明第 5 节由单段说明拆成 5.1~5.5:为什么需要这一层
  (三个缺口 + 两个被否决的替代方案)、设计思想(五条不变量)、环内布局
  与 start 推导、API 契约、要点与禁令。
- 新增两张配图(生成器 + 产物同批提交):
  采样环缓冲_为什么需要.svg —— 上游 1 kHz 覆盖写只交最新一帧 vs 下游
  20 点滑窗,缺口 19 点,以及三个消费者必须同源;
  采样环缓冲_环形布局.svg —— 40 格环里哪 20 格是本窗、写指针指「下一格」、
  回绕方向与三条设计不变量。
- 5.5 补一条「读者与写者必须同任务」,与故障录波设计说明的取窗竞态口径一致。
- 门禁:doc-links --sections-strict 通过(28 处章节引用全部可判定);
  id-check 新增行无未登记编号;generated-fresh 21 个生成器 16 一致、0 新漂移。
2026-10-29 17:15:48 +08:00

277 lines
9.8 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/采样环缓冲_环形布局.svg —— 采样环缓冲的设计思想落在图上的三件事:
写指针指「下一格」而不是「最后一格」、窗永远紧贴指针身后、整窗随每次写入右移一格。
为什么不用 Mermaid:本图要读的是"40 个格子里哪 20 格是窗、指针停在第几格"——
Mermaid 画不出等宽格子与指针位置,盒子宽度随文本变,"窗贴着指针"这个关系就没了。
用法(仓库根目录,任意 python3):
python tools/arch_diagram/gen_sample_ring_layout_svg.py
输出:
docs/images/采样环缓冲_环形布局.svg
改内容:容量(周波数 / 每周波点数)或窗长变了才重画;调坐标 / 文案编辑下方常量后重跑。
"""
import html
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", "采样环缓冲_环形布局.svg")
# ----------------------------- 画布与尺寸 -----------------------------
W = 1060
H = 580
PAD = 46
RIGHT = W - PAD
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
TITLE_FONT = 21
SUB_FONT = 13
CARD_FONT = 13.5
BODY_FONT = 11.5
TINY_FONT = 11
SLOT_FONT = 9
INK = "#0f172a"
LINE = "#334155"
MUTED = "#64748b"
FAINT = "#94a3b8"
RULE = "#cbd5e1"
WHITE = "#ffffff"
C_BLUE = "#2563eb"
BG_BLUE = "#eff6ff"
C_AMBER = "#d97706"
BG_AMBER = "#fffbeb"
C_GREEN = "#16a34a"
BG_GREEN = "#f0fdf4"
C_ROSE = "#e11d48"
BG_GREY = "#f1f5f9"
# ----------------------------- 数据(改这里) -----------------------------
DEPTH = 40 # SAMPLE_RING_DEPTH
WIN_N = 20 # 示例窗长(一周波)
HEAD = 30 # 示例写指针位置
START = (HEAD + DEPTH - WIN_N) % DEPTH # = 10
TITLE = "40 格环:写指针指「下一格」,窗永远紧贴它身后"
SUB1 = "DEPTH = 2 周波 × 20 点 = 40;窗长 n 取 1~40,取的是 [head−n, head−1] 这 n 格。"
SUB2 = "图例:蓝 = 本窗 20 点,灰 = 更旧的 10 点,琥珀 = 最旧的 10 点(下一批被覆盖)。"
SUB3 = ("窗首 start = (head + 40 − n) % 40 —— 先加 40 再减,减法就不落到负数;"
"每写一格整窗右移一格。")
MARK_START = "start(最老点)"
MARK_NEW = "head−1(最新点)"
MARK_HEAD = "head(下一拍写这里)"
WRAP_NOTE = "写满后回绕:39 的下一格是 0 —— 窗可能跨过环尾,所以取数必须逐点取模"
CARDS = [
("整帧写完才推进 head", C_GREEN, BG_GREEN,
"9 路语义值一次写齐、再动指针。单帧内不存在「半新半旧」,"
"消费侧取到的窗永远落在整帧边界上。"),
("只存不算是分层契约", C_BLUE, BG_BLUE,
"同刻性归上游 adc_input 负责;本层不假设、不校正,也不做任何跨通道运算——"
"所以它能脱离硬件做主机侧单测。"),
("容量取 2 周波", C_AMBER, BG_AMBER,
"1 周波就够算全周傅氏;另 1 周波是给 1.5~2 周波长窗(抗衰减直流)留的余量,"
"改容量只动一个宏。"),
]
FOOTNOTE = ("n = 40 时 start == head:环满时「下一拍要覆盖的那一格」恰好就是最老点 —— 不是特例,是唯一自洽解。"
"同理 n = 20 时窗正好是 head 身后的 20 格,不多不少。")
# ----------------------------- 布局 -----------------------------
SLOT_W = 22
SLOT_GAP = 2
SLOT_H = 54
SLOT_X0 = PAD
ROW_Y = 200
MARK_Y = 176 # 标记文字基线
STEM_Y1 = 184 # 标记引线
STEM_Y2 = ROW_Y
WRAP_Y = 286
WRAP_TEXT_Y = 308
CARD_TOP = 340
CARD_W = 300
CARD_GAP = 19
CARD_H = 120
# ----------------------------- 工具函数 -----------------------------
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 ('<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 rect(x, y, w, h, fill, stroke=None, rx=8, sw=1.6, dash=None):
s = "" if stroke is None else ' stroke="%s" stroke-width="%g"' % (stroke, sw)
d = "" if dash is None else ' stroke-dasharray="%s"' % dash
return '<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="%g" fill="%s"%s%s/>' % (
x, y, w, h, rx, fill, s, d)
def tri(points, fill):
return '<polygon points="%s" fill="%s"/>' % (
" ".join("%.1f,%.1f" % (px, py) for px, py in points), fill)
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
def slot_x(i):
return SLOT_X0 + i * (SLOT_W + SLOT_GAP)
def slot_c(i):
"""第 i 格的横向中心。"""
return slot_x(i) + SLOT_W / 2.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"), (SUB3, SUB_FONT, "sub3")):
check_fit(s, fs, avail, where)
if OVERFLOW:
for o in OVERFLOW:
emit("OVERFLOW WARN " + o)
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))
add(txt(PAD, 68, SUB1, SUB_FONT, MUTED))
add(txt(PAD, 88, SUB2, SUB_FONT, MUTED))
add(txt(PAD, 108, SUB3, SUB_FONT, MUTED))
# ---- 40 个格子 ----
win_lo = START
win_hi = HEAD - 1
for i in range(DEPTH):
x = slot_x(i)
if win_lo <= i <= win_hi:
fill, stroke, sw = BG_BLUE, C_BLUE, 2.0
elif i >= HEAD:
fill, stroke, sw = BG_AMBER, C_AMBER, 1.4
else:
fill, stroke, sw = BG_GREY, FAINT, 1.2
add(rect(x, ROW_Y, SLOT_W, SLOT_H, fill, stroke, rx=3, sw=sw))
add(txt(slot_c(i), ROW_Y + SLOT_H / 2.0 + 3.5, str(i), SLOT_FONT, MUTED, anchor="middle"))
# ---- 三个关键位置标记 ----
marks = [
(slot_c(START), MARK_START, C_BLUE, "start"),
(slot_c(HEAD - 1), MARK_NEW, C_BLUE, "end"),
(slot_c(HEAD), MARK_HEAD, C_AMBER, "start"),
]
for cx, label, color, anchor in marks:
add(line(cx, STEM_Y1, cx, STEM_Y2 - 2, color, 1.4))
add(tri([(cx, STEM_Y2), (cx - 4.5, STEM_Y2 - 8), (cx + 4.5, STEM_Y2 - 8)], color))
check_fit(label, TINY_FONT, 240, "mark %s" % label)
add(txt(cx, MARK_Y, label, TINY_FONT, color, weight=700, anchor=anchor))
# ---- 回绕提示:39 的下一格是 0 ----
x_from = slot_c(DEPTH - 1)
x_to = slot_c(0)
add(line(x_from, WRAP_Y, x_to + 8, WRAP_Y, C_ROSE, 1.6, dash="7 5"))
add(tri([(x_to, WRAP_Y), (x_to + 9, WRAP_Y - 4.5), (x_to + 9, WRAP_Y + 4.5)], C_ROSE))
for ln in wrap(WRAP_NOTE, TINY_FONT, avail, 1, "wrap note"):
add(txt(PAD, WRAP_TEXT_Y, ln, TINY_FONT, C_ROSE, weight=700))
# ---- 三条设计不变量 ----
inner = CARD_W - 24
for i, (name, c, bg, body) in enumerate(CARDS):
x = PAD + i * (CARD_W + CARD_GAP)
add(rect(x, CARD_TOP, CARD_W, CARD_H, bg, c, rx=8, sw=1.8))
check_fit(name, CARD_FONT, inner, "card %d name" % i)
add(txt(x + 12, CARD_TOP + 24, name, CARD_FONT, INK, weight=700))
y = CARD_TOP + 46
for ln in wrap(body, BODY_FONT, inner, 5, "card %d body" % i):
add(txt(x + 12, y, ln, BODY_FONT, MUTED))
y += 15
# ---- 底部说明 ----
fy = CARD_TOP + CARD_H + 30
add(line(PAD, fy, RIGHT, fy, RULE, 1.0))
for i, ln in enumerate(wrap(FOOTNOTE, SUB_FONT, avail, 2, "foot wrap")):
add(txt(PAD, fy + 24 + 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:
emit("!! %d overflow warnings" % len(OVERFLOW))
if __name__ == "__main__":
build()