PRJ-03Легко60 миндеталей: 8Есть в симуляторе

Робот, объезжающий препятствия

Машина получает ультразвуковой датчик HC-SR04. Она едет вперёд, а если что-то оказывается ближе 25 см, отъезжает назад, поворачивает направо и снова едет вперёд.

Что делает робот

Робот едет вперёд. Когда препятствие ближе 25 см, он 0,3 секунды едет назад, 0,38 секунды разворачивается вправо и снова едет вперёд.

Робот, объезжающий препятствия

Фото: Anani A. George · CC0

Видеона YouTube

Этот робот сам ездит по комнате и отворачивает от всего, что оказывается у него на пути. Ультразвуковой датчик спереди измеряет расстояние перед роботом. Когда что-то ближе 25 см, робот отъезжает назад, поворачивает направо и снова едет вперёд. Вы узнаете, как HC-SR04 измеряет расстояние, как конечный автомат упорядочивает действия робота и как millis() позволяет роботу измерять на ходу. Это несложный проект, на него уйдёт около 60 минут.

Что понадобится

Детали для этого проектаВсё есть в наборе Набор робот-машины 2WD с Arduino

Это машина из проекта «Первый заезд» с ультразвуковым датчиком HC-SR04 спереди. Если машина у вас уже есть, посмотрите в Маркете дополнительный набор для объезда препятствий.

Как это работает

1 Чувствовать: HC-SR04 измеряет расстояние. 2 Думать: Arduino Uno выполняет скетч. 3 Действовать: L298N крутит моторы.

Как HC-SR04 измеряет расстояние

Спереди у HC-SR04 две круглые детали. Одна излучает ультразвук — звук частотой 40 кГц, слишком высокий, чтобы человек мог его услышать. Другая слушает эхо. Одно измерение проходит так:

  1. Uno переводит TRIG в HIGH на 10 мкс, и датчик посылает короткий ультразвуковой сигнал.
  2. Датчик переводит ECHO в HIGH и слушает.
  3. Когда эхо возвращается, датчик снова переводит ECHO в LOW.

Значит, ECHO остаётся в HIGH столько времени, сколько нужно звуку, чтобы долететь до предмета и вернуться. При 20 °C скорость звука — около 343 м/с. На каждый сантиметр расстояния звук пролетает 2 см, туда и обратно, и на это уходит около 58 мкс. Поэтому расстояние в сантиметрах — это время ECHO в микросекундах, делённое на 58. Например, 1450 мкс — это 1450 / 58 = 25 см.

У датчика есть ограничения:

Ограничение Что это значит для робота
Дальность примерно от 2 см до 4 м предметы ближе или дальше дают неверные показания или не дают их совсем
Луч шириной около 15 градусов предметы сбоку от луча не видны
Мягкие или наклонные поверхности звук поглощается или уходит в сторону, и эхо не возвращается
Около 60 мс между измерениями старому эху нужно время, чтобы затихнуть

Конечный автомат

В каждый момент робот делает ровно одно из трёх действий. Каждое из них — это состояние:

Состояние Моторы Состояние заканчивается, когда Следующее состояние
Drive (езда) drive(190, 190): вперёд что-то ближе 25 см Back
Back (задний ход) drive(-160, -160): назад прошло 300 мс Turn
Turn (поворот) drive(170, -170): разворот вправо прошло 380 мс Drive

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

Измерения без ожидания

loop() никогда не вызывает delay(). Эта функция выполняется тысячи раз в секунду и каждый раз делает две вещи:

  1. Измерить, если пора. Если с последнего измерения прошло 60 мс, она измеряет расстояние и выводит его.
  2. Выполнить текущее состояние. Она задаёт работу моторов и проверяет, не пора ли закончить это состояние.

Чтобы знать, сколько прошло времени, скетч использует millis() — число миллисекунд с момента запуска Uno. Немного ждёт только само измерение, потому что pulseIn() ждёт эхо. Для препятствия в 25 см это около 1,5 мс. Если в зоне действия ничего нет, ожидание прекращается через 25 мс. Во время этого короткого ожидания моторы продолжают работать.

Почему бы просто не написать delay(300) для состояния Back? Потому что на время этой паузы робот ослепнет. С конечным автоматом он измеряет расстояние каждые 60 мс в любом состоянии. Этот скетч пока не использует показания в состояниях Back и Turn, но вы можете это сделать: первое задание научит робота прекращать поворот, как только путь свободен.

Сборка

1. Возьмите робота в симуляторе

У Obstacle avoider, машины, которая есть в игре с самого начала, HC-SR04 уже стоит спереди на верхней пластине (ярусе). Провода у неё подключены не так, как на этой странице, поэтому на шаге 2 вы подключите их заново.

В реальной сборке: возьмите свою машину из проекта «Первый заезд». Закрепите HC-SR04 спереди, ровно, чтобы обе круглые детали смотрели прямо вперёд. Если датчик смотрит вниз, он видит пол. Если вверх — не замечает низкие препятствия.

2. Подключите датчик

ОткудаКудаЗачем
Батарея +L298N +12VПитание моторов: около 6 В от четырёх батареек AA
Батарея -L298N GNDМинус батареи
L298N +5VUno 5VСтабилизатор драйвера питает Uno: около 4,4 В
L298N GNDUno GNDОбщая земля: сигналам нужен общий 0 В
Uno D5L298N ENAСкорость левого мотора (ШИМ); перемычка ENA снята
Uno D4L298N IN1Направление левого мотора
Uno D7L298N IN2Направление левого мотора
Uno D8L298N IN3Направление правого мотора
Uno D12L298N IN4Направление правого мотора
Uno D6L298N ENBСкорость правого мотора (ШИМ); перемычка ENB снята
Левый мотор M+L298N OUT1
Левый мотор M-L298N OUT2
Правый мотор M+L298N OUT4Переставлено специально: этот мотор стоит зеркально
Правый мотор M-L298N OUT3Переставлено специально: этот мотор стоит зеркально
HC-SR04 VCCL298N +5V5 В для датчика
HC-SR04 GNDUno GNDЗемля
Uno D9HC-SR04 TRIGЗапускает измерение: импульс 10 мкс
Uno D10HC-SR04 ECHOИмпульс длиной во время полёта эха туда и обратно

Подключение машины остаётся таким же, как в проекте «Первый заезд». Датчик подключается четырьмя проводами: VCC — к +5V драйвера L298N, GND — к пину GND платы Uno, TRIG — к D9, ECHO — к D10. На +5V драйвера L298N те же 5 В, что питают Uno, потому что эта клемма соединена проводом с пином 5V платы Uno.

В реальной сборке: в клемме +5V уже зажат провод к Uno. Вставьте провод VCC от датчика в ту же клемму рядом с ним и затяните винт. Дважды проверьте TRIG и ECHO: их легко перепутать.

3. Загрузите и проверьте

Откройте «Код», вставьте скетч ObstacleAvoider с этой страницы, нажмите «Проверить», а затем «Загрузить».

В реальной сборке: поставьте машину на коробку, чтобы колёса висели в воздухе, подключите USB-кабель и загрузите скетч. Откройте монитор порта (Serial Monitor) на скорости 9600 бод. Поводите рукой перед датчиком: числа меняются вслед за рукой. Когда рука ближе 25 см, колёса сначала крутятся назад, а потом — как при развороте вправо.

4. Пусть робот поездит

Запустите заезд на арене с препятствиями. Робот едет, пока что-нибудь не окажется ближе 25 см, затем отъезжает назад, поворачивает и снова едет вперёд. Откройте монитор порта: каждые 60 мс робот выводит одно расстояние в сантиметрах. Значение 400 означает, что эхо не вернулось, то есть в пределах примерно 4 м ничего нет. Расстояние видно и на панели показаний в реальном времени.

В реальной сборке: отключите USB-кабель и поставьте машину на пол среди нескольких коробок или книг. Твёрдые плоские предметы дают самое хорошее эхо.

5. Подберите числа

Настройка Что задаёт Попробуйте
STOP_CM = 25 насколько близко робот подпускает препятствие 35, если робот натыкается на предметы
CRUISE = 190 скорость движения вперёд 150 — для более спокойного робота
BACK_MS = 300 сколько робот едет назад больше в тесных местах
TURN_MS = 380 сколько робот разворачивается 600 — для поворота побольше

Быстрому роботу нужен STOP_CM побольше: ему требуется больше места, чтобы остановиться.

Код

ObstacleAvoider.ino
// Obstacle avoider: drive ahead; when something is close, back off and turn.
// Written without delay() in loop(): a state machine and millis() timers,
// so the robot keeps measuring while it moves.
// L298N: ENA -> D5, IN1 -> D4, IN2 -> D7 (left), ENB -> D6, IN3 -> D8, IN4 -> D12 (right)
// HC-SR04: TRIG -> D9, ECHO -> D10

const byte ENA = 5, IN1 = 4, IN2 = 7;
const byte ENB = 6, IN3 = 8, IN4 = 12;
const byte TRIG = 9, ECHO = 10;

const int CRUISE = 190;               // forward speed, 0..255
const int STOP_CM = 25;               // closer than this: avoid it
const unsigned long PING_MS = 60;     // the HC-SR04 needs a pause between pings
const unsigned long BACK_MS = 300;
const unsigned long TURN_MS = 380;

enum class State : byte { Drive, Back, Turn };
State state = State::Drive;
unsigned long stateStart = 0;
unsigned long lastPing = 0;
int distanceCm = 400;

void setMotor(byte enablePin, byte inA, byte inB, int speed) {
  speed = constrain(speed, -255, 255);
  digitalWrite(inA, speed > 0);
  digitalWrite(inB, speed < 0);
  analogWrite(enablePin, abs(speed));
}

void drive(int left, int right) {
  setMotor(ENA, IN1, IN2, left);
  setMotor(ENB, IN3, IN4, right);
}

// Distance in centimetres; 400 when no echo came back (nothing within about 4 m).
int readDistanceCm() {
  digitalWrite(TRIG, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG, HIGH);            // a 10 microsecond pulse starts a measurement
  delayMicroseconds(10);
  digitalWrite(TRIG, LOW);
  unsigned long us = pulseIn(ECHO, HIGH, 25000UL);  // wait at most 25 ms
  if (us == 0) return 400;
  return us / 58;                      // sound goes there and back: 58 us per cm
}

void enter(State next) {
  state = next;
  stateStart = millis();
  if (next == State::Drive) lastPing = 0;  // measure again before driving on
}

void setup() {
  const byte pins[] = {ENA, IN1, IN2, ENB, IN3, IN4, TRIG};
  for (byte pin : pins) pinMode(pin, OUTPUT);
  pinMode(ECHO, INPUT);
  Serial.begin(9600);
}

void loop() {
  unsigned long now = millis();
  if (now - lastPing >= PING_MS) {
    lastPing = now;
    distanceCm = readDistanceCm();
    Serial.println(distanceCm);
  }

  switch (state) {
    case State::Drive:
      drive(CRUISE, CRUISE);
      if (distanceCm < STOP_CM) enter(State::Back);
      break;
    case State::Back:
      drive(-160, -160);
      if (now - stateStart >= BACK_MS) enter(State::Turn);
      break;
    case State::Turn:
      drive(170, -170);                // spin right on the spot
      if (now - stateStart >= TURN_MS) enter(State::Drive);
      break;
  }
}

Состояние и таймеры

enum class State : byte { Drive, Back, Turn };
State state = State::Drive;
unsigned long stateStart = 0;
unsigned long lastPing = 0;
int distanceCm = 400;

enum class State даёт имена трём состояниям, а благодаря : byte состояние занимает один байт. В state хранится текущее состояние; робот начинает с состояния Drive. stateStart запоминает, когда началось текущее состояние, а lastPing — когда было последнее измерение. Обе переменные имеют тип unsigned long — именно этот тип возвращает millis(). Начальное значение distanceCm — 400, что означает «ничего не видно».

readDistanceCm(): одно измерение

  digitalWrite(TRIG, HIGH);            // a 10 microsecond pulse starts a measurement
  delayMicroseconds(10);
  digitalWrite(TRIG, LOW);
  unsigned long us = pulseIn(ECHO, HIGH, 25000UL);  // wait at most 25 ms
  if (us == 0) return 400;
  return us / 58;                      // sound goes there and back: 58 us per cm

Перед этими строками TRIG 2 мкс удерживается в LOW, чтобы импульс начался чисто. pulseIn() измеряет в микросекундах, сколько времени ECHO остаётся в HIGH. Если эхо не вернулось за 25 мс, результат равен 0, и тогда функция возвращает 400. Иначе она делит время на 58 и получает сантиметры.

enter(): смена состояния

void enter(State next) {
  state = next;
  stateStart = millis();
  if (next == State::Drive) lastPing = 0;  // measure again before driving on
}

Каждая смена состояния проходит через enter(). Функция сохраняет новое состояние и время его начала. Когда робот возвращается в Drive, lastPing = 0 заставляет сделать новое измерение при следующем проходе loop(). Так робот принимает решение по свежему расстоянию, измеренному уже после поворота.

loop(): сначала измерить, потом действовать

  unsigned long now = millis();
  if (now - lastPing >= PING_MS) {
    lastPing = now;
    distanceCm = readDistanceCm();
    Serial.println(distanceCm);
  }

now - lastPing >= PING_MS спрашивает: «Прошло ли 60 мс?» Записанная через вычитание, эта проверка остаётся верной, даже когда примерно через 49,7 дня millis() снова начинает счёт с 0.

Дальше идёт switch, в котором для каждого состояния есть свой case. В состоянии Drive оба мотора получают скорость CRUISE, а когда distanceCm становится меньше STOP_CM, робот переходит в Back. В состоянии Back робот едет назад со скоростью 160 и через BACK_MS переходит в Turn. В состоянии Turn он разворачивается вправо — левое колесо вперёд, правое назад — и через TURN_MS переходит в Drive. setMotor() и drive() такие же, как в проекте «Первый заезд».

Если что-то не работает

  • Монитор порта всё время показывает 400. В зоне действия ничего нет, или датчик подключён неправильно. Проверьте VCC и GND, а также то, что TRIG идёт на D9, а ECHO — на D10.
  • Монитор порта показывает непонятные символы (в реальной сборке). Выставьте в нём 9600 бод — ту же скорость, что в Serial.begin(9600).
  • Робот врезается в стены, к которым подъезжает под углом. Стена под углом отражает звук мимо датчика. Мягкие вещи, например шторы, его поглощают. Увеличьте STOP_CM или уменьшите CRUISE.
  • Робот едет назад, хотя впереди ничего нет. Возможно, датчик видит пол — установите его ровно. Эхо может вернуть и провод, который свисает перед датчиком.
  • Робот задевает ножки стульев или угол коробки. Тонкие предметы отражают мало звука, а то, что сбоку от узкого луча, датчик не видит. Это ограничение самого датчика.
  • Колесо крутится не в ту сторону. Поменяйте местами два провода этого мотора, как в проекте «Первый заезд».
  • Робот перезагружается, когда начинает ехать назад. Резкая смена хода с «вперёд» на «назад» требует большого тока, и напряжение батареи проседает. Поставьте новые батарейки и используйте короткие провода питания. Добавьте конденсатор 100–470 мкФ между +12V и GND рядом с драйвером, минусовой ножкой к GND. Подробнее — в статье о том, почему роботы перезагружаются.

Что дальше

Этот робот отвечает только «да» или «нет»: ближе 25 см или нет. В следующем проекте скорость выбирается по самому расстоянию: это робот, держащий дистанцию.

Подробнее о таймерах и состояниях — в уроке про millis() и состояния. О датчике читайте в уроке про HC-SR04 и в статье о том, как «видят» ультразвуковые датчики.

Сделайте лучше

  1. Сделайте так, чтобы робот выходил из состояния Turn, как только путь свободен, например когда расстояние больше 50 см, а не через заданное время.
  2. Пусть робот поворачивает налево или направо наугад: random(2) даёт 0 или 1.
  3. Научите робота ехать медленнее, когда до препятствия от 25 до 50 см.