深色模式
第 8 章:循迹探头与自动驾驶
本章是本册中最经典的部分。
8.1 轨道搭设:黑色电工胶布铺轨
循迹(自动驾驶)的第一步是铺设轨道。D3 的轨道无需专用器材:用常见的黑色电工胶布(绝缘胶布)粘贴在浅色地板上即可制成——黑色胶带与浅色地面形成的强烈反差,正是循迹探头识别黑线的依据。
材料:黑色电工胶布(宽约 18 mm)一卷,浅色、平整的地板或 KT 板;
粘贴:边拉边贴、压实抹平,避免气泡与翘边(翘边会导致探头读数跳变);
弯道:所有转弯处用圆弧过渡,弯道半径不小于 20 cm,不要出现直角弯或锐角弯;
规模:建议先铺设 1.5~3 m 的环形闭合轨道(圆角矩形或椭圆形),跑通后再增加岔路、十字等复杂元素;
环境:避开阳光直射与强反光地面;地面本身偏深时,需垫浅色背景板。
铺轨要点:

说明:闭环轨道最适合入门——小车可以持续绕圈行驶,便于反复观察与调试。调试通过后,可在轨道末端放置蜡烛,衔接第 10 章的综合任务,优秀的自动循迹算法的轨道不用铺的像示意图那样规整。
用黑色电工胶布在浅色地板上贴出任意轨迹,让小车沿轨迹行驶且不偏离轨道。

8.2 模块原理:四路循迹探头
循迹探头(⑪)安装于车头底部,向下发射红外光并接收反射光。黑色表面吸收红外光、探头收不到回波,白色表面反射红外光、探头能收到回波,模块据此输出数字信号:**压黑线(收不到回波)输出 0,对应指示灯点亮;位于白色区域(收到回波)输出 1,指示灯熄灭。**四个探头横向排列,根据哪些探头位于黑线上方,即可判断黑线相对于车身的位置。
重要提示:四个探头从左至右的物理顺序为 X2、X1、X3、X4(即 2134),并非 1234。循迹程序效果不符合预期时,多数情况并非算法或代码错误,而是探头编号识别错误。编程前务必按 8.3 节的方法实测确认。
| 探头编号 | Pico 管脚 | 物理位置(从左至右) |
|---|---|---|
| X2 | GP11 | 第 1 个(最左) |
| X1 | GP10 | 第 2 个 |
| X3 | GP12 | 第 3 个 |
| X4 | GP13 | 第 4 个(最右) |
8.3 代码实现第一步:读取四个探头
循环读取 GP10~GP13 四个管脚的数值并显示。将小车分别置于"探头压黑线"与"探头位于白色区域"两种状态,观察读数与指示灯的对应关系:压黑线的探头读数为 0、指示灯点亮;位于白色区域的探头读数为 1、指示灯熄灭。同时验证 8.2 表格中的物理顺序,确认实际小车的编号排列与表格一致。

python
# 四个循迹探头:压黑线输出 0(指示灯亮),白色区域输出 1(指示灯灭)
# 接管脚:S1→GP10(X1)、S2→GP11(X2)、S3→GP12(X3)、S4→GP13(X4)
# 注意物理顺序:从左至右是 S2、S1、S3、S4(即 X2、X1、X3、X4),不是 1234!
GP10 = Pin(10, Pin.IN)
GP11 = Pin(11, Pin.IN)
GP12 = Pin(12, Pin.IN)
GP13 = Pin(13, Pin.IN)
while True:
# 循环读取四个管脚并存入变量 S1~S4,再合并打印出来
S1 = GP10.value()
S2 = GP11.value()
S3 = GP12.value()
S4 = GP13.value()
# (截图打印的最后一个变量是笔误,应为 S4,代码按 S4 写)
print("左→右:", S1, S2, S3, S4)cpp
// 四个循迹探头:压黑线输出 0(指示灯亮),白色区域输出 1(指示灯灭)
// 接管脚:S1→GP10(X1)、S2→GP11(X2)、S3→GP12(X3)、S4→GP13(X4)
// 注意物理顺序:从左至右是 S2、S1、S3、S4(即 X2、X1、X3、X4),不是 1234!
const int GP10 = 10;
const int GP11 = 11;
const int GP12 = 12;
const int GP13 = 13;
void setup() {
Serial.begin(115200);
pinMode(GP10, INPUT);
pinMode(GP11, INPUT);
pinMode(GP12, INPUT);
pinMode(GP13, INPUT);
}
void loop() {
// 循环读取四个管脚并存入变量 S1~S4,再合并打印出来
int S1 = digitalRead(GP10);
int S2 = digitalRead(GP11);
int S3 = digitalRead(GP12);
int S4 = digitalRead(GP13);
// (截图打印的最后一个变量是笔误,应为 S4,代码按 S4 写)
Serial.print("左→右:");
Serial.print(S1); Serial.print(" ");
Serial.print(S2); Serial.print(" ");
Serial.print(S3); Serial.print(" ");
Serial.println(S4);
}8.4 循迹(自动驾驶)策略
循迹的基本逻辑为:**黑线偏向哪一侧,车身即向该侧修正。**探头组合与修正动作的对应关系如下(压黑线 = 读数 0,不压线 = 读数 1):
| 压黑线(读数 0)的探头 | 黑线位置判断 | 修正动作 |
|---|---|---|
| X1、X3 同时压线 | 黑线位于车身正下方(居中) | 前进 |
| 仅 X1 压线 | 黑线微偏左 | 向左微调:左轮稍减速 / 右轮稍加速 |
| 仅 X2 压线 | 黑线大偏左 | 向左大转:左轮大减速 / 右轮大加速 |
| 仅 X3 压线 | 黑线微偏右 | 向右微调:左轮稍加速 / 右轮稍减速 |
| 仅 X4 压线 | 黑线大偏右 | 向右大转:左轮大加速 / 右轮大减速 |
| 四个均为 1(全部脱线) | 丢失黑线 | 继续上次转向,利用惯性找回黑线 |
![]()
8.5 代码实现第二步:完整循迹(自动驾驶)程序
程序结构:循环执行「读取四个探头 → 按 8.4 表格判定动作 → 执行修正」。
8.5.1 循迹算法详解
同电压下两电机转速不可能绝对一致,地板也非绝对平整。若只要探头偏线就大幅转向,车身会剧烈摆尾;若修正力度不分路况,弯道又容易丢线。
解决思路:不自己算轮速,而是复用第 4 章的差速转弯函数 turn1()——转向量既是方向也是力度:负值向左修、正值向右修,绝对值越大转得越急。程序用两个变量把"方向"和"力度"分开算,最后相乘交给 turn1(e * n):
方向 e:由中间两个探头 S1、S3 决定。压黑线读 0、不压线读 1,因此
e = S1 - S3——仅 S1 压线(黑线偏左)时 e = -1,向左修;仅 S3 压线(黑线偏右)时 e = 1,向右修;S1、S3 同时压线(居中)时 e = 0,直行。左负右正,一目了然。力度 n:由外侧两个探头 S2、S4 判断偏离的严重程度。S2 或 S4 压线,说明黑线已经跑到最外侧探头底下、车身严重偏离,n 置为 1,用最大力度回轨;外侧没压线而中间有探头压线,说明只是轻微偏离,n 置为 0.25,即最大拐弯力度的四分之一,轻轻带一把即可。
脱线处理:若 S1、S3 同时为 1(中间探头都没压到线),说明黑线已脱出中间视野——此刻若按
e = S1 - S3计算会得到 0,小车直行、彻底丢线。因此这种情况下不重新计算,直接继承脱线前上一瞬间的方向:e = last_e,遵循左负右正——刚才在向左拐就继续向左拐,刚才在向右拐就继续向右拐,靠惯性把黑线找回来。
程序每轮循环依次做三件事:
读取四路探头状态,存入变量
S1~S4;按上面三条规则算出
e和n,并记录last_e = e;调用第 4 章
turn1(e * n)执行修正。
turn1() 内部 sp = 150、p = 80:e * n = 0 时左右轮均为 150,直行;e * n = ±0.25 时两侧轮速 170 / 130,差速 40,车身微微调整;e * n = ±1 时两侧轮速 230 / 70,差速 160,快速回轨。
如此,直线段行驶平顺不摆尾,入弯时又能及时大力修正,脱线时还会沿着原来的方向把线找回来。
8.5.1.1 可指定鸣笛次数的蜂鸣器函数
编写循迹代码前,先定义一个可指定鸣笛次数的蜂鸣器函数,方便后续调试调用。以蜂鸣器声响标识程序状态,是实用的调试技巧。

8.5.1.2 完整循迹程序
![]()
python
# 蜂鸣器提示音函数(蜂鸣器接 GP14):鸣 n 声
buzzer = Pin(14, Pin.OUT)
def beep(n):
# 重复 n 次:高电平响 0.2 秒 → 低电平停 0.2 秒
for _ in range(n):
buzzer.value(1)
time.sleep(0.2)
buzzer.value(0)
time.sleep(0.2)
# 四个探头:S1→GP10(X1)、S2→GP11(X2)、S3→GP12(X3)、S4→GP13(X4)
# 物理从左至右:S2、S1、S3、S4
# 读数规律:压黑线 = 0(指示灯亮),不压线 = 1(指示灯灭)
GP10 = Pin(10, Pin.IN)
GP11 = Pin(11, Pin.IN)
GP12 = Pin(12, Pin.IN)
GP13 = Pin(13, Pin.IN)
e = 0 # 方向:负 = 向左修,正 = 向右修,0 = 直行
last_e = 0 # 上一瞬间的方向(脱线时继承)
n = 0.25 # 力度系数:0.25 = 微调,1 = 大转
stop_all() # 先确保四轮停稳
beep(3) # 出发提示:鸣三声
time.sleep(2) # 原地等待 2 秒,从容把车放到轨道上
while True:
# 读取四个探头
S1 = GP10.value()
S2 = GP11.value()
S3 = GP12.value()
S4 = GP13.value()
# 方向:中间两个探头都没压线(都为 1)= 脱线,
# 继承上一瞬间的方向 e,继续原来的转向把线找回来;
# 否则 e = S1 - S3:仅 S1 压线得 -1(向左修),
# 仅 S3 压线得 1(向右修),同时压线得 0(直行)
if S1 == 1 and S3 == 1:
e = last_e
else:
e = S1 - S3
# 力度:外侧探头压线(S2 或 S4 为 0)= 严重偏离,全力大转;
# 仅中间探头压线 = 轻微偏离,用四分之一力度微调
if S2 == 0 or S4 == 0:
n = 1
elif S1 == 0 or S3 == 0:
n = 0.25
# 脱线时上面两个分支都不进,n 保持上次的值
last_e = e # 记录本次方向,供脱线时继承
turn1(e * n) # 调用第 4 章差速转弯函数:负左正右,绝对值越大转得越急cpp
// 蜂鸣器提示音函数(蜂鸣器接 GP14):鸣 n 声
const int BUZZER = 14;
void beep(int n) {
// 重复 n 次:高电平响 0.2 秒 → 低电平停 0.2 秒
for (int i = 0; i < n; i++) {
digitalWrite(BUZZER, HIGH);
delay(200);
digitalWrite(BUZZER, LOW);
delay(200);
}
}
// 四个探头:S1→GP10(X1)、S2→GP11(X2)、S3→GP12(X3)、S4→GP13(X4)
// 物理从左至右:S2、S1、S3、S4
// 读数规律:压黑线 = 0(指示灯亮),不压线 = 1(指示灯灭)
const int GP10 = 10;
const int GP11 = 11;
const int GP12 = 12;
const int GP13 = 13;
float e = 0; // 方向:负 = 向左修,正 = 向右修,0 = 直行
float last_e = 0; // 上一瞬间的方向(脱线时继承)
float n = 0.25; // 力度系数:0.25 = 微调,1 = 大转
void setup() {
pinMode(BUZZER, OUTPUT);
pinMode(GP10, INPUT);
pinMode(GP11, INPUT);
pinMode(GP12, INPUT);
pinMode(GP13, INPUT);
stop_all(); // 先确保四轮停稳
beep(3); // 出发提示:鸣三声
delay(2000); // 原地等待 2 秒,从容把车放到轨道上
}
void loop() {
// 读取四个探头
int S1 = digitalRead(GP10);
int S2 = digitalRead(GP11);
int S3 = digitalRead(GP12);
int S4 = digitalRead(GP13);
// 方向:中间两个探头都没压线(都为 1)= 脱线,
// 继承上一瞬间的方向 e,继续原来的转向把线找回来;
// 否则 e = S1 - S3:仅 S1 压线得 -1(向左修),
// 仅 S3 压线得 1(向右修),同时压线得 0(直行)
if (S1 == 1 && S3 == 1) {
e = last_e;
} else {
e = S1 - S3;
}
// 力度:外侧探头压线(S2 或 S4 为 0)= 严重偏离,全力大转;
// 仅中间探头压线 = 轻微偏离,用四分之一力度微调
if (S2 == 0 || S4 == 0) {
n = 1;
} else if (S1 == 0 || S3 == 0) {
n = 0.25;
}
// 脱线时上面两个分支都不进,n 保持上次的值
last_e = e; // 记录本次方向,供脱线时继承
turn1(e * n); // 调用第 4 章差速转弯函数:负左正右,绝对值越大转得越急
}注意:循迹效果受探头离地高度与环境光强度影响。若读数异常,先检查探头是否松动、离地间隙是否过大,再用黑色绝缘胶带现场测试确认。
8.6 常见 bug 及理解误区
循迹是整册教程中"看起来简单、调起来容易怀疑人生"的一章。下面这些都是极易踩中的坑,循迹效果不对时,先逐条对照排查,再怀疑代码。
误区一:以为"识别到黑线 = 1"
直觉上"探测到了东西"就该输出 1,这款探头恰恰相反。它靠红外回波工作:黑色表面吸光,探头收不到回波,压黑线时读数为 0;浅色表面反射回波,读数为 1。把极性想当然地记反,是循迹程序最常见的理解误区——写出的程序会"看见黑线就躲",在黑线与地板之间来回横跳。
误区二:拿着小车悬空调试
小车被拿在手里悬空时,探头离地面太远,同样收不到回波,读数也是 0,和压黑线一模一样。所以悬空读到 0 ≠ 检测到黑线。观察读数、验证程序时,请把小车放在铺好轨道的地面上进行,否则会被"假压线"误导。
误区三:以为指示灯亮 = 有信号
这款探头的指示灯逻辑与直觉相反:灯亮 = 读到 0(压黑线),灯灭 = 读到 1(未压线)。用指示灯判断探头状态完全可行,而且比看串口数据更快捷,但前提是先记住这个反直觉的对应关系,否则越调越乱。
误区四:把物理顺序当成 1234
四个探头从左至右是 X2、X1、X3、X4,不是 1234。程序跑起来效果不对时,先别急着改算法——多数情况是探头编号想错了。按 8.3 节的方法逐个遮挡实测,确认每个变量对应车底哪个探头。
误区五:灵敏度旋钮没调准
每个探头上都有灵敏度调节旋钮,出厂状态不一定适合实际的地面环境。标准调试流程:
- 小车置于平整桌面,不开电源开关,用电脑 USB 连接 Pico 供电即可;
- 在 A4 纸上贴一条黑色轨迹;
- 左右移动小车,观察黑轨对准哪个探头时,哪个探头的灯是否亮起;
- 对不上的,旋转对应探头的灵敏度旋钮,直到四个探头都能"黑轨对谁谁亮"。
误区六:读数全部异常时只会改代码
如果灵敏度调准后读数仍然异常(比如四个探头一直读 0 或一直读 1),问题可能不出在本章程序:之前某次实验下载过的程序如果把 GP10~GP13 配置成了下拉电阻模式,会持续干扰这四个管脚的读取,本章示例代码怎么改都没有用。解决方法:向 Pico 重新刷入固件包,把管脚配置恢复干净,再重新下载本章程序。
