Этот робот сам ездит по комнате и отворачивает от всего, что оказывается у него на пути. Ультразвуковой датчик спереди измеряет расстояние перед роботом. Когда что-то ближе 25 см, робот отъезжает назад, поворачивает направо и снова едет вперёд. Вы узнаете, как HC-SR04 измеряет расстояние, как конечный автомат упорядочивает действия робота и как millis() позволяет роботу измерять на ходу. Это несложный проект, на него уйдёт около 60 минут.
Что понадобится
Arduino Uno R3Есть в симуляторе× 1Цену смотрите в магазине
Модуль драйвера моторов L298NЕсть в симуляторе× 1Цену смотрите в магазине
Мотор-редуктор TT 1:48 с колесом 65 ммЕсть в симуляторе× 2Цену смотрите в магазине
Батарейный отсек 4 × AAЕсть в симуляторе× 1Цену смотрите в магазинеШаровая опора 20 ммЕсть в симуляторе× 1Цену смотрите в магазине
Ультразвуковой датчик HC-SR04Есть в симуляторе× 1Цену смотрите в магазине
Набор проводов-перемычек× 1Цену смотрите в магазине
Это машина из проекта «Первый заезд» с ультразвуковым датчиком HC-SR04 спереди. Если машина у вас уже есть, посмотрите в Маркете дополнительный набор для объезда препятствий.
Как это работает
Как HC-SR04 измеряет расстояние
Спереди у HC-SR04 две круглые детали. Одна излучает ультразвук — звук частотой 40 кГц, слишком высокий, чтобы человек мог его услышать. Другая слушает эхо. Одно измерение проходит так:
- Uno переводит TRIG в HIGH на 10 мкс, и датчик посылает короткий ультразвуковой сигнал.
- Датчик переводит ECHO в HIGH и слушает.
- Когда эхо возвращается, датчик снова переводит 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(). Эта функция выполняется тысячи раз в секунду и каждый раз делает две вещи:
- Измерить, если пора. Если с последнего измерения прошло 60 мс, она измеряет расстояние и выводит его.
- Выполнить текущее состояние. Она задаёт работу моторов и проверяет, не пора ли закончить это состояние.
Чтобы знать, сколько прошло времени, скетч использует 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 +5V | Uno 5V | Стабилизатор драйвера питает Uno: около 4,4 В |
| L298N GND | Uno GND | Общая земля: сигналам нужен общий 0 В |
| Uno D5 | L298N ENA | Скорость левого мотора (ШИМ); перемычка ENA снята |
| Uno D4 | L298N IN1 | Направление левого мотора |
| Uno D7 | L298N IN2 | Направление левого мотора |
| Uno D8 | L298N IN3 | Направление правого мотора |
| Uno D12 | L298N IN4 | Направление правого мотора |
| Uno D6 | L298N ENB | Скорость правого мотора (ШИМ); перемычка ENB снята |
| Левый мотор M+ | L298N OUT1 | |
| Левый мотор M- | L298N OUT2 | |
| Правый мотор M+ | L298N OUT4 | Переставлено специально: этот мотор стоит зеркально |
| Правый мотор M- | L298N OUT3 | Переставлено специально: этот мотор стоит зеркально |
| HC-SR04 VCC | L298N +5V | 5 В для датчика |
| HC-SR04 GND | Uno GND | Земля |
| Uno D9 | HC-SR04 TRIG | Запускает измерение: импульс 10 мкс |
| Uno D10 | HC-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 побольше: ему требуется больше места, чтобы остановиться.
Код
// 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 и в статье о том, как «видят» ультразвуковые датчики.
Сделайте лучше
- Сделайте так, чтобы робот выходил из состояния Turn, как только путь свободен, например когда расстояние больше 50 см, а не через заданное время.
- Пусть робот поворачивает налево или направо наугад: random(2) даёт 0 или 1.
- Научите робота ехать медленнее, когда до препятствия от 25 до 50 см.
