教程 · 中级 · 阅读约 14 分钟

用红外传感器阵列检测线迷宫中的路口

循迹机器人是如何区分 T 型路口和十字路口、死胡同和终点的——以及为什么仅凭一次传感器读数,永远不足以做出判断。

循迹车只有一项工作:让线保持在中间。线迷宫机器人有两项,而第二项正是新手卡住的地方。它仍然要跟着线走,但还必须注意到线提供选择的那些时刻——并判断那是哪一种选择。

这个决策层坐在 PID 环之上,而不是里面。控制器永远不该知道迷宫是什么。它负责朝一条线转向;路口层盯着同一批传感器读数,寻找意味着“有东西变了”的模式,并在这种模式出现时接管。

路口只可能是六种

胶带线迷宫里的每一个路口,都可以归到六种情况之一。用一个 8 通道反射阵列,黑胶带读 1、白板读 0,每一种都会产生一个特征签名:

六栏图示,展示画在浅色板上的黑色胶带的线迷宫路口类型,每一栏都叠加一个八通道传感器条:只有左转读 11111000,只有右转读 00011111,T 路口读全 1,十字路口读全 1,死胡同读全 0,终点标记是实心块读全 1。
六种路口类型,以及每一种在 8 通道阵列上留下的模式。其中三种在到达的那一刻无法区分。 下载 SVG
  • 只有左转——最外侧的左传感器变黑,右侧保持白。
  • 只有右转——镜像。
  • T 路口——胶带横跨整个宽度,前方什么都没有。
  • 十字——胶带横跨整个宽度,而且线继续向前。
  • 死胡同——每个传感器都读白;线单纯地停了。
  • 终点——一个实心填充块,又宽又长,开进去一直是黑的。

单次读数是有歧义的

看 T 路口、十字路口和终点标记。三者都报 11111111。一个左岔而线同时继续直行,报的数和一条普通的左转完全一样。

这一点值得刻进脑子里:**瞬时的传感器模式无法给路口分类。**一台在外侧传感器变黑的那一刻就做决定的机器人,会在十字路口自信地左转,而那里它本该直穿过去。

解法是运动,而不是更多的传感器。当阵列第一次报出宽黑模式时:

  1. 记下哪一侧(或哪几侧)变黑了——那就是可用岔路的集合。
  2. 继续向前开大约一个车长。
  3. 再读一次。如果中心传感器仍然看到胶带,说明正前方也是一个选项。如果全是白的,那是 T 路口。如果全仍然是黑的,你开进终点块了。

只有到这一步你才知道自己刚离开的是什么路口,也只有到这一步迷宫层才能选择。

Junction classify(Sensors& s) {
  Branches at = s.readWideBranches();      // left / right seen on arrival
  driveForward(ONE_ROBOT_LENGTH);          // commit past the intersection
  Reading after = s.read();

  if (after.allBlack())  return FINISH;    // still on tape = solid block
  at.straight = after.centreOnLine();      // line continued through
  return at.toJunction();
}

注意这个分类器会开车。对一个感知函数来说这很不寻常,而这正是重点——路口类型是一段短轨迹的属性,不是单次采样的属性。

选择哪条岔

路口分类之后,迷宫层按一条固定规则挑岔。左手法则——永远走最左边可用的岔,在死胡同掉头——是标准选择,因为它无状态、在无环迷宫上是穷尽的,而且手工验证起来很简单。

这里一致性比聪明更重要。一台永远优先向左的机器人最终会访问到每一条可达的岔,而它的转弯记录正是路线化简那一步之后被折叠成最优路径的东西。在迷宫求解模拟器里看同样的“先探索再优化”行为——那台机器感知的是墙而不是胶带,但感知之上的决策层完全相同。

阵列必须比线更宽

路口检测向传感器问的是一个很具体的问题:**最外侧通道变黑了吗,而中心仍然在线上?**一个勉强比胶带宽一点的阵列,物理上就答不出这个问题。

   left branch                    8-channel array, ~60 mm wide
   ────────────────┐          [ 1 1 1 0 0 0 0 0 ]   outer left black -> a branch exists
                   │
   ════════════════╪═══════   [ 0 0 0 1 1 0 0 0 ]   centre still on the line
                   │
胶带宽度 最小可用阵列宽度 原因
19 mm(标准) 约 60 mm 外侧传感器必须能跨出一个完整传感器间距才躲开胶带
25 mm 约 75 mm
12 mm 约 40 mm

经验法则是胶带宽度的三倍。再窄,机器人就分不出“线急弯”和“线分岔”——而这个判断正是整个项目赖以成立的核心决策。8 通道、8 mm 间距的阵列约 60 mm,这就是它成为 19 mm 胶带标准选择的原因。

给路口消抖

阵列要经过好几个循环迭代才跨过路口,每一轮都是一个新鲜读数。一个在第一次宽模式就触发的分类器,常常会为同一个路口触发两三次,把记录下来的路径填满根本不是真实转弯的字符。

const uint8_t CONFIRM_SAMPLES = 3;      // consecutive readings that must agree
const unsigned long JUNCTION_DEADTIME_MS = 300;

uint8_t agreeCount = 0;
uint8_t lastPattern = 0;
unsigned long lastJunctionAt = 0;

bool junctionDetected(uint8_t pattern) {
  // Ignore everything for a short while after committing to one.
  if (millis() - lastJunctionAt < JUNCTION_DEADTIME_MS) return false;

  if (pattern == lastPattern) agreeCount++;
  else { lastPattern = pattern; agreeCount = 1; }

  if (agreeCount >= CONFIRM_SAMPLES && isWide(pattern)) {
    lastJunctionAt = millis();
    agreeCount = 0;
    return true;
  }
  return false;
}

确定死区时间是一个真实的取舍,而且值得用数字而不是凭感觉来做:

deadtime < (minimum spacing between junctions) / speed

在 0.2 m/s、路口间距不小于 15 cm 的条件下,余量有 750 ms——所以 300 ms 是安全的。而在间距 10 cm 的迷宫里以 0.5 m/s 行驶,预算就掉到 200 ms,这正是探索阶段要慢慢跑的原因。定下这个速度的是这条约束,不是算法。

转弯本身

给路口分类只是活的一半;不丢线地把转弯执行出来是另一半,而固定角度转弯是错误的做法。

方法 怎么结束 可靠性
转固定时间 N 毫秒后 差——随电池电压和地面变化
用编码器转固定角度 N 个计数后 好一些,但背隙和打滑会累积
转到重新看见线为止 中心传感器看到胶带时 最好——自修正,结束在线所在处
void turnUntilLine(int direction) {
  pivot(direction);
  delay(120);                        // clear the junction's own tape first
  while (!centreOnLine()) {          // then rotate until the new line appears
    if (millis() - startedAt > TURN_TIMEOUT_MS) { stopMotors(); fault(); return; }
  }
  stopMotors();
}

delay(120) 很关键。转弯刚开始时,中心传感器还压在路口自己的胶带上,于是朴素的“看见线就转完”会立刻停下,机器人根本转不起来。先离开路口,再去看。

超时同样关键:一台转过了岔的机器人——因为跑得太快,或者岔被误判了——会永远原地旋转下去。

带直行判定的完整分类

把消抖、开过去和转弯合在一起:

Junction classifyAndTurn() {
  const Branches at = readWideBranches();     // left and/or right, on arrival

  driveForward(ONE_ROBOT_LENGTH);             // commit past the intersection
  const Reading after = read();

  if (after.allBlack()) return FINISH;        // still on tape = the solid block

  Junction j;
  j.left     = at.left;
  j.right    = at.right;
  j.straight = after.centreOnLine();          // resolved only by moving

  if (!j.left && !j.right && !j.straight) return DEAD_END;
  return j;
}

分类器会开车,这对感知函数来说很不寻常,而这就是全部重点:路口类型是一段短轨迹的属性,不是单次采样的属性。六种情况里有三种——T、十字和终点——在到达的那一刻无法区分。

不建迷宫也能测它

验证这一层并不需要迷宫,而在检测还没正常工作之前先造一个迷宫,是常见的浪费一个晚上的方式。在板上每种路口各摆一个,让机器人逐个开过去并打印分类结果:

测试件 预期 常见失败
素直线 完全不报路口 在缓弯上触发——外侧阈值太灵敏
缓弯,半径 15 cm 不报路口 同上
只有左岔 LEFT 判成 T——死区太短,触发了两次
只有右岔 RIGHT 上一条的镜像
T 路口 T 判成 LEFT——没有开过去判定直行
十字 CROSS 判成 T——开过去的距离太短
死胡同 DEAD_END 判成丢线,机器人开了过去
实心终点块 FINISH 判成 CROSS——块比路口长

缓弯那一项抓到的缺陷最多,而它恰恰是没人想到要跑的那一项。一个急弯会瞬间把好几个外侧传感器压在胶带上,看起来和一条岔一模一样。如果你的机器人在弯道上凭空造出路口,修法是要求外侧传感器在最小距离上保持为黑,而不只是在最少采样数上保持——弯道很快就会松开它们,真正的岔不会。

故障排查

现象 可能原因 解决
弯道上被判成路口 外侧阈值太灵敏 要求该模式在最小距离上持续存在
一个路口被记了两次 没有死区时间 确认路口后 200–300 ms
T 路口报成左转 没有开过去判定直行 分类器必须先动再决定
十字报成 T 开过去的距离太短 开过整个路口、越过整整一个车长
终点报成十字 块比路口长 继续开;它一直是黑的,十字不会
路口整个漏掉 太快,或阵列太窄 探索时慢跑;阵列 ≥ 胶带宽度的 3 倍
转弯冲过新线 固定时间或固定角度转弯 转到中心传感器重新抓住为止
路口之后永远旋转 没有转弯超时 加上,并干净地报故障
根本不转 把路口自己的胶带读成了新线 先离开路口再去看
这块板能用那块板不能 标定问题,或环境光 每次上电重新标定;用关发射器相减

在真胶带上做到可靠

有三件事在硬件上打断路口检测的频率,远高于算法本身:

  • **标定漂移。**每次上电都在黑胶带和白板各跑一遍阵列的标定扫描,换房间就重跑。阳光和荧光灯带来的环境红外会改变读数。
  • **传感器高度。**离地 5–8 mm。太高对比度会塌掉;太低,一块略微翘曲的板子就会刮到阵列。
  • **胶带质量。**用 18–20 mm 的胶带,并且把路口做得物理上干净。毛边拐角会被读成时有时无的岔,而代码没法修复一把会对你说谎的胶带。

先在简单的椭圆赛道上把普通循迹做稳——在循迹模拟器里调好,再上真胶带。迷宫会把机械和传感器的问题藏在一个看起来像逻辑缺陷的东西后面。

探索关联图谱

本教程所属的搭建

包含本教程的项目和学习路线。

延伸阅读

参考资料