Данная статья устарела 1 января 2018 года. Есть смысл рассматривать её только как инструкцию по сборке. Вопросы программирования гораздо лучше освещены в новой статье.
Возникла задача реализовать роботизированную машинку на колёсах. Обзор опубликованных в свободном доступе решений позволил сразу отказаться от рудиментов и выработать уникальные технические требования:
- Для всей системы должен быть только один источник питания. Желательно, аккумуляторы. Любимый формат - 18650.
- Простая и надёжная силовая часть. Никаких гипер-сложных "силовых шилдов".
- Поведение, целиком определяемое прошивкой
- Отладка в результате натурных испытаний (смена режимов сопровождается звуковым сигналом).
Рассмотрим этапы решения.
В первую очередь был заказан набор для сборки механической части шасси из Китая. Через месяц пришёл хорошо укомплектованный набор.
Состав набора:
- Две акриловые пластины корпуса
- Четыре мотора
- Четыре колеса
- Футляр для аккумуляторов
- Метизы
Далее пришлось открывать верхний ящик стола и на ощупь искать дополнительные комплектующие. Нам пригодятся:
- Микроконтроллерный модуль
- Плата расширения Sensor Shield v.5
- Модуль драйвера моторов на базе L298N
- Сонар HC-SR04 с крепёжной скобой
- Соединительные провода
В качестве микроконтроллерного модуля был выбран один из клонов платформы Arduino на базе Atmel ATmega 328/P. После долгих размышлений и взглядов в сторону Raspberry Pi или "голого" PIC16 с аскетичным ассемблером решено было остановиться именно на этой платформе, т.к. она обеспечивает простоту сопряжений с другими блоками, имеет массу примеров кода, опубликованных в свободном доступе, унифицирована с колоссальным количеством датчиков - другими словами, развязывает руки разработчику и позволяет сосредоточиться на воплощении задумки.
Более того, неоспоримый дополнительный плюс - это масштабируемость.
Исполнение выбиралось с точки зрения дизайна: я постарался выдержать всю конструкцию в жёлто-чёрной палитре.
Фактически, вся сборка начинается с модуля драйвера моторов на базе L298N и крутится вокруг него. На мой взгляд, именно этот модуль следовало бы назвать ядром всей системы. Фактически, это умощнённая версия выпускаемой ранее микросхемы/платы L293D. Совместимость сохранена.
Важно отметить, что входы ENABLE A/B отвечают за возможность вращения моторов, причём на них можно подавать как логические уровни ("низкий" - запрет, "высокий" - разрешено), так и аналоговые сигналы (например, посредством ШИМ) для регулировки скорости вращения. Разработчики предусмотрели два пятивольтовых вывода рядом с ENABLE A/B - они позволяют установить "намертво" перемычки на плату, тогда всегда будет разрешена полная скорость вращения; таким образом можно сэкономить две выходные линии микроконтроллера. Подавать отдельно +5В на эти вспомогательные выводы не требуется!
Ещё одна перемычка видна вблизи колодки питания, в глубине платы: она должна быть установлена, если питание модуля L298N будет превышать +12В. Можно запитывать модуль L298N и большим напряжением (вплоть до 30 вольт), тогда придётся снять данный джампер, а также придётся дополнительно подводить к модулю +5В. В моём случае планируется применение аккумуляторов на 7-8 вольт, поэтому я смело оставил джампер установленным и радостно воспользовался имеющимися пятью вольтами для питания логики.
Сборка робота начинается с подготовки модуля L298N: он с трудом влезает на шасси, а очень хочется установить его по центру. Кроме того, клеммы выходов на моторы оказываются расположены совершенно впритык к корпусам моторов, как туда впихнуть провода - остаётся загадкой. Было принято решение всё же зафиксировать драйвер строго по центру (радиатор будет смотреть назад), для чего пришлось выпаивать колодки, заменяя их проводами. В итоге вся силовая часть оказалась пропаяна, что лично меня весьма устраивает.
Стойки для драйвера оказались слишком высоки (радиатор не влезал), поэтому пришлось изготовить собственные низкопрофильные стоечки.
Все тонкости пайки показаны на рисунке выше. Моторы запаиваем крест-накрест между собой, затем припаиваем выходные (красные) провода от драйвера.
Собираем первый этаж. Никаких приключений, только винты, гайки и отвёртка. Ну вот только с запайкой клемм двигателей пришлось повозиться.
И, да, не забываем, что редукторы у нас не железные, колёса придётся насаживать нежно! Есть смысл капнуть каплю клея на оси, так как практика показала, что колёса имеют тенденцию отваливаться на поворотах по ковру.
Теперь выполняем соединения. Самое главное правило: присоединить шлейф красиво к драйверу - то есть все шесть проводов выстраиваются в ряд. Это очень важно, ибо туда мы больше не полезем, скорее всего.
Сведём все коммуникации в один понятный рисунок.
Собираем второй этаж.
Теперь заложим в нашего робота простой алгоритм, который позволит машине самостоятельно перемещаться, избегая соударения с препятствиями.
Суть алгоритма:
- При подаче питания исполняем патриотические ноты
- Выполняем тестовые движения: вперёд, назад, разворот через правое плечо, разворот через левое плечо
- Едем вперёд, пока не увидим перед собой препятствие на расстоянии 12 см или меньше
- Меняем траекторию: немного откатываемся назад (на случайное расстояние) и выполняем разворот в случайную сторону на случайный угол.
- см. (3)
Листинг программы для прошивки.
// 4WD RoboCar // With Sonar // 2017-July-24 long randomNumber;
// Описываем подключение драйвера к микроконтроллеру
// A - правый борт
// В - левый борт
int pinB1 = 1;
int pinB2 = 2;
int enableB = 3;
int pinA2 = 4;
int pinA1 = 5;
int enableA = 6; // Setup Ultrasonic Sensor pins #define trigPin 8 #define echoPin 9 // Setup passive buzzer pins int tonePin = 12; void setup() { // Направление работы портов: pinMode (enableA, OUTPUT); pinMode (pinA1, OUTPUT); pinMode (pinA2, OUTPUT); pinMode (enableB, OUTPUT); pinMode (pinB1, OUTPUT); pinMode (pinB2, OUTPUT); pinMode(trigPin, OUTPUT); pinMode(echoPin, INPUT); pinMode(13 , OUTPUT); // Warming-up movement midi(); // play song enableMotors(); pisk(); delay(500); forward(1300); delay(500); pisk(); backward(1300); delay(500); pisk(); right(1300); delay(500); pisk(); left(1300); disableMotors(); delay(1000); pisk(); digitalWrite(13, LOW); // Выключаем встроенный диод } // Управление моторами void motorAforward() { digitalWrite (pinA1, HIGH); digitalWrite (pinA2, LOW); } void motorBforward() { digitalWrite (pinB1, LOW); digitalWrite (pinB2, HIGH); } void motorAbackward() { digitalWrite (pinA1, LOW); digitalWrite (pinA2, HIGH); } void motorBbackward() { digitalWrite (pinB1, HIGH); digitalWrite (pinB2, LOW); } void motorAstop() { digitalWrite (pinA1, HIGH); digitalWrite (pinA2, HIGH); } void motorBstop() { digitalWrite (pinB1, HIGH); digitalWrite (pinB2, HIGH); } void motorAcoast() { digitalWrite (pinA1, LOW); digitalWrite (pinA2, LOW); } void motorBcoast() { digitalWrite (pinB1, LOW); digitalWrite (pinB2, LOW); } void motorAon() { digitalWrite (enableA, HIGH); } void motorBon() { digitalWrite (enableB, HIGH); } void motorAoff() { digitalWrite (enableA, LOW); } void motorBoff() { digitalWrite (enableB, LOW); } // Управление движениями void forward (int duration) { motorAforward(); motorBforward(); delay (duration); } void backward (int duration) { motorAbackward(); motorBbackward(); delay (duration); } void right (int duration) { motorAbackward(); motorBforward(); delay (duration); } void left (int duration) { motorAforward(); motorBbackward(); delay (duration); } void coast (int duration) { motorAcoast(); motorBcoast(); delay (duration); } void breakRobot (int duration) { motorAstop(); motorBstop(); delay (duration); } void disableMotors() { motorAoff(); motorBoff(); } void enableMotors() { motorAon(); motorBon(); } // Setup Ultrasonic Sensor distance measuring int distance() { int duration, distance; digitalWrite(trigPin, HIGH); delayMicroseconds(1000); digitalWrite(trigPin, LOW); duration = pulseIn(echoPin, HIGH); distance = (duration/2) / 29.1; return distance; } // Setup the main car function void launch() { int distance_0; distance_0 = distance(); // Debugging: Serial.print(distance_0); Serial.println(" inch. "); // Keep moving forward in a straight line while distance of objects in front > 12 cm away while(distance_0 > 12) { motorAon(); motorBon(); forward(10); // Сколько ехать вперёд? distance_0 = distance(); } breakRobot(0); } void avoid() { // Go back and turn slightly right to move car in new direction if object detected tone(tonePin, 999, 400); delay (1000); randomNumber = random(1,2); backward(randomNumber); tone(tonePin, 900, 400); delay (400); randomNumber = random(1,10); if (randomNumber < 5) { right(random(50,460)); } else { left(random(50,460)); }; } void pisk(){ tone(tonePin, 700, 350); delay(350); tone(tonePin, 900, 350); delay(350); tone(tonePin, 1100, 750); delay(550); noTone(tonePin); } void midi() { tone(tonePin, 174, 249.99975); delay(277.7775); tone(tonePin, 233, 499.9995); delay(555.555); tone(tonePin, 174, 374.999625); delay(416.66625); tone(tonePin, 195, 124.999875); delay(138.88875); tone(tonePin, 220, 499.9995); delay(555.555); tone(tonePin, 146, 249.99975); delay(277.7775); tone(tonePin, 146, 249.99975); delay(277.7775); tone(tonePin, 195, 499.9995); delay(555.555); tone(tonePin, 174, 374.999625); delay(416.66625); tone(tonePin, 155, 124.999875); delay(138.88875); tone(tonePin, 174, 499.9995); delay(555.555); tone(tonePin, 116, 249.99975); delay(277.7775); tone(tonePin, 116, 249.99975); delay(277.7775); tone(tonePin, 130, 499.9995); delay(555.555); tone(tonePin, 130, 374.999625); delay(416.66625); tone(tonePin, 146, 124.999875); delay(138.88875); tone(tonePin, 155, 499.9995); delay(555.555); tone(tonePin, 155, 374.999625); delay(416.66625); tone(tonePin, 174, 124.999875); delay(138.88875); tone(tonePin, 195, 499.9995); delay(555.555); tone(tonePin, 220, 374.999625); delay(416.66625); tone(tonePin, 233, 124.999875); delay(138.88875); tone(tonePin, 261, 749.99925); delay(833.3325); tone(tonePin, 174, 249.99975); delay(277.7775); } void loop() { randomNumber = random(2,5); // increase randomization :) launch(); // function keeps moving car forward while... avoid(); // function makes car go back, turn slightly right to move forward in new direction }
Фрагмент реального заезда на видео прекрасно иллюстрирует поведение подопытного.
В результате имеем работающую машинку, которая способна самостоятельно избегать соударения!
Хотелось бы отметить, что очень тщательно выбиралась пара переменных: критическое расстояние Distance и время движения вперёд, т.е. параметр для Forward(). Дело в том, что от пары этих переменных будет очень сильно зависеть поведение робота вблизи препятствий. Я постарался сделать максимально точный аппарат, который будет тормозить в самый последний миг, останавливаясь в миллиметрах от преграды. С другой стороны, ударов тоже допускать нельзя (домашнюю мебель жалко).
Для программирования и прошивки использовалась интегрированная среда разработки UECIDE. Крайне рекомендую данную ИСР, т.к. во-первых, она бесплатна. Во-вторых, она обладает развитым функционалом текстового редактора кода, после которого тяжело перейти на любой другой пакет. Иллюстрирую феноменальную разметку с помощью одного скриншота:
Не прошло и полгода, а уже захотелось доработать поделку.
Ниже приведён листинг доработанной программы для прошивки. Добавлен фотосенсор, теперь в темноте зажигаются фары, а скорость моторов немного снижена, чтобы машинка была не такой резвой. Встроенный светодиод теперь сигнализирует об обнаружении препятствия и о движении назад или развороте.
// 4WD RoboCar // Sonar (NO Servo) + fara + LED13 // 2017-December-10 // v.0.4 (хорошая версия) // Global variables: long randomNumber; int velocity = 220; int light = 0; // Сюда подключены фары // Описываем подключение драйвера к микроконтроллеру // A - правый борт // В - левый борт int pinB1 = 1; int pinB2 = 2; int enableB = 3; int pinA2 = 4; int pinA1 = 5; int enableA = 6; // Подключаем ультразвуковой датчик #define trigPin 8 #define echoPin 9 // Фоторезистор висит на А0 #define PHOTO_SENSOR A0 // Подключаем зуммер int tonePin = 12; void setup() { // Определяем направление работы линий pinMode (enableA, OUTPUT); pinMode (pinA1, OUTPUT); pinMode (pinA2, OUTPUT); pinMode (enableB, OUTPUT); pinMode (pinB1, OUTPUT); pinMode (pinB2, OUTPUT); pinMode(trigPin, OUTPUT); pinMode(echoPin, INPUT); pinMode(13 , OUTPUT); // Warming-up midi(); enableMotors(); pisk(); delay(2000); digitalWrite(13, LOW); // Выключаем встроенный диод // Serial.begin(9600); // Отправляем данные на ПЭВМ } // Описываем варианты работы моторов void motorAforward() { digitalWrite (pinA1, HIGH); digitalWrite (pinA2, LOW); } void motorBforward() { digitalWrite (pinB1, LOW); digitalWrite (pinB2, HIGH); } void motorAbackward() { digitalWrite (pinA1, LOW); digitalWrite (pinA2, HIGH); } void motorBbackward() { digitalWrite (pinB1, HIGH); digitalWrite (pinB2, LOW); } void motorAstop() { digitalWrite (pinA1, HIGH); digitalWrite (pinA2, HIGH); } void motorBstop() { digitalWrite (pinB1, HIGH); digitalWrite (pinB2, HIGH); } void motorAon() { digitalWrite (enableA, HIGH); } void motorBon() { digitalWrite (enableB, HIGH); } void motorAoff() { digitalWrite (enableA, LOW); } void motorBoff() { digitalWrite (enableB, LOW); } // Описываем варианты движения машины void forward (int duration) { motorAforward(); motorBforward(); delay (duration); } void backward (int duration) { motorAbackward(); motorBbackward(); delay (duration); } void right (int duration) { motorAbackward(); motorBforward(); delay (duration); } void left (int duration) { motorAforward(); motorBbackward(); delay (duration); } void FullStop (int duration) { motorAstop(); motorBstop(); delay (duration); } void disableMotors() { motorAoff(); motorBoff(); } void enableMotors() { motorAon(); motorBon(); } // Пользуемся УЗ датчиком расстояния int distance() { int duration, distance; digitalWrite(trigPin, HIGH); delayMicroseconds(1000); digitalWrite(trigPin, LOW); duration = pulseIn(echoPin, HIGH); distance = (duration/2) / 29.1; // Переводим в сантиметры return distance; } // Функция запуска автомобиля void launch() { int distance_0; distance_0 = distance(); // Debugging: // Serial.print(distance_0); // Serial.println(" сантиметров. "); // Движемся вперёд, пока расстояние до преграды > 12 cm while(distance_0 > 12) { // motorAon(); // motorBon(); analogWrite (enableA, velocity); analogWrite (enableB, velocity); forward(10); // Сколько ехать вперёд? distance_0 = distance(); } FullStop(0); } void avoid() { digitalWrite(13, HIGH); // Включаем встроенный диод! // Сначала откатываемся назад на случайное количество шагов tone(tonePin, 999, 400); delay (1000); randomNumber = random(2,3); backward(randomNumber); tone(tonePin, 900, 400); delay (400); // Случайным образом выбираем направление и угол поворота: randomNumber = random(1,10); if (randomNumber < 5) { right(random(50,460)); } else { left(random(50,460)); }; digitalWrite(13, LOW); // Выключаем встроенный диод } void pisk(){ tone(tonePin, 700, 350); delay(350); tone(tonePin, 900, 350); delay(350); tone(tonePin, 1100, 750); delay(550); noTone(tonePin); } void midi() { tone(tonePin, 174, 249.99975); delay(277.7775); tone(tonePin, 233, 499.9995); delay(555.555); tone(tonePin, 174, 374.999625); delay(416.66625); tone(tonePin, 195, 124.999875); delay(138.88875); tone(tonePin, 220, 499.9995); delay(555.555); tone(tonePin, 146, 249.99975); delay(277.7775); tone(tonePin, 146, 249.99975); delay(277.7775); tone(tonePin, 195, 499.9995); delay(555.555); tone(tonePin, 174, 374.999625); delay(416.66625); tone(tonePin, 155, 124.999875); delay(138.88875); tone(tonePin, 174, 499.9995); delay(555.555); tone(tonePin, 116, 249.99975); delay(277.7775); tone(tonePin, 116, 249.99975); delay(277.7775); tone(tonePin, 130, 499.9995); delay(555.555); tone(tonePin, 130, 374.999625); delay(416.66625); tone(tonePin, 146, 124.999875); delay(138.88875); tone(tonePin, 155, 499.9995); delay(555.555); tone(tonePin, 155, 374.999625); delay(416.66625); tone(tonePin, 174, 124.999875); delay(138.88875); tone(tonePin, 195, 499.9995); delay(555.555); tone(tonePin, 220, 374.999625); delay(416.66625); tone(tonePin, 233, 124.999875); delay(138.88875); tone(tonePin, 261, 749.99925); delay(833.3325); } void fara () { int val = analogRead(PHOTO_SENSOR); if (val < 500) { // Светло, выключаем фары digitalWrite(light, HIGH); } else { // Темновато, включаем фары digitalWrite(light, LOW); } } void loop() { randomNumber = random(2,5); // вхолостую выбираем псевдо-случайное число launch(); // запускаем автомобиль вперёд avoid(); // откатываемся от препятствия и как-то поворачиваемся fara (); // проверяем, не пора ли зажигать фары? }
В результате движения стали более плавными и точными, зажигаются фары (т.е. светодиоды) в соответствии с показаниями датчика освещённости, но их можно и не подключать. Моторы работают в чуть более щадящем режиме. Обо всех действиях сообщает писк зуммера.
Отзывы
Написал Денис
Опубликовано в: Настройка модуля HC-06Написал deman696
Опубликовано в: Настройка модуля HC-06Написал Борис
Опубликовано в: Сравнение современных СУБДНаписал Den
Опубликовано в: Редактирование сейвов Mass Effect 1Написал Артём
Опубликовано в: Запрет обновлений Google Chrome