Skip to content

第 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 调试方法

  1. 分状态调试:先将"状态"变量固定为某一值,单独验证该状态的动作是否正确,全部通过后再串联运行;

  2. 状态提示音:进入每个状态时让蜂鸣器发出不同次数的鸣响(一声 = 循迹,两声 = 对准……),无需连接电脑即可判断小车当前状态;

  3. 阈值集中管理:将火焰判定、循迹判定用到的阈值统一定义在程序开头的变量中,更换场地时只需修改这几个数值。

10.4 拓展练习

  • 任务计时:记录从出发到灭火完成的用时,优化程序并刷新纪录;

  • 双车对抗:两辆 D3 配合,一辆运行磁吸跟随程序进行追踪,另一辆按预设程序行驶逃逸;

  • 巡逻车:循迹行驶的同时,由舵机带动超声波模块持续扫描,发现障碍物即停车报警。

不积跬步,无以至千里;万里之行,始于当下。