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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 01:20:54