使用pyserial、matplotlib与tkinter实时绘图时串口冻结问题求助
问题解决:Arduino串口实时绘图冻结问题
问题核心
串口读取与Matplotlib绘图更新都在tkinter主线程中执行,canvas.draw()是耗时的阻塞操作,再加上s.readline()的阻塞特性,导致主线程被持续占用,串口数据无法及时读取,最终出现冻结现象。
解决方案
1. 线程分离串口读取与GUI更新
用threading模块创建后台线程专门处理串口数据读取,通过queue.Queue传递解析后的数据,彻底避免主线程被阻塞。
2. 优化绘图更新
使用canvas.draw_idle()替代canvas.draw(),仅在GUI空闲时执行重绘,减少不必要的资源占用。
3. 非阻塞串口配置
设置串口超时时间,避免s.readline()因等待数据无限阻塞线程。
修改后的完整代码
import matplotlib from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.figure import Figure import pyautogui as gui import tkinter as tk import numpy as np import serial as sr import threading import queue matplotlib.use('TkAgg') # ------------全局变量 ------------# data_x = np.array([]) data_y = np.array([]) screen_width, screen_height = gui.size() cond = False data_queue = queue.Queue() # 线程间数据传递队列 def mapValues(num, x_or_y): maxValX = 1023 maxValY = 1023 if x_or_y == 1: proportion = num / maxValX new_val = round(screen_width * proportion) new_val = min(new_val, screen_width) else: proportion = num / maxValY new_val = round(screen_height * proportion) new_val = min(new_val, screen_height) return max(new_val, 0) def serial_read_thread(s): """后台线程:读取串口数据并放入队列""" global cond while True: if cond: try: data = s.readline().decode().strip() if data: coordinates = data.split() if len(coordinates) == 2: x_val = int(coordinates[0]) y_val = int(coordinates[1]) data_queue.put((x_val, y_val)) except Exception as e: print(f"串口读取错误: {e}") def plot_data(): global data_x, data_y len_line = 4 # 从队列批量获取数据更新 while not data_queue.empty(): x_raw, y_raw = data_queue.get() x_val = mapValues(x_raw, 1) y_val = mapValues(y_raw, 2) print(x_val, y_val) if len(data_x) < len_line: data_x = np.append(data_x, x_val) data_y = np.append(data_y, 320 - y_val) else: data_x[0:len_line-1] = data_x[1:len_line] data_x[len_line-1] = x_val data_y[0:len_line-1] = data_y[1:len_line] data_y[len_line-1] = 320 - y_val line1.set_xdata(data_x) line1.set_ydata(data_y) # 用draw_idle替代draw,减少主线程阻塞 canvas.draw_idle() root.after(20, plot_data) # 调整调用间隔,平衡实时性与性能 def start_plot(): global cond, s s.reset_input_buffer() cond = True def stop_plot(): global cond cond = False if __name__ == '__main__': print('Starting...') # ----------主GUI代码 --------------------# root = tk.Tk() root.title('Real Time Motus Hand') root.configure(background='black') app_width = 1200 app_height = 800 x_offset = (screen_width - app_width) / 2 y_offset = (screen_height - app_height) / 2 root.geometry(f'{app_width}x{app_height}+{int(x_offset)}+{int(y_offset)}') # ---------- 创建绘图对象 ----------# fig = Figure() ax = fig.add_subplot(111) ax.set_xlabel('x-axis') ax.set_ylabel('y-axis') ax.set_xlim(0, screen_width) ax.set_ylim(0, screen_height) line1 = ax.plot([], [])[0] plot_width = 800 plot_height = 600 canvas = FigureCanvasTkAgg(fig, master=root) canvas.get_tk_widget().place(x=(app_width - plot_width) / 2, y=(app_height - plot_height) / 2, width=plot_width, height=plot_height) canvas.draw() # ----------创建启停按钮 ------------- start = tk.Button(root, text='start drawing', font=('calibri', 12), command=start_plot) start.place(x=100, y=app_height - 50) stop = tk.Button(root, text='stop drawing', font=('calibri', 12), command=stop_plot) stop.place(x=300, y=app_height - 50) # --------启动串口通信 --------- s = sr.Serial('/dev/cu.usbmodem142201', 9600, timeout=0.1) # 设置超时,避免阻塞 s.reset_input_buffer() # 启动串口读取后台线程 threading.Thread(target=serial_read_thread, args=(s,), daemon=True).start() root.after(100, plot_data) root.mainloop()
关键修改说明
- 新增
serial_read_thread后台线程,独立处理串口数据读取与解析,将有效坐标放入队列,不占用主线程资源。 plot_data函数改为从队列获取数据,绘图更新用canvas.draw_idle(),仅在GUI空闲时重绘,降低主线程负载。- 串口初始化添加
timeout=0.1,避免s.readline()因等待数据无限阻塞。 - 调整
root.after()调用间隔,避免过于频繁触发更新,平衡实时性与程序稳定性。
内容的提问来源于stack exchange,提问作者JCrypp
相关产品推荐
相关产品推荐

