You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 03:16:05