#!/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 ÷32(Q5) EDGE = 2 # 双沿捕获(CCAPM1=0x31) BASE = 32768 SENS_ENTER = 60 # 档1 进入表值(/65536) ALFA = 79 # Flt_Reg(FLT 拨码 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('&', '&').replace('<', '<').replace('>', '>') 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('%s' % (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('' % (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('' % (s, fill, stroke, w, op)) def rect(x, y, w_, h_, fill='#fff', stroke='none', rx=4, op=1.0, sw=1.2): add('' % (x, y, w_, h_, rx, fill, stroke, sw, op)) def dot(x, y, r=3.0, fill='#c1121f'): add('' % (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('' % (W, H)) txt(28, 44, 'PD132T 端到端时序图(检测通道:线圈 → CD4060 ÷32 → PCA 捕获 → 窗结账 → IIR → 基线 → 继电器)', 21, '#0b2c4d', weight='bold') txt(28, 70, '站点:线圈 100 kHz / 捕获时钟 3 MHz(12MHz÷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, 'Σ Xn(tick)', 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, '32768(BASE 标称)', 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 ms,IIR 主导', '⇒ 延迟 ≈ 窗平均(≤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 = ('' % (W, H, W, H) + '\n'.join(P) + '\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))