Files
STM32F4-Base/tools/arch_diagram/gen_sample_ring_start_svg.py
T
wandering1 e71d623e60 docs(algo): 5.3 节补窗首下标的圆周图(三种写法对照)
- 新增配图 采样环缓冲_窗首下标.svg:左侧 40 格圆环走一遍 head = 5、n = 20
  的「往回退 19 步」(跨过 0 与 39),右侧把直接减 / 16 位回绕后再取模 /
  先加 DEPTH 三种写法的结果并列,只有第三种落在 25。
- 与 5.2 的环形布局图分工:那张讲「窗在哪几格」,这张讲「下标怎么算出来」,
  不重复同一张图的语义。
- 生成器 gen_sample_ring_start_svg.py 顺带把 OVERFLOW 明细改到末尾统一打印
  (原先只在开头打,折行类告警只报计数、看不出是哪一条)。
- 门禁:doc-links --sections-strict 通过;id-check 新增行无未登记编号;
  generated-fresh 22 个生成器 17 一致、0 新漂移;产物连跑两次 sha1 不变。
2026-10-29 17:28:48 +08:00

317 lines
11 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 —— 窗首 start = head − n 为什么要先加一圈 DEPTH:
head 不够减时直接减会走到负数,取模就再也回不到环上。
为什么不用 Mermaid:本图要读的是"沿圆环往回退 19 格,途中跨过 0"这条**弧**,
以及三种写法算出来的三个**不同的数**。Mermaid 画不出圆周上的定位与角度,
盒子宽度也随文本变,"退半圈落在哪一格"这个关系就没了。
与 5.2 的环形布局图分工:那张讲「窗在哪几格」(线性排开 40 格 + 指针),
本图讲「这个下标怎么算出来」(圆周 + 三种写法对照),不重复。
用法(仓库根目录,任意 python3):
python tools/arch_diagram/gen_sample_ring_start_svg.py
输出:
docs/images/采样环缓冲_窗首下标.svg
改内容:示例(head / n)或三种写法的结论变了才重画;调坐标 / 文案编辑下方常量后重跑。
"""
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", "采样环缓冲_窗首下标.svg")
# ----------------------------- 画布与尺寸 -----------------------------
W = 1060
H = 600
PAD = 46
RIGHT = W - PAD
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
MONO = "Consolas, Cascadia Mono, DejaVu Sans Mono, monospace"
TITLE_FONT = 21
SUB_FONT = 13
BOX_FONT = 13
BODY_FONT = 11.5
TINY_FONT = 11
DOT_LABEL_FONT = 12
INK = "#0f172a"
LINE = "#334155"
MUTED = "#64748b"
FAINT = "#94a3b8"
RULE = "#cbd5e1"
WHITE = "#ffffff"
C_BLUE = "#2563eb"
BG_BLUE = "#eff6ff"
C_ROSE = "#e11d48"
BG_ROSE = "#fff1f2"
C_GREEN = "#16a34a"
BG_GREEN = "#f0fdf4"
C_AMBER = "#d97706"
# ----------------------------- 数据(改这里) -----------------------------
DEPTH = 40
WIN_N = 20
HEAD = 5 # 示例写指针:故意取"不够减"的位置
START = (HEAD + DEPTH - WIN_N) % DEPTH # = 25
TITLE = "窗首 = head − n:head 不够减会走到负数,所以先加一圈 DEPTH 再减"
SUB1 = ("例:head = 5、n = 20 —— 最新点在 4,沿环往回退 19 步落在 25,途中跨过 0 与 39。"
"DEPTH = 40 时 20 点窗正好占半圈,所以这条弧看起来是个半圆。")
SUB2 = "同一个例子三种写法,只有第三种同时做到「同余正确」与「不发生下溢」:"
ARC_LABEL = "往回退 19 步"
CENTER_L1 = "环 = 40 格"
CENTER_L2 = "窗 = 20 格(半圈)"
HEAD_LABEL = "head = 5"
START_LABEL = "start = 25"
# (公式, 算出来是多少, 判定, 逐条理由, 主色, 底色)
ROWS = [
("5 - 20", "= -15", "✗",
["负数。C99 的 % 向零截断,(-15) % 40 仍是 -15;",
"转成无符号就是 65521,拿去索引 s_ring 即越界读。"],
C_ROSE, BG_ROSE),
("(uint16_t)(5 - 20) % 40", "= 65521 % 40 = 1", "✗",
["65536 不是 40 的倍数:(x mod 65536) mod 40 ≠ x mod 40。",
"回绕后再取模不等价,读到的是环尾附近而不是窗首。"],
C_ROSE, BG_ROSE),
("(5 + 40 - 20) % 40", "= 25 % 40 = 25", "✓",
["就是窗首。加 40 不改同余类(40 ≡ 0 mod 40),",
"中间值恒在 [0, 2·DEPTH - 2],两种提升规则都不下溢。"],
C_GREEN, BG_GREEN),
]
FOOTNOTE = ("加 DEPTH 不是「凑数」:它是环的周长,加一整圈等价于原地不动——"
"但把减法从「可能为负」搬到了「必然非负」,取模才恢复成数学上的同余解。")
# ----------------------------- 布局 -----------------------------
CX = 296.0
CY = 352.0
R = 148.0
DOT_R = 5.0
DOT_R_HL = 6.0
BOX_X = 596
BOX_W = RIGHT - BOX_X
BOX_TOP = 186
BOX_H = 96
BOX_GAP = 14
NOTE_Y = 556
# ----------------------------- 工具函数 -----------------------------
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, mono=False):
a = "" if anchor is None else ' text-anchor="%s"' % anchor
w = "" if weight == 400 else ' font-weight="%d"' % weight
f = "" if not mono else ' font-family="%s"' % MONO
return ('<text x="%.1f" y="%.1f" font-size="%g"%s%s%s fill="%s">%s</text>'
% (x, y, fs, a, w, f, 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 circle(cx, cy, r, fill, stroke=None, sw=1.5):
s = "" if stroke is None else ' stroke="%s" stroke-width="%g"' % (stroke, sw)
return '<circle cx="%.1f" cy="%.1f" r="%g" fill="%s"%s/>' % (cx, cy, r, fill, s)
def tri(points, fill):
return '<polygon points="%s" fill="%s"/>' % (
" ".join("%.1f,%.1f" % (px, py) for px, py in points), fill)
def pt(r, deg):
"""圆上一点:deg 以 +x 轴为 0、屏幕坐标 y 向下(故 90° 在正下方)。"""
a = math.radians(deg)
return CX + r * math.cos(a), CY + r * math.sin(a)
def angle_of(idx):
"""第 idx 格的角度:0 格在正上方,顺时针每格 360/DEPTH 度。"""
return -90.0 + idx * (360.0 / DEPTH)
def arc_arrow(r, a_from, a_to, color, width):
"""从 a_from 逆时针画到 a_to(a_to < a_from),末端带箭头。"""
x0, y0 = pt(r, a_from)
x1, y1 = pt(r, a_to)
large = 1 if abs(a_to - a_from) > 180.0 else 0
d = "M %.1f %.1f A %.1f %.1f 0 %d 0 %.1f %.1f" % (x0, y0, r, r, large, x1, y1)
out = '<path d="%s" fill="none" stroke="%s" stroke-width="%g"/>' % (d, color, width)
# 末端切向:用 a_to 与其后方 1° 两点之差,得到"行进方向"
xb, yb = pt(r, a_to + 1.0)
vx, vy = x1 - xb, y1 - yb
n = math.hypot(vx, vy) or 1.0
ux, uy = vx / n, vy / n
hl = 10.0
bx, by = x1 - ux * hl, y1 - uy * hl
px, py = -uy, ux
out += tri([(x1, y1), (bx + px * 4.6, by + py * 4.6), (bx - px * 4.6, by - py * 4.6)], color)
return out
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 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)
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))
# ---- 左:40 格圆环 ----
win_lo = START
win_hi = HEAD - 1
add(circle(CX, CY, R, "none", RULE, 1.2))
for i in range(DEPTH):
x, y = pt(R, angle_of(i))
in_win = (i >= win_lo) or (i <= win_hi)
if i == HEAD:
add(circle(x, y, DOT_R_HL, C_AMBER))
elif in_win:
add(circle(x, y, DOT_R_HL, C_BLUE))
else:
add(circle(x, y, DOT_R, FAINT))
# 往回退的弧(贴着点圈内侧走,末端箭头落在 start 上)
add(arc_arrow(R - 18.0, angle_of(win_hi), angle_of(win_lo) - 360.0, C_ROSE, 1.8))
lx, ly = pt(R - 70.0, angle_of(33))
add(txt(lx, ly, ARC_LABEL, TINY_FONT, C_ROSE, weight=700, anchor="middle"))
# 圆周上的刻度名
for i in (0, 10, 20, 30):
x, y = pt(R + 22.0, angle_of(i))
anchor = "middle"
if i == 10:
anchor = "start"
elif i == 30:
anchor = "end"
add(txt(x, y + 4, str(i), DOT_LABEL_FONT, MUTED, anchor=anchor))
# head / start 两个关键点
hx, hy = pt(R + 26.0, angle_of(HEAD))
check_fit(HEAD_LABEL, TINY_FONT, 200, "head label")
add(txt(hx, hy, HEAD_LABEL, TINY_FONT, C_AMBER, weight=700, anchor="start"))
sx, sy = pt(R + 26.0, angle_of(START))
check_fit(START_LABEL, TINY_FONT, 200, "start label")
add(txt(sx, sy, START_LABEL, TINY_FONT, C_BLUE, weight=700, anchor="end"))
# 圆心说明
add(txt(CX, CY - 6, CENTER_L1, TINY_FONT, FAINT, anchor="middle"))
add(txt(CX, CY + 14, CENTER_L2, TINY_FONT, FAINT, anchor="middle"))
# ---- 右:三种写法 ----
inner = BOX_W - 32
for i, (formula, value, mark, why, c, bg) in enumerate(ROWS):
y = BOX_TOP + i * (BOX_H + BOX_GAP)
add(rect(BOX_X, y, BOX_W, BOX_H, bg, c, rx=8, sw=1.8))
add(txt(BOX_X + 14, y + 26, "%s %s" % (mark, formula), BOX_FONT, c, weight=700, mono=True))
add(txt(BOX_X + BOX_W - 14, y + 26, value, BOX_FONT, c, weight=700, anchor="end", mono=True))
yy = y + 48
for ln in why:
for sub in wrap(ln, BODY_FONT, inner, 1, "row %d why" % i):
add(txt(BOX_X + 14, yy, sub, BODY_FONT, MUTED))
yy += 16
# ---- 底部说明 ----
add(line(PAD, NOTE_Y - 26, RIGHT, NOTE_Y - 26, RULE, 1.0))
for i, ln in enumerate(wrap(FOOTNOTE, SUB_FONT, avail, 2, "foot wrap")):
add(txt(PAD, NOTE_Y + 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))
if __name__ == "__main__":
build()