进阶: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 的机会。一个没有任何 await 的 while 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 巡线更稳的主要原因之一。
调参顺序(永远这个顺序)
KI = KD = 0,调 KP:从小加大,加到轻微振荡,然后退回 60-70%。- 加 KD:从 0 加大到振荡消失、弯道跟随干脆。KD 通常是 KP 的 3-10 倍。
- 大多数巡线不需要 KI(巡线没有持续稳态误差)。除非发现机器人始终骑在偏离中值的位置,才加一点点(0.001 量级)。
- 提高 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 并行。剩下的差距只在两件事:机械设计和练习时长。
最后的练习:
- 用调参顺序把你的 PID 巡线调到 60% 以上基础速度仍然稳定。
- 把
follow_line_deg、straight_pid、turn整理成一个函数库文件,全队共用——这就是你们队的"武功秘籍"。