Руководство · Средний · 14 min read

Обнаружение развилок линейного лабиринта ИК-массивом

Как робот, следующий по линии, отличает Т-развилку от перекрёстка, тупик от финиша — и почему одного показания датчика никогда недостаточно для решения.

У робота, следующего по линии, одна задача: удерживать линию по центру. У робота для прохождения лабиринта их две, и именно на второй новички застревают. Он должен по-прежнему следовать по линии, но также замечать моменты, когда появляется выбор, и определять тип развилки.

Этот слой принятия решений находится поверх контура ПИД (PID), а не внутри него. Регулятор никогда не должен знать, что такое лабиринт. Он ведёт робот по линии; слой развилок наблюдает те же показания датчиков, ищет шаблоны, означающие «что-то изменилось», и берёт управление на себя, когда такой шаблон появляется.

Развилка может быть шести видов

Каждая развилка в линейном лабиринте с лентой сводится к одному из шести случаев. При 8-канальном массиве датчиков отражённого света (IR), который читает 1 для чёрной ленты и 0 для белой доски, каждый случай даёт свой шаблон:

Шесть панелей с типами развилок линейного лабиринта, нарисованными чёрной лентой на светлой доске; поверх каждого показан восьмиканальный массив датчиков: только левый поворот читает 11111000, только правый — 00011111, Т-развилка — все единицы, перекрёсток — все единицы, тупик — все нули, а финишная метка — сплошной блок, читающий все единицы.
Шесть типов развилок и шаблон, который каждый из них создаёт на 8-канальном массиве. Три из них в момент прибытия неразличимы. Скачать SVG
  • Только левый поворот — показания крайних левых датчиков соответствуют чёрному, а правая сторона остаёся белой.
  • Только правый поворот — зеркальное отображение.
  • Т-развилка — лента проходит по всей ширине, впереди ничего нет.
  • Перекрёсток — лента проходит по всей ширине, и линия продолжается впереди.
  • Тупик — все датчики читают белый; линия просто закончилась.
  • Финиш — сплошной закрашенный блок, широкий и длинный, который остаётся чёрным при въезде.

Одного показания недостаточно

Посмотрите на Т-развилку, перекрёсток и финишную метку. Все три сообщают 11111111. Ответвление влево, когда линия одновременно продолжается прямо, сообщает то же самое, что и простой левый поворот.

Вот что стоит запомнить: мгновенный шаблон датчиков не может классифицировать развилку. Робот, который принимает решение в момент, когда показания крайних датчиков соответствуют чёрному, уверенно повернёт налево на перекрёстке, где следовало проехать прямо.

Исправление — движение, а не дополнительные датчики. Когда массив впервые сообщает широкий чёрный шаблон:

  1. Запишите, где с одной или с обеих сторон появился чёрный сигнал, — это множество доступных ответвлений.
  2. Продолжайте ехать вперёд примерно на длину робота.
  3. Считайте снова. Если центральные датчики всё ещё видят ленту, прямое продолжение тоже было доступно. Если все датчики показывают белое, это была Т-развилка. Если все по-прежнему показывают чёрное, вы въехали в финишный блок.

Только теперь известно, какую развилку робот проехал, и только теперь слой лабиринта может выбрать направление.

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 безопасно. При движении 0.5 m/s в лабиринте с шагом 10 cm бюджет падает до 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;
}

Классификатор управляет движением, что необычно для функции восприятия и является сутью: тип развилки — свойство короткой траектории, а не одного измерения. Три из шести случаев — Т-развилка, перекрёсток и финиш — в момент прибытия неразличимы.

Проверка без лабиринта

Чтобы проверить этот слой, лабиринт не нужен, а строить его до рабочего обнаружения — обычная потеря вечера. Разложите на доске по одной развилке каждого типа и проезжайте по ним по очереди, печатая классификацию:

Тестовый элемент Ожидается Типичный сбой
Простая прямая Вообще никаких развилок Срабатывает на небольшом повороте — внешний порог слишком чувствителен
Плавный поворот, радиус 15 cm Нет развилки То же
Только левое ответвление LEFT Обнаружено как T — мёртвое время слишком мало, сработало дважды
Только правое ответвление RIGHT Зеркальное отображение предыдущего
Т-развилка T Обнаружена как LEFT — не проехали вперёд для разрешения прямого пути
Перекрёсток CROSS Обнаружен как T — расстояние проезда слишком мало
Тупик DEAD_END Обнаружена потеря линии, и робот продолжил движение
Сплошной финишный блок FINISH Обнаружен как CROSS — блок длиннее развилки

Тест плавного поворота ловит больше всего ошибок, и именно его никто не вспоминает. Резкий поворот временно помещает несколько крайних датчиков на ленту, что выглядит точно как ответвление. Если робот придумывает развилки на поворотах, исправление — требовать, чтобы показания крайних датчиков соответствовали чёрному на минимальном расстоянии, а не только при минимальном числе отсчётов: на повороте они быстро освобождаются, настоящее ответвление — нет.

Диагностика

Симптом Вероятная причина Исправление
На поворотах обнаруживаются развилки Внешний порог слишком чувствителен Требовать, чтобы шаблон сохранялся на минимальном расстоянии
Одна развилка записана дважды Нет мёртвого времени 200–300 ms после фиксации развилки
Т сообщается как левый поворот Не проехали вперёд для разрешения прямого пути Классификатор должен двигаться до решения
Перекрёсток сообщается как Т Слишком малое расстояние проезда Одна полная длина робота за перекрёстком
Финиш сообщается как перекрёсток Блок длиннее развилки Продолжайте движение; он остаётся чёрным, перекрёсток — нет
Развилки полностью пропускаются Слишком быстро или слишком узкий массив Замедлите разведку; массив ≥ 3× ширины ленты
Поворот перелетает новую линию Фиксированное время или угол Поворачивать до повторного обнаружения центральными датчиками
После развилки робот вращается вечно Нет тайм-аута поворота Добавьте его и корректно сообщите об отказе
Робот вообще не поворачивает Собственная лента развилки принята за новую линию Покиньте развилку перед поиском
На одной доске работает, на другой нет Калибровка или внешний свет Повторяйте калибровку при каждом включении; вычитание при выключенном излучателе

Как добиться надёжности на реальной ленте

Три вещи ломают обнаружение развилок на реальном оборудовании гораздо чаще алгоритма:

  • Дрейф калибровки. При каждом включении проводите калибровочный проход массива по чёрной ленте и белой доске, а при переходе в другую комнату повторяйте его. Внешний инфракрасный свет от солнца и люминесцентных ламп меняет показания.
  • Высота датчиков. 5–8 mm над полом. Слишком высоко — контраст падает; слишком низко — слегка изогнутая доска задевает массив.
  • Качество ленты. Используйте ленту 18–20 mm и делайте развилки физически чистыми. Разлохленный угол читается как прерывистое ответвление, и никакой код не исправит ленту, которая вам лжёт.

Сначала добейтесь стабильного обычного следования по линии на простом овале — настройте его в симуляторе робота, следующего по линии, затем на реальной ленте. Лабиринт скрывает механические проблемы и проблемы датчиков за тем, что выглядит как логическая ошибка.

Исследуйте граф

Входит в эти сборки

Проекты и учебные пути, в которые входит это руководство.

Дополнительные материалы

Источники