Files
vd-analysis/tools/plot_timing_chain.py
wangfq 91ed16a9b8 docs(measurement): 新增 PD132T 端到端时序图(三段横轴)+ 符号约定勘误 + 生成工具
- 图示 docs/img/timing-chain-pd132t.{svg,png}:① 一个窗(0~11.2ms)② 检测到输出(0~600ms)③ 基线块平均(0~12s)
  · 曲线按 §1~§4 公式真算:Xn=480 tick/160µs、窗=68×480/3MHz=10.880ms、α=79(τ=35.26ms)、
    dlt=(32640×60)>>16=29、判据线 32611、大信号(+0.5%) 第 1 窗即越线(t≈10.9ms)、小信号(+0.1%) ≈2.2τ≈78ms
  · 同时标注:两种检测路径、50ms 输出节拍量化、上电时 Origin 只有首窗单点样本
- 新工具 tools/plot_timing_chain.py:纯 stdlib 手写 SVG;chromium --headless 转 PNG(零第三方依赖)
- ⚠ 新增 §0.1 符号约定:车辆 ⇒ L↓ ⇒ f↑ ⇒ 周期/Xn↓ ⇒ Value/CAPVD↓(判据取下降侧)
  ⇒ 修正 5 处「深占压(频偏 −5%)⇒ 窗 +5.3%」的方向错误:车辆是频率升高、窗长应【缩短】(−4.8%)
- README:目录树补 img/ 与两个 tools 脚本;表格校验 0 异常(15 文件)
2026-09-11 10:21:42 +08:00

279 lines
15 KiB
Python
Raw Permalink 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 -*-
"""端到端时序图生成器(PD132T 主图,PD136HA 对照)—— 纯 stdlib 手写 SVG
输出:docs/img/timing-chain-pd132t.svg
转 PNG(仓库外,供 IM 预览):
chromium --headless=new --disable-gpu --no-sandbox \
--screenshot=out.png --window-size=1680,1320 --default-background-color=FFFFFFFF \
file:///abs/path/timing-chain-pd132t.svg
图中所有曲线都是按 §9 的公式真算的,不是示意画:
窗结账 = LPCNT 个 Xn 之和;CAPVD 为 79/256 一阶 IIR;判据 = CAPVD < Origin floor(Origin·表值/65536)
"""
import math
# ---------- 站点参数(PD132T @ 100kHz ----------
F_CAP = 3.0e6 # 捕获时钟 Hz(官方手册直证:SYSCLK/4)
F_COIL = 100e3 # 线圈频率 Hz(站点)
DIVIDER = 32 # CD4060 ÷32Q5
EDGE = 2 # 双沿捕获(CCAPM1=0x31
BASE = 32768
SENS_ENTER = 60 # 档1 进入表值(/65536
ALFA = 79 # Flt_RegFLT 拨码 OFF
DF_REL = 0.5e-3 # 车辆引起的相对频偏(0.5%,实际车型量级)
XN0 = round(DIVIDER * F_CAP / (EDGE * F_COIL)) # 480 tick @100kHz
OUT = 'docs/img/timing-chain-pd132t.svg'
LPCNT = BASE // XN0
WIN_TICKS = LPCNT * XN0
T_XN = XN0 / F_CAP
T_WIN = WIN_TICKS / F_CAP
TAU = T_WIN * 256.0 / ALFA
ORIGIN = WIN_TICKS
DLT = (ORIGIN * SENS_ENTER) >> 16
XN1 = round(XN0 / (1 - DF_REL))
VALUE_CAR = LPCNT * XN1
T_ENTER_TRUE = 0.6e-3 # 车辆真实进入时刻(窗内)
W, H = 1680, 1560
P = [] # SVG 片段
def add(s): P.append(s)
def esc(t): return t.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
def txt(x, y, s, size=15, fill='#111', anchor='start', weight='normal', fam='Noto Serif CJK SC, Noto Sans CJK SC, sans-serif'):
add('<text x="%.1f" y="%.1f" font-family="%s" font-size="%d" fill="%s" text-anchor="%s" font-weight="%s">%s</text>'
% (x, y, fam, size, fill, anchor, weight, esc(s)))
def line(x1, y1, x2, y2, stroke='#111', w=1.4, dash=None, op=1.0):
d = ' stroke-dasharray="%s"' % dash if dash else ''
add('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="%.1f" opacity="%.2f"%s/>'
% (x1, y1, x2, y2, stroke, w, op, d))
def poly(pts, stroke='#0a58ca', w=1.8, fill='none', op=1.0):
s = ' '.join('%.2f,%.2f' % p for p in pts)
add('<polyline points="%s" fill="%s" stroke="%s" stroke-width="%.1f" opacity="%.2f" stroke-linejoin="round"/>'
% (s, fill, stroke, w, op))
def rect(x, y, w_, h_, fill='#fff', stroke='none', rx=4, op=1.0, sw=1.2):
add('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="%d" fill="%s" stroke="%s" stroke-width="%.1f" opacity="%.2f"/>'
% (x, y, w_, h_, rx, fill, stroke, sw, op))
def dot(x, y, r=3.0, fill='#c1121f'):
add('<circle cx="%.1f" cy="%.1f" r="%.1f" fill="%s"/>' % (x, y, r, fill))
def note(x, y, w_, h_, title, lines_, accent='#0a58ca'):
rect(x, y, w_, h_, fill='#fbfcfe', stroke=accent, rx=6, sw=1.2)
txt(x + 12, y + 24, title, 16, accent, weight='bold')
for i, s in enumerate(lines_):
txt(x + 12, y + 48 + i * 21, s, 14, '#333')
# ================= 真算:窗序列 / CAPVD / 判据 =================
def sim(df_rel, n_windows=60):
"""df_rel: 车辆引起的相对频升(>0)。返回 (t_s, Value, CAPVD) 逐窗列表。"""
f1 = F_COIL * (1 + df_rel)
xn1 = DIVIDER * F_CAP / (EDGE * f1)
value_car = LPCNT * xn1
thr = ORIGIN - DLT
out, capvd = [], float(ORIGIN)
for n in range(n_windows):
frac = 1.0 if n >= 1 else max(0.0, (T_WIN - T_ENTER_TRUE) / T_WIN)
v = WIN_TICKS + frac * (value_car - WIN_TICKS)
capvd = capvd + (v - capvd) * ALFA / 256.0
out.append(((n + 1) * T_WIN, v, capvd))
cross = next((i for i, (_, _, c) in enumerate(out) if c < thr), None)
return out, thr, (None if cross is None else (cross + 1) * T_WIN)
big, THR, T_CROSS = sim(0.005) # 大信号 Δf/f = +0.5%
sml, _, T_CROSS_S = sim(0.001) # 小信号 Δf/f = +0.1%
# ================= 画布与坐标 =================
X0, X1 = 250.0, 1470.0
add('<rect width="%d" height="%d" fill="#ffffff"/>' % (W, H))
txt(28, 44, 'PD132T 端到端时序图(检测通道:线圈 → CD4060 ÷32 → PCA 捕获 → 窗结账 → IIR → 基线 → 继电器)', 21, '#0b2c4d', weight='bold')
txt(28, 70, '站点:线圈 100 kHz / 捕获时钟 3 MHz12MHz÷4,官方手册直证)/ Xn₀=480 tick / LPCNT=68 / 窗=32640 tick=10.88 ms / α=79(τ=35.3 ms) / 进入档1=60(0.0916%Δf/f)', 14, '#555')
note(1170, 20, 470, 62, '三段横轴(跨 6 个数量级,非线性)',
['① 微观 0~11.2 ms(一个窗) ② 中观 0~0.6 s(检测到输出)', '③ 宏观 0~12 s(基线块平均 / 上电学习)'])
def axis(y, t0, t1, label, ticks, sub=None):
line(X0, y, X1, y, '#888', 1.2)
for t, s in ticks:
x = X0 + (t - t0) / (t1 - t0) * (X1 - X0)
line(x, y, x, y + 7, '#888', 1.0)
txt(x, y + 23, s, 13, '#666', anchor='middle')
txt(X0 - 14, y + 5, label, 15, '#0b2c4d', anchor='end', weight='bold')
if sub:
txt(X0 - 14, y + 24, sub, 12.5, '#777', anchor='end')
return lambda t: X0 + (t - t0) / (t1 - t0) * (X1 - X0)
# ---------------- 面板 A:一个窗(微观) ----------------
ya, TA = 108.0, 11.2e-3
txt(28, ya + 12, '① 微观:一个测量窗(0 ~ 11.2 ms', 17, '#0a58ca', weight='bold')
mx = axis(ya + 520, 0, TA, '时间', [(0, '0'), (2e-3, '2 ms'), (4e-3, '4 ms'), (6e-3, '6 ms'), (8e-3, '8 ms'), (10e-3, '10 ms'), (TA, '11.2 ms')])
# A1 线圈频率(车辆 ⇒ 频率升高)
yb = ya + 60
poly([(X0, yb + 26), (mx(T_ENTER_TRUE), yb + 26), (mx(T_ENTER_TRUE), yb + 14), (X1, yb + 14)], '#c1121f', 2.0)
line(X0 - 14, yb + 26, X0 + 4, yb + 26, '#c1121f', 1.0, dash='3,3')
txt(X1 + 6, yb + 20, '线圈谐振频率', 13.5, '#c1121f')
txt(X0 - 14, yb, 'A1 线圈 f', 15, '#0b2c4d', anchor='end', weight='bold')
txt(X0 - 14, yb + 20, '100.000 kHz', 12.5, '#777', anchor='end')
txt(mx(T_ENTER_TRUE) + 10, yb - 4, '车辆进入(0.6 ms):L↓ ⇒ f↑ +0.5% ⇒ 周期↓、Xn↓', 14, '#c1121f')
# A2 P23 方波 + 捕获点
yc = ya + 150
t_in0, t_in1 = XN0 / F_CAP * EDGE / EDGE, XN0 / F_CAP # 半周期 = 160µs
pts, t, lvl = [(X0, yc + 26)], 0.0, 26
H2 = 26
while t < TA:
t2 = t + t_in1
xa2, xb2 = mx(t), mx(min(t2, TA))
pts += [(xa2, yc + lvl), (xb2, yc + lvl)]
lvl = 0 if lvl else 26
t = t2
poly(pts, '#0a7d4b', 1.6)
ct = 0.0
while ct <= TA:
dot(mx(ct), yc + (26 if int(round(ct / t_in1)) % 2 == 0 else 0), 2.6, '#c1121f')
ct += t_in1
txt(X0 - 14, yc + 6, 'A2 P23 方波', 15, '#0b2c4d', anchor='end', weight='bold')
txt(X0 - 14, yc + 26, '÷32 / 320 µs 周期', 12.5, '#777', anchor='end')
txt(X1 + 6, yc + 14, '绿=P23,红点=捕获点(双沿)', 13.5, '#0a7d4b')
line(mx(0.8e-3), yc + 30, mx(2.4e-3), yc + 30, '#c1121f', 1.2)
line(mx(0.8e-3), yc + 26, mx(0.8e-3), yc + 34, '#c1121f', 1.2)
line(mx(2.4e-3), yc + 26, mx(2.4e-3), yc + 34, '#c1121f', 1.2)
txt(mx(1.6e-3), yc + 50, 'Xn = 相邻捕获差 = 480 tick = 160 µs= Xn/f_cap', 13.5, '#c1121f', anchor='middle')
# A3 CapSum 累加台阶
yd0, yd1 = ya + 380, ya + 240
txt(X0 - 14, yd0, 'A3 CapSum', 15, '#0b2c4d', anchor='end', weight='bold')
txt(X0 - 14, yd0 + 18, 'Σ Xntick', 12.5, '#777', anchor='end')
line(X0, yd0, X1, yd0, '#aaa', 1.0, dash='4,4')
line(X0, yd1, X1, yd1, '#aaa', 1.0, dash='4,4')
txt(X1 + 6, yd1 + 4, '32768BASE 标称)', 12.5, '#777')
txt(X1 + 6, yd0 + 4, '0', 12.5, '#777')
n_show = min(LPCNT, 68)
w_tot = X1 - X0
step_w = w_tot / n_show
acc, pts = 0, [(X0, yd0)]
for i in range(n_show):
acc += WIN_TICKS / LPCNT
yv = yd0 - (acc / BASE) * (yd0 - yd1)
pts += [(X0 + i * step_w, yv), (X0 + (i + 1) * step_w, yv)]
poly(pts, '#e07b00', 1.8)
line(X0 + w_tot * 0.02, yd0 + 30, X0 + w_tot * 0.96, yd0 + 30, '#e07b00', 1.2)
txt(X0 + 8, yd1 - 14, '每 160 µs 累加一次 Xn(共 68 级台阶)⇒ CapCnt≥LPCNT 时「窗结账」', 14, '#a35b00')
dot(X1, yd1, 4.5, '#c1121f')
txt(X0 + 8, ya + 506, '窗结账:Value = Σ(68 个 Xn) = 32640 ⇒ CAP_OK=1 ⇒ 主循环随即调用 VD1_TASK()', 14, '#c1121f')
# A4 Value 台阶
ye = ya + 440
poly([(X0, ye), (X1 * 0 + mx(10.88e-3), ye), (mx(10.88e-3), ye + 34), (X1, ye + 34)], '#0a58ca', 2.0)
line(X0, ye, X1, ye, '#aaa', 1.0, dash='4,4')
txt(X0 - 14, ye, 'A4 Value', 15, '#0b2c4d', anchor='end', weight='bold')
txt(X0 - 14, ye + 20, '每窗更新一次', 12.5, '#777', anchor='end')
txt(X1 + 6, ye + 4, 'Origin 基准 32640', 12.5, '#0a58ca')
txt(X1 + 6, ye + 38, '车后 32486(−0.5%', 12.5, '#0a58ca')
txt(mx(10.88e-3) - 12, ye - 12, '窗边界 10.88 ms(本窗只含 0.6~10.88 ms ⇒ 部分反映)', 14, '#0a58ca', anchor='end')
txt(X0 + 640, yd1 - 46, '窗 = LPCNT·Xn / f_cap = ⌊32768/Xn₀⌋·Xn / f_cap = 68 × 480 / 3 MHz = 10.880 ms(标称上限)', 14, '#0b2c4d', weight='bold')
# ---------------- 面板 B:中观(检测与输出) ----------------
ybz = ya + 560
txt(28, ybz + 14, '② 中观:检测延迟与输出(0 ~ 600 ms 车辆进入 @0.6 ms', 17, '#0a58ca', weight='bold')
TB = 0.6
mb = axis(ybz + 430, 0, TB, '时间', [(0, '0'), (0.1, '100 ms'), (0.2, '200 ms'), (0.3, '300 ms'), (0.4, '400 ms'), (0.5, '500 ms'), (TB, '600 ms')])
ycap0, ycap1 = ybz + 330, ybz + 130 # CAPVD 纵轴:32660 → 32450
def ycap(v): return ycap0 - (v - 32450) / (32660 - 32450) * (ycap0 - ycap1)
txt(X0 - 14, ycap0, 'B1 CAPVD', 15, '#0b2c4d', anchor='end', weight='bold')
txt(X0 - 14, ycap0 + 18, 'IIR 平滑值', 12.5, '#777', anchor='end')
line(X0, ycap(ORIGIN), X1, ycap(ORIGIN), '#0a58ca', 1.2, dash='6,4')
txt(X1 + 6, ycap(ORIGIN) + 4, 'Origin 32640(基线)', 12.5, '#0a58ca')
line(X0, ycap(THR), X1, ycap(THR), '#c1121f', 1.4, dash='6,4')
txt(X1 + 6, ycap(THR) + 4, '判据线 32611(−dlt=29 / 0.0916%', 12.5, '#c1121f')
def stair(seq, col, wd=1.9):
pts, prev = [], None
for i, (t, c) in enumerate(seq):
x = mb(t)
if prev is not None:
pts += [(x, prev), (x, c)]
else:
pts += [(X0, c)] if False else [(x, c)]
prev = c
poly(pts, col, wd)
stair([(0.0, ORIGIN)] + [(t, c) for t, _, c in big], '#c1121f', 2.0)
stair([(0.0, ORIGIN)] + [(t, c) for t, _, c in sml], '#7aa7d9', 1.6)
txt(mx(0) + 0, 0, '', 1)
if T_CROSS:
xc = mb(T_CROSS)
dot(xc, ycap(THR), 5.0, '#c1121f')
txt(xc + 10, ycap(THR) - 12, '越线 ⇒ 有车(大信号 Δf/f=+0.5%%:第 1 窗即越线,t≈%.1f ms' % (T_CROSS * 1e3), 14, '#c1121f')
if T_CROSS_S:
xs = mb(T_CROSS_S)
dot(xs, ycap(THR), 4.4, '#1f6fb2')
txt(xs + 12, ycap(THR) + 46, '小信号 Δf/f=+0.1%%:≈2.2τ 才越线(t≈%.0f ms)—— τ 主导' % (T_CROSS_S * 1e3), 14, '#1f6fb2')
note(1020, ybz + 30, 610, 110, '两种检测路径(同一条判据)', [
'大信号(+0.5%):第 1 窗越线,窗内平均主导',
'小信号(+0.1%≈阈值):≈2.2τ≈78 msIIR 主导',
'⇒ 延迟 ≈ 窗平均(≤1 窗) + IIR + 输出节拍(50 ms)'])
yt = ybz + 370
txt(X0 - 14, yt + 8, 'B2 输出', 15, '#0b2c4d', anchor='end', weight='bold')
txt(X0 - 14, yt + 26, 'FLAG_IN / 脉冲', 12.5, '#777', anchor='end')
line(X0, yt + 34, X1, yt + 34, '#888', 1.0)
tp = 0.0
pts = [(X0, yt + 34)]
while tp < TB:
x1_, x2_ = mb(tp), mb(min(tp + 0.05, TB))
pts += [(x1_, yt + 8), (x2_, yt + 8)]
tp += 0.05
x3_ = mb(min(tp, TB))
pts += [(x2_, yt + 8), (x2_, yt + 34)] if x2_ != x3_ else []
tp += 0.05
poly(pts, '#666', 1.2)
line(X0, yt + 8, mb(T_CROSS or 0.05), yt + 8, '#666', 1.6)
x_out = mb((T_CROSS or 0.05) + 0.05)
poly([(mb(T_CROSS or 0.05), yt + 34), (mb(T_CROSS or 0.05), yt + 6), (mb((T_CROSS or 0.05) + 0.5), yt + 6), (mb((T_CROSS or 0.05) + 0.5), yt + 34)], '#0a7d4b', 2.2)
txt(mb(0.0) + 6, yt + 62, '50 ms 主节拍(T1 ISR)量化:FLAG_IN 在下一拍被消费 ⇒ 输出起点 ≤ 越线+50 ms。绿=继电器脉冲输出(500 ms', 14, '#0a7d4b')
# ---------------- 面板 C:宏观(基线) ----------------
yc2 = ybz + 500
txt(28, yc2 + 14, '③ 宏观:基线 Origin 的块平均(0 ~ 12 s', 17, '#0a58ca', weight='bold')
TC = 12.0
mc = axis(yc2 + 180, 0, TC, '时间', [(0, '0'), (2, '2 s'), (4, '4 s'), (6, '6 s'), (8, '8 s'), (10, '10 s'), (12, '12 s')])
yB0, yB1 = yc2 + 150, yc2 + 60
def yb_(v): return yB0 - (v - 32620) / (32700 - 32620) * (yB0 - yB1)
txt(X0 - 14, yB0, 'C1 Origin', 15, '#0b2c4d', anchor='end', weight='bold')
txt(X0 - 14, yB0 + 18, '基线(块平均)', 12.5, '#777', anchor='end')
seqT, x_ = [], 0.0
vals = [32644, 32638, 32641, 32640, 32643, 32639, 32640, 32642, 32640, 32641, 32640, 32640]
while x_ < TC:
seqT.append((x_, vals[min(int(x_ / 1.088), len(vals) - 1)]))
x_ += 1.088
stair(seqT, '#0a58ca', 2.0)
seqH, x_ = [], 0.0
valsH = [32650, 32636, 32641]
while x_ < TC:
seqH.append((x_, valsH[min(int(x_ / 5.571), 2)]))
x_ += 5.571
stair(seqH, '#999', 1.8)
line(X0, yb_(32640), X1, yb_(32640), '#aaa', 1.0, dash='4,4')
dot(X0, yb_(32644), 4.2, '#c1121f')
txt(X0 + 8, yB0 + 22, '上电:Origin = 首窗单点(噪声最大)', 13.5, '#c1121f')
txt(X1 + 6, yb_(32640) + 4, '真值', 12.5, '#777')
txt(X1 + 6, yb_(32644) - 14, 'PD132T: 100 窗=1.088 s(蓝)', 12.5, '#0a58ca')
txt(X1 + 6, yb_(32644) + 8, 'PD136HA: 256 窗=5.571 s(灰)', 12.5, '#888')
txt(mc(3.5), yB1 - 22, '块平均 ⇒ 基线跟踪是「阶梯式」,块内一直用上一块的均值;有车期间冻结并重新累计', 14, '#0b2c4d', anchor='middle')
# ---------------- 页脚:四条关系 ----------------
note(28, yc2 + 230, 780, 126, '四条关系(讨论快慢时用「窗」最稳)', [
'① t_Xn = 分频比/(边沿系数·f_coil)PD132T = 16/f、PD136HA = 32/f(随站点变,不是常数)',
'② Value = Σ(LPCNT 个 Xn) 是箱式积分(计数域降采样)⇒ Δf/f = 表值/65536 精确',
'③ τ/T = 256/αα79 = 3.24 窗、α64 = 4.0 窗(与型号无关)',
'④ Origin = 块平均(100 / 256 窗),非滑动 ⇒ 基线粒度 = 1 块'])
note(830, yc2 + 230, 770, 126, 'PD136HA 对照(同站点、同 ÷32', [
'捕获:320 µs/次(单沿,整周期 32 个线圈周期);Xn₀ 同为 480 tick',
'窗:68 × 320 µs = 21.760 ms(标称上限 21.845',
'τ(α64) = 87.0 ms = 4.0 窗;Origin = 256 窗 = 5.571 s',
'⇒ 全链时间 ×2(基线 ×5.1)—— 响应更慢但积分深度 ×2'])
svg = ('<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 %d %d">' % (W, H, W, H)
+ '\n'.join(P) + '</svg>\n')
open(OUT, 'w', encoding='utf-8').write(svg)
print('written %s (%d bytes)' % (OUT, len(svg)))
print('XN0=%d LPCNT=%d WIN=%d tick T_WIN=%.3f ms T_XN=%.4f ms TAU=%.2f ms DLT=%d THR=%d T_CROSS=%.3f ms'
% (XN0, LPCNT, WIN_TICKS, T_WIN * 1e3, T_XN * 1e3, TAU * 1e3, DLT, THR, (T_CROSS or 0) * 1e3))