Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 
 
 

290 linhas
11 KiB

  1. # -*- coding: utf-8 -*-
  2. """
  3. 逻辑分析仪时频曲线查看器(PLSR 项目用)
  4. 主视图:频率-时间曲线(每个脉冲的瞬时频率,阶梯线绘制)
  5. 辅助视图:原始波形(run 级绘制,大文件也流畅)
  6. 用法:
  7. python wave_viewer.py <bin文件> [采样率Hz] [--thresh=阈值] [--nowave] [--nofreq] [--ymax=Hz]
  8. 示例:
  9. python wave_viewer.py "Document/PLSR_document/波形/10段.bin" 6250000
  10. python wave_viewer.py xxx.bin 6250000 --nowave # 只看时频曲线
  11. python wave_viewer.py xxx.bin 6250000 --ymax=12000 # 固定频率轴上限
  12. 数据格式: 每字节 1 个采样点;电平 >= 阈值(默认128) 判为高,否则为低。
  13. 频率定义: 每脉冲频率 = 采样率 / (该脉冲高电平+下一低电平的周期)。
  14. 交互:
  15. 滚轮 放大/缩小(以鼠标位置为中心)
  16. 左键拖拽 平移
  17. 双击 复位到全图
  18. 上子图 时频曲线(阶梯线 + 散点)
  19. 下子图 原始波形
  20. 红色虚线 长低电平(>=2ms,段间间隙/慢爬所在处)
  21. 自检模式(不弹窗,仅打印统计):
  22. python wave_viewer.py <bin文件> 6250000 --selftest
  23. 依赖: pip install numpy matplotlib
  24. """
  25. import sys
  26. import os
  27. import numpy as np
  28. # 中文字体(GUI 与无头渲染统一使用,避免文件名/标签中的中文缺字形)
  29. try:
  30. import matplotlib
  31. matplotlib.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei']
  32. matplotlib.rcParams['axes.unicode_minus'] = False
  33. except Exception:
  34. pass
  35. class WaveViewer:
  36. def __init__(self, path, fs, threshold=128):
  37. self.path = path
  38. self.fs = float(fs)
  39. self.threshold = threshold
  40. # 读取并二值化(0/1)
  41. data = np.fromfile(path, dtype=np.uint8)
  42. if data.size == 0:
  43. raise ValueError("文件为空")
  44. self.samples = (data >= threshold).astype(np.int8)
  45. self.total = int(self.samples.size)
  46. self.duration = self.total / self.fs
  47. # run 级压缩:连续相同电平段 (start_idx, end_idx, level)
  48. changes = np.flatnonzero(np.diff(self.samples) != 0) + 1
  49. starts = np.concatenate(([0], changes))
  50. ends = np.concatenate((changes, [self.total]))
  51. levels = self.samples[starts]
  52. self.runs = np.column_stack((starts, ends, levels))
  53. # 脉冲周期与频率:每个"高电平 run"与其后"低电平 run"配对
  54. self._build_pulses()
  55. def _build_pulses(self):
  56. hi = np.flatnonzero(self.runs[:, 2] == 1)
  57. observed_rise = self.runs[hi, 0]
  58. observed_rise = observed_rise[observed_rise > 0]
  59. self.pulse_count = len(observed_rise)
  60. # A period is measurable only between two observed rising edges. The
  61. # trailing capture idle after the final pulse is not part of a period.
  62. t_start = observed_rise[:-1]
  63. t_end = observed_rise[1:]
  64. period = t_end - t_start
  65. self.pulse_start = t_start / self.fs
  66. self.pulse_end = t_end / self.fs
  67. self.pulse_time = (t_start + t_end) / 2.0 / self.fs
  68. self.pulse_freq = self.fs / np.maximum(period, 1)
  69. # ---------- 交互 ----------
  70. def on_scroll(self, event):
  71. if event.inaxes not in (self.ax_wave, self.ax_freq):
  72. return
  73. if event.xdata is None:
  74. return
  75. x0, x1 = self.ax_wave.get_xlim()
  76. factor = 1.0 / 1.5 if event.button == 'up' else 1.5
  77. n0 = event.xdata - (event.xdata - x0) * factor
  78. n1 = event.xdata + (x1 - event.xdata) * factor
  79. if n1 - n0 < 5.0 / self.fs: # 最小窗口:5 个采样
  80. return
  81. self.ax_wave.set_xlim(max(n0, 0.0), min(n1, self.duration))
  82. self.redraw()
  83. def on_press(self, event):
  84. if event.inaxes in (self.ax_wave, self.ax_freq) and event.button == 1:
  85. self._press_x = event.xdata
  86. self._press_lim = self.ax_wave.get_xlim()
  87. def on_motion(self, event):
  88. if getattr(self, '_press_x', None) is None:
  89. return
  90. if event.inaxes not in (self.ax_wave, self.ax_freq) or event.xdata is None:
  91. return
  92. dx = event.xdata - self._press_x
  93. x0, x1 = self._press_lim
  94. n0 = x0 - dx
  95. n1 = x1 - dx
  96. if n1 - n0 < 1e-9:
  97. return
  98. self.ax_wave.set_xlim(n0, n1)
  99. self.redraw()
  100. def on_release(self, event):
  101. self._press_x = None
  102. def on_double_click(self, event):
  103. if event.dblclick:
  104. self.ax_wave.set_xlim(0.0, self.duration)
  105. self.redraw()
  106. def on_mouse_move_status(self, event):
  107. if event.xdata is None:
  108. self.status.set_text("")
  109. return
  110. # 鼠标处对应的频率(找最近的脉冲)
  111. near = np.searchsorted(self.pulse_time, event.xdata)
  112. txt = "t = %.6f s (%.1f ms)" % (event.xdata, event.xdata * 1000)
  113. for i in (near, near - 1):
  114. if 0 <= i < len(self.pulse_time) and abs(self.pulse_time[i] - event.xdata) < 0.05:
  115. txt += " f = %.0f Hz" % self.pulse_freq[i]
  116. break
  117. self.status.set_text(txt)
  118. # ---------- 绘制 ----------
  119. def redraw(self):
  120. x0, x1 = self.ax_wave.get_xlim()
  121. t0, t1 = min(x0, x1), max(x0, x1)
  122. i0 = max(int(t0 * self.fs), 0)
  123. i1 = min(int(t1 * self.fs), self.total)
  124. # ===== 主图:时频曲线 =====
  125. self.ax_freq.clear()
  126. pm = (self.pulse_time >= t0) & (self.pulse_time <= t1)
  127. if np.any(pm):
  128. t = self.pulse_time[pm]
  129. f = self.pulse_freq[pm]
  130. # 阶梯线:每个脉冲频率在 [pulse_start, pulse_end] 保持
  131. st = self.pulse_start[pm]
  132. en = self.pulse_end[pm]
  133. xs = np.empty(2 * len(t))
  134. ys = np.empty(2 * len(t))
  135. xs[0::2] = st
  136. xs[1::2] = en
  137. ys[0::2] = f
  138. ys[1::2] = f
  139. self.ax_freq.plot(xs, ys, color='C1', lw=1.0, alpha=0.9, zorder=2)
  140. self.ax_freq.plot(t, f, '.', ms=2.5, color='C3', zorder=3)
  141. # y 轴:自适应或固定
  142. if self.ymax > 0:
  143. self.ax_freq.set_ylim(0, self.ymax)
  144. else:
  145. fmax = np.max(f)
  146. self.ax_freq.set_ylim(0, fmax * 1.08)
  147. self.ax_freq.set_ylabel("frequency (Hz)")
  148. self.ax_freq.grid(True, alpha=0.3)
  149. self.ax_freq.set_title(
  150. "%s [%.4f, %.4f] s / total %.3f s, samples %d, pulses %d"
  151. % (os.path.basename(self.path), t0, t1, self.duration,
  152. self.total, self.pulse_count))
  153. # ===== 辅助图:原始波形 =====
  154. if self.show_wave:
  155. self.ax_wave.clear()
  156. rs, re = self.runs[:, 0], self.runs[:, 1]
  157. vis = (re >= i0) & (rs <= i1)
  158. if np.any(vis):
  159. s = np.maximum(rs[vis], i0)
  160. e = np.minimum(re[vis], i1)
  161. lv = self.runs[vis, 2]
  162. self.ax_wave.hlines(lv, s / self.fs, e / self.fs,
  163. color='C0', linewidth=1.0, zorder=2)
  164. self.ax_wave.hlines(0, t0, t1, color='C0', linewidth=1.0, zorder=1)
  165. self.ax_wave.set_ylim(-0.15, 1.15)
  166. self.ax_wave.set_ylabel("Y0")
  167. self.ax_wave.grid(True, alpha=0.3)
  168. # 长低电平(>= 2ms)红色虚线:段间间隙 / 慢爬
  169. low_w = (self.runs[:, 2] == 0) & ((self.runs[:, 1] - self.runs[:, 0]) / self.fs >= 0.002)
  170. li = np.flatnonzero(low_w & (self.runs[:, 1] >= i0) & (self.runs[:, 0] <= i1))
  171. for j in li:
  172. self.ax_wave.axvline(self.runs[j, 0] / self.fs,
  173. color='r', ls='--', lw=0.8, alpha=0.6, zorder=3)
  174. self.ax_wave.set_xlabel("time (s)")
  175. self.fig.canvas.draw_idle()
  176. def run(self):
  177. import matplotlib
  178. matplotlib.use('TkAgg')
  179. import matplotlib.pyplot as plt
  180. if self.show_wave:
  181. self.fig, (self.ax_freq, self.ax_wave) = plt.subplots(
  182. 2, 1, figsize=(14, 8), sharex=True, gridspec_kw={'height_ratios': [2, 1]})
  183. else:
  184. self.fig, self.ax_freq = plt.subplots(figsize=(14, 6))
  185. self.ax_wave = self.ax_freq # 占位,实际不绘制
  186. self.fig.subplots_adjust(bottom=0.08, top=0.93)
  187. self.status = self.fig.text(0.01, 0.01, "", fontsize=9)
  188. self._press_x = None
  189. self._press_lim = None
  190. self.ax_wave.set_xlim(0.0, self.duration)
  191. self.redraw()
  192. self.fig.canvas.mpl_connect('scroll_event', self.on_scroll)
  193. self.fig.canvas.mpl_connect('button_press_event', self.on_press)
  194. self.fig.canvas.mpl_connect('button_release_event', self.on_release)
  195. self.fig.canvas.mpl_connect('motion_notify_event', self.on_motion)
  196. self.fig.canvas.mpl_connect('motion_notify_event', self.on_mouse_move_status)
  197. self.fig.canvas.mpl_connect('button_press_event', self.on_double_click)
  198. print("打开窗口:滚轮缩放 / 左键拖拽平移 / 双击复位")
  199. print("波形总时长 %.3f s, 采样 %d, 检测到脉冲 %d 个"
  200. % (self.duration, self.total, self.pulse_count))
  201. plt.show()
  202. def selftest(self):
  203. import statistics
  204. print("文件: %s" % self.path)
  205. print("总采样: %d (%.3f s @ %.2f MS/s)" % (self.total, self.duration, self.fs / 1e6))
  206. print("高电平段: %d 低电平段: %d" % (
  207. int(np.sum(self.runs[:, 2] == 1)), int(np.sum(self.runs[:, 2] == 0))))
  208. print("检测到脉冲(上升沿): %d" % self.pulse_count)
  209. print("可测完整周期(相邻上升沿): %d" % len(self.pulse_freq))
  210. if len(self.pulse_freq):
  211. print("频率 min=%.0f max=%.0f 中位=%.0f Hz"
  212. % (np.min(self.pulse_freq), np.max(self.pulse_freq),
  213. statistics.median(self.pulse_freq)))
  214. low_w = (self.runs[:, 2] == 0) & ((self.runs[:, 1] - self.runs[:, 0]) / self.fs >= 0.002)
  215. print("长低电平(>=2ms)处数: %d" % int(np.sum(low_w)))
  216. def main():
  217. if len(sys.argv) < 2:
  218. print(__doc__)
  219. sys.exit(1)
  220. path = sys.argv[1]
  221. fs = 6.25e6
  222. thresh = 128
  223. show_wave = True
  224. ymax = 0.0
  225. selftest = False
  226. for a in sys.argv[1:]:
  227. if a == '--selftest':
  228. selftest = True
  229. elif a == '--nowave':
  230. show_wave = False
  231. elif a == '--nofreq':
  232. show_wave = True # 无此开关时保留波形视图(兼容旧参数名,忽略)
  233. elif a.startswith('--thresh='):
  234. thresh = int(a.split('=', 1)[1])
  235. elif a.startswith('--ymax='):
  236. ymax = float(a.split('=', 1)[1])
  237. elif a != path:
  238. try:
  239. fs = float(a)
  240. except ValueError:
  241. pass
  242. if not os.path.isfile(path):
  243. print("错误:文件不存在 - %s" % path)
  244. sys.exit(1)
  245. viewer = WaveViewer(path, fs, thresh)
  246. viewer.show_wave = show_wave
  247. viewer.ymax = ymax
  248. if selftest:
  249. viewer.selftest()
  250. else:
  251. viewer.run()
  252. if __name__ == '__main__':
  253. main()