循迹小车:用红外/灰度传感器让小车自己沿黑线跑
- 讲清红外反射传感器为什么能区分黑白线,以及数字量和模拟量两种读法的取舍
- 用3路红外+差速底盘写出能跑的if-else循迹代码,并标定黑白阈值
- 理解if-else开关式循迹的抖动局限,看懂比例调速如何把跟线变平滑
- 排查冲出线、剧烈抖动、认不出黑白、强光干扰等常见故障
桌上铺一张白纸,用黑色电工胶带贴一条弯弯曲曲的线。小车放上去,一通电,它就顺着这条线自己走——拐弯、绕圈、回到起点,全程没人碰它。这是我见过最能让初学者眼睛发亮的项目,比闪个 LED、转个电机都更有"它活了"的感觉。
这一篇就带你把它做出来。前提是你已经能让两个轮子各自正反转、调速——也就是 差速底盘 那一篇的内容。循迹小车说穿了,就是在差速底盘上加一双"眼睛",让它知道黑线在左边还是右边,然后决定哪个轮子快、哪个轮子慢。运动函数我们直接复用,不重写。
红外传感器为什么能看见黑线
先说"眼睛"是怎么回事。最常用的是 TCRT5000 这种红外反射式传感器,一个模块上挤着两颗东西:一颗红外发射管(往下打不可见光),一颗红外接收管(收反射回来的光)。
关键在地面的反射率差异。白纸反光强,发出去的红外大部分弹回来,接收管收到很多光;黑线吸光,反射回来的少,接收管几乎收不到。Vishay 的 TCRT5000 数据手册 里给的最佳工作距离是 2.5mm 左右,反射率高低直接决定输出电流——这就是黑白能被分开的物理根源。
模块给你两种输出,搞清楚区别很重要:
- 数字量(DO):模块上一般带个电位器,你手动拧一个阈值。反射强过阈值输出低电平(看到白),弱于阈值输出高电平(看到黑)。读起来就是
digitalRead(),非黑即白,简单。 - 模拟量(AO):直接给你反射强度的连续电压,用
analogRead()读到 0~4095(ESP32 是 12 位 ADC)的数。白纸读数高、黑线读数低,中间还有灰度过渡。
入门我建议先用数字量跑通逻辑,简单不烧脑。等你想做平滑跟线了,再切模拟量——后面比例控制那段就靠它。模拟量的读法和坑,可以顺便看 模数转换 ADC 和 数字量与模拟量 两篇打底。
传感器布局:几路够用
传感器不是越多越好,但太少会瞎。常见配置:
- 2 路:一左一右骑在线两侧。线偏左了左边那颗压到黑线,就往左修。能跑,但中间是盲区,直线段容易左右画龙。
- 3 路:左、中、右。中间那颗负责"我正压在线上",两侧负责"我偏了"。这是入门最划算的方案,本文就用它。
- 5 路:左、中左、中、中右、右。能判断偏移的程度(偏一点还是偏很多),是做比例/PID 控制的标配。
三颗传感器横向排开,间距比黑线略宽一点(黑线常用 18mm 胶带,传感器间距 15~25mm 比较合适),装在车头底部。
第一版:3路 if-else 开关式循迹
逻辑用大白话讲就是:中间压着线就直走,线跑到左边去了就左转把它追回来,跑到右边就右转。三颗传感器八种组合,但常用的就那么几种。
假设传感器数字量接法是:压到黑线读 HIGH(1),白纸读 LOW(0)。注意——这个高低关系一定要先实测确认,不同模块、不同接法可能正好相反,后面标定那节会讲怎么验。
// 循迹小车 v1:3路红外 + 差速底盘 if-else 开关式
// 引脚示意,以你的实际接线为准
const int IR_LEFT = 32; // 左传感器 DO
const int IR_MID = 33; // 中传感器 DO
const int IR_RIGHT = 34; // 右传感器 DO
// setMotors(左轮速, 右轮速),范围 -255~255,正为前进
// 这个函数在「差速底盘」那篇已经写好,这里直接复用,不重复贴
extern void setMotors(int left, int right);
const int SPEED_FWD = 160; // 直行基准速度
const int SPEED_TURN = 110; // 转弯时慢的那一侧
void setup() {
pinMode(IR_LEFT, INPUT);
pinMode(IR_MID, INPUT);
pinMode(IR_RIGHT, INPUT);
// GPIO34 在 ESP32 上是仅输入引脚,且无内部上拉,模块自带输出即可
// 仅输入引脚的细节见 ESP32 GPIO 文档
}
void loop() {
int L = digitalRead(IR_LEFT); // 1 = 压到黑线
int M = digitalRead(IR_MID);
int R = digitalRead(IR_RIGHT);
if (M == 1 && L == 0 && R == 0) {
// 只有中间压线:正对线,直走
setMotors(SPEED_FWD, SPEED_FWD);
} else if (L == 1) {
// 左边压到线:车身偏右了,往左修
setMotors(SPEED_TURN, SPEED_FWD);
} else if (R == 1) {
// 右边压到线:车身偏左了,往右修
setMotors(SPEED_FWD, SPEED_TURN);
} else {
// 三颗都没压线:冲出线了,原地慢转找线(这里假设上次往右丢的)
setMotors(-SPEED_TURN, SPEED_TURN);
}
delay(5); // 给一点采样间隔,别空转太狠
}
这版能跑,弯不太急的赛道它就老老实实沿线走了。但你盯着看一会儿会发现一个毛病:它不是平滑地跟线,而是"左一下、右一下"地抖着走,像喝多了。弯急一点甚至会冲出去。
为什么?因为开关式控制只有三档——要么全速直行,要么固定速度猛转。它不知道"我只偏了一点点"和"我偏得很厉害"的区别,每次修正都用同样的力度,于是修过头、再修回来,来回振荡。
第二版:加权误差 → 比例调速
要让它平滑,核心思路是:偏得越多,修得越狠;偏一点点,轻轻带一下。这就是比例控制的雏形。
第一步,把"偏在哪"量化成一个数字——误差。给三颗(这里演示 5 路更直观,3 路同理)传感器各赋一个位置权重,左为负、右为正,中间为零,按谁压到线加权平均:
// 循迹小车 v2:加权误差 + 比例调速(5路示意,模拟量读法)
const int IR_PINS[5] = {32, 33, 34, 35, 36};
const int WEIGHTS[5] = {-2, -1, 0, 1, 2}; // 位置权重
const int BASE_SPEED = 150;
const float KP = 40.0; // 比例系数,自己调
int threshold[5]; // 每颗传感器的黑白分界,标定后填入
void loop() {
long weightedSum = 0;
int active = 0;
for (int i = 0; i < 5; i++) {
int v = analogRead(IR_PINS[i]);
if (v < threshold[i]) { // 假设黑线读数低于阈值
weightedSum += WEIGHTS[i];
active++;
}
}
float error = (active > 0) ? (float)weightedSum / active : 0;
// error: 负=偏右需左修, 正=偏左需右修, 0=正对
int correction = (int)(KP * error);
int leftSpeed = BASE_SPEED + correction;
int rightSpeed = BASE_SPEED - correction;
setMotors(constrain(leftSpeed, -255, 255),
constrain(rightSpeed, -255, 255));
delay(3);
}
error 是连续的:偏一点 error 小、修正轻;偏很多 error 大、修正猛。KP 这个系数决定脾气——调大了反应快但容易振荡,调小了温吞但稳。先从小往大试。
这版已经明显比 if-else 顺滑了。但你会发现 KP 调大就抖、调小就追不上急弯,怎么都不完美。这是因为纯比例控制天生有这个矛盾——它只看"现在偏多少",不看"偏的趋势"。要彻底解决,得加上微分和积分项,也就是完整的 PID。那是单独一篇的内容,循迹 PID 控制 接着往下讲。
标定阈值:这一步偷懒一定翻车
代码里那个 threshold(或数字量模块的电位器)不是拍脑袋定的,得实测。方法很笨但有效:
- 把传感器固定到正常离地高度,写个小程序循环
analogRead并打印。 - 让传感器对着白纸,记下读数(比如 3200 上下)。
- 移到黑线上,再记一遍(比如 600 上下)。
- 阈值取中间值,比如 (3200 + 600) / 2 = 1900。每颗单独标,因为它们个体有差异。
顺便用这一步确认黑白的高低方向——到底是黑线读数高还是白纸读数高,别想当然。数字量模块同理:拧电位器,让它的指示灯在白纸上灭、黑线上亮(或反过来),调到临界点再回退一点。
两个常被忽略的物理量:
- 离地高度:TCRT5000 这类传感器对距离敏感,太高反射弱、信号糊,太低又容易蹭地。2
10mm 是合理区间,46mm 通常最稳。装好后用上面的方法验一下黑白读数差够不够大(差几百以上才靠谱)。 - 环境光:阳光、强射灯里都有红外成分,会污染接收管。尽量在室内常态光下跑,传感器加个遮光罩能改善不少。
安全提醒一句:电机和主控的供电要分开。电机启停瞬间拉低电压,容易把主控干复位,循迹时表现就是"莫名其妙重启、乱走"。差速底盘那篇讲过的供电分离,这里照办。
你应该看到什么
接好线、标定完阈值、传给小车,正常的话:
- 放到线上一通电,小车稳稳地沿黑线往前走,中间传感器基本一直压着线。
- 遇到弯道,它会减一侧轮速自然地拐过去,不脱线。
- v1(if-else)会有可见的左右小抖动,这正常;v2(比例)明显顺滑很多。
- 万一冲出线,三颗都读白,它会按你写的找线逻辑原地慢转,把线找回来。
如果不是这样,对着下面的表排查。
故障排查
| 现象 | 多半的原因 | 怎么办 |
|---|---|---|
| 一上来就冲出线、不跟线 | 黑白判断方向反了(HIGH/LOW 接反) | 重新标定,确认压黑线时读到的电平/读数,对应改 if 条件 |
| 抖动剧烈、来回画龙 | if-else 开关式的固有毛病,或 KP 太大 | 降基准速度;v2 里调小 KP;想根治上 PID |
| 黑白完全认不出 | 离地太高/太低,或阈值没标 | 调到 4~6mm,重新标定,确认黑白读数差几百以上 |
| 某一路始终不响应 | 那颗传感器接线松、引脚选错(如用了仅输出脚做输入) | 单独打印这一路读数;ESP32 上 GPIO34~39 仅输入,别误当输出脚 |
| 室内好好的,到窗边就乱 | 阳光红外干扰 | 远离强光,加遮光罩,或改用抗干扰更好的模块 |
| 走着走着主控重启 | 电机和主控共电,启停掉压 | 电源分离,电机供电单独走 |
两个进阶变体
变体一:升到 5 路,跟线更稳。 三路只能分辨"偏没偏",五路能分辨"偏多少"——这正是比例/PID 控制的弹药。v2 代码已经按 5 路写了,把权重数组和阈值标定铺满五颗即可。急弯赛道上五路的优势非常明显。
变体二:处理十字路口。 如果赛道有交叉,某一刻会出现三颗(或五颗)同时压到黑线的情况。开关式逻辑里没处理这个,小车会懵。简单做法:检测到"全压线"就判定为路口,按预设规则走(比如恒定直行通过,或读一个计数器决定第几个路口左转)。这是从"只会跟线"迈向"会走迷宫"的第一步。
动手挑战
用黑胶带在白纸或白色 KT 板上,贴一条闭合的赛道——带至少两个不同角度的弯,首尾接成一圈。目标:让你的小车从任意位置出发,不脱线地跑完完整一圈回到起点。
先用 v1 跑通,再换 v2,亲手感受两者过弯时的差别。如果急弯总冲出去,别急着怪代码,先回去查离地高度和阈值——八成是"眼睛"没看清,不是"脑子"不会算。
小结
循迹小车把"传感器输入 → 控制决策 → 电机输出"这条机器人最核心的闭环,浓缩进了一个看得见摸得着的小项目。你应该已经体会到:从 if-else 开关式到比例调速,跟线一步步变平滑,背后是同一个问题——"偏了多少,该修多狠"——被回答得越来越精细。
但循迹只是让小车沿着既定的线走,它还不会"看见前面有东西"。下一步给它装上能感知前方障碍的传感器,让它学会停下、绕开——避障小车 接着讲。想把跟线做到极致顺滑的,直接奔 循迹 PID。更多机器人项目在 机器人专题。