Skip to content

进阶:async 与 PID

最后一课,两个主题:理解 async 并发(SPIKE 3 Python 的地基),以及用 PID 写出比赛级巡线和直行。

async:到底发生了什么

hub 只有一个 CPU,却要"同时"管马达、传感器、灯光。async 的做法是协作式多任务:程序在每个 await 处让出 CPU,runloop 趁机去推进别的任务。

  • await 一个动作 = "这事要花时间,做完叫我,先让别人跑"。
  • 不加 await 调用你自己定义的 async 函数,动作不会执行(只是创建了协程对象)——这是最常见的 bug:
python
straight(50)          # ❌ 什么都不会发生(straight 是自定义 async 函数)
await straight(50)    # ✅ 正确

注意区别:LEGO 原生 API(motor.run_for_degrees 等)不加 await 也会开始动作,只是程序不等它做完——相当于"启动"积木。会"悄悄不执行"的只有你自己写的 async def

并行执行:runloop.run 多个任务

runloop.run() 可以接收多个协程,同时跑——相当于 Word Blocks 里的多个帽子积木:

python
import runloop, motor_pair
from hub import port, light_matrix

motor_pair.pair(motor_pair.PAIR_1, port.A, port.E)

async def drive():
    await motor_pair.move_for_degrees(motor_pair.PAIR_1, 3600, 0, velocity=400)

async def blink():
    while True:
        light_matrix.show_image(light_matrix.IMAGE_HEART)
        await runloop.sleep_ms(500)
        light_matrix.clear()
        await runloop.sleep_ms(500)

runloop.run(drive(), blink())     # 边跑边闪灯

无限循环里必须有 await

blink 里的 sleep_ms 不只是延时——它是让出 CPU 的机会。一个没有任何 awaitwhile True 会独占 CPU,其他任务全部卡死。

边开车边干活

FLL 省时间的大招:去程路上就把机械臂抬到位。注意 runloop.run() 只能在顶层调用一次,不能嵌在协程里面——要并行,就把两个动作各写成一个协程,一起交给顶层的 runloop.run():

python
async def raise_arm():
    await motor.run_for_degrees(port.C, 120, 300)

async def drive_out():
    await straight(60)
    await turn(90)
    # 到达时臂已就位

runloop.run(raise_arm(), drive_out())   # 抬臂和开车同时进行

PID 控制

P 控制只看当前偏差,有两个天生缺陷:小偏差修不动(留稳态误差),大 Kp 又会振荡。PID 补上两项:

修正量 = Kp*偏差 + Ki*偏差累积 + Kd*偏差变化率
   P:现在偏多少     I:历史欠账      D:趋势刹车
  • I(积分):把每圈的偏差累加。持续的小偏差越积越多,最终推动修正——消灭稳态误差。
  • D(微分):本圈偏差 − 上圈偏差。偏差在快速回归时输出负修正,提前"收油",抑制过冲和振荡。

比赛级 PID 巡线

python
import runloop, motor_pair, color_sensor
from hub import port

motor_pair.pair(motor_pair.PAIR_1, port.A, port.E)

TARGET = 48        # 线边缘中值,实测标定
KP, KI, KD = 1.2, 0.002, 6.0
BASE = 300         # 基础速度,度/秒

async def follow_line_deg(degrees):
    """PID 巡线,走指定的马达度数后停止。"""
    import motor
    motor.reset_relative_position(port.A, 0)
    integral = 0
    last_error = 0
    while abs(motor.relative_position(port.A)) < degrees:
        # 巡左边缘(左白右黑):偏白 -> 正偏差 -> 向右修,同巡线课的约定
        error = color_sensor.reflection(port.F) - TARGET
        integral += error
        integral = max(-1000, min(1000, integral))      # 防积分爆炸
        derivative = error - last_error
        last_error = error
        correction = KP * error + KI * integral + KD * derivative
        motor_pair.move_tank(motor_pair.PAIR_1,
                             int(BASE + correction),
                             int(BASE - correction))
        await runloop.sleep_ms(5)                        # 让出 CPU;循环间隔 5 ms
    motor_pair.stop(motor_pair.PAIR_1)

async def main():
    await follow_line_deg(2000)

runloop.run(main())

实现要点:

  • move_tank 直接控制左右轮速差,比 steering 参数更线性。
  • 积分限幅(clamp)必不可少,否则起步时的大偏差会积累成巨大的过冲。
  • 循环间隔 5 ms;实际读数更新受颜色传感器 100 Hz 采样率限制,循环本身也有开销。即便如此仍远密于 Word Blocks 的循环——这是 Python 巡线更稳的主要原因之一。

调参顺序(永远这个顺序)

  1. KI = KD = 0,调 KP:从小加大,加到轻微振荡,然后退回 60-70%。
  2. 加 KD:从 0 加大到振荡消失、弯道跟随干脆。KD 通常是 KP 的 3-10 倍。
  3. 大多数巡线不需要 KI(巡线没有持续稳态误差)。除非发现机器人始终骑在偏离中值的位置,才加一点点(0.001 量级)。
  4. 提高 BASE 速度后,回到第 1 步微调。

PID 陀螺直行

同一套 PID,偏差换成偏航角,就是顶级的走直线:

python
import runloop, motor_pair
from hub import port, motion_sensor

async def straight_pid(degrees, velocity=500, kp=4.0, kd=8.0):
    import motor
    motion_sensor.reset_yaw(0)
    motor.reset_relative_position(port.A, 0)
    last_error = 0
    while abs(motor.relative_position(port.A)) < degrees:
        error = -motion_sensor.tilt_angles()[0] / 10     # 目标 yaw = 0
        d = error - last_error
        last_error = error
        correction = kp * error + kd * d
        motor_pair.move_tank(motor_pair.PAIR_1,
                             int(velocity + correction),
                             int(velocity - correction))
        await runloop.sleep_ms(5)
    motor_pair.stop(motor_pair.PAIR_1)

你已经到顶了

走到这里,你掌握的技术栈和世界级 FLL 队伍相同:编码器测距 + 陀螺仪定向 + PID 控制 + async 并行。剩下的差距只在两件事:机械设计练习时长

最后的练习:

  1. 用调参顺序把你的 PID 巡线调到 60% 以上基础速度仍然稳定。
  2. follow_line_degstraight_pidturn 整理成一个函数库文件,全队共用——这就是你们队的"武功秘籍"。