教程 · 入门 · 阅读约 20 分钟
制作循迹机器人:从传感器到 PID 转向
一台循迹机器人到底是怎样工作的:底部的反射式传感器、由读数算出的加权位置误差,以及让差速底盘平滑转向的 PID 控制环。
循迹车是经典的第一个自主机器人:它读取引脚下的线并转向,让车始终留在线上。底层的控制问题很小——把传感器读数变成一个转向修正量——这让它成为学习反馈控制的绝佳切入点。可以在循迹模拟器里跟着做。
感知这条线
反射传感器向下发射红外光,测量有多少光被反射回来。深色的线比周围明亮的地面反射得少,所以每个传感器报的就是自己是否压在线上。车头横着排一排若干传感器,告诉你的不仅是在不在线上,还有线相对于中心在哪儿。
五个传感器是个不错的起点。低速下三个就够;竞赛机器人常用八个,以获得更细的分辨率。
把读数变成一个误差数
不要对单个传感器做反应,而是把它们合成一个加权位置。给每个传感器一个位置值(左侧为负、中间为零、右侧为正),再按各传感器看到线的强度加权取平均:
// positions: {-2, -1, 0, 1, 2}; readings: reflectance per sensor
float weightedPosition = sum(position[i] * reading[i]) / sum(reading[i]);
得到的就是你的误差:零表示居中,负值表示线在左边,正值表示线在右边。一个平滑连续的误差,正是控制器需要的输入。
用 PID 转向
把这个误差喂给 PID 控制器。修正量会让一个轮子加速、另一个减速,把车转回线上:
float correction = kp * error + ki * integral + kd * derivative;
setMotorSpeeds(baseSpeed - correction, baseSpeed + correction);
比例增益承担了转向动作的大部分;微分增益抑制弯道上的摆动;一个小的积分项消除稳态漂移。如果你刚开始学怎么选增益,先走一遍怎样整定 PID 控制器。
修掉常见故障
- **围着线振荡:**比例增益太大或微分太小。先降速,然后重新整定。在真实硬件上,循环里一个长
delay()会引入死区时间,把微分项彻底废掉。 - **急弯处丢线:**车跑得比传感器更新率还快。减速,把横排加宽,或者复用上一次已知的线的方向,让车转回来而不是径直冲下赛道。
- **偏向一侧漂移:**标定传感器。让它们在线上和地面上扫一遍,记下每个传感器的最小值和最大值,再把实时读数按这个范围归一化。
整个闭环放在一起
上面那些片段只有把时序、标定和丢线这三种情况一并处理,才会变成一台能用的车:
const int N = 5;
const int PIN[N] = {A0, A1, A2, A3, A4};
const int POS[N] = {-2000, -1000, 0, 1000, 2000}; // weights, in the error's units
int minv[N], maxv[N];
long lastError = 0;
float integral = 0;
unsigned long lastMicros = 0;
const float KP = 0.10f, KI = 0.0f, KD = 1.6f;
const int BASE = 140; // out of 255
void loop() {
// 1. Real elapsed time — never assume the loop period.
const unsigned long now = micros();
const float dt = (now - lastMicros) * 1e-6f;
if (dt < 0.002f) return; // cap the rate at 500 Hz
lastMicros = now;
// 2. Position from the weighted average.
long weighted = 0, total = 0;
for (int i = 0; i < N; i++) {
const int v = normalized(analogRead(PIN[i]), i);
if (v > 100) { weighted += (long)v * POS[i]; total += v; }
}
// 3. Line lost: hold the last error, do NOT zero it.
const long error = (total == 0) ? lastError : (weighted / total);
// 4. PID, with derivative on measurement and conditional integration.
const float derivative = -(float)(error - lastError) / dt;
const float candidate = integral + error * dt;
const float raw = KP * error + KI * candidate + KD * derivative;
const float correction = constrain(raw, -BASE, BASE);
if (raw == correction) integral = candidate;
lastError = error;
// 5. Differential steering.
setMotors(BASE - correction, BASE + correction);
}
这里面有五处,正是它们把“能循线的车”和“差一点就能循线的车”分开。
**真实的 dt。**积分项和微分项都要除以流逝的时间。循环时长会随着你往上加功能而改变——它一定会变——如果你假定一个固定周期,这两项算出来的都是假的。
**v > 100 这个阈值。**没有它,五个通道各自读到的一个小小的底噪值会把加权平均拖向中心,不管线实际在哪里。症状是:粗线跟得很好,浅线跟得很差。
**丢线时保持上一次误差。**零意味着“居中”,所以一台把误差归零的车会以全速直行——而线几乎肯定是弯的,于是车就冲下赛道了。保持上一次误差,车会继续朝它已经在转的方向转。
**微分作用在测量量上。**对误差求微分意味着设定点一变就会产生一个巨大的尖峰。这里设定点永远是零,所以差别不大,但这个习惯值得养成。
**条件积分。**一旦修正量饱和,继续累积积分只是把一次超调存起来,留着以后还。
一套行得通的整定顺序
不要三个一起调。把 KI = 0、KD = 0 设好,然后:
| 步骤 | 改什么 | 观察什么 | 什么时候停 |
|---|---|---|---|
| 1 | 从小值开始加 KP |
车开始对线有反应 | 能跟缓弯,还有一点蛇形摆动 |
| 2 | 继续加 KP |
摆动长大成振荡 | 稳定振荡——把这个值记为 Ku |
| 3 | 把 KP 设为约一半 Ku |
响应快,有超调 | — |
| 4 | 加 KD |
摆动被压掉 | 直线跑干净;再加 KD 就开始发神经 |
| 5 | 只有在持续偏心行驶时才加一点 KI |
偏差被关掉 | 关掉即可——循迹车很少需要 KI |
| 6 | 提 BASE 速度 |
所有表现一起变差 | 在新速度下重调 KP 和 KD |
第 6 步是大家会忘的那一步:**增益是随速度变的。**在 140 占空比上完美的整定,到 220 就会振荡,因为同样的转向修正量在单位时间内会产生更大的航向变化。只要速度有明显变化,就预期要重新整定。
积分在这里确实很少有用。循迹车的设定点是零,扰动又是瞬态的,所以通常没有稳态偏差留给 KI 去关。如果你看到持续存在的偏差,先怀疑电机trim不均或传感器没标定,而不是伸手去加积分增益。
循环频率悄悄地给一切设了上限
| 循环频率 | 周期 | 以 0.4 m/s 行进的距离 |
|---|---|---|
| 20 Hz | 50 ms | 每两次决策之间走 20 mm |
| 50 Hz | 20 ms | 8 mm |
| 200 Hz | 5 ms | 2 mm |
| 500 Hz | 2 ms | 0.8 mm |
一辆每 20 mm 才做一次决策的车,无论增益怎么设都跟不紧一个弯,而微分项也救不回根本没被采样到的信息。
常见肇因很好清除:循环里任意处的 delay()、9600 波特率下 Serial.print 超过几个值,以及读了根本不用的传感器。把它们去掉,通常能把第一台车从 50 Hz 提到几百 Hz,**而跟踪效果不碰任何增益就变好了。**调参之前先量频率,而不是之后。
故障排查
| 现象 | 可能原因 | 解决 |
|---|---|---|
| 直道上蛇形摆动 | KP 太大或 KD 太小 |
KP 减半,再加 KD |
| 越荡越快 | KP 超过临界增益 |
往回减直到稳定 |
| 弯道切角 | 速度超出传感器的超前量 | 减速,或把阵列前移 |
| 丢线后径直开出 | 丢线时把误差归零 | 保持上一次误差 |
| 一个方向跟得好、另一个方向差 | 阵列不水平,或电机trim不均 | 把阵列调平;逐个电机做trim |
| 偏向一侧漂移 | 传感器没标定,或电机不一致 | 逐通道标定,然后再trim |
| 台架上好、到地面就乱走 | 离地高度变了 | 3–8 mm 刚性安装后重新标定 |
| 昨天还好、今天就没劲 | 电池电压 | 增益是被控对象的属性——重新整定,或闭合一个速度环 |
| 只在低速时抖动 | 低于电机死区 | 量出每个电机的起动占空比并做补偿 |
KD 完全没反应 |
循环里有 delay() |
死区时间把微分作用彻底废掉 |
自己试试
在循迹模拟器里你可以画一条赛道,改传感器数量和速度,一边看误差和单圈时间一边调增益。你在加权误差和转向环上学到的一切,都能原样搬到一台 Arduino 小车上。
探索关联图谱
本教程所属的搭建
包含本教程的项目和学习路线。
延伸阅读