Мой первый радиоуправляемый робот на Arduino

9 марта 2017

Мало что может сравниться по крутизне с разработкой высоконагруженных веб-проектов типа Facebook или ковырянии ядра Linux. В качестве примечательного исключения можно привести разработку самопальных роботов у себя дома. И знаете, что? Оказывается, полученных нами на данный момент знаний в электронике и программировании микроконтроллеров уже более чем достаточно для создания первого робота!

Управление электродвигателями из Arduino

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

Типичная схема подключения электродвигателя превосходно объяснена на wiki-сайте Амперки, поэтому я просто приведу здесь их иллюстрацию:

Типичная схема подключения электродвигателя

Там же можно прочитать интересную статью про полевые МОП-транзисторы (a.k.a. MOSFET). В отличие от биполярных транзисторов, полевые транзисторы управляются напряжением, а не током. Полевые транзисторы используются, когда нужно управлять сравнительно большим током (сотни миллиампер). Типичный полевой транзистор, используемый во многих проектах, что я видел — IRF3205.

Дополнение: Также вас может заинтересовать Шпаргалка в картинках по использованию MOSFET’ов.

Описанная схема позволяет вращать электродвигателем только в одну сторону. Однако колеса в роботе должны крутиться не только вперед, но и назад. Для решения этой проблемы используется конструкция под названием H-мост. При желании его можно спаять и самостоятельно, но в наше время обычно используют готовые микросхемы. Одной из самых популярных является микросхема L298P. В частности, именно она используется в Motor Shield от Амперки. Если вы полистаете даташит [PDF], то обнаружите, что это довольно скучная микросхема, построенная на транзисторах и логических вентилях — никакой магии.

Из драйверов двигателей, аналогичных L298P, стоит упомянуть L293D и L293B. Максимальный ток на канал первого составляет 0.6 А против 2 А у L298P. Зато L293D имеет встроенную диодную защиту от паразитных токов и стоит заметно дешевле. L293B также намного дешевле L298P. Максимальный ток на канал этого чипа — 1 А, встроенной диодной защиты нет.

Если вы решите изготовить свой Motor Shield при помощи ЛУТ или ФР, вот соответствующая схема для одного электродвигателя:

Схема подключения L298P

Картинку было лень рисовать самому, поэтому я позаимствовал ее из поста H Bridge Motor Control Circuit Using L298 в блоге circuitstoday.com. Кстати, с виду весьма интересный блог. В качестве возвратных диодов Motor Shield от Амперки использует диоды Шоттки SS14. На аналогичной по функционалу китайской плате используются более дешевые диоды M7.

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

Собираем радиоуправляемого робота

В мире Arduino существует великое множество готовых ходовых частей — как «собери сам», так и уже собранных; двухколесных, четырехколесных, гусеничных, паукообразных и даже гуманоидных. Я лично использовал гусеничную платформу Rover 5, так как она продается в собраном виде и мне в свое время ее советовал @sum3rman. На самом деле, чтобы повторить описанные далее шаги, подойдет абсолютно любая ходовая часть на колесах или гусеницах, содержащая 2 или 4 электродвигателя (не сервопривода, это сильно другая штука!).

Также я использовал упомянутый выше Motor Shield от Амперки. Вы можете использовать его, или любую аналогичную плату на базе чипа L298P. Наконец, нам понадобится пара NRF24L01, два адаптера со стабилизатором напряжения к ним, или, предпочтительнее, один адаптер и один Joystick Shield. Напомню, что эти устройства ранее рассматривались в заметке Arduino и беспроводная связь при помощи NRF24L01. Если вы знакомы с другими способами управления Arduino по беспроводной связи, можете использовать их, однако код придется соответствующим образом переписать.

Сборка робота осуществляется элементарно. Motor Shield ставится на Arduino. К винтовым клеммникам шилда подключаются электродвигатели, провода фиксируются при помощи отвертки. Если у вашей ходовой части 4 двигателя, подключите к каждому клеммнику по два электродвигателя, расположенных с одной стороны. В один момент времени эти двигатели все равно крутятся в одну и ту же сторону.

Клеммник питания я подключил к шести последовательно соединенным батарейкам AA. Соответствующий отсек для батареек шел вместе с гусеничной платформой. Помимо прочего, отсек еще и обеспечивает платформе низкий центр тяжести. Подойдет и другой источник питания на 9 В, например, крона. Чтобы постоянно не доставать батарейки для выключения робота, а затем не вставлять их обратно, имеет смысл припаять какой-нибудь выключатель.

На вашем Motor Shield или его аналоге обязательно должна быть перемычка, определяющая, питаются ли Arduino и шилд от одного или двух независимых источников питания. Во время программирования Arduino перемычка должна стоять на использовании двух независимых источников. После отключения Arduino от USB верните перемычку так, чтобы использовался один общий источник. Впрочем, как альтернативный вариант, можно использовать в роботе и два независимых источника питания.

Робот и пульт управления им в собранном виде:

Радиоуправляемый робот на Arduino

Дополнение: Впоследствии робот и джойстик к нему были существенно переработаны. Кроме того, был добавлен FPV. Фото улучшенной версии можно посмотреть здесь (JPG, 224 Кб).

Код прошивки робота:

#include <Arduino.h>
#include <SPI.h>
#include "nRF24L01.h"
#include "RF24.h"

typedef struct {
  uint8_t directionM1;
  uint8_t directionM2;
  uint8_t speedM1;
  uint8_t speedM2;
} EnginesState;

const int directionM1 = 4;
const int speedM1 = 5;
const int speedM2 = 6;
const int directionM2 = 7;

const int FORWARD = HIGH;
const int BACKWARD = LOW;

const uint64_t addresses[] = { 0xAF1510009001LL, 0xAF1510009002LL };

RF24 radio(9, 10);
EnginesState enginesState;
unsigned long lastEnginesStateUpdateMs = 0;

void setDefaultEnginesState()
{
  enginesState.directionM1 = FORWARD;
  enginesState.directionM2 = FORWARD;
  enginesState.speedM1 = 0;
  enginesState.speedM2 = 0;
}

void setup()
{
  setDefaultEnginesState();

  pinMode(directionM1, OUTPUT);
  pinMode(directionM2, OUTPUT);
  pinMode(speedM1, OUTPUT);
  pinMode(speedM2, OUTPUT);

  radio.begin();
  radio.setChannel(108);
  radio.setPALevel(RF24_PA_LOW);
  radio.setDataRate(RF24_250KBPS);
  // radio.openWritingPipe(addresses[0]); // not used yet
  radio.openReadingPipe(1, addresses[1]);
  radio.startListening();
}

void loop()
{
  delay(10);

  if(radio.available())
  {
    radio.read( &enginesState, sizeof(enginesState) );
    lastEnginesStateUpdateMs = millis();
  }
  else if(millis() - lastEnginesStateUpdateMs > 500)
  {
    setDefaultEnginesState();
    lastEnginesStateUpdateMs = millis();
  }

  digitalWrite(directionM1, enginesState.directionM1);
  analogWrite(speedM1, enginesState.speedM1);

  digitalWrite(directionM2, enginesState.directionM2);
  analogWrite(speedM2, enginesState.speedM2);
}

Как видите, робот просто получает направление и скорость вращения двигателей от пульта. Если пульт не посылал никаких данных в течение 500 мс, робот останавливается. NRF24L01 иногда теряет пакеты, особенно при наличии помех от самих двигателей робота. Благодаря тому, что робот какое-то время помнит последнюю полученную команду, он будет двигаться плавно, даже если 98% пакетов теряются.

Прошивка джойстика:

#include <Arduino.h>
#include <SPI.h>
#include "nRF24L01.h"
#include "RF24.h"

typedef struct {
  uint8_t directionM1;
  uint8_t directionM2;
  uint8_t speedM1;
  uint8_t speedM2;
} EnginesState;

/* depends on your point of view */
/*
const int FORWARD = HIGH;
const int BACKWARD = LOW;
*/

const int FORWARD = LOW;
const int BACKWARD = HIGH;

const int joystickX = A0;
const int joystickY = A1;
const int joystickBtn = 2;

const int THRESHOLD_LOW = 510;
const int THRESHOLD_HIGH = 530;

const uint64_t addresses[] = { 0xAF1510009001LL, 0xAF1510009002LL };

RF24 radio(9, 10);

void setup()
{
  pinMode(joystickX, INPUT);
  pinMode(joystickY, INPUT);
  pinMode(joystickBtn, INPUT);

  radio.begin();
  radio.setChannel(108);
  radio.setPALevel(RF24_PA_LOW);
  radio.setDataRate(RF24_250KBPS);
  radio.openWritingPipe(addresses[1]);
  // radio.openReadingPipe(1, addresses[0]); // not used yet
}

void loop()
{
  EnginesState enginesState;
  delay(10);

  int x = analogRead(joystickX);
  int y = analogRead(joystickY);

  if(y < THRESHOLD_LOW)
  {
    enginesState.directionM1 = FORWARD;
    enginesState.directionM2 = FORWARD;
    enginesState.speedM1 = map(y, THRESHOLD_LOW, 0, 0, 255) ;
    enginesState.speedM2 = enginesState.speedM1;
  }
  else if(y > THRESHOLD_HIGH)
  {
    enginesState.directionM1 = BACKWARD;
    enginesState.directionM2 = BACKWARD;
    enginesState.speedM1 = map(y, THRESHOLD_HIGH, 1023, 0, 255);
    enginesState.speedM2 = enginesState.speedM1;
  }
  else
  {
    enginesState.directionM1 = FORWARD;
    enginesState.directionM2 = FORWARD;
    enginesState.speedM1 = 0;
    enginesState.speedM2 = enginesState.speedM1;
  }

  if(x < THRESHOLD_LOW)
  {
    enginesState.speedM2 = map(x, THRESHOLD_LOW, 0,
                                  enginesState.speedM2, 0);
  }
  else if(x > THRESHOLD_HIGH)
  {
    enginesState.speedM1 = map(x, THRESHOLD_HIGH, 1023,
                                  enginesState.speedM1, 0);
  }

  if(digitalRead(joystickBtn) == LOW) /* rotation mode */
  {
    if(x < THRESHOLD_LOW)
    {
      enginesState.directionM1 = FORWARD;
      enginesState.directionM2 = BACKWARD;
      enginesState.speedM1 = map(x, THRESHOLD_LOW, 0, 0, 255);
      enginesState.speedM2 = enginesState.speedM1;
    }
    else if(x > THRESHOLD_HIGH)
    {
      enginesState.directionM1 = BACKWARD;
      enginesState.directionM2 = FORWARD;
      enginesState.speedM1 = map(x, THRESHOLD_HIGH, 1023, 0, 255);
      enginesState.speedM2 = enginesState.speedM1;
    }
  }

  radio.write( &enginesState, sizeof(enginesState) );
}

Здесь текущее положение джойстика довольно тривиально переводится в направление и скорость вращения двигателей, которые затем посылаются роботу. При нажатии на джойстик (напомню, что он кликабелен) осуществляется переход в альтернативный режим, при котором двигатели вращаются в противоположных направлениях. Этот режим позволяет роботу вращаться на месте.

Заключение

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

А пробовали ли вы делать роботов? Если да, то какие компоненты использовали? Если нет, собираетесь ли попробовать?

Дополнение: Управление серводвигателями на примере робо-руки MeArm

Метки: , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.