基于Matplotlib的STM32示波器可视化:时基与性能优化问询
解决Matplotlib示波器的时基缩放与性能优化问题
一、时基缩放实现(优先级高)
要解决信号细节丢失的问题,核心是维护固定容量的数据缓存,并结合Matplotlib的交互式导航工具,根据用户的缩放/平移操作动态截取对应时间范围的数据绘制。
关键思路
- 预计算时间轴:根据STM32的ADC采样率,生成每个采样点对应的时间戳(例如采样率
fs=100000Hz时,单帧时间间隔dt=1/fs)。 - 环形数据缓存:用固定长度数组存储最新N个采样点(如N=20000),既避免内存溢出,又能回溯历史数据。
- 绑定坐标轴变化事件:当用户调整X轴范围时,自动筛选缓存中对应时间区间的数据,更新波形显示。
代码实现片段
import numpy as np import matplotlib.pyplot as plt import serial import threading # 硬件配置 SERIAL_PORT = 'COM3' BAUD_RATE = 115200 FS = 100000 # 对应STM32的ADC采样率 DT = 1 / FS BUFFER_SIZE = 20000 # 缓存采样点数 # 初始化数据缓存 time_buf = np.zeros(BUFFER_SIZE) ch1_buf = np.zeros(BUFFER_SIZE) ch2_buf = np.zeros(BUFFER_SIZE) ch3_buf = np.zeros(BUFFER_SIZE) buf_idx = 0 # 缓存写入指针 # 串口同步(等待8个0xAA) ser = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=1) sync_data = b'' while len(sync_data) < 8: sync_data += ser.read(8 - len(sync_data)) if sync_data != b'\xaa'*8: raise ValueError("设备同步失败,请检查连接") # 串口数据读取线程 def read_serial(): global buf_idx while True: # 读取3通道ADC数据(假设每通道2字节小端、12位精度) raw_frame = ser.read(6) if len(raw_frame) != 6: continue # 解析并转换为电压(参考电压3.3V) ch1 = int.from_bytes(raw_frame[0:2], 'little') & 0xFFF ch2 = int.from_bytes(raw_frame[2:4], 'little') & 0xFFF ch3 = int.from_bytes(raw_frame[4:6], 'little') & 0xFFF # 更新缓存 time_buf[buf_idx] = buf_idx * DT ch1_buf[buf_idx] = ch1 * 3.3 / 4095 ch2_buf[buf_idx] = ch2 * 3.3 / 4095 ch3_buf[buf_idx] = ch3 * 3.3 / 4095 buf_idx = (buf_idx + 1) % BUFFER_SIZE threading.Thread(target=read_serial, daemon=True).start() # 初始化绘图 fig, ax = plt.subplots(figsize=(10,6)) line1, = ax.plot([], [], label='CH1', linewidth=1) line2, = ax.plot([], [], label='CH2', linewidth=1) line3, = ax.plot([], [], label='CH3', linewidth=1) ax.set_ylabel('电压(V)') ax.set_xlabel('时间(s)') ax.legend() ax.set_ylim(0, 3.3) # 时基缩放回调:X轴范围变化时更新显示数据 def update_plot(event): if event.inaxes != ax: return x_min, x_max = ax.get_xlim() # 筛选缓存中符合时间范围的数据 mask = (time_buf >= x_min) & (time_buf <= x_max) if np.any(mask): line1.set_data(time_buf[mask], ch1_buf[mask]) line2.set_data(time_buf[mask], ch2_buf[mask]) line3.set_data(time_buf[mask], ch3_buf[mask]) fig.canvas.draw_idle() # 绑定X轴变化事件 fig.canvas.mpl_connect('xlim_changed', update_plot) # 初始显示最新1秒数据 ax.set_xlim(max(0, buf_idx*DT - 1), buf_idx*DT) update_plot(None) # 启用Matplotlib原生导航工具栏(支持缩放、平移) plt.show()
二、性能优化方案
针对可视化速度跟不上串口传输的问题,从渲染机制和数据处理两方面优化:
1. 渲染优化:使用Blitting位图缓存
Blitting仅重绘画布中变化的区域,大幅降低渲染开销。修改绘图更新逻辑:
# 初始化时缓存背景 fig.canvas.draw() background = fig.canvas.copy_from_bbox(ax.bbox) def update_plot_blit(): global background # 恢复背景 fig.canvas.restore_region(background) # 更新线条数据 x_min, x_max = ax.get_xlim() mask = (time_buf >= x_min) & (time_buf <= x_max) line1.set_data(time_buf[mask], ch1_buf[mask]) line2.set_data(time_buf[mask], ch2_buf[mask]) line3.set_data(time_buf[mask], ch3_buf[mask]) # 重绘线条 ax.draw_artist(line1) ax.draw_artist(line2) ax.draw_artist(line3) # 更新画布 fig.canvas.blit(ax.bbox) fig.canvas.flush_events() # 缩放时更新背景缓存 def on_xlim_change(event): global background fig.canvas.draw() background = fig.canvas.copy_from_bbox(ax.bbox) update_plot_blit() fig.canvas.mpl_connect('xlim_changed', on_xlim_change) # 定时更新波形(30ms/次,平衡流畅度与性能) import time def animate(): while True: update_plot_blit() time.sleep(0.03) threading.Thread(target=animate, daemon=True).start()
2. 数据处理优化
- 批量解析串口数据:避免逐帧读取解析,一次性读取多帧后用numpy批量处理,减少IO调用次数:
def read_serial(): global buf_idx while True: raw_data = ser.read_all() if len(raw_data) < 6: continue # 按6字节为一组批量解析 num_frames = len(raw_data) // 6 frames = raw_data[:num_frames*6] data = np.frombuffer(frames, dtype=np.uint16).reshape(-1,3) data = data & 0xFFF # 保留12位有效数据 volts = data * 3.3 / 4095 # 批量更新缓存 for i in range(num_frames): time_buf[buf_idx] = buf_idx * DT ch1_buf[buf_idx] = volts[i,0] ch2_buf[buf_idx] = volts[i,1] ch3_buf[buf_idx] = volts[i,2] buf_idx = (buf_idx + 1) % BUFFER_SIZE
3. 其他优化点
- 关闭非必要渲染元素:
ax.grid(False),减少绘图负担。 - 复用绘图对象:始终使用初始化时创建的
line1/2/3,避免重复创建新的Line2D实例。 - 降低线条宽度:设置
linewidth=1,减少绘制复杂度。
内容的提问来源于stack exchange,提问作者LimetaPeta
相关产品推荐
相关产品推荐

