深色模式
第 10 章:循迹(自动驾驶)+ 灭火综合教程
10.1 任务描述
综合任务:小车沿黑色轨道自动行驶,轨道旁设置一处火源(点燃的蜡烛)。小车检测到火焰后停车、对准、吹灭火源,发出提示音后返回轨道继续行驶,到达终点后停车。该任务综合运用第 3~9 章的全部内容。
10.2 程序设计:状态机
综合任务不宜将全部逻辑平铺在主循环中。标准做法是引入状态机:将任务拆分为若干状态,小车在任意时刻只处于一个状态,满足迁移条件时切换到下一状态。状态划分如下:
| 状态 | 执行内容 | 迁移条件 |
|---|---|---|
| 状态 1:循迹前进 | 按第 8 章逻辑沿轨道行驶 | 火焰探测器 D 口检测到火焰 → 状态 2 |
| 状态 2:对准则火 | 停车,原地旋转并读取 A 口,在读数最小的方向停止 | 方向锁定且读数低于阈值 → 状态 3 |
| 状态 3:灭火 | 风扇正转吹风,结束后关闭风扇并复测火焰 | 火焰消失 → 状态 4;火焰未灭 → 继续本状态 |
| 状态 4:回归轨道 | 平移 / 转向返回轨道上方 | 循迹探头重新检测到黑线 → 状态 1 |
| 状态 5:到达终点 | 停车,蜂鸣器长鸣,板载 LED 闪烁 | 任务结束 |

在 Mind+ 中定义一个变量(如"状态")记录当前状态编号,主循环中通过"如果……那么……"结构按状态执行对应逻辑即可。
本章无新知识点,Mind+ 图形化代码从略。重点在于通过算法锻炼编程思维。学员可根据下方 MicroPython 代码自行拖拽还原图形化程序,如需协助请联系编者微信。
python
# 循迹 + 灭火综合任务:状态机版
# 用到的子函数全部来自前面章节:
# wheel_*/stop_all/turn1(第3、4章)、go_sp(第4章)、turn_angle(第4章)、
# servo_angle/distance(第5章)、beep(第8章)、aim_flame/flame_detected(第9章)
# ===== 阈值集中管理:更换场地时只改这几个数 =====
FLAME_ON = 1 # D 口有火时的读数(按 9.2 的实测,有火 = 1)
FLAME_NEAR = 50 # A 口"足够近"阈值:读数低于它才算够近(火焰越近读数越小)
FLAME_LOCK = 200 # A 口"方向锁定"阈值:对准后读数低于它才进入灭火
# 四个探头:S1→GP10(X1)、S2→GP11(X2)、S3→GP12(X3)、S4→GP13(X4)
# 读数规律:压黑线 = 0(指示灯亮),不压线 = 1(指示灯灭)
GP10 = Pin(10, Pin.IN)
GP11 = Pin(11, Pin.IN)
GP12 = Pin(12, Pin.IN)
GP13 = Pin(13, Pin.IN)
led = Pin(25, Pin.OUT) # 板载 LED,终点提示用
# (buzzer、flame_D、flame_A、fan_F、fan_R 已在第 8、9 章定义,直接沿用)
state = 1 # 当前状态:1 循迹 / 2 对准 / 3 灭火 / 4 回归 / 5 终点
last_state = 0 # 上一个状态,用于进出状态时鸣提示音
e = 0 # 循迹方向(8.5 的算法:负左正右,脱线继承上次值)
last_e = 0 # 上一瞬间的循迹方向
n = 0.25 # 循迹力度系数:0.25 = 微调,1 = 大转
while True:
# 状态提示音:刚进入新状态时,鸣响"状态编号"次数
# 一声 = 循迹,两声 = 对准……不用连电脑也知道小车在哪一步
if state != last_state:
beep(state)
last_state = state
if state == 1:
# ── 状态 1:循迹前进(8.5 的算法单步版),同时盯着火焰 D 口 ──
S1 = GP10.value()
S2 = GP11.value()
S3 = GP12.value()
S4 = GP13.value()
if S1 == 0 and S2 == 0 and S3 == 0 and S4 == 0:
stop_all()
state = 5 # 四路全压线(全 0)= 终点横线 → 状态 5
else:
# 方向:中间两探头都没压线(都为 1)= 脱线,继承上次方向;
# 否则 e = S1 - S3(左内压线得 -1,右内压线得 1,都压线得 0)
if S1 == 1 and S3 == 1:
e = last_e
else:
e = S1 - S3
# 力度:外侧探头压线 = 严重偏离,全力大转;仅中间压线 = 微调
if S2 == 0 or S4 == 0:
n = 1
elif S1 == 0 or S3 == 0:
n = 0.25
last_e = e
turn1(e * n)
# 循迹的同时盯着火焰:D 口发现火焰 → 状态 2
if flame_detected():
stop_all()
state = 2
elif state == 2:
# ── 状态 2:对准则火(第 9 章函数)──
aim_flame() # 旋转一圈找 A 口最小读数方向
if flame_A.read_u16() < FLAME_LOCK:
state = 3 # 方向锁定且读数够小(火焰够近)→ 灭火
# 读数不够小说明可能是误检,留在本状态下一轮再对准一次
elif state == 3:
# ── 状态 3:灭火(第 9 章流程)──
go_sp(150, 1) # 先靠近到"足够近"
while flame_A.read_u16() > FLAME_NEAR:
time.sleep(0.05)
stop_all()
fan_F.value(1) # 吹 5 秒
fan_R.value(0)
time.sleep(5)
fan_F.value(0)
fan_R.value(0)
time.sleep(0.5)
if not flame_detected():
state = 4 # 复测:灭了 → 回归轨道;没灭 → 留在本状态再吹
elif state == 4:
# ── 状态 4:回归轨道 ──
# 缓慢原地旋转,直到任一循迹探头重新看到黑线(读到 0)
# 默认左旋找回;若轨道在另一侧,把两侧速度正负对调即可
wheel_left_1(-150)
wheel_left_2(-150)
wheel_right_1(150)
wheel_right_2(150)
if GP10.value() == 0 or GP11.value() == 0 or GP12.value() == 0 or GP13.value() == 0:
stop_all()
state = 1 # 找回黑线 → 回到循迹
elif state == 5:
# ── 状态 5:到达终点 ──
stop_all()
buzzer.value(1) # 蜂鸣器长鸣 1 秒
time.sleep(1)
buzzer.value(0)
for _ in range(10): # 板载 LED 闪烁 10 次
led.value(1)
time.sleep(0.2)
led.value(0)
time.sleep(0.2)
break # 任务结束,退出主循环
time.sleep(0.01)cpp
// 循迹 + 灭火综合任务:状态机版
// 用到的子函数全部来自前面章节:
// wheel_*/stop_all/turn1(第3、4章)、go_sp(第4章)、turn_angle(第4章)、
// distance(第5章)、beep(第8章)、aim_flame/flame_detected(第9章)
// ===== 阈值集中管理:更换场地时只改这几个数 =====
const int FLAME_ON = 1; // D 口有火时的读数(按 9.2 的实测,有火 = 1)
const int FLAME_NEAR = 50; // A 口"足够近"阈值:读数低于它才算够近
const int FLAME_LOCK = 200; // A 口"方向锁定"阈值:读数低于它才进入灭火
// 四个探头:S1→GP10(X1)、S2→GP11(X2)、S3→GP12(X3)、S4→GP13(X4)
// 读数规律:压黑线 = 0(指示灯亮),不压线 = 1(指示灯灭)
const int GP10 = 10;
const int GP11 = 11;
const int GP12 = 12;
const int GP13 = 13;
const int LED = 25; // 板载 LED,终点提示用
// (BUZZER 见第 8 章,FLAME_D、FLAME_A、FAN_F、FAN_R 见第 9 章,直接沿用)
int state = 1; // 当前状态:1 循迹 / 2 对准 / 3 灭火 / 4 回归 / 5 终点
int last_state = 0; // 上一个状态,用于进出状态时鸣提示音
float e = 0; // 循迹方向(8.5 的算法:负左正右,脱线继承上次值)
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);
pinMode(FLAME_D, INPUT);
pinMode(FAN_F, OUTPUT); pinMode(FAN_R, OUTPUT);
pinMode(LED, OUTPUT);
}
void loop() {
// 状态提示音:刚进入新状态时,鸣响"状态编号"次数
if (state != last_state) {
beep(state);
last_state = state;
}
if (state == 1) {
// ── 状态 1:循迹前进(8.5 的算法单步版),同时盯着火焰 D 口 ──
int S1 = digitalRead(GP10), S2 = digitalRead(GP11);
int S3 = digitalRead(GP12), S4 = digitalRead(GP13);
if (S1 == 0 && S2 == 0 && S3 == 0 && S4 == 0) {
stop_all();
state = 5; // 四路全压线(全 0)= 终点横线 → 状态 5
} else {
// 方向:中间两探头都没压线(都为 1)= 脱线,继承上次方向;
// 否则 e = S1 - S3(左内压线得 -1,右内压线得 1,都压线得 0)
if (S1 == 1 && S3 == 1) {
e = last_e;
} else {
e = S1 - S3;
}
// 力度:外侧探头压线 = 严重偏离,全力大转;仅中间压线 = 微调
if (S2 == 0 || S4 == 0) {
n = 1;
} else if (S1 == 0 || S3 == 0) {
n = 0.25;
}
last_e = e;
turn1(e * n);
// 循迹的同时盯着火焰:D 口发现火焰 → 状态 2
if (flame_detected()) {
stop_all();
state = 2;
}
}
} else if (state == 2) {
// ── 状态 2:对准则火(第 9 章函数)──
aim_flame(); // 旋转一圈找 A 口最小读数方向
if (analogRead(FLAME_A) < FLAME_LOCK) {
state = 3; // 方向锁定且读数够小(火焰够近)→ 灭火
}
// 读数不够小说明可能是误检,留在本状态下一轮再对准一次
} else if (state == 3) {
// ── 状态 3:灭火(第 9 章流程)──
go_sp(150, 1); // 先靠近到"足够近"
while (analogRead(FLAME_A) > FLAME_NEAR) {
delay(50);
}
stop_all();
digitalWrite(FAN_F, HIGH); // 吹 5 秒
digitalWrite(FAN_R, LOW);
delay(5000);
digitalWrite(FAN_F, LOW);
digitalWrite(FAN_R, LOW);
delay(500);
if (!flame_detected()) {
state = 4; // 复测:灭了 → 回归轨道;没灭 → 再吹
}
} else if (state == 4) {
// ── 状态 4:回归轨道 ──
// 缓慢原地旋转,直到任一循迹探头重新看到黑线(读到 0)
// 默认左旋找回;若轨道在另一侧,把两侧速度正负对调即可
wheel_left_1(-150);
wheel_left_2(-150);
wheel_right_1(150);
wheel_right_2(150);
if (digitalRead(GP10) == 0 || digitalRead(GP11) == 0 ||
digitalRead(GP12) == 0 || digitalRead(GP13) == 0) {
stop_all();
state = 1; // 找回黑线 → 回到循迹
}
} else if (state == 5) {
// ── 状态 5:到达终点 ──
stop_all();
digitalWrite(BUZZER, HIGH); // 蜂鸣器长鸣 1 秒
delay(1000);
digitalWrite(BUZZER, LOW);
for (int i = 0; i < 10; i++) { // 板载 LED 闪烁 10 次
digitalWrite(LED, HIGH);
delay(200);
digitalWrite(LED, LOW);
delay(200);
}
while (true) {} // 任务结束,停在这里
}
delay(10);
}10.3 调试方法
分状态调试:先将"状态"变量固定为某一值,单独验证该状态的动作是否正确,全部通过后再串联运行;
状态提示音:进入每个状态时让蜂鸣器发出不同次数的鸣响(一声 = 循迹,两声 = 对准……),无需连接电脑即可判断小车当前状态;
阈值集中管理:将火焰判定、循迹判定用到的阈值统一定义在程序开头的变量中,更换场地时只需修改这几个数值。
10.4 拓展练习
任务计时:记录从出发到灭火完成的用时,优化程序并刷新纪录;
双车对抗:两辆 D3 配合,一辆运行磁吸跟随程序进行追踪,另一辆按预设程序行驶逃逸;
巡逻车:循迹行驶的同时,由舵机带动超声波模块持续扫描,发现障碍物即停车报警。
