# -*- coding: utf-8 -*- """ 逻辑分析仪时频曲线查看器(PLSR 项目用) 主视图:频率-时间曲线(每个脉冲的瞬时频率,阶梯线绘制) 辅助视图:原始波形(run 级绘制,大文件也流畅) 用法: python wave_viewer.py [采样率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 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()