进阶:async 与 PID
最后一课,两个主题:理解 async 并发(SPIKE 3 Python 的地基),以及用 PID 写出比赛级巡线和直行。
async:到底发生了什么
hub 只有一个 CPU,却要"同时"管马达、传感器、灯光。async 的做法是协作式多任务:程序在每个 await 处让出 CPU,runloop 趁机去推进别的任务。
await一个动作 = "这事要花时间,做完叫我,先让别人跑"。- 不加
await调用你自己定义的 async 函数,动作不会执行(只是创建了协程对象)——这是最常见的 bug:
straight(50) # ❌ 什么都不会发生(straight 是自定义 async 函数)
await straight(50) # ✅ 正确注意区别:LEGO 原生 API(motor.run_for_degrees 等)不加 await 也会开始动作,只是程序不等它做完——相当于"启动"积木。会"悄悄不执行"的只有你自己写的 async def。
并发执行:runloop.run 多个任务
runloop.run() 可以接收多个协程,并发推进(单核轮流执行,宏观上"同时")——相当于 Word Blocks 里的多个帽子积木:
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():
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 里 I、D 项都要乘/除采样周期 dt;本文循环间隔近似固定(sleep 5 ms 加上计算与调度开销),增益把实际 dt 吸收进去了——所以代码不显式出现 dt。换了采样间隔就必须重调增益。
实用 PID 巡线
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)) # 积分抗饱和限幅(anti-windup)
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 参数更线性。 - 积分抗饱和限幅(anti-windup clamp)强烈建议保留,否则起步时的大偏差会积累成巨大的过冲。
- 循环间隔 5 ms;实际读数更新受颜色传感器 100 Hz 采样率限制,循环本身也有开销。即便如此仍远密于 Word Blocks 的循环——这是 Python 巡线更稳的主要原因之一。
- 教学版从简:真正部署到比赛还应加超时退出和堵转检测(马达 velocity 长时间为 0 就停止),防止卡住后原地烧电机。
调参顺序(推荐流程)
KI = KD = 0,调 KP:从小加大,加到轻微振荡,然后退回 60-70%。- 加 KD:从 0 加大到振荡消失、弯道跟随干脆。在本文的采样间隔和单位下 KD 常落在 KP 的 3-10 倍——这是经验值,不是普适规则,换采样间隔或单位就会变。
- 大多数巡线不需要 KI(巡线没有持续稳态误差)。除非发现机器人始终骑在偏离中值的位置,才加一点点(0.001 量级)。
- 提高 BASE 速度后,回到第 1 步微调。
PID 陀螺直行
同一套 PID,偏差换成偏航角,就是顶级的走直线:
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:
# App 3 原始 yaw 左转为正,数值上恰好就是"0 − 右转为正偏航"这个偏差
error = motion_sensor.tilt_angles()[0] / 10
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 并发。剩下的差距只在两件事:机械设计和练习时长。
常见问答
调用自己写的 async 函数怎么什么都没发生?
自定义 async 函数不加 await 只会创建协程对象,不会执行——这是最常见的 bug。注意 LEGO 原生 API 相反:不加 await 也会开始动作,只是程序不等它。
PID 巡线一直振荡怎么办?
先把 KP 退回到振荡消失的 60-70%,再从 0 逐步加 KD 抑制过冲。换速度或采样间隔后所有增益都要重调。
到底要不要积分项 KI?
大多数巡线不需要——它没有持续稳态误差要消。只有机器人始终骑在偏离中值的位置时才加一点点(0.001 量级),并务必配积分抗饱和限幅。
最后的练习:
- 用调参顺序把你的 PID 巡线调到 60% 以上基础速度仍然稳定。
- 把
follow_line_deg、straight_pid、turn整理成一个函数库文件,全队共用——这就是你们队的"武功秘籍"。