Робот на Arduino: моторы, датчики и поведение
Робот на Arduino у меня впервые поехал в стену: я радовался миганию светодиода, платформа сорвалась с места и снесла кружку со стола. После этого я понял, что между «машинка едет» и «машинка себя ведёт» — три разных скетча. Здесь разбор именно второй части: как моторы слушаются ШИМ, как HC-SR04 и ИК-датчики дают роботу «глаза», и как поведение складывается из прямо-поворот-объезд без слепых delay().
Ниже — три класса поведения на Arduino Uno и драйвере L298N: робот-бампер с одной кнопкой, объездщик препятствий по HC-SR04 и линия-фолловер на трёх ИК-датчиках. Схемы, скетчи, грабли с током моторов и длительностью pulseIn, и почему П-контроллер в 12 строк честнее нагромождения if-else. Логику пинов драйвера и светодиода можно «пощупать» в симуляторе платы до пайки — компонент button на D2, светодиод на D13.
Моторы через L298N: четыре пина и ШИМ-скорость
L298N — мост, у которого на каждый мотор два входа направления (IN1/IN2 для левого, IN3/IN4 для правого) и один вход скорости (ENA/ENB). Четыре пина направления — обычный digitalWrite, два пина скорости — analogWrite от 0 до 255. Базовое правило: IN1=HIGH и IN2=LOW — вперёд; наоборот — назад; оба LOW — накат; оба HIGH — динамический тормоз. Скорость задаётся ШИМ на ENA: 255 — полный ход, 180 — уверенно, ниже 120 TT-мотор не тянет под нагрузкой. Логику пинов IN1-IN4 можно проверить в симуляторе — они рисуются как индикаторы стенда, а полный разбор драйвера с ШИМ — в уроке про L298N с реверсом.
- L298N: 4 пина IN1-IN4 на направление, 2 пина ENA/ENB на ШИМ-скорость.
- Скорость ниже 120: TT-мотор гудит на месте под нагрузкой. Рабочий диапазон 150-255.
- Падение 1,4-2,5 В на выходе L298N: при питании 7,4 В на моторы доходит 5-6 В — узкое место L298N, на TB6612 потери 0,5 В.
- Общая земля: GND батареи, драйвера и платы обязана быть соединена. Без этого ШИМ-сигнал ENA «не доходит» — мотор молчит.
- Конденсатор 470-1000 мкФ по питанию драйвера: при пуске мотора ток 600-700 мА, без конденсатора плата ребутится.
Бампер-робот: одна кнопка, три маневра
const int IN1=5, IN2=6, IN3=9, IN4=10;const int ENA=3, ENB=11; // ШИМ-пины (3,5,6,9,10,11 на Uno)const int BUMP=2; // кнопка-бампер с INPUT_PULLUPint state=0; // 0=вперёд, 1=назад, 2=разворотunsigned long t0=0;const unsigned long D_NAZAD=700, D_RAZVOROT=500; void forward() { digitalWrite(IN1,HIGH); digitalWrite(IN2,LOW); digitalWrite(IN3,HIGH); digitalWrite(IN4,LOW); analogWrite(ENA,200); analogWrite(ENB,200); }void backward() { digitalWrite(IN1,LOW); digitalWrite(IN2,HIGH); digitalWrite(IN3,LOW); digitalWrite(IN4,HIGH); analogWrite(ENA,200); analogWrite(ENB,200); }void spin() { digitalWrite(IN1,HIGH); digitalWrite(IN2,LOW); digitalWrite(IN3,LOW); digitalWrite(IN4,HIGH); analogWrite(ENA,200); analogWrite(ENB,200); } void setup() { for (int i=5;i<=10;i++) pinMode(i,OUTPUT); pinMode(ENA,OUTPUT); pinMode(ENB,OUTPUT); pinMode(BUMP,INPUT_PULLUP); Serial.begin(9600); forward(); t0=millis();}void loop() { if (digitalRead(BUMP)==LOW && state==0) { state=1; backward(); t0=millis(); Serial.println("udar -> nazad"); } if (state==1 && millis()-t0>=D_NAZAD) { state=2; spin(); t0=millis(); Serial.println("-> razvorot"); } if (state==2 && millis()-t0>=D_RAZVOROT) { state=0; forward(); t0=millis(); Serial.println("-> vperyod"); }}Скетч без единого delay() — это важно. delay(700) в loop() блокирует чтение кнопки, и второй удар за время манёвра робот не заметит. На millis() робот реагирует на удар в любой момент, а маневр проходит по таймеру. После трёх-четырёх заездов становится понятно, что D_NAZAD=700 и D_RAZVOROT=500 — числа под конкретный пол и пару моторов; на ковре D_RAZVOROT поднимаю до 800, на ламинате — снижаю до 400. Полный разбор планирования манёвров без delay — в уроке про millis вместо delay.
HC-SR04: глаза на ультразвуке и pulseIn
HC-SR04 — датчик расстояния 2-400 см, точность ±3 см, угол обзора 15°. VCC на 5V, Trig на D8, Echo на D7. Расстояние: pulseIn(ECHO, HIGH) / 58 — пересчёт микросекунд в сантиметры при скорости звука 340 м/с. Затык с pulseIn — он блокирует скетч до 25 мс на каждое измерение, и в это время моторы «не слушаются» (но крутятся — у них свой ход). Решение: измерять раз в 100 мс, между измерениями — читать кнопку и обновлять манёвры. Подробно про pulseIn и пересчёт — в уроке про HC-SR04, а пример с зуммером-парктроником — в уроке про парктроник.
const int TRIG=8, ECHO=7;const int OBSTACLE=15; // см — порог объезда long readDist() { digitalWrite(TRIG, HIGH); delayMicroseconds(10); digitalWrite(TRIG, LOW); long mkss = pulseIn(ECHO, HIGH, 25000); // таймаут 25 мс if (mkss==0) return 999; // нет эха — считаем «чисто» return mkss / 58;} void setup() { pinMode(TRIG,OUTPUT); pinMode(ECHO,INPUT); Serial.begin(9600);}void loop() { long d = readDist(); Serial.print("d="); Serial.println(d); if (d > 0 && d < OBSTACLE) { spin(); delay(400); } else { forward(); } delay(100);}Порог OBSTACLE=15 см — это не «тормозной путь». Тормозной путь у TT-мотора на скорости 200 — около 8 см, и 15 даёт запас на срабатывание sensor+loop. На скорости 255 тормозной путь 12-15 см, и OBSTACLE поднимаю до 25. PulseIn с таймаутом 25 мс — страховка: если HC-SR04 завис или потерял эхо, скетч не ждёт вечно, а возвращает 999 («чисто»). Без таймаута робот раз в 5 минут замирает на 2 секунды. Логика ИК-пульта и L298N с таймаутом связи — в уроке про ИК-робота; там же стоп по millis-таймауту после потери сигнала пульта.
Линия-фолловер: три ИК-датчика и П-контроллер
ИК-модуль TCRT5000 (3 шт. по 30-50 ₽) на A0, A1, A2 — крайние левый/правый, средний по линии. Скетч читает три аналоговых, нормализует в 0 или 1, и сводит к ошибке: -1 (ушёл вправо), 0 (на линии), +1 (ушёл влево). На ошибке держим П-поправку: левый мотор получает baseSpeed + K*err, правый baseSpeed - K*err. Чистый П-контроллер в 12 строк честнее нагромождения if-else: поворот плавный, а не рывками, и на прямых робот едет ровно. Разбор машины состояний на enum и switch для сенсорной логики — в уроке про конечный автомат: та же логика «состояние + действие», только для моторов.
- TCRT5000: ИК-фотодиод + фототранзистор, A0 выход, 30-50 ₽, чувствительность подстроечным резистором.
- Калибровка на белый/чёрный: на белом листе ~ 200, на чёрной изоленте ~ 800 — сохраняю в EEPROM, чтобы не ловить разную засветку.
- Ошибка err: (left - right), диапазон -800..+800; П-коэффициент K=0.2 для baseSpeed=150.
- П-контроллер против if-else: при if-else на повороте мотор рывком стартует-стоп, при П — плавно снижает скорость до нужной.
- Падение с линии: если все три датчика показывают «не линия» — едем назад 200 мс, ищем линию заново.
Поведение: стейт-машина против if-else
Когда поведения три (поиск линии, объезд, экстренный стоп), if-else запутывается: сработал бампер во время объезда — какой if выше по списку? Состояния в стейт-машине: state=SEEK_LINE, AVOID, BACK_OFF. В loop() switch(state) — и каждый кейс читает свои датчики, ставит свои моторы и решает, в какое состояние перейти. Читается как алгоритм, а не как «код». Стейт-машина для объезда: SEEK → видит препятствие → AVOID (поворот 400 мс) → SEEK. Если в AVOID ударился бампером → BACK_OFF (700 мс назад) → AVOID. Полный разбор машины состояний на enum и switch — в уроке про конечный автомат.
Грабли робота, на которые я наступал
- Питание моторов с 5V пина Arduino: ток пуска 600 мА, плата ребутится. Отдельный БП 5V/1A на реле-питание L298N.
- Trig и Echo перепутаны: pulseIn возвращает 0 всегда, робот едет в стену. Маркирую провода скотчем сразу.
- HC-SR04 на расстоянии < 2 см: эхо приходит раньше, чем датчик успевает послать — возвращает 0. Порог OBSTACLE поднимаю до 5 см минимум.
- ИК-датчики на солнце: ИК от солнца «слепит» фототранзистор, и на белом листе он видит «чёрное». Ставьте робота в тень или экран на датчики сверху.
- П-коэффициент не пересчитан при смене скорости: при baseSpeed=255 K=0.2 ок, при baseSpeed=120 нужен K=0.4. Частая забытая правка.