Руководства · 15 мин чтения

Робот, объезжающий препятствия, на Arduino: руководство по HC-SR04 и сервоприводу

Собери на Arduino робота, объезжающего препятствия: HC-SR04 на сервоприводе, подключение L298N или L293D, логика «стоп — назад — поворот», ИК-датчики и ручной режим по Bluetooth.

Робот, объезжающий препятствия, — это самоходная робот-машинка, которая замечает стену, стул или коробку впереди, не врезаясь в них, и находит новый путь в обход. Чаще всего его собирают из Arduino Uno, ультразвукового датчика расстояния HC-SR04, сервопривода SG90, который поворачивает датчик влево и вправо, двух DC-моторов с редуктором и драйвера моторов L298N (или L293D). Робот едет прямо, останавливается, приблизившись к препятствию, немного отъезжает назад, поворачивает «голову» в обе стороны, чтобы провести измерения, и затем продолжает путь в самом свободном направлении.

В этом руководстве мы превратим двухколёсную (2WD) машинку из статьи Робот-машинка на Arduino в автономного робота, сохранив ту же распиновку. Шаг за шагом разберём таблицы подключения, расчёт порогов расстояния, код, который растёт в три этапа, поверхности, которые датчик не видит, и как закрыть эти пробелы инфракрасными датчиками, а в конце — переход в ручной режим по Bluetooth с телефона. Код для управления тем же роботом с ESP32 или Raspberry Pi Pico — в конце статьи.

Ультразвуковой датчик расстояния HC-SR04
HC-SR04: излучатель с пометкой T посылает звук 40 kHz, а приёмник с пометкой R слушает эхо. Это глаз робота, объезжающего препятствия; он измеряет 2–400 см в узком угле около 15 градусов.

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

Любой автономный робот десятки раз в секунду повторяет один и тот же цикл из трёх шагов: восприятие, решение, действие. За восприятие отвечает HC-SR04: он посылает короткий звук 40 kHz, который ухо не слышит, и измеряет, сколько времени эхо идёт обратно; если разделить это время в микросекундах на 58, получится расстояние в сантиметрах. Решения принимает Arduino, сравнивая измеренное расстояние с заданными порогами. Действия берут на себя L298N и моторы. Поскольку датчик стоит на сервоприводе, робот может «смотреть» влево и вправо, не поворачивая корпус, и поэтому принимает гораздо более удачные решения о поворотах, чем робот с неподвижным датчиком. Подробнее о том, как работает датчик, читай в статье Датчик расстояния HC-SR04.

ДетальЗадачаПримечание
Arduino Uno (или Nano)Мозг: измеряет, принимает решения и командует моторамиПодойдёт и ESP32 или Pico
HC-SR04Глаз: измеряет расстояние до препятствия впередиРаботает от 5 V, 2–400 см
Сервопривод SG90Шея: поворачивает датчик влево и вправоДатчик крепится к качалке сервопривода пластиковым кронштейном
L298N или шилд L293DМышцы: задаёт моторам направление и скоростьПин Arduino не может дать ток для мотора
2 TT-мотора с редуктором и поворотное колесоДифференциальный приводПоворот выполняется вращением двух колёс в разные стороны
2 аккумулятора 18650 (7,4 V)ПитаниеБатарейки-«кроны» на 9 V не хватит
2 ИК-датчика препятствий (по желанию)Ловят препятствия по углам и сбокуМодули на LM393, например FC-51
HC-05 (по желанию)Ручное управление с телефонаРаботает только с Android
Arduino Uno
Arduino Uno
L298N
L298N
TT-мотор
TT-мотор
Литий-ионный аккумулятор 18650
Литий-ионный аккумулятор 18650

Питание: не дай сервоприводу перезагрузить робота

У робота, объезжающего препятствия, к моторам добавляется сервопривод, поэтому распределение питания здесь важнее, чем у обычной робот-машинки. SG90 в покое потребляет около 10 mA; в момент начала движения ток подскакивает до 100–250 mA, а под нагрузкой приближается к половине ампера. Если этот всплеск совпадёт с пусковым током моторов, линия 5 V у Arduino на мгновение проседает, и плата перезапускается. Решение простое: моторы питаются от аккумулятора через L298N, Arduino — от аккумулятора через VIN, а сервопривод, HC-SR04, ИК-датчики и HC-05 делят пин 5V платы Arduino, причём рядом с выводами питания сервопривода ставится конденсатор 470 µF. Даже когда сервопривод движется, общая нагрузка на линию 5 V остаётся меньше половины ампера, а так как она кратковременная, стабилизатор Uno с ней справляется. Чтобы рассчитать время работы от аккумулятора со своими числами, воспользуйся утилитой Время работы от батареи.

УзелОткуда питаниеТипичный ток
2 TT-мотораКлемма 12V на L298N ← 2 × 18650 (7,4 V)0,3–0,6 A при движении, до 2 A при старте
Arduino UnoVIN ← плюс аккумулятора (после выключателя)Около 50 mA
Сервопривод SG905V Arduino + конденсатор 470 µF~10 mA в покое, 100–250 mA в движении
HC-SR045V ArduinoОколо 15 mA
2 ИК-датчика5V Arduino20–40 mA каждый
HC-055V Arduino30–40 mA при сопряжении, около 10 mA после подключения
Соедини все GND вместе (минус аккумулятора, L298N, Arduino, сервопривод, датчики); без общей земли датчик выдаёт бессмысленные расстояния, а сервопривод дёргается. Не питай сервопривод через тот же тонкий провод-перемычку, что и моторы. Если используешь более мощный сервопривод, например MG90S или MG996R, питай его не от Arduino, а от отдельного понижающего модуля, настроенного на 5 V (например, LM2596).

Схема подключения: Arduino Uno, ESP32 и Pico

Пины моторов те же, что в руководстве по робот-машинке, поэтому перейти к этому проекту можно, просто добавив головку с датчиком и не разбирая машинку. Пины скорости на 5 и 6 выбраны намеренно: на Uno библиотека Servo использует Timer1 и потому отключает ШИМ на пинах 9 и 10; если бы скорости моторов были на этих пинах, регулировка скорости сломалась бы сразу после подключения сервопривода. HC-SR04 и ИК-датчики садятся на A0–A3. Аналоговые пины Uno при необходимости работают и как цифровые пины 14–19, а такой выбор упрощает использование того же кода с шилдом L293D, потому что шилд эти пины не трогает. Расположение пинов своей платы можно посмотреть в утилите Распиновка.

ПодключениеArduino UnoESP32Raspberry Pi Pico
L298N ENA (скорость левого мотора)D5GPIO 25GP15
L298N IN1, IN2D7, D8GPIO 27, 33GP14, GP13
L298N ENB (скорость правого мотора)D6GPIO 26GP10
L298N IN3, IN4D9, D4GPIO 32, 4GP12, GP11
Сигнал сервопривода (оранжевый)D3GPIO 13GP16
Сервопривод + (красный), − (коричневый)5V, GNDL298N 5V, GNDL298N 5V, GND
HC-SR04 TRIGA0GPIO 18GP17
HC-SR04 ECHOA1 (напрямую)GPIO 19 через делитель 1 kΩ / 2 kΩGP18 через делитель 1 kΩ / 2 kΩ
HC-SR04 VCC5VL298N 5VL298N 5V
OUT левого ИК, OUT правого ИКA2, A3GPIO 23, 22GP19, GP20
VCC ИК-датчиков5V3V33V3
HC-05 TXD, RXDD10, D11 (RXD через делитель)Не нужен (встроенный Bluetooth)GP1, GP0 (UART0)
Питание платыVIN ← аккумулятор (после выключателя)5V (VIN) ← L298N 5VVSYS ← L298N 5V
Пин ECHO у HC-SR04 выдаёт сигнал 5 V, а пины ESP32 и Pico работают на 3,3 V и 5 V не выдерживают. Собери делитель напряжения по схеме ECHO → 1 kΩ → точка, идущая к плате → 2 kΩ → GND: 5 V × 2 / 3 ≈ 3,3 V. Для TRIG делитель не нужен: сигнала 3,3 V хватает, чтобы запустить датчик. На ESP32 и Pico плата, сервопривод и HC-SR04 делят один выход 5V на L298N, поэтому можно приблизиться к пределу этого маленького стабилизатора примерно в 0,5 A; если начнутся перезагрузки, питай сервопривод от отдельного источника 5 V.

Установка и проверка головки с датчиком

Микросервопривод SG90
SG90: микросервопривод весом 9 граммов, который при 4,8 V поворачивается на 60 градусов примерно за 0,1 секунды. Пластиковый кронштейн с HC-SR04 прикручивается к качалке сервопривода.
  1. Подключи сервопривод без качалки и загрузи тестовый скетч ниже. В конце каждого прохода код ставит сервопривод на 90 градусов и ждёт 2 секунды; если в этой паузе отключить кабель USB, сервопривод останется на месте. Надень качалку на вал так, чтобы датчик смотрел прямо вперёд; тогда 90 градусов будут означать «прямо».
  2. Закрепи HC-SR04 на качалке пластиковым кронштейном или термоклеем. Провода датчика должны быть достаточно длинными, чтобы не натягиваться при повороте сервопривода; кабель, цепляющийся за шасси во время поворота на 140 градусов, — частая неисправность.
  3. Установи датчик на высоте 8–12 см от пола и ближе к самой передней точке корпуса. Слишком низко — и он ловит эхо от пола и фактуры ковра; слишком высоко — и не видит низкие препятствия.
  4. Наклони датчик вверх на 3–5 градусов. Датчик, который стоит идеально ровно и низко, может принять эхо от пола за препятствие, особенно на неровном полу.
  5. Открой монитор порта на скорости 9600 бод. Если при руке слева от датчика маленькие значения появляются в строках 125 и 160 градусов, значит, левой стороне соответствуют большие углы; если наоборот, в продвинутом скетче поставь SERVO_DIR в -1, а в первом скетче поменяй местами значения LEFT_ANGLE и RIGHT_ANGLE.
C++ (Arduino IDE)
1#include <Servo.h>
2
3const int SERVO_PIN = 3;
4const int TRIG = A0, ECHO = A1;        // analog pinler dijital pin olarak / analog pins used as digital pins
5Servo head;
6
7int pingCm() {
8  digitalWrite(TRIG, LOW);  delayMicroseconds(2);
9  digitalWrite(TRIG, HIGH); delayMicroseconds(10);   // 10 µs tetik darbesi / 10 µs trigger pulse
10  digitalWrite(TRIG, LOW);
11  unsigned long t = pulseIn(ECHO, HIGH, 25000UL);    // en fazla 25 ms bekle (~4,3 m) / wait at most 25 ms (~4.3 m)
12  return t / 58;                                     // 0 = yankı yok / 0 = no echo
13}
14
15void setup() {
16  Serial.begin(9600);
17  pinMode(TRIG, OUTPUT);
18  pinMode(ECHO, INPUT);
19  head.attach(SERVO_PIN);
20  head.write(90);                                    // önce ortala / center first
21  delay(1000);
22}
23
24void loop() {
25  const int angles[] = {20, 55, 90, 125, 160};
26  for (int a : angles) {
27    head.write(a);
28    delay(400);                                      // servo yerine otursun / let the servo settle
29    Serial.print(a);
30    Serial.print(F(" derece / degrees: "));
31    Serial.print(pingCm());
32    Serial.println(F(" cm"));
33  }
34  Serial.println();
35  head.write(90);
36  delay(2000);
37}

В каждой строке показаны угол и расстояние в этом направлении. Если в пределах примерно 4 метров от датчика ничего нет, ты увидишь 0 cm: pulseIn() возвращает 0, когда эхо не приходит в течение 25 мс. В основном коде мы будем считать 0 «путь свободен», но датчик с отошедшим проводом или перепутанными TRIG и ECHO обычно тоже показывает 0. Поэтому, прежде чем ставить робота на пол, подержи руку на расстоянии 10, 30 и 60 см и убедись, что значения действительно меняются; робот, запущенный без такой проверки, въезжает в стену, даже не притормозив.

Алгоритм принятия решений: стоп, назад, поворот в самую свободную сторону

Если разделить поведение робота на зоны по расстоянию, код получается и простым, и надёжным. Без препятствия впереди робот едет на полной скорости, по мере приближения к препятствию замедляется, а дойдя до порога остановки, останавливается и сканирует. Во время сканирования датчик смотрит на 70 и 35 градусов влево и на 35 и 70 градусов вправо; в каждом направлении снимаются три показания и берётся их медиана. Робот поворачивает в сторону с наибольшим расстоянием на соответствующий угол. Если свободного направления нет, например когда робот заехал в угол, он немного сдаёт назад и разворачивается на 180 градусов.

Измеренное расстояниеЗонаЧто делает робот
Больше 60 см (или нет эха)СвободноПолный вперёд (PWM 200)
25–60 смСближениеЗамедляется пропорционально расстоянию (PWM от 200 до 130)
12–25 смПрепятствиеОстанавливается, подтверждает показание, сканирует в обе стороны, поворачивает в самую свободную
Меньше 12 смСлишком близкоСначала отъезжает назад 350 мс, затем сканирует
Во всех направлениях меньше 35 смТупикСдаёт назад и разворачивается на 180 градусов

Эти пороги выбраны не наугад. Расстояние остановки складывается из трёх запасов: запаса на реакцию (путь между двумя измерениями), запаса на торможение (сколько робот прокатится после отключения моторов) и запаса на поворот (чтобы нос не задел препятствие при развороте на месте). Машинка с TT-моторами на 6 V едет со скоростью около 0,5–0,7 м/с и за интервал измерения 60 мс проходит 3–4 см. Запас на торможение — около 5 см, а для корпуса шириной 15 см, разворачивающегося на месте, запас на поворот — около 10 см. В сумме получается около 18–20 см; с запасом прочности в несколько сантиметров сверху STOP_CM = 25 — хорошая отправная точка. Если твой робот быстрее или пол скользкий, подними порог до 30–35 см. А в узких коридорах слишком большой порог заставляет робота постоянно останавливаться и сканировать, поэтому окончательную настройку делай на своей трассе.

Пошаговый код 1: первый робот, объезжающий препятствия

Первая версия — это скелет алгоритма: ехать вперёд, остановиться на 25 см, отъехать назад, посмотреть влево и вправо, повернуть в более свободную сторону. Функция моторов drive(left, right) та же, что в руководстве по робот-машинке, и принимает значения от -255 до 255; положительные — вперёд, отрицательные — назад. После загрузки кода сначала поставь машинку на коробку так, чтобы колёса не касались пола, и понаблюдай, как она останавливается и поворачивает, когда подносишь руку к датчику; если всё выглядит правильно, ставь робота на пол.

C++ (Arduino IDE)
1#include <Servo.h>
2
3const int ENA = 5, IN1 = 7, IN2 = 8;            // sol motor / left motor
4const int ENB = 6, IN3 = 9, IN4 = 4;            // sağ motor / right motor
5const int SERVO_PIN = 3;
6const int TRIG = A0, ECHO = A1;
7
8const int STOP_CM = 25;                         // bu mesafede dur / stop at this distance
9const int SPEED = 170;                          // düz gidiş hızı / cruising speed
10const int TURN = 170;                           // dönüş hızı / turning speed
11const int LEFT_ANGLE = 160, RIGHT_ANGLE = 20;   // ters ise yer değiştir / swap if reversed
12
13Servo head;
14
15void drive(int left, int right) {               // her biri -255 … 255 / each -255 … 255
16  digitalWrite(IN1, left > 0);  digitalWrite(IN2, left < 0);
17  digitalWrite(IN3, right > 0); digitalWrite(IN4, right < 0);
18  analogWrite(ENA, abs(left));
19  analogWrite(ENB, abs(right));
20}
21
22int pingCm() {
23  digitalWrite(TRIG, LOW);  delayMicroseconds(2);
24  digitalWrite(TRIG, HIGH); delayMicroseconds(10);
25  digitalWrite(TRIG, LOW);
26  unsigned long t = pulseIn(ECHO, HIGH, 25000UL);
27  if (t == 0) return 400;                       // yankı yok: önü açık say / no echo: treat as open
28  return t / 58;
29}
30
31int lookAt(int angle) {
32  head.write(angle);
33  delay(400);                                   // servo dönsün ve dursun / let the servo turn and settle
34  return pingCm();
35}
36
37void setup() {
38  int pins[] = {ENA, IN1, IN2, ENB, IN3, IN4, TRIG};
39  for (int p : pins) pinMode(p, OUTPUT);
40  pinMode(ECHO, INPUT);
41  drive(0, 0);
42  head.attach(SERVO_PIN);
43  head.write(90);
44  delay(3000);                                  // yere koyman için süre / time to put it on the floor
45}
46
47void loop() {
48  int front = pingCm();
49  if (front > STOP_CM) {
50    drive(SPEED, SPEED);                        // yol açık: ileri / path clear: forward
51    delay(60);                                  // ölçümler arası 60 ms / 60 ms between readings
52    return;
53  }
54
55  drive(0, 0);                                  // 1) dur / 1) stop
56  delay(150);
57  drive(-SPEED, -SPEED);                        // 2) biraz geri çekil / 2) back off a little
58  delay(300);
59  drive(0, 0);
60
61  int left = lookAt(LEFT_ANGLE);                // 3) iki yana bak / 3) look both ways
62  int right = lookAt(RIGHT_ANGLE);
63  head.write(90);
64  delay(300);
65
66  if (left < STOP_CM && right < STOP_CM) {      // iki yan kapalı: geri dön / both blocked: turn around
67    drive(-TURN, TURN);
68    delay(700);
69  } else if (left > right) {                    // 4) daha açık tarafa dön / 4) turn to the more open side
70    drive(-TURN, TURN);
71    delay(350);
72  } else {
73    drive(TURN, -TURN);
74    delay(350);
75  }
76  drive(0, 0);
77  delay(100);
78}

Этот код справляется с задачей, но у него есть слабые места: одно неверное показание зря останавливает робота, к препятствиям он всегда подъезжает с одной и той же скоростью, смотрит только в две стороны, а время поворота не зависит от угла. А поскольку он ждёт с помощью delay(), во время сканирования он ни на что не может реагировать. Подробности логики ожидания — в статье millis() и delay(); следующая версия исправляет большинство этих проблем.

Пошаговый код 2: фильтрация, плавное замедление, ИК и Bluetooth

Продвинутая версия работает с тем же подключением, и ничего страшного, если ИК-датчики и HC-05 ещё не установлены. Входы ИК настроены как INPUT_PULLUP, поэтому неподключённый датчик читается как «нет препятствия» (HIGH); без HC-05 робот просто стартует в автоматическом режиме. В таблице ниже объясняется, что делает каждая часть кода.

C++ (Arduino IDE)
1#include <Servo.h>
2#include <SoftwareSerial.h>
3
4// Pinler / Pins
5const int ENA = 5, IN1 = 7, IN2 = 8;     // sol motor / left motor
6const int ENB = 6, IN3 = 9, IN4 = 4;     // sağ motor / right motor
7const int SERVO_PIN = 3;
8const int TRIG = A0, ECHO = A1;
9const int IR_LEFT = A2, IR_RIGHT = A3;   // LOW = engel (takılı değilse HIGH) / LOW = obstacle (HIGH if not fitted)
10SoftwareSerial bt(10, 11);               // RX, TX → HC-05 TXD, RXD (RXD bölücüyle / RXD via divider)
11
12// Ayarlar / Settings
13const int BACK_CM = 12;                  // daha yakınsa geri çekil / closer: back off first
14const int STOP_CM = 25;                  // dur ve tara / stop and scan
15const int SLOW_CM = 60;                  // yavaşlamaya başla / start slowing down
16const int GUARD_CM = 15;                 // manuel modda çarpışma koruması / collision guard in manual mode
17const int MAX_CM = 400;
18const int CRUISE = 200, SLOW = 130, TURN = 170;   // PWM hızları / PWM speeds
19const int SERVO_CENTER = 90;             // sensör öne bakmıyorsa düzelt / adjust if not facing ahead
20const int SERVO_DIR = 1;                 // +70 sola bakmıyorsa -1 yap / set to -1 if +70 doesn't look left
21const float MS_PER_DEG = 4.0;            // derece başına ms, kalibre et / ms per degree, calibrate
22const unsigned long BT_TIMEOUT = 600;    // manuelde komut kesilirse dur / stop if commands stop
23
24Servo head;
25bool autoMode = true;
26int manualLeft = 0, manualRight = 0, manualSpeed = 200;
27unsigned long lastCmd = 0, lastPing = 0;
28
29void drive(int left, int right) {        // her biri -255 … 255 / each -255 … 255
30  left = constrain(left, -255, 255);
31  right = constrain(right, -255, 255);
32  digitalWrite(IN1, left > 0);  digitalWrite(IN2, left < 0);
33  digitalWrite(IN3, right > 0); digitalWrite(IN4, right < 0);
34  analogWrite(ENA, abs(left));
35  analogWrite(ENB, abs(right));
36}
37
38int pingCm() {
39  digitalWrite(TRIG, LOW);  delayMicroseconds(2);
40  digitalWrite(TRIG, HIGH); delayMicroseconds(10);
41  digitalWrite(TRIG, LOW);
42  unsigned long t = pulseIn(ECHO, HIGH, 25000UL);  // ~4,3 m zaman aşımı / ~4.3 m timeout
43  return t == 0 ? MAX_CM : (int)(t / 58);          // yankı yok = açık alan / no echo = open space
44}
45
46int distanceCm() {                       // 3 ölçümün ortancası / median of 3 readings
47  int a = pingCm(); delay(60);
48  int b = pingCm(); delay(60);
49  int c = pingCm();
50  return max(min(a, b), min(max(a, b), c));
51}
52
53bool irBlocked(int pin) {
54  return digitalRead(pin) == LOW;
55}
56
57int lookAt(int offset) {                 // + sol, - sağ (derece) / + left, - right (degrees)
58  head.write(SERVO_CENTER + SERVO_DIR * offset);
59  delay(300);                            // servo yerine otursun / let the servo settle
60  return distanceCm();
61}
62
63void turnBy(int deg) {                   // + sola, - sağa / + left, - right
64  if (deg > 0) drive(-TURN, TURN);
65  else drive(TURN, -TURN);
66  delay((unsigned long)(abs(deg) * MS_PER_DEG));
67  drive(0, 0);
68  delay(100);
69}
70
71void avoid() {
72  drive(0, 0);                           // 1) dur / 1) stop
73  delay(120);
74  int front = distanceCm();              // ortancayla doğrula / confirm with the median
75  bool sides = irBlocked(IR_LEFT) || irBlocked(IR_RIGHT);
76  if (front >= STOP_CM && !sides) return;          // yanlış alarm / false alarm
77
78  if (front < BACK_CM || sides) {        // 2) çok yakın: geri çekil / 2) too close: back off
79    drive(-TURN, -TURN);
80    delay(350);
81    drive(0, 0);
82  }
83
84  const int offsets[] = {70, 35, -35, -70};        // 3) tara / 3) scan
85  int bestOffset = 0, bestCm = 0;
86  for (int off : offsets) {
87    int cm = lookAt(off);
88    if (cm > bestCm) { bestCm = cm; bestOffset = off; }
89  }
90  head.write(SERVO_CENTER);
91  delay(200);
92
93  if (bestCm < STOP_CM + 10) {           // her yön kapalı: çıkmaz / every way blocked: dead end
94    drive(-TURN, -TURN);
95    delay(400);
96    turnBy(180);
97  } else {
98    turnBy(bestOffset);                  // 4) en açık yöne dön / 4) turn to the most open direction
99  }
100}
101
102void autoStep() {
103  if (millis() - lastPing < 60) return;  // 60 ms'de bir ölç / measure every 60 ms
104  lastPing = millis();
105  int d = pingCm();
106  bool l = irBlocked(IR_LEFT), r = irBlocked(IR_RIGHT);
107
108  if (d < STOP_CM || (l && r)) {
109    avoid();
110  } else if (l) {                        // sol köşede engel: sağa kaç / front-left: dodge right
111    drive(TURN, -TURN);
112    delay(150);
113  } else if (r) {                        // sağ köşede engel: sola kaç / front-right: dodge left
114    drive(-TURN, TURN);
115    delay(150);
116  } else if (d < SLOW_CM) {
117    int s = map(d, STOP_CM, SLOW_CM, SLOW, CRUISE);   // yaklaştıkça yavaşla / slow down as you get closer
118    drive(s, s);
119  } else {
120    drive(CRUISE, CRUISE);
121  }
122}
123
124void manualStep() {
125  if (millis() - lastCmd > BT_TIMEOUT) {           // komut yok: dur / no commands: stop
126    manualLeft = 0;
127    manualRight = 0;
128  }
129  static int front = MAX_CM;
130  if (millis() - lastPing >= 60) {
131    lastPing = millis();
132    front = pingCm();
133  }
134  bool forward = manualLeft > 0 && manualRight > 0;
135  if (forward && front < GUARD_CM) drive(0, 0);    // çarpışma koruması / collision guard
136  else drive(manualLeft, manualRight);
137}
138
139void handleBluetooth() {
140  while (bt.available()) {
141    char c = bt.read();
142    Serial.println(c);                   // gelen harfi göster / show the incoming letter
143    lastCmd = millis();
144    if (c == 'A') {                      // otomatik mod / auto mode
145      autoMode = true;
146      continue;
147    }
148    if (c == 'M') {                      // manuel mod / manual mode
149      autoMode = false;
150      manualLeft = manualRight = 0;
151      drive(0, 0);
152      head.write(SERVO_CENTER);
153      continue;
154    }
155    if (autoMode) continue;              // otomatikte yön harflerini yok say / ignore in auto mode
156    int s = manualSpeed;
157    switch (c) {
158      case 'F': manualLeft = s;  manualRight = s;  break;   // ileri / forward
159      case 'B': manualLeft = -s; manualRight = -s; break;   // geri / backward
160      case 'L': manualLeft = -s; manualRight = s;  break;   // yerinde sola / spin left
161      case 'R': manualLeft = s;  manualRight = -s; break;   // yerinde sağa / spin right
162      case 'S': manualLeft = 0;  manualRight = 0;  break;   // dur / stop
163      default:
164        if (c >= '0' && c <= '9') manualSpeed = map(c - '0', 0, 9, 100, 240);   // hız kademesi / speed level
165    }
166  }
167}
168
169void setup() {
170  int outs[] = {ENA, IN1, IN2, ENB, IN3, IN4, TRIG};
171  for (int p : outs) pinMode(p, OUTPUT);
172  drive(0, 0);
173  pinMode(ECHO, INPUT);
174  pinMode(IR_LEFT, INPUT_PULLUP);
175  pinMode(IR_RIGHT, INPUT_PULLUP);
176  Serial.begin(9600);
177  bt.begin(9600);                        // HC-05 veri modu hızı / HC-05 data mode baud rate
178  head.attach(SERVO_PIN);
179  head.write(SERVO_CENTER);
180  delay(3000);                           // yere koyman için süre / time to put it on the floor
181}
182
183void loop() {
184  handleBluetooth();
185  if (autoMode) autoStep();
186  else manualStep();
187}
Функция или настройкаЧто делает
pingCm()Снимает одно показание; если эхо не приходит в течение 25 мс, возвращает 400 см (свободное пространство)
distanceCm()Снимает 3 показания с интервалом 60 мс и возвращает медиану; отбрасывает одиночный выброс
autoStep()Измеряет каждые 60 мс; в зависимости от расстояния и ИК-датчиков едет дальше, замедляется, уворачивается или вызывает avoid()
avoid()Останавливается, подтверждает показание, при необходимости отъезжает назад, смотрит в 4 стороны и поворачивает в самую свободную
turnBy(градусы)Разворачивает робота на месте на заданный угол; время рассчитывается через MS_PER_DEG
manualStep()Выполняет направление, присланное с телефона; при движении вперёд останавливается на 15 см, а если команд нет 600 мс, тоже останавливается
SERVO_CENTER, SERVO_DIRУгол, при котором датчик смотрит прямо вперёд, и в какую сторону лево
STOP_CM, SLOW_CM, BACK_CMПороги остановки, замедления и отъезда назад

Для робота, объезжающего препятствия, медианный фильтр лучше среднего: для трёх показаний вроде 24, 26 и 400 см среднее выходит 150 см и сбивает робота с толку, а медиана выбирает 26 см. Если хочешь копнуть глубже, загляни в статью Фильтрация данных с датчиков. Разделение на автоматический и ручной режим — по сути тоже небольшой конечный автомат; этот подход подробно разобран в руководстве Конечный автомат на Arduino. После загрузки кода настрой робота под свою комнату в таком порядке:

  1. Центр сервопривода: поставь робота ровно напротив плоской стены; если датчик смотрит на неё не прямо, меняй SERVO_CENTER небольшими шагами, например на 85 или 95.
  2. Время поворота: временно добавь строку turnBy(360); в конец setup() и посмотри, делает ли робот полный оборот. Если он поворачивается на 300 градусов вместо 360, поставь MS_PER_DEG = 4.0 × 360 / 300 = 4.8. Это значение меняется в зависимости от аккумулятора и пола, поэтому настраивай его с полностью заряженным аккумулятором на том полу, по которому будет ездить робот.
  3. Порог остановки: направь робота к стене и измерь, на каком расстоянии от стены остановится его нос. Если меньше 10 см, увеличь STOP_CM на 5 см.
  4. Медленная скорость: если SLOW ниже порога трогания моторов, робот застревает в зоне сближения; в этом случае подними SLOW до 150.

Слепые зоны HC-SR04: мягкие поверхности, стены под углом и тонкие препятствия

Ультразвуковой датчик видит поверхности, которые хорошо отражают звук, но далеко не каждое препятствие возвращает звук к датчику. Для хорошего результата производитель рекомендует цель площадью не меньше 0,5 м² и как можно более плоскую. А в настоящей комнате робот натыкается на шторы, диваны, ножки стульев и углы стен. Звук отражается под тем же углом, под которым пришёл, как бильярдный шар: датчик, смотрящий на стену прямо, получает сильное эхо, но если линия его взгляда отклоняется от перпендикуляра больше чем примерно на 30–40 градусов, звук скользит в другую сторону и не возвращается. Тогда pulseIn() возвращает 0, и код считает, что путь свободен. Это самая частая причина, по которой роботы чиркают по стенам.

Препятствие или поверхностьПочему не видноРешение
Шторы, диваны, подушки, одежда, шерсть питомцевМягкие пористые поверхности поглощают звук, и эхо возвращается очень слабымДобавь ИК-датчик для ближней зоны и веди робота медленно
Подъезд к стене под угломЗвук отражается в сторону, и эхо не доходит до датчикаПоставь на передних углах ИК-датчики, развёрнутые наружу, и сканируй под меньшими углами
Ножки стульев и столов, тонкие стойкиОтражающая поверхность крошечная, эхо слабоеСнизь скорость, используй медианный фильтр, добавь ИК-датчики по бокам
Низкие препятствия ниже датчика (кабели, пороги, тапочки)Они остаются под звуковым конусомДобавь направленный вниз ИК-датчик или микропереключатель на передний бампер
Края стола, лестничные пролётыУльтразвуковой датчик смотрит вперёд и не видит провала в полуОбнаружение края с помощью TCRT5000, направленного в пол
Предметы ближе 2 смМёртвая зона датчика: эхо возвращается раньше, чем закончится звуковой импульсНастрой робота на отъезд назад при 12 см
Углы и узкие коридорыЗвук отражается между двумя стенами и возвращается с опозданием, поэтому расстояние кажется большеМедианный фильтр и измерения под несколькими углами
Закрыть то, чего не видит один датчик, другим датчиком всегда эффективнее, чем пытаться сделать один датчик идеальным. Но первым шагом должно быть снижение скорости: робот, едущий со скоростью 0,3 м/с, после того же неверного показания ударяется не так сильно, как робот на 0,7 м/с, и остаётся время на второе измерение. Сильные и слабые стороны разных датчиков расстояния мы собрали в статье Сравнение датчиков расстояния для Arduino.

Добавляем ИК-датчики

Ультразвуковые и инфракрасные (ИК) датчики восполняют слабые стороны друг друга. ИК-датчики препятствий вроде FC-51 состоят из ИК-светодиода и приёмника; когда отражённый свет достаточно сильный, компаратор LM393 переводит выход в LOW, и на плате загорается светодиод. Дальность регулируется подстроечным резистором примерно от 2 до 30 см, но расстояние датчик не измеряет, а только сообщает «препятствие» или «нет препятствия». Если поставить два ИК-датчика на передних углах робота, развернув их на 30–45 градусов наружу, они поймают ножки стульев и стены под углом, которые выпадают из узкого конуса ультразвукового датчика. В продвинутом коде робот, когда срабатывает левый ИК-датчик, ненадолго уворачивается вправо, а когда правый — влево; если срабатывают оба сразу, он останавливается и сканирует, как будто впереди препятствие.

ХарактеристикаHC-SR04 (ультразвуковой)FC-51 (ИК-датчик препятствий)TCRT5000 (ИК-датчик отражения)
ВыходРасстояние (см)Да / нет (цифровой)Аналоговое значение и цифровой выход
Дальность2–400 смПримерно 2–30 см (регулируется)Примерно 1–15 мм
Угол обнаруженияОколо 15°Около 35°Очень узкий
Слабое местоМягкие, наклонные и тонкие поверхностиЧёрные матовые поверхности, солнечный светПерепады высоты, солнечный свет
Хорошо видитСтекло, чёрные предметы, большие плоские поверхностиТкань, углы, близкие препятствияЦвет пола и края стола
Место на роботеГлавный глаз, поворачивается сервоприводомПередние углыПод передним бампером, смотрит в пол
Инфракрасный датчик отражения TCRT5000
TCRT5000: если установить его вниз на высоте 5–10 мм от пола, он замечает края стола и лестничные пролёты; как только отражение пропадает, робот может остановиться и отъехать назад.

Для роботов, которые катаются по столу, самое опасное препятствие — это на самом деле обрыв. Если поставить под передним бампером модуль TCRT5000, направленный в пол, ты узнаешь о том, что пол исчез, в ту же секунду. Подключи цифровой выход модуля к D2, который на Uno ещё свободен; сначала проверь в мониторе порта, какой уровень появляется на выходе, когда пола нет, затем читай этот пин в самом начале autoStep() и, если пол пропал, сразу останавливай моторы и отводи робота назад. Эта проверка должна идти раньше измерения расстояния: падение всегда обходится дороже, чем удар.

Переход в ручной режим по Bluetooth

Иногда автономным роботом хочется поуправлять вручную: перевезти его из одной комнаты в другую, подвести к препятствию и проверить реакцию или просто поиграть. Подключение HC-05 в продвинутом коде делает это возможным. Когда телефон отправляет букву A, робот переходит в автоматический режим; когда отправляет M — в ручной. В ручном режиме используются управляющие буквы из руководства по робот-машинке (F, B, L, R, S) с одним отличием: если при движении вперёд робот видит препятствие ближе 15 см, он останавливается вопреки твоей команде. Такая защита от столкновений позволяет пользоваться датчиками автономного робота и при ручном управлении. Шаги сопряжения HC-05 и AT-команды — в статье Bluetooth-модуль HC-05.

БукваДействиеВ каком режиме?
AПерейти в автоматический режимВ обоих режимах
MПерейти в ручной режим, остановить роботаВ обоих режимах
F / BВперёд / назадРучной
L / RРазворот влево / вправо на местеРучной
SСтопРучной
0–9Уровень скорости (PWM 100–240)Ручной
  1. Подключи пин TXD модуля HC-05 к D10, пин RXD — к D11 через делитель 1 kΩ / 2 kΩ, VCC — к 5V, а GND — к GND.
  2. Выполни сопряжение с HC-05 в настройках Bluetooth телефона на Android с PIN-кодом 1234 (если не получится — 0000).
  3. Установи из Play Store приложение-пульт для Bluetooth-машинки и подключи его к HC-05. Если оставить Arduino подключённой по USB и открыть монитор порта на скорости 9600 бод, код будет печатать каждую букву, пришедшую с телефона.
  4. В большинстве приложений-пультов есть дополнительные кнопки, например фары или сигнал, и каждая отправляет свою букву. Посмотри эти буквы в мониторе порта и подставь их вместо 'A' и 'M' в коде; если хочется настроить кнопки по-своему, можно сделать собственный пульт по руководству App Inventor и Arduino по Bluetooth.
  5. Отключи кабель USB и щёлкни выключателем; через 3 секунды робот тронется в автоматическом режиме. Переключись в ручной режим и управляй им с телефона.
На Uno библиотека SoftwareSerial примерно на 1 мс откладывает другие прерывания, пока принимает байт; в это время импульсы библиотеки Servo могут растягиваться, и головка с датчиком может слегка дёрнуться. На поведение робота это не влияет, но если мешает, можно перейти на встроенный Bluetooth ESP32. HC-05 и HC-06 не работают с iPhone; для iPhone нужен BLE-модуль или ESP32, запрограммированная на BLE.

Сборка с моторным шилдом L293D

Моторный шилд с драйвером L293D
Моторный шилд L293D: вставляется прямо в Arduino Uno; четыре DC-мотора подключаются к клеммам M1–M4, а два сервопривода — к разъёмам в углу.

Если не нравится клубок проводов, вместо L298N можно взять моторный шилд L293D (Adafruit Motor Shield V1 и его клоны), который вставляется прямо в Arduino Uno. Шилд управляет четырьмя DC-моторами, имеет два разъёма для сервоприводов и питает их от линии 5 V платы Arduino. Моторы подключены не напрямую к пинам Arduino, а через сдвиговый регистр 74HC595 на плате, поэтому управлять ими нужно через библиотеку Adafruit Motor Shield (для V1, а не для V2), то есть AFMotor, установленную через «Менеджер библиотек» (Library Manager). Шилд занимает D3, D4, D5, D6, D7, D8, D11 и D12 под моторы и D9 и D10 под сервоприводы; A0–A5, D2 и D13 остаются свободными. HC-SR04 и ИК-датчики и так стоят на A0–A3, поэтому менять нужно только код моторов, пин сервопривода и пины HC-05.

ПараметрС L298NС шилдом L293D
Подключение моторовКлеммы OUT1–OUT4Клеммы M1 (левый) и M2 (правый)
Код моторовdigitalWrite и analogWrite внутри drive()AFMotor внутри drive()
SERVO_PIN39 или 10, в зависимости от разъёма, в который вставлен сервопривод
HC-05D10, D11A4 (RX) и A5 (TX): SoftwareSerial bt(A4, A5)
Ток на канал2 A длительно0,6 A длительно, 1,2 A пиковый
Питание моторовКлемма 12VКлемма EXT_PWR; с установленной перемычкой PWR Arduino питается от того же источника
C++ (Arduino IDE)
1// L293D shield: ENA–IN4 tanımlarını ve setup()'taki pinMode listesinden bu pinleri sil, drive() fonksiyonunu aşağıdakiyle değiştir
2// L293D shield: delete the ENA–IN4 definitions, remove them from the pinMode list in setup() and replace drive() with the one below
3// Ayrıca / Also: SERVO_PIN = 9 ya da / or 10, SoftwareSerial bt(A4, A5)
4#include <AFMotor.h>                     // Adafruit Motor Shield V1 kütüphanesi / library
5
6AF_DCMotor leftMotor(1);                 // M1 klemensi / M1 terminal
7AF_DCMotor rightMotor(2);                // M2 klemensi / M2 terminal
8
9void drive(int left, int right) {        // her biri -255 … 255 / each -255 … 255
10  leftMotor.setSpeed(min(abs(left), 255));
11  rightMotor.setSpeed(min(abs(right), 255));
12  leftMotor.run(left > 0 ? FORWARD : (left < 0 ? BACKWARD : RELEASE));
13  rightMotor.run(right > 0 ? FORWARD : (right < 0 ? BACKWARD : RELEASE));
14}

Предела L293D в 0,6 A на канал хватает для обычной езды 2WD-робота с TT-моторами, но когда колесо упирается в препятствие и стопорится, мотор может потреблять больше 1 A, и микросхема быстро нагревается. Поэтому алгоритм сканирования, который останавливает робота до того, как он упрётся в препятствие, заодно продлевает жизнь шилда. Настройки перемычек шилда и скетч для четырёхмоторной машинки — в статье Моторный шилд L293D, а внутреннее устройство L298N — в руководстве Управление DC-мотором через L298N.

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

На ESP32 есть два важных отличия. Во-первых, библиотека Servo для Arduino на ESP32 не работает, поэтому сигнал сервопривода мы формируем напрямую через LEDC, ШИМ-модуль платы: в сигнале 50 Hz импульс 0,544–2,4 мс соответствует 0–180 градусам. Моторы работают на 1 kHz, а сервопривод на 50 Hz, поэтому сначала настраиваются каналы моторов, а за ними канал сервопривода, чтобы сервопривод получил собственный таймер. Во-вторых, в ESP32 встроен классический Bluetooth, так что HC-05 не нужен; робот отображается на телефоне как «BlueGrays-Robot», а буквы направления и режима те же, что в версии для Uno. Код написан для версии 3.x ядра ESP32 для Arduino (ledcAttach и ledcWrite работают с номерами пинов), и, поскольку ему нужен классический Bluetooth, для ESP32-S3 или ESP32-C3 он не скомпилируется.

C++ (Arduino IDE)
1// ESP32 Arduino çekirdeği 3.x / ESP32 Arduino core 3.x
2#include "BluetoothSerial.h"
3
4const int ENA = 25, IN1 = 27, IN2 = 33;   // sol motor / left motor
5const int ENB = 26, IN3 = 32, IN4 = 4;    // sağ motor / right motor
6const int SERVO_PIN = 13;
7const int TRIG = 18, ECHO = 19;           // ECHO 1 kΩ / 2 kΩ bölücüyle / ECHO via a 1 kΩ / 2 kΩ divider
8const int IR_LEFT = 23, IR_RIGHT = 22;    // LOW = engel / LOW = obstacle
9
10const int BACK_CM = 12, STOP_CM = 25, SLOW_CM = 60, GUARD_CM = 15, MAX_CM = 400;
11const int CRUISE = 200, SLOW = 130, TURN = 170;
12const int SERVO_CENTER = 90, SERVO_DIR = 1;
13const float MS_PER_DEG = 4.0;             // kalibre et / calibrate
14
15BluetoothSerial bt;
16bool autoMode = true;
17int manualLeft = 0, manualRight = 0;
18unsigned long lastCmd = 0, lastPing = 0;
19
20void drive(int left, int right) {         // her biri -255 … 255 / each -255 … 255
21  left = constrain(left, -255, 255);
22  right = constrain(right, -255, 255);
23  digitalWrite(IN1, left > 0);  digitalWrite(IN2, left < 0);
24  digitalWrite(IN3, right > 0); digitalWrite(IN4, right < 0);
25  ledcWrite(ENA, abs(left));
26  ledcWrite(ENB, abs(right));
27}
28
29void servoWrite(int angle) {              // 0–180° → 544–2400 µs
30  angle = constrain(angle, 0, 180);
31  uint32_t us = map(angle, 0, 180, 544, 2400);
32  ledcWrite(SERVO_PIN, us * 16384 / 20000);   // 14 bit, 20 ms periyot / 14-bit, 20 ms period
33}
34
35int pingCm() {
36  digitalWrite(TRIG, LOW);  delayMicroseconds(2);
37  digitalWrite(TRIG, HIGH); delayMicroseconds(10);
38  digitalWrite(TRIG, LOW);
39  unsigned long t = pulseIn(ECHO, HIGH, 25000UL);
40  return t == 0 ? MAX_CM : (int)(t / 58);
41}
42
43int distanceCm() {                        // 3 ölçümün ortancası / median of 3 readings
44  int a = pingCm(); delay(60);
45  int b = pingCm(); delay(60);
46  int c = pingCm();
47  return max(min(a, b), min(max(a, b), c));
48}
49
50bool irBlocked(int pin) { return digitalRead(pin) == LOW; }
51
52void turnBy(int deg) {                    // + sola, - sağa / + left, - right
53  if (deg > 0) drive(-TURN, TURN);
54  else drive(TURN, -TURN);
55  delay((unsigned long)(abs(deg) * MS_PER_DEG));
56  drive(0, 0);
57  delay(100);
58}
59
60void avoid() {
61  drive(0, 0);
62  delay(120);
63  int front = distanceCm();
64  bool sides = irBlocked(IR_LEFT) || irBlocked(IR_RIGHT);
65  if (front >= STOP_CM && !sides) return;            // yanlış alarm / false alarm
66  if (front < BACK_CM || sides) {                    // geri çekil / back off
67    drive(-TURN, -TURN);
68    delay(350);
69    drive(0, 0);
70  }
71  const int offsets[] = {70, 35, -35, -70};
72  int bestOffset = 0, bestCm = 0;
73  for (int off : offsets) {
74    servoWrite(SERVO_CENTER + SERVO_DIR * off);
75    delay(300);
76    int cm = distanceCm();
77    if (cm > bestCm) { bestCm = cm; bestOffset = off; }
78  }
79  servoWrite(SERVO_CENTER);
80  delay(200);
81  if (bestCm < STOP_CM + 10) {                       // çıkmaz / dead end
82    drive(-TURN, -TURN);
83    delay(400);
84    turnBy(180);
85  } else {
86    turnBy(bestOffset);
87  }
88}
89
90void setup() {
91  int outs[] = {IN1, IN2, IN3, IN4, TRIG};
92  for (int p : outs) pinMode(p, OUTPUT);
93  pinMode(ECHO, INPUT);
94  pinMode(IR_LEFT, INPUT_PULLUP);
95  pinMode(IR_RIGHT, INPUT_PULLUP);
96  ledcAttach(ENA, 1000, 8);               // motor PWM: 1 kHz, 8 bit
97  ledcAttach(ENB, 1000, 8);
98  ledcAttach(SERVO_PIN, 50, 14);          // servo: 50 Hz, 14 bit
99  drive(0, 0);
100  servoWrite(SERVO_CENTER);
101  bt.begin("BlueGrays-Robot");            // telefonda görünecek ad / name shown on the phone
102  delay(3000);
103}
104
105void loop() {
106  while (bt.available()) {
107    char c = bt.read();
108    lastCmd = millis();
109    if (c == 'A') autoMode = true;                                  // otomatik / auto
110    else if (c == 'M') {                                            // manuel / manual
111      autoMode = false;
112      manualLeft = manualRight = 0;
113      servoWrite(SERVO_CENTER);
114    }
115    else if (!autoMode) {
116      if (c == 'F')      { manualLeft = 200;  manualRight = 200; }  // ileri / forward
117      else if (c == 'B') { manualLeft = -200; manualRight = -200; } // geri / backward
118      else if (c == 'L') { manualLeft = -200; manualRight = 200; }  // sola / left
119      else if (c == 'R') { manualLeft = 200;  manualRight = -200; } // sağa / right
120      else if (c == 'S') { manualLeft = 0;    manualRight = 0; }    // dur / stop
121    }
122  }
123
124  if (millis() - lastPing < 60) return;               // 60 ms'de bir / every 60 ms
125  lastPing = millis();
126  int d = pingCm();
127
128  if (!autoMode) {
129    if (millis() - lastCmd > 600) manualLeft = manualRight = 0;     // komut yok: dur / no command: stop
130    bool forward = manualLeft > 0 && manualRight > 0;
131    if (forward && d < GUARD_CM) drive(0, 0);                       // çarpışma koruması / collision guard
132    else drive(manualLeft, manualRight);
133    return;
134  }
135
136  bool l = irBlocked(IR_LEFT), r = irBlocked(IR_RIGHT);
137  if (d < STOP_CM || (l && r)) avoid();
138  else if (l) { drive(TURN, -TURN); delay(150); }
139  else if (r) { drive(-TURN, TURN); delay(150); }
140  else if (d < SLOW_CM) { int s = map(d, STOP_CM, SLOW_CM, SLOW, CRUISE); drive(s, s); }
141  else drive(CRUISE, CRUISE);
142}

Робот, объезжающий препятствия, на Raspberry Pi Pico (MicroPython)

На Raspberry Pi Pico функция MicroPython time_pulse_us() работает как pulseIn() в Arduino; если эхо не приходит, она возвращает отрицательное значение, и мы считаем его равным 400 см, то есть свободному пространству. Для сервопривода используем ШИМ 50 Hz и duty_ns(), которая принимает ширину импульса в наносекундах. Сервопривод на GP16 и пины скорости моторов на GP15 и GP10 находятся на разных срезах ШИМ (slice), поэтому не сбивают друг другу частоту. На этот раз скорость задаётся в процентах. Bluetooth у Pico нет, так что для ручного режима можно подключить HC-05 к UART0 (GP0, GP1), как в руководстве по робот-машинке.

MicroPython
1from machine import Pin, PWM, time_pulse_us
2import time
3
4ena = PWM(Pin(15)); ena.freq(1000)          # sol motor hızı / left motor speed
5enb = PWM(Pin(10)); enb.freq(1000)          # sağ motor hızı / right motor speed
6in1, in2 = Pin(14, Pin.OUT), Pin(13, Pin.OUT)
7in3, in4 = Pin(12, Pin.OUT), Pin(11, Pin.OUT)
8servo = PWM(Pin(16)); servo.freq(50)        # servo sinyali, 50 Hz / servo signal, 50 Hz
9trig = Pin(17, Pin.OUT, value=0)
10echo = Pin(18, Pin.IN)                      # 1 kΩ / 2 kΩ bölücü üzerinden / via a 1 kΩ / 2 kΩ divider
11ir_left = Pin(19, Pin.IN, Pin.PULL_UP)      # 0 = engel / 0 = obstacle
12ir_right = Pin(20, Pin.IN, Pin.PULL_UP)
13
14STOP_CM, BACK_CM = 25, 12
15CRUISE, TURN = 75, 65                       # hız yüzdesi / speed in percent
16MS_PER_DEG = 4                              # kalibre et / calibrate
17SERVO_CENTER = 90                           # öne bakmıyorsa düzelt / adjust if not facing ahead
18
19def drive(left, right):                     # -100 … 100 (%)
20    in1.value(left > 0); in2.value(left < 0)
21    in3.value(right > 0); in4.value(right < 0)
22    ena.duty_u16(int(min(abs(left), 100) * 655))
23    enb.duty_u16(int(min(abs(right), 100) * 655))
24
25def head(offset):                           # + sol, - sağ / + left, - right
26    angle = max(0, min(180, SERVO_CENTER + offset))
27    servo.duty_ns((544 + angle * 1856 // 180) * 1000)   # 0,544–2,4 ms darbe / 0.544–2.4 ms pulse
28
29def ping_cm():
30    trig.value(1); time.sleep_us(10); trig.value(0)     # 10 µs tetik / 10 µs trigger
31    t = time_pulse_us(echo, 1, 25000)
32    return 400 if t < 0 else t // 58                    # negatif = yankı yok / negative = no echo
33
34def distance_cm():                          # 3 ölçümün ortancası / median of 3 readings
35    r = []
36    for _ in range(3):
37        r.append(ping_cm())
38        time.sleep_ms(60)
39    return sorted(r)[1]
40
41def turn_by(deg):                           # + sola, - sağa / + left, - right
42    if deg > 0:
43        drive(-TURN, TURN)
44    else:
45        drive(TURN, -TURN)
46    time.sleep_ms(int(abs(deg) * MS_PER_DEG))
47    drive(0, 0)
48    time.sleep_ms(100)
49
50def avoid():
51    drive(0, 0)
52    time.sleep_ms(120)
53    front = distance_cm()
54    sides = not ir_left.value() or not ir_right.value()
55    if front >= STOP_CM and not sides:
56        return                              # yanlış alarm / false alarm
57    if front < BACK_CM or sides:            # geri çekil / back off
58        drive(-TURN, -TURN); time.sleep_ms(350); drive(0, 0)
59    best_off, best_cm = 0, 0
60    for off in (70, 35, -35, -70):          # tara / scan
61        head(off)
62        time.sleep_ms(300)
63        cm = distance_cm()
64        if cm > best_cm:
65            best_off, best_cm = off, cm
66    head(0)
67    time.sleep_ms(200)
68    if best_cm < STOP_CM + 10:              # çıkmaz / dead end
69        drive(-TURN, -TURN); time.sleep_ms(400)
70        turn_by(180)
71    else:
72        turn_by(best_off)
73
74drive(0, 0)
75head(0)
76time.sleep(3)                               # yere koyman için süre / time to put it down
77while True:
78    d = ping_cm()
79    left_hit, right_hit = not ir_left.value(), not ir_right.value()
80    if d < STOP_CM or (left_hit and right_hit):
81        avoid()
82    elif left_hit:
83        drive(TURN, -TURN); time.sleep_ms(150)   # sağa kaç / dodge right
84    elif right_hit:
85        drive(-TURN, TURN); time.sleep_ms(150)   # sola kaç / dodge left
86    else:
87        drive(CRUISE, CRUISE)
88    time.sleep_ms(60)                        # ölçümler arası / between readings

Идеи проектов с роботом, объезжающим препятствия

  • Экран радара: отправляй углы и расстояния из сканирования по последовательному порту и рисуй на компьютере зелёный экран радара; подробности — в статье Радар на Arduino.
  • Линия плюс препятствие: пусть робот едет по трассе, как робот, следующий по линии, объезжает коробку на пути и возвращается на линию; это частое соревновательное задание.
  • Робот, едущий вдоль стены: держи датчик повёрнутым на 60 градусов вправо и веди робота на постоянном расстоянии 20 см от стены; это первый шаг к прохождению лабиринтов.
  • Робот с дисплеем: выводи расстояние и режим на маленький OLED-дисплей; отладка станет намного проще.
  • Прототип робота-пылесоса: набросай чистящего робота, который бродит по дому со случайным выбором направления, концевиками на бампере и датчиком края.
  • Умная трость: преврати ту же логику измерения расстояния в вибрационное предупреждение для людей с нарушениями зрения; подробнее — в статье Умная трость на Arduino.
  • Школьный или соревновательный проект: покажи его на полосе препятствий с замером времени и демонстрацией автоматического и ручного режимов; за идеями загляни в статью Идеи проектов на Arduino для научных конкурсов.

Частые ошибки и как их исправить

ПроблемаВероятная причинаРешение
Робот врезается в стену, не останавливаясьДатчик показывает 0 (обрыв провода, перепутаны TRIG и ECHO), а код считает это свободным пространствомЗапусти тестовый скетч, проверь рукой на 10–60 см, проверь пины TRIG и ECHO
Задевает стену при подъезде под угломЗвук отражается от наклонной поверхности в другую сторонуПоставь ИК-датчики на передних углах и снизь скорость
Arduino перезагружается, когда робот останавливается для сканированияТок сервопривода и моторов проваливает линию 5 VКонденсатор 470 µF на сервоприводе, полностью заряженные 18650; при необходимости питай сервопривод от отдельного источника 5 V
Робот останавливается и сканирует без причиныОдиночное неверное показание или эхо от полаПодними датчик на 8–12 см и слегка наклони вверх; используй подтверждение по медиане
Робот поворачивает в сторону, где препятствиеСервопривод установлен наоборот, лево и право перепутаныПоставь SERVO_DIR в -1
Повороты слишком короткие или слишком длинныеMS_PER_DEG не откалиброван или аккумулятор разряженИзмерь через turnBy(360) и перенастрой с полным аккумулятором
В углу постоянно крутится то влево, то вправоОбе стороны близки к порогу, и решение всё время меняетсяУвеличь порог тупика (STOP_CM + 10) или сделай так, чтобы робот предпочитал последнее направление поворота
Головка с датчиком дёргаетсяСлабое питание сервопривода или прерывания SoftwareSerialУкороти кабель сервопривода, добавь конденсатор, не отправляй команды по Bluetooth чаще, чем нужно
Быстрее всего найти источник проблемы, если оставить робота на USB, запустить его с колёсами в воздухе и выводить значения pingCm() в монитор порта. Если значения разумные, проблема в механике или в питании. О других причинах, по которым Arduino постоянно перезагружается, читай в статье Arduino постоянно перезагружается.

Робот, объезжающий препятствия, объединяет в одном проекте чтение датчиков, фильтрацию, принятие решений и управление моторами, поэтому это лучший вход в робототехнику. Сначала посмотри на базовое поведение с первым скетчем, затем настрой пороги под свою комнату с продвинутой версией и в конце усиль робота ИК-датчиками и Bluetooth. В приложении BlueGrays ты найдёшь пошаговые проекты робота, объезжающего препятствия, машинки на L298N с управлением по Bluetooth, измерения расстояния на HC-SR04 и парктроника со схемами подключения и кодом.

Частые вопросы

Что нужно, чтобы собрать робота, объезжающего препятствия?

Для простого робота, объезжающего препятствия, нужны Arduino Uno, ультразвуковой датчик HC-SR04, сервопривод SG90, драйвер моторов L298N или L293D, набор шасси 2WD с двумя DC-моторами с редуктором и два аккумулятора 18650. Можно добавить два ИК-датчика препятствий, чтобы видеть углы, и Bluetooth-модуль HC-05, чтобы управлять роботом с телефона.

Можно ли собрать робота, объезжающего препятствия, без сервопривода?

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

Почему HC-SR04 не видит некоторые препятствия?

Ультразвуковой датчик измеряет по эху. Мягкие поверхности вроде штор и диванов поглощают звук, стены под углом отражают его в другую сторону, а тонкие предметы вроде ножек стульев дают очень слабое эхо. Кроме того, датчик не видит препятствия за пределами своего конуса примерно в 15 градусов или ниже него; эти пробелы можно закрыть ИК-датчиками.

На каком расстоянии должен останавливаться робот, объезжающий препятствия?

Хорошее начальное значение — 25 см: путь между двумя измерениями, тормозной путь и запас на разворот на месте в сумме дают около 20 см, плюс небольшой запас прочности. Для более быстрых роботов и скользких полов подними его до 30–35 см; если препятствие ближе 12 см, робот должен сначала отъехать назад.

Что лучше для робота: L298N или шилд L293D?

L298N выдаёт до 2 A на канал, поэтому он выносливее и позволяет свободно выбирать пины. Шилд L293D вставляется прямо в Uno, поэтому проводов меньше и он может управлять четырьмя моторами, но его длительный ток — 0,6 A на канал, и он занимает много пинов. Для небольшого 2WD-робота с TT-моторами подойдёт любой из них.

Можно ли управлять роботом, объезжающим препятствия, с телефона?

Да. Добавь Bluetooth-модуль HC-05 и отправляй буквы из приложения-пульта на телефоне с Android, чтобы переключаться между автоматическим и ручным режимом. Если в ручном режиме использовать ультразвуковой датчик как защиту от столкновений, робот не врежется в стену по твоей вине. С ESP32 отдельный модуль не нужен благодаря встроенному Bluetooth.

Все проекты
Измерение расстояния HC-SR04 (вывод в порт)
#27Новичок

Измерение расстояния HC-SR04 (вывод в порт)

Датчик HC-SR04 работает совсем как летучая мышь, которая находит дорогу в темноте: он посылает звук, которого ты не слышишь, слушает эхо и измеряет, насколько далеко предмет перед ним. Arduino каждые

4.6(46)
71
Робот, объезжающий препятствия
#81Продвинутый
Premium

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

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

5.0(38)
49
Машинка на L298N с управлением по Bluetooth
#80Продвинутый
Premium

Машинка на L298N с управлением по Bluetooth

Преврати телефон в пульт и управляй своей машинкой-роботом. Arduino получает команды из приложения-пульта на телефоне через Bluetooth-модуль и вращает колёса через драйвер моторов; машинка едет вперёд

5.0(38)
65
Работа с сервоприводом
#35Новичок
Premium

Работа с сервоприводом

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

4.7(48)
67
Парктроник на HC-SR04
#28Средний
Premium

Парктроник на HC-SR04

Мини-версия автомобильного парктроника: чем ближе предмет, тем чаще сигнал. Датчик расстояния измеряет предмет перед собой; если он дальше 20 сантиметров, горит зелёный светодиод, а если ближе — мигае

4.8(64)
85
Чтение отражения (приближения) с TCRT5000
#77Средний
Premium

Чтение отражения (приближения) с TCRT5000

С маленьким датчиком, который посылает невидимый свет и измеряет, сколько его отражается обратно, можно определить поверхность перед ним. Чем ближе предмет или светлая поверхность, тем больше света во

4.6(33)
43
Определение чёрной и белой поверхности с TCRT5000
#78Средний
Premium

Определение чёрной и белой поверхности с TCRT5000

С маленьким датчиком, который служит глазом роботов, едущих по линии, можно узнать, чёрный пол или белый. TCRT5000 светит на пол невидимым светом и проверяет, сколько его отражается обратно; на белом

4.7(40)
49
Ультразвуковой датчик расстояния HC-SR04 на ESP32
#116Средний

Ультразвуковой датчик расстояния HC-SR04 на ESP32

Ультразвуковой датчик HC-SR04 вычисляет расстояние по времени, за которое испущенная им звуковая волна отражается от предмета и возвращается (Time of Flight). В этом проекте ESP32 подаёт импульс 10 микросекунд на пин TRIG датчика, а длительность эха читает с пина ECHO через pulseIn(). Вычисленное расстояние в сантиметрах выводится на I2C LCD 16x2. HC-SR04 работает от 5V, поэтому на пине ECHO нужен делитель напряжения.

4.9(28)
45