|
- # -*- coding: utf-8 -*-
- """
- 逻辑分析仪时频曲线查看器(PLSR 项目用)
-
- 主视图:频率-时间曲线(每个脉冲的瞬时频率,阶梯线绘制)
- 辅助视图:原始波形(run 级绘制,大文件也流畅)
-
- 用法:
- python wave_viewer.py <bin文件> [采样率Hz] [--thresh=阈值] [--nowave] [--nofreq] [--ymax=Hz]
- 示例:
- python wave_viewer.py "Document/PLSR_document/波形/10段.bin" 6250000
- python wave_viewer.py xxx.bin 6250000 --nowave # 只看时频曲线
- python wave_viewer.py xxx.bin 6250000 --ymax=12000 # 固定频率轴上限
-
- 数据格式: 每字节 1 个采样点;电平 >= 阈值(默认128) 判为高,否则为低。
- 频率定义: 每脉冲频率 = 采样率 / (该脉冲高电平+下一低电平的周期)。
-
- 交互:
- 滚轮 放大/缩小(以鼠标位置为中心)
- 左键拖拽 平移
- 双击 复位到全图
- 上子图 时频曲线(阶梯线 + 散点)
- 下子图 原始波形
- 红色虚线 长低电平(>=2ms,段间间隙/慢爬所在处)
-
- 自检模式(不弹窗,仅打印统计):
- python wave_viewer.py <bin文件> 6250000 --selftest
-
- 依赖: pip install numpy matplotlib
- """
-
- import sys
- import os
-
- import numpy as np
-
- # 中文字体(GUI 与无头渲染统一使用,避免文件名/标签中的中文缺字形)
- try:
- import matplotlib
- matplotlib.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei']
- matplotlib.rcParams['axes.unicode_minus'] = False
- except Exception:
- pass
-
-
- class WaveViewer:
- def __init__(self, path, fs, threshold=128):
- self.path = path
- self.fs = float(fs)
- self.threshold = threshold
-
- # 读取并二值化(0/1)
- data = np.fromfile(path, dtype=np.uint8)
- if data.size == 0:
- raise ValueError("文件为空")
- self.samples = (data >= threshold).astype(np.int8)
- self.total = int(self.samples.size)
- self.duration = self.total / self.fs
-
- # run 级压缩:连续相同电平段 (start_idx, end_idx, level)
- changes = np.flatnonzero(np.diff(self.samples) != 0) + 1
- starts = np.concatenate(([0], changes))
- ends = np.concatenate((changes, [self.total]))
- levels = self.samples[starts]
- self.runs = np.column_stack((starts, ends, levels))
-
- # 脉冲周期与频率:每个"高电平 run"与其后"低电平 run"配对
- self._build_pulses()
-
- def _build_pulses(self):
- hi = np.flatnonzero(self.runs[:, 2] == 1)
- observed_rise = self.runs[hi, 0]
- observed_rise = observed_rise[observed_rise > 0]
- self.pulse_count = len(observed_rise)
-
- # A period is measurable only between two observed rising edges. The
- # trailing capture idle after the final pulse is not part of a period.
- t_start = observed_rise[:-1]
- t_end = observed_rise[1:]
- period = t_end - t_start
- self.pulse_start = t_start / self.fs
- self.pulse_end = t_end / self.fs
- self.pulse_time = (t_start + t_end) / 2.0 / self.fs
- self.pulse_freq = self.fs / np.maximum(period, 1)
-
- # ---------- 交互 ----------
- def on_scroll(self, event):
- if event.inaxes not in (self.ax_wave, self.ax_freq):
- return
- if event.xdata is None:
- return
- x0, x1 = self.ax_wave.get_xlim()
- factor = 1.0 / 1.5 if event.button == 'up' else 1.5
- n0 = event.xdata - (event.xdata - x0) * factor
- n1 = event.xdata + (x1 - event.xdata) * factor
- if n1 - n0 < 5.0 / self.fs: # 最小窗口:5 个采样
- return
- self.ax_wave.set_xlim(max(n0, 0.0), min(n1, self.duration))
- self.redraw()
-
- def on_press(self, event):
- if event.inaxes in (self.ax_wave, self.ax_freq) and event.button == 1:
- self._press_x = event.xdata
- self._press_lim = self.ax_wave.get_xlim()
-
- def on_motion(self, event):
- if getattr(self, '_press_x', None) is None:
- return
- if event.inaxes not in (self.ax_wave, self.ax_freq) or event.xdata is None:
- return
- dx = event.xdata - self._press_x
- x0, x1 = self._press_lim
- n0 = x0 - dx
- n1 = x1 - dx
- if n1 - n0 < 1e-9:
- return
- self.ax_wave.set_xlim(n0, n1)
- self.redraw()
-
- def on_release(self, event):
- self._press_x = None
-
- def on_double_click(self, event):
- if event.dblclick:
- self.ax_wave.set_xlim(0.0, self.duration)
- self.redraw()
-
- def on_mouse_move_status(self, event):
- if event.xdata is None:
- self.status.set_text("")
- return
- # 鼠标处对应的频率(找最近的脉冲)
- near = np.searchsorted(self.pulse_time, event.xdata)
- txt = "t = %.6f s (%.1f ms)" % (event.xdata, event.xdata * 1000)
- for i in (near, near - 1):
- if 0 <= i < len(self.pulse_time) and abs(self.pulse_time[i] - event.xdata) < 0.05:
- txt += " f = %.0f Hz" % self.pulse_freq[i]
- break
- self.status.set_text(txt)
-
- # ---------- 绘制 ----------
- def redraw(self):
- x0, x1 = self.ax_wave.get_xlim()
- t0, t1 = min(x0, x1), max(x0, x1)
- i0 = max(int(t0 * self.fs), 0)
- i1 = min(int(t1 * self.fs), self.total)
-
- # ===== 主图:时频曲线 =====
- self.ax_freq.clear()
- pm = (self.pulse_time >= t0) & (self.pulse_time <= t1)
- if np.any(pm):
- t = self.pulse_time[pm]
- f = self.pulse_freq[pm]
- # 阶梯线:每个脉冲频率在 [pulse_start, pulse_end] 保持
- st = self.pulse_start[pm]
- en = self.pulse_end[pm]
- xs = np.empty(2 * len(t))
- ys = np.empty(2 * len(t))
- xs[0::2] = st
- xs[1::2] = en
- ys[0::2] = f
- ys[1::2] = f
- self.ax_freq.plot(xs, ys, color='C1', lw=1.0, alpha=0.9, zorder=2)
- self.ax_freq.plot(t, f, '.', ms=2.5, color='C3', zorder=3)
- # y 轴:自适应或固定
- if self.ymax > 0:
- self.ax_freq.set_ylim(0, self.ymax)
- else:
- fmax = np.max(f)
- self.ax_freq.set_ylim(0, fmax * 1.08)
- self.ax_freq.set_ylabel("frequency (Hz)")
- self.ax_freq.grid(True, alpha=0.3)
- self.ax_freq.set_title(
- "%s [%.4f, %.4f] s / total %.3f s, samples %d, pulses %d"
- % (os.path.basename(self.path), t0, t1, self.duration,
- self.total, self.pulse_count))
-
- # ===== 辅助图:原始波形 =====
- if self.show_wave:
- self.ax_wave.clear()
- rs, re = self.runs[:, 0], self.runs[:, 1]
- vis = (re >= i0) & (rs <= i1)
- if np.any(vis):
- s = np.maximum(rs[vis], i0)
- e = np.minimum(re[vis], i1)
- lv = self.runs[vis, 2]
- self.ax_wave.hlines(lv, s / self.fs, e / self.fs,
- color='C0', linewidth=1.0, zorder=2)
- self.ax_wave.hlines(0, t0, t1, color='C0', linewidth=1.0, zorder=1)
- self.ax_wave.set_ylim(-0.15, 1.15)
- self.ax_wave.set_ylabel("Y0")
- self.ax_wave.grid(True, alpha=0.3)
-
- # 长低电平(>= 2ms)红色虚线:段间间隙 / 慢爬
- low_w = (self.runs[:, 2] == 0) & ((self.runs[:, 1] - self.runs[:, 0]) / self.fs >= 0.002)
- li = np.flatnonzero(low_w & (self.runs[:, 1] >= i0) & (self.runs[:, 0] <= i1))
- for j in li:
- self.ax_wave.axvline(self.runs[j, 0] / self.fs,
- color='r', ls='--', lw=0.8, alpha=0.6, zorder=3)
- self.ax_wave.set_xlabel("time (s)")
- self.fig.canvas.draw_idle()
-
- def run(self):
- import matplotlib
- matplotlib.use('TkAgg')
- import matplotlib.pyplot as plt
-
- if self.show_wave:
- self.fig, (self.ax_freq, self.ax_wave) = plt.subplots(
- 2, 1, figsize=(14, 8), sharex=True, gridspec_kw={'height_ratios': [2, 1]})
- else:
- self.fig, self.ax_freq = plt.subplots(figsize=(14, 6))
- self.ax_wave = self.ax_freq # 占位,实际不绘制
- self.fig.subplots_adjust(bottom=0.08, top=0.93)
- self.status = self.fig.text(0.01, 0.01, "", fontsize=9)
-
- self._press_x = None
- self._press_lim = None
- self.ax_wave.set_xlim(0.0, self.duration)
- self.redraw()
-
- self.fig.canvas.mpl_connect('scroll_event', self.on_scroll)
- self.fig.canvas.mpl_connect('button_press_event', self.on_press)
- self.fig.canvas.mpl_connect('button_release_event', self.on_release)
- self.fig.canvas.mpl_connect('motion_notify_event', self.on_motion)
- self.fig.canvas.mpl_connect('motion_notify_event', self.on_mouse_move_status)
- self.fig.canvas.mpl_connect('button_press_event', self.on_double_click)
-
- print("打开窗口:滚轮缩放 / 左键拖拽平移 / 双击复位")
- print("波形总时长 %.3f s, 采样 %d, 检测到脉冲 %d 个"
- % (self.duration, self.total, self.pulse_count))
- plt.show()
-
- def selftest(self):
- import statistics
- print("文件: %s" % self.path)
- print("总采样: %d (%.3f s @ %.2f MS/s)" % (self.total, self.duration, self.fs / 1e6))
- print("高电平段: %d 低电平段: %d" % (
- int(np.sum(self.runs[:, 2] == 1)), int(np.sum(self.runs[:, 2] == 0))))
- print("检测到脉冲(上升沿): %d" % self.pulse_count)
- print("可测完整周期(相邻上升沿): %d" % len(self.pulse_freq))
- if len(self.pulse_freq):
- print("频率 min=%.0f max=%.0f 中位=%.0f Hz"
- % (np.min(self.pulse_freq), np.max(self.pulse_freq),
- statistics.median(self.pulse_freq)))
- low_w = (self.runs[:, 2] == 0) & ((self.runs[:, 1] - self.runs[:, 0]) / self.fs >= 0.002)
- print("长低电平(>=2ms)处数: %d" % int(np.sum(low_w)))
-
-
- def main():
- if len(sys.argv) < 2:
- print(__doc__)
- sys.exit(1)
- path = sys.argv[1]
- fs = 6.25e6
- thresh = 128
- show_wave = True
- ymax = 0.0
- selftest = False
- for a in sys.argv[1:]:
- if a == '--selftest':
- selftest = True
- elif a == '--nowave':
- show_wave = False
- elif a == '--nofreq':
- show_wave = True # 无此开关时保留波形视图(兼容旧参数名,忽略)
- elif a.startswith('--thresh='):
- thresh = int(a.split('=', 1)[1])
- elif a.startswith('--ymax='):
- ymax = float(a.split('=', 1)[1])
- elif a != path:
- try:
- fs = float(a)
- except ValueError:
- pass
- if not os.path.isfile(path):
- print("错误:文件不存在 - %s" % path)
- sys.exit(1)
- viewer = WaveViewer(path, fs, thresh)
- viewer.show_wave = show_wave
- viewer.ymax = ymax
- if selftest:
- viewer.selftest()
- else:
- viewer.run()
-
-
- if __name__ == '__main__':
- main()
|