You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

163 lines
5.5 KiB

  1. # -*- coding: utf-8 -*-
  2. """逻辑分析仪波形 .bin → 时频曲线拟合。
  3. 文件格式(自动探测):
  4. 16 通道逻辑分析仪导出:每采样 2 字节(小端 uint16),bit0..15 = CH0..CH15。
  5. 信号通道 = 值 0x0100 的位(bit8),即字节流的奇数索引字节承载 CH8。
  6. 采样率默认 100MS/s(可用 --fs 覆盖;也可自动验证 2000/5000Hz 命中)。
  7. 用法:
  8. python plot_waveform.py <xxx.bin> [--fs 100e6] [--ch 8] [--out <png>]
  9. """
  10. import argparse
  11. import sys
  12. import numpy as np
  13. import matplotlib
  14. matplotlib.use("Agg")
  15. import matplotlib.pyplot as plt
  16. def extract_channel(m, ch):
  17. """从 2 字节/采样(小端 uint16)格式中提取通道 ch 的数字序列。"""
  18. # 目标位在 16bit 值中的字节位置:bit<8 → 低字节(偶索引);>=8 → 高字节(奇索引)
  19. if ch < 8:
  20. lo = m[0::2]
  21. return (lo >> ch) & 1
  22. hi = m[1::2]
  23. return (hi >> (ch - 8)) & 1
  24. def rising_edges(sig):
  25. """上升沿索引(数字序列)。"""
  26. return np.nonzero((sig[1:] == 1) & (sig[:-1] == 0))[0] + 1
  27. def main():
  28. ap = argparse.ArgumentParser()
  29. ap.add_argument("bin_path")
  30. ap.add_argument("--fs", type=float, default=None, help="采样率 Hz")
  31. ap.add_argument("--ch", type=int, default=0, help="信号通道号")
  32. ap.add_argument("--out", default=None, help="输出png路径")
  33. args = ap.parse_args()
  34. m = np.memmap(args.bin_path, dtype=np.uint8, mode="r")
  35. if len(m) % 2 != 0:
  36. sys.exit("odd file size, not 2-byte/sample format?")
  37. nsamp = len(m) // 2
  38. print("samples:", nsamp)
  39. ch = args.ch
  40. sig = extract_channel(m, ch)
  41. ones = int(sig.sum())
  42. print("CH%d: %d ones / %d samples (%.2f%%)"
  43. % (ch, ones, nsamp, 100.0 * ones / nsamp))
  44. nz = np.nonzero(sig)[0]
  45. if len(nz) == 0:
  46. sys.exit("channel has no signal")
  47. lo, hi = int(nz[0] * 0.97), int(nz[-1] * 1.03)
  48. print("signal range: sample %d .. %d" % (lo, hi))
  49. # ---- 采样率:自动验证(检查匀速段 2000/5000Hz 命中) ----
  50. if args.fs is None:
  51. candidates = [100e6, 50e6, 200e6, 25e6]
  52. best = []
  53. for fs in candidates:
  54. down = max(1, int(fs // 5000000))
  55. s = sig[lo:hi:down]
  56. r = rising_edges(s)
  57. if len(r) < 20:
  58. continue
  59. per = np.diff(r) * down / fs
  60. f = 1.0 / np.maximum(per, 1e-9)
  61. f = f[(f > 100) & (f < 20000)]
  62. if len(f) < 10:
  63. continue
  64. h, e = np.histogram(f, bins=400, range=(0, 20000))
  65. e2 = h[(e[:-1] >= 1940) & (e[:-1] <= 2060)].sum()
  66. e5 = h[(e[:-1] >= 4850) & (e[:-1] <= 5150)].sum()
  67. best.append(((e2 + e5) / max(h.sum(), 1), fs))
  68. best.sort(reverse=True)
  69. fs = best[0][1]
  70. print("sample-rate ranking:", ["%g:%.3f" % (f, s) for s, f in best])
  71. print("using fs = %g Hz" % fs)
  72. else:
  73. fs = float(args.fs)
  74. # ---- 上升沿 → 时频 ----
  75. r = rising_edges(sig[lo:hi])
  76. r = r + lo
  77. t_rise = r / fs
  78. per = np.diff(t_rise)
  79. freq = 1.0 / np.maximum(per, 1e-9)
  80. t_mid = (t_rise[:-1] + t_rise[1:]) / 2.0
  81. print("rising edges: %d" % len(r))
  82. print("first pulse @ %.3fs, last @ %.3fs" % (t_rise[0], t_rise[-1]))
  83. print("total pulses: %d" % len(r))
  84. print("average freq: %.1f Hz"
  85. % (len(r) / max(t_rise[-1] - t_rise[0], 1e-9)))
  86. # ---- 三段结构(按 10ms 桶取中位频率) ----
  87. buckets = np.arange(t_rise[0] - 0.05, t_rise[-1] + 0.05, 0.01)
  88. med = []
  89. bt = []
  90. for i in range(len(buckets) - 1):
  91. sel = (t_mid >= buckets[i]) & (t_mid < buckets[i + 1])
  92. if sel.sum() > 0:
  93. med.append(np.median(freq[sel]))
  94. bt.append((buckets[i] + buckets[i + 1]) / 2)
  95. med = np.array(med)
  96. bt = np.array(bt)
  97. active = med > 100
  98. edges = np.diff(active.astype(int))
  99. starts = bt[1:][edges == 1]
  100. stops = bt[1:][edges == -1]
  101. if len(bt) > 0 and active[0]:
  102. starts = np.concatenate(([bt[0]], starts))
  103. if len(bt) > 0 and active[-1]:
  104. stops = np.concatenate((stops, [bt[-1]]))
  105. print("segments:")
  106. for s, e in zip(starts, stops):
  107. sel = (t_rise >= s) & (t_rise <= e)
  108. n = sel.sum()
  109. print(" %.3f ~ %.3fs dur=%.3fs pulses=%d avg=%.0fHz"
  110. % (s, e, e - s, n, n / max(e - s, 1e-9)))
  111. for i in range(len(stops) - 1):
  112. print(" gap seg%d->%d: %.3fms" % (i + 1, i + 2,
  113. (starts[i + 1] - stops[i]) * 1e3))
  114. # ---- 画图 ----
  115. out = args.out or (args.bin_path.rsplit(".", 1)[0] + "_timefreq.png")
  116. fig, axes = plt.subplots(2, 1, figsize=(14, 9), sharex=True,
  117. gridspec_kw={"height_ratios": [1, 3]})
  118. ax = axes[0]
  119. step = max(1, int(fs // 200000))
  120. xs = np.arange(lo, hi, step) / fs
  121. ax.plot(xs, sig[lo:hi:step], lw=0.4, color="steelblue")
  122. ax.set_ylabel("CH%d raw" % ch)
  123. ax.set_yticks([0, 1])
  124. ax.grid(alpha=0.3)
  125. ax = axes[1]
  126. ax.plot(t_mid, freq, lw=0.8, color="crimson")
  127. ax.axhline(2000, color="gray", ls="--", lw=0.8)
  128. ax.axhline(5000, color="gray", ls="--", lw=0.8)
  129. ax.set_ylabel("instantaneous freq (Hz)")
  130. ax.set_xlabel("time (s)")
  131. ax.set_ylim(0, 6000)
  132. ax.grid(alpha=0.3)
  133. ax.legend(["freq", "2000Hz", "5000Hz"], loc="upper right", fontsize=8)
  134. fig.suptitle("CH%d time-frequency: %d pulses, fs=%g Hz, ch=%d"
  135. % (ch, len(r), fs, ch))
  136. fig.tight_layout()
  137. fig.savefig(out, dpi=130)
  138. print("saved:", out)
  139. if __name__ == "__main__":
  140. main()