Raspberry Pi 4B基于Python Turtle的蚀刻素描项目延迟问题求助
问题
在Raspberry Pi 4B上用Python3结合Turtle库制作蚀刻素描项目,通过旋转编码器控制绘图。项目能正常运行,但随着时间推移,线条会出现延迟,控制难度增加,全屏模式下问题更严重。
已尝试调整screen.ontimer轮询间隔、用Turtle内置函数替代全局变量,问题仍未解决。当前系统内存占用750MB(同时运行Firefox和该程序),负载14%,程序无报错。代码如下:
#!/usr/bin/python3 import RPi.GPIO as GPIO import turtle # Setup GPIO clearBtn = 12 liftBtn = 16 ColorBtn = 20 # GPIO pins for the encoders encoder1_pins = (24, 23) # A and B pins for encoder 1 (x-axis) encoder2_pins = (14, 15) # A and B pins for encoder 2 (y-axis) GPIO.setmode(GPIO.BCM) GPIO.setup(clearBtn, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(liftBtn, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(ColorBtn, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(encoder1_pins[0], GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(encoder1_pins[1], GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(encoder2_pins[0], GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(encoder2_pins[1], GPIO.IN, pull_up_down=GPIO.PUD_UP) # Variables to store encoder state encoder1_last_state = (GPIO.input(encoder1_pins[0]) << 1) | GPIO.input(encoder1_pins[1]) encoder2_last_state = (GPIO.input(encoder2_pins[0]) << 1) | GPIO.input(encoder2_pins[1]) # Encoder positions x_coord = 0 y_coord = 0 penState = False # False for pen down, True for pen up i = 0 # index of current color # Set up the turtle screen screen = turtle.Screen() #screen.setup(width=1.0, height=1.0) # Set the window to full-screen mode canvas = screen.getcanvas() # Create a RawTurtle object with the canvas raw_turtle = turtle.RawTurtle(canvas) raw_turtle.shape('circle') raw_turtle.shapesize(0.5) # Cursor size: 0.5 is half of normal raw_turtle.width(6) # Line width raw_turtle.speed(0) #BUTTON FUNCTIONS #toggle though colors def Change_Color(channel): global i colors = [(1, 0, 0), (0, 1, 0), (0, 0, 1), (0, 1, 1), (1, 0, 1), (1, 1, 0), (1, 1, 1), (0, 0, 0)] # List of RGB colors raw_turtle.pencolor(colors[i % len(colors)]) # Set pen color based on the current index of the length of the color list i += 1 def ClearScrnBtn(channel): raw_turtle.reset() # Reset the turtle, resets everything including point size raw_turtle.shapesize(0.5) # Cursor size: 0.5 is half of normal raw_turtle.width(6) # Callback function to lift or lower the pen def lift_pen(channel): global penState penState = not penState if penState: raw_turtle.penup() else: raw_turtle.pendown() # Callback function for encoder 1 (x-axis) def encoder1_callback(channel): global encoder1_last_state, x_coord a = GPIO.input(encoder1_pins[0]) b = GPIO.input(encoder1_pins[1]) new_state = (a << 1) | b if (encoder1_last_state == 0b00 and new_state == 0b10) or (encoder1_last_state == 0b11 and new_state == 0b01): x_coord += 5 elif (encoder1_last_state == 0b10 and new_state == 0b00) or (encoder1_last_state == 0b01 and new_state == 0b11): x_coord -= 5 encoder1_last_state = new_state # Callback function for encoder 2 (y-axis) def encoder2_callback(channel): global encoder2_last_state, y_coord a = GPIO.input(encoder2_pins[0]) b = GPIO.input(encoder2_pins[1]) new_state = (a << 1) | b if (encoder2_last_state == 0b00 and new_state == 0b10) or (encoder2_last_state == 0b11 and new_state == 0b01): y_coord -= 5 elif (encoder2_last_state == 0b10 and new_state == 0b00) or (encoder2_last_state == 0b01 and new_state == 0b11): y_coord += 5 encoder2_last_state = new_state # Add event detection for encoder 1 (x-axis) GPIO.add_event_detect(encoder1_pins[0], GPIO.BOTH, callback=encoder1_callback) GPIO.add_event_detect(encoder1_pins[1], GPIO.BOTH, callback=encoder1_callback) # Add event detection for encoder 2 (y-axis) GPIO.add_event_detect(encoder2_pins[0], GPIO.BOTH, callback=encoder2_callback) GPIO.add_event_detect(encoder2_pins[1], GPIO.BOTH, callback=encoder2_callback) # Add event listener to the button pins GPIO.add_event_detect(clearBtn, GPIO.FALLING, callback=ClearScrnBtn, bouncetime=300) GPIO.add_event_detect(liftBtn, GPIO.FALLING, callback=lift_pen, bouncetime=300) GPIO.add_event_detect(ColorBtn, GPIO.FALLING, callback=Change_Color, bouncetime=300) # Function to update the turtle's position based on encoder callbacks def update_position(): global x_coord, y_coord screen.title(f"X: {x_coord}, Y: {y_coord}") raw_turtle.goto(x_coord, y_coord) screen.ontimer(update_position, 100) # Schedule the next update # Start updating the position update_position() # Start the turtle main loop turtle.mainloop() # Clean up GPIO on keyboard interrupt GPIO.cleanup() #turtle.done() #not mentioned in turtle documentation for raw
解决方案
1. 用线程安全队列传递编码器增量,避免全局变量竞态
GPIO回调运行在后台线程,直接修改全局坐标变量可能导致竞态,且turtle操作必须在主线程执行。改用队列传递坐标增量,主线程处理移动:
- 导入
queue模块,创建两个队列存储x、y方向的增量 - 修改编码器回调,将增量放入队列而非直接修改全局坐标
- 在
update_position中读取队列,累加增量并移动乌龟
2. 关闭Turtle动画追踪,手动控制刷新
全屏模式下频繁自动重绘会增加负载,关闭动画追踪后手动刷新,提升响应速度:
- 调用
screen.tracer(0)关闭自动动画 - 每次移动乌龟后调用
screen.update()刷新画面 - 注意
raw_turtle.reset()会重置追踪状态,需在ClearScrnBtn中重新设置screen.tracer(0)
3. 优化位置更新逻辑,减少不必要的绘图操作
只在有坐标增量时才移动乌龟,避免定时空操作:
- 修改
update_position,检查队列是否有增量,没有则直接调度下一次检查,减少无效的goto调用
修改后的完整代码
#!/usr/bin/python3 import RPi.GPIO as GPIO import turtle import queue # Setup GPIO clearBtn = 12 liftBtn = 16 ColorBtn = 20 # GPIO pins for the encoders encoder1_pins = (24, 23) # A and B pins for encoder 1 (x-axis) encoder2_pins = (14, 15) # A and B pins for encoder 2 (y-axis) GPIO.setmode(GPIO.BCM) GPIO.setup(clearBtn, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(liftBtn, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(ColorBtn, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(encoder1_pins[0], GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(encoder1_pins[1], GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(encoder2_pins[0], GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(encoder2_pins[1], GPIO.IN, pull_up_down=GPIO.PUD_UP) # Variables to store encoder state encoder1_last_state = (GPIO.input(encoder1_pins[0]) << 1) | GPIO.input(encoder1_pins[1]) encoder2_last_state = (GPIO.input(encoder2_pins[0]) << 1) | GPIO.input(encoder2_pins[1]) # Thread-safe queues for coordinate increments x_queue = queue.Queue() y_queue = queue.Queue() penState = False # False for pen down, True for pen up i = 0 # index of current color # Set up the turtle screen screen = turtle.Screen() #screen.setup(width=1.0, height=1.0) # Uncomment for full-screen screen.tracer(0) # Disable automatic animation canvas = screen.getcanvas() # Create a RawTurtle object with the canvas raw_turtle = turtle.RawTurtle(canvas) raw_turtle.shape('circle') raw_turtle.shapesize(0.5) raw_turtle.width(6) raw_turtle.speed(0) # BUTTON FUNCTIONS def Change_Color(channel): global i colors = [(1, 0, 0), (0, 1, 0), (0, 0, 1), (0, 1, 1), (1, 0, 1), (1, 1, 0), (1, 1, 1), (0, 0, 0)] raw_turtle.pencolor(colors[i % len(colors)]) i += 1 screen.update() # Refresh after color change def ClearScrnBtn(channel): raw_turtle.reset() raw_turtle.shapesize(0.5) raw_turtle.width(6) screen.tracer(0) # Re-enable tracer after reset screen.update() def lift_pen(channel): global penState penState = not penState if penState: raw_turtle.penup() else: raw_turtle.pendown() screen.update() # ENCODER CALLBACKS def encoder1_callback(channel): global encoder1_last_state a = GPIO.input(encoder1_pins[0]) b = GPIO.input(encoder1_pins[1]) new_state = (a << 1) | b if (encoder1_last_state == 0b00 and new_state == 0b10) or (encoder1_last_state == 0b11 and new_state == 0b01): x_queue.put(5) elif (encoder1_last_state == 0b10 and new_state == 0b00) or (encoder1_last_state == 0b01 and new_state == 0b11): x_queue.put(-5) encoder1_last_state = new_state def encoder2_callback(channel): global encoder2_last_state a = GPIO.input(encoder2_pins[0]) b = GPIO.input(encoder2_pins[1]) new_state = (a << 1) | b if (encoder2_last_state == 0b00 and new_state == 0b10) or (encoder2_last_state == 0b11 and new_state == 0b01): y_queue.put(-5) elif (encoder2_last_state == 0b10 and new_state == 0b00) or (encoder2_last_state == 0b01 and new_state == 0b11): y_queue.put(5) encoder2_last_state = new_state # Set up GPIO event detection GPIO.add_event_detect(encoder1_pins[0], GPIO.BOTH, callback=encoder1_callback) GPIO.add_event_detect(encoder1_pins[1], GPIO.BOTH, callback=encoder1_callback) GPIO.add_event_detect(encoder2_pins[0], GPIO.BOTH, callback=encoder2_callback) GPIO.add_event_detect(encoder2_pins[1], GPIO.BOTH, callback=encoder2_callback) GPIO.add_event_detect(clearBtn, GPIO.FALLING, callback=ClearScrnBtn, bouncetime=300) GPIO.add_event_detect(liftBtn, GPIO.FALLING, callback=lift_pen, bouncetime=300) GPIO.add_event_detect(ColorBtn, GPIO.FALLING, callback=Change_Color, bouncetime=300) # Function to update the turtle's position def update_position(): delta_x = 0 delta_y = 0 # Read all pending increments from queues while not x_queue.empty(): delta_x += x_queue.get_nowait() while not y_queue.empty(): delta_y += y_queue.get_nowait() if delta_x != 0 or delta_y != 0: raw_turtle.setx(raw_turtle.xcor() + delta_x) raw_turtle.sety(raw_turtle.ycor() + delta_y) screen.title(f"X: {int(raw_turtle.xcor())}, Y: {int(raw_turtle.ycor())}") screen.update() # Only refresh when position changes screen.ontimer(update_position, 20) # Shorter interval for faster response # Start updating the position update_position() # Start the turtle main loop turtle.mainloop() # Clean up GPIO GPIO.cleanup()
额外优化建议
- 降低编码器步进值:当前每次增量是5,可改为更小的值(如2),让移动更平滑,减少单次绘图的负载
- 关闭不必要的系统服务:Raspberry Pi上的后台服务可能占用资源,关闭无用服务可提升响应速度
- 使用硬件加速:确保Turtle使用的Tkinter开启了硬件加速(默认应该开启,但可检查系统是否启用GPU加速)
内容的提问来源于stack exchange,提问作者leoh Jones
相关产品推荐
相关产品推荐

