Программируем робот-манипулятор MeArm.

Тема статьи: 
Электронные поделки и механизмы

https://youtu.be/3KGHmLxB0tw

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

ArduinoManipulatorMeArm01.jpg

Он включает в себя несколько деталей, соединяющихся по определённой схеме винтиками. В результате получается робот-рука, с приводящими её в движение четырьмя моторами. Мы купили набор на ближайшем радиорынке. Детали в нём сделаны в Китае из фанеры, нарезанной лазером.
В механизме используется серводвигатель «Tower Pro Mikro Servo 9G SG90».

ArduinoManipulatorMeArm02.jpg

Серводвигатель – это специальный электродвигатель с отрицательной обратной связью, который предназначен для применения в станках с ЧПУ (числовым программным управлением). Они обладают высокой точностью позиционирования, миниатюрными размерами, большими скоростями.
От мотора отходят три провода.

ArduinoManipulatorMeArm03.jpg

Красный провод — питание.
Коричневый — земля.
Желтый — сигнальный или информационный провод.
По желтому проводу мы подаём команды, на какой угол совершить оборот двигателю.
Робот приводится в движение четырьмя моторами. Один — круговое вращение, второй — движение вверх-вниз, третий — вперед-назад, четвёртый — открывает-закрывает клешню.
После сборки робота, нужно сделать систему управления и написать для неё программу. В качестве управляющего элемента мы выбрали миниатюрную плату Arduino NANO.

ArduinoManipulatorMeArm04.jpg

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

ArduinoManipulatorMeArm05.jpg

С верхней стороны припаяли к ней разъемы для подключения всех контактов Arduino, а также штырьки (по 3 шт в ряду), для подключения сервомоторов робота. Еще припаяли конденсатор для уменьшения помех по питанию от работающих моторов.

ArduinoManipulatorMeArm06.jpg

ArduinoManipulatorMeArm07.jpg

С обратной стороны соединили проводками определённые контакты от моторов с контактами на Arduino.

ArduinoManipulatorMeArm08.jpg

К самому первому ряду должны подключаться коричневые провода от моторов, то есть «минуса». От этих объединённых контактов тянется провод к контакту Arduino «GND» - «земля». Ко второму ряду присоединяются красные провода «+». Их нужно объединить вместе, соединить с ножкой конденсатора и вывести на контакт Arduino 5V. То есть, на них будет подаваться питание в 5V. Третий ряд — это желтые провода. Каждый из них соединяется с цифровыми контактами Arduino с D4 по D7 — это для моторов, и с аналоговыми контактами с A1 по А4 — для управляющих моторами элементов.

Все соединение с Arduino должно осуществляться по схеме:

ArduinoManipulatorMeArm09.jpg

Затем, устанавливаем плату Arduino NANO на своё место.

ArduinoManipulatorMeArm10.jpg

Подключаем к устройству провода от четырёх моторов, так, чтобы жёлтые провода соединялись с цифровыми контактами с D4 по D7.

ArduinoManipulatorMeArm11.jpg

Три мотора у нас подключились нормально, а провод от верхнего мотора не дотягивался. Мы удлинили его, воспользовавшись вот таким набором проводочков с разъёмами на концах.

ArduinoManipulatorMeArm12.jpg

ArduinoManipulatorMeArm13.jpg

Собранное устройство уже можно программировать.
Первое, что нужно было выяснить — это максимальный и минимальный угол поворота каждого из моторов в данной конструкции.

Напишем небольшую программу, которая просто посылает определённому мотору команду повернуться на такой-то угол.

/*Программа для робота-манипулятора MeArm. Задаёт для четырёх моторов определённый угол поворота*/

//подключаем библиотеку servo
#include <Servo.h>

/*создаём четыре объекта класса servo, для каждого мотора*/
Servo servo_round, servo_up, servo_front, servo_hand;

/*стандартная функция с начальной инициализацией всех компонентов
программы*/

void setup()
{
//моторам назначаем номера портов Arduino D4-D7
  servo_round.attach(4);
  servo_up.attach(5);
  servo_front.attach(6);
  servo_hand.attach(7);

//задаём углы поворота для моторов (от 0 до 180)
  servo_hand.write(90);
  delay(15); //задержка 15 мс
  servo_up.write(90);
  delay(15);
  servo_front.write(90);
  delay(15);
  servo_hand.write(40);
  delay(15);
}

/*стандартная функция с реализацией программы, в которой все действия циклически повторяются*/
void loop(){
}

С помощью этой программы определяем возможные границы вращения для каждого из моторов.
Мы установили следующие:
для мотора, поворачивающего руку - от 0 до 180 градусов,
двигающего вверх-вниз - от 60 до 160 градусов,
двигающего вперё-назад - от 90 до 160 градусов,
открывающего-закрывающего клешню от 0 до 40 градусов.
Зная границы — можно написать программу посложнее, которая задает определённое поведение всей конструкции.
В новой программе нам сразу нужно определить ключевые слова (INI — начальное положение мотора, MAX — максимальное положение мотора, MIN — минимальное положение мотора, CUR — текущее положение мотора). Эти переменные будут индексами, дающими доступ к цифрам, разным для каждого из четырёх моторов.

ArduinoManipulatorMeArm14.jpg

Информацию о каждом моторе удобнее хранить в структуре, которую мы назвали SERVO_MOTOR.
В ней хранится объект класса servo, массив positions_data, содержащий установки мотора: его 0-й элемент (соответствует INI) — это начальное положение мотора, 1-й (MAX) — максимально возможное положение мотора, 2-й (MIN) — минимально возможное, 3й (CUR) — текущее положение.
Еще структура будет содержать целочисленный массив steps, задающий поведение мотора за определённое количество шагов, у нас это — 16.
Каждый его элемент ссылается на одну из трёх установок positions_data (INI, MAX или MIN), то есть ссылается на угол поворота, к которому плавно, продвигаясь на 1 градус мотор перейдёт из текущего положения.
Создаём четыре объекта структуры SERVO_MOTOR.

ArduinoManipulatorMeArm15.jpg

Инициируем объекты в функции InitServoObjects(), присваивая каждому мотору, по примеру предыдущей программы номер контакта Arduino, границы вращения и программу поведения.

ArduinoManipulatorMeArm16.jpg

Программа сделана для детей таким образом, чтобы им легко было вносить изменения.
Достаточно переставить местами или поменять переменные MIN, MAX и INI в инициализации, как рука будет вести себя по-другому.
В цикле loop() реализуем вращение всех моторов.
Ниже полный код программы с подробными пояснениями.

/*Программа для робота-манипулятора MeArm. Задаёт определённое, циклически повторяющееся поведение робота*/
//подключаем библиотеку servo
#include <Servo.h>

/*определяем ключевые слова (INI — начальное положение мотора, MAX — максимальное положение мотора, MIN — минимальное положение мотора, CUR — текущее положение мотора), которые будут использоваться как индексы для доступа к цифрам, своим для каждого мотора*/
#define INI 0
#define MAX 1
#define MIN 2
#define CUR 3

/*константная глобальная переменная, хранящая количество шагов в задаваемой программе, по достижению последнего шага, программа воспроизводится с начала*/
const int prog_steps_count = 16;

/*глобальная переменная, хранящая количество шагов уже совершенных программой (всё время меняется)*/
int current_prog_step = 0;

/*константные глобальные переменные хранящие время задержки в мс*/
const int delay_setup = 500;
const int delay_move = 10;

/*Структура, хранящая всю необходимую информацию о моторе*/
struct SERVO_MOTOR{
//объект мотора servo
  Servo servo_obj;
/*массив целочисленных переменных, у которого 0й элемент (соответствует INI) — это начальное положение мотора, 1й (MAX) — максимально возможное, 2й (MIN) — минимально возможное, 3й (CUR) — текущее положение.*/
  int positions_data[4] = {0,0,0,0};
/*целочисленный массив, каждый элемент которого ссылается на угол поворота (INI, MAX или MIN), т.о. задаётся поведение мотора за заданное в глобальной переменной prog_steps_count количество шагов. На каждом шаге происходит плавный переход от текущего положения мотора к задаваемому в массиве*/
  int steps[prog_steps_count] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
};

//создаём четыре объекта структуры SERVO_MOTOR
SERVO_MOTOR servo_round, servo_up, servo_front, servo_hand;

/*функция InitServoObjects содержит начальную инициализацию всех четырёх моторов*/
void InitServoObjects()
{
/*создаём четыре локальных массива с программами поведения для каждого из моторов*/
  int servo_round_steps[prog_steps_count] = {INI, INI, INI, INI, MAX, MAX, MAX, MAX, INI, INI, INI, INI, MIN, MIN, MIN, MIN};
  int servo_up_steps[prog_steps_count] = {MAX, INI, INI, MAX, MAX, INI, INI, MAX, MAX, INI, INI, MAX, MAX, INI, INI, MAX};
  int servo_front_steps[prog_steps_count] = {MIN, MAX, MAX, MIN, MIN, MAX, MAX, MIN, MIN, MAX, MAX, MIN, MIN, MAX, MAX, MIN};
  int servo_hand_steps[prog_steps_count] = {MIN, MIN, MAX, MAX, MAX, MAX, MIN, MIN, MIN, MIN, MAX, MAX, MAX, MAX, MIN, MIN};

//Инициируем объект servo_round

  servo_round.positions_data[INI] = 90; //начальный угол поворота
  servo_round.positions_data[MAX] = 180; //максимальный угол поворота
  servo_round.positions_data[MIN] = 0; //минимальный угол поворота
//текущий угол поворота
  servo_round.positions_data[CUR] = servo_round.positions_data[INI];
//назначаем объекту класса servo контакт Arduino D4
  servo_round.servo_obj.attach(4);
//устанавливаем мотор на начальный угол поворота
  servo_round.servo_obj.write(servo_round.positions_data[INI]);

/*присваиваем массиву объекта steps данные из заданного в начале функции локального массива*/
  for(int i=0; i < prog_steps_count; i++)
  {
   servo_round.steps[i] = servo_round_steps[i];
  }

//задержка
  delay(delay_setup);

//Инициируем объект servo_up
  servo_up.positions_data[INI] = 100; //начальный угол поворота
  servo_up.positions_data[MAX] = 160; //максимальный угол поворота
  servo_up.positions_data[MIN] = 60; //минимальный угол поворота
//текущий угол поворота
  servo_up.positions_data[CUR] = servo_up.positions_data[INI];
//назначаем объекту класса servo контакт Arduino D5
  servo_up.servo_obj.attach(5);
//устанавливаем мотор на начальный угол поворота
  servo_up.servo_obj.write(servo_up.positions_data[INI]);

/*присваиваем массиву объекта steps данные из заданного в начале функции локального массива*/
  for(int i=0; i < prog_steps_count; i++)
  {
    servo_up.steps[i] = servo_up_steps[i];
  }

//задержка
  delay(delay_setup);

//Инициируем объект servo_front
  servo_front.positions_data[INI] = 90; //начальный угол поворота
  servo_front.positions_data[MAX] = 160; //максимальный угол поворота
  servo_front.positions_data[MIN] = 90; //минимальный угол поворота
//текущий угол поворота
  servo_front.positions_data[CUR] = servo_front.positions_data[INI];
//назначаем объекту класса servo контакт Arduino D6
  servo_front.servo_obj.attach(6);
//устанавливаем мотор на начальный угол поворота
  servo_front.servo_obj.write(servo_front.positions_data[INI]);

/*присваиваем массиву объекта steps данные из заданного в начале функции локального массива*/
  for(int i=0; i < prog_steps_count; i++)
  {
    servo_front.steps[i] = servo_front_steps[i];
  }

//задержка
  delay(delay_setup);

//Инициируем объект servo_hand
  servo_hand.positions_data[INI] = 40; //начальный угол поворота
  servo_hand.positions_data[MAX] = 40; //максимальный угол поворота
  servo_hand.positions_data[MIN] = 0; //минимальный угол поворота
//текущий угол поворота
  servo_hand.positions_data[CUR] = servo_hand.positions_data[INI];
//назначаем объекту класса servo контакт Arduino D7
  servo_hand.servo_obj.attach(7);
//устанавливаем мотор на начальный угол поворота
  servo_hand.servo_obj.write(servo_hand.positions_data[INI]);

/*присваиваем массиву объекта steps данные из заданного в начале функции локального массива*/
  for(int i=0; i < prog_steps_count; i++)
  {
    servo_hand.steps[i] = servo_hand_steps[i];
  }

//задержка
  delay(delay_setup);
}

/*функция поворачивает мотор в нужном направлении на 1 градус. Принимает ссылку на объект структуры SERVO_MOTOR, возвращает true, если мотор повёрнут на заданный в массиве steps градус, false, если еще не дошёл*/
bool SetMotorStep(SERVO_MOTOR &obj)
{
/*определяем угол на который должен быть повёрнут мотор — берём данные из массива steps под индексом текущего шага obj.steps[current_prog_step]. Эти данные равны INI, MAX или MIN. Если текущее положение мотора obj.positions_data[CUR] больше заданного (obj.positions_data[INI], obj.positions_data[MAX], obj.positions_data[MIN]), то уменьшаем его на 1 и вращаем мотор*/
  if(obj.positions_data[CUR] > obj.positions_data[ obj.steps[current_prog_step] ])
  {
    obj.positions_data[CUR]--;
    obj.servo_obj.write(obj.positions_data[CUR]);
    return false;
  }
//если меньше, то увеличиваем на 1
  if(obj.positions_data[CUR] < obj.positions_data[ obj.steps[current_prog_step] ])
  {
    obj.positions_data[CUR]++;
    obj.servo_obj.write(obj.positions_data[CUR]);
    return false;
  }
/*если равно — ничего не делаем и возвращаем true, то есть поворот завершён*/
  else if(obj.positions_data[CUR] == obj.positions_data[ obj.steps[current_prog_step] ])
    return true;
  }

/*стандартная функция с начальной инициализацией всех компонентов
программы*/

void setup()
{
//инициируем все объекты в отдельной функции
  InitServoObjects();
}

/*стандартная функция с реализацией программы, в которой все действия циклически повторяются*/
void loop(){
/*булева переменная, нужна для того, чтобы определить все ли моторы повернулись на заданный в массиве поворот и можно ли переходить к следующему шагу*/
  bool gotoNextStep = true;
/*по-очереди посылаем функции SetMotorStep ссылки на все четыре мотора. В ней они совершают поворот на 1 градус в нужном направлении. Если функция возвращает false — то к следующему шагу переходить ещё нельзя, если true — то мотор завершил поворот*/
  if( SetMotorStep(servo_round) == false )
    gotoNextStep = false;
  if( SetMotorStep(servo_up) == false )
    gotoNextStep = false;
  if( SetMotorStep(servo_front) == false )
    gotoNextStep = false;
  if( SetMotorStep(servo_hand) == false )
    gotoNextStep = false;

/*если все моторы завершили поворот, переходим к следующему шагу — увеличиваем переменную current_prog_step на 1 и проверяем на выход за предел максимально возможного количества шагов*/
  if(gotoNextStep == true)
  {
    current_prog_step++;
    if(current_prog_step >= prog_steps_count)
      current_prog_step = 0;
  }

//задержка
  delay(delay_move);
}

Следующая задача — сделать так, чтобы ребенок мог использовать блок управления моторами.
В качестве рукоятки мы использовали переменный резистор.

ArduinoManipulatorMeArm17.jpg

Мы припаяли к ему три провода, помня, что информационный (у нас жёлтый провод) должен быть центральным.

ArduinoManipulatorMeArm18.jpg

Всего у нас должно быть 4 таких рукоятки, каждая отвечает за работу своего мотора.
Чтобы объединить их вместе в один блок, мы разработали и напечатали на 3D принтере такой элемент.

ArduinoManipulatorMeArm19.jpg

В его ячейки мы продеваем все 4 рукоятки и подключаем к нашему устройству так, чтобы провода-заземления шли по первому ряду контактов на печатной плате, красные провода-питания по второму ряду, а желтые провода, подключались к третьему ряду, где они соединяются с
аналоговыми контактами Arduino с A1 по А4.

ArduinoManipulatorMeArm20.jpg

ArduinoManipulatorMeArm21.jpg

Затем надеваем на переменные резисторы красивые колпачки и устройство готово.

ArduinoManipulatorMeArm22.jpg

Программа для него сходна с предыдущей, но в ней исключается массив, задающий поведение моторов, зато в цикле loop() включается обработка информации от аналоговых контактов, регистрирующих в каком положении находятся рукоятки-переменные резисторы, и на основании этих данных рассчитывается положение мотора в рамках установленных для него границ вращения.
Ниже полный код программы с подробными пояснениями.

/*Программа для робота-манипулятора MeArm. Получает данные от четырёх переменных резисторов и устанавливает моторы робота в положения, соответствующие этим данным*/

//подключаем библиотеку servo
#include <Servo.h>

/*определяем ключевые слова (INI — начальное положение мотора, MAX — максимальное положение мотора, MIN — минимальное положение мотора, STP — сколько единиц, полученных с аналоговых контактов переменных резисторов соответствуют 1 градусу поворота мотора, CUR — текущее положение мотора), которые будут использоваться как индексы для доступа к цифрам, своим для каждого мотора*/
#define INI 0
#define MAX 1
#define MIN 2
#define STP 3
#define CUR 4

/*константные глобальные переменные хранящие время задержки в мс*/
int delay_setup = 500;
int delay_move = 10;

/*назначаем константным переменным номера аналоговых контактов, к которым подключены переменные резисторы*/
const int Upravlen_servo_round = 1;
const int Upravlen_servo_up = 2;
const int Upravlen_servo_front = 3;
const int Upravlen_servo_hand = 4;

//переменная, хранящая считаный результат аналогового контакта
int read_result_var = 0;

/*Структура, хранящая всю необходимую информацию о моторе*/
struct SERVO_MOTOR{
//объект мотора servo
  Servo servo_obj;
/*массив целочисленных переменных, у которого 0й элемент (соответствует INI) — это начальное положение мотора, 1й (MAX) — максимально возможное, 2й (MIN) — минимально возможное, 3й (STP) — единицы соответствующие шагу в 1 градус, 4й (CUR) — текущее положение.*/
  int positions_data[5] = {0,0,0,0,0};
};

//создаём четыре объекта структуры SERVO_MOTOR
SERVO_MOTOR servo_round, servo_up, servo_front, servo_hand;

/*функция InitServoObjects содержит начальную инициализацию всех четырёх моторов*/
void InitServoObjects()
{
//Инициируем объект servo_round
  servo_round.positions_data[INI] = 90; //начальный угол поворота
  servo_round.positions_data[MAX] = 180; //максимальный угол поворота
  servo_round.positions_data[MIN] = 0; //минимальный угол поворот
/*1023 — максимально возможное число, которое можно считать с аналогового контакта, делим его на максимально возможный угол поворота мотора, получаем количество единиц, соответствующее одному углу поворота*/
  servo_round.positions_data[STP] = 1023/(servo_round.positions_data[MAX] - servo_round.positions_data[MIN]);
  servo_round.positions_data[CUR] = 0; //текущий угол поворота
//назначаем объекту класса servo контакт Arduino D4
  servo_round.servo_obj.attach(4);
//устанавливаем мотор на начальный угол поворота
  servo_round.servo_obj.write(servo_round.positions_data[INI]);
//задержка
  delay(delay_setup);

//Инициируем объект servo_up
  servo_up.positions_data[INI] = 100; //начальный угол поворота
  servo_up.positions_data[MAX] = 160; //максимальный угол поворота
  servo_up.positions_data[MIN] = 60; //минимальный угол поворот
  servo_up.positions_data[STP] = 1023/(servo_up.positions_data[MAX] - servo_up.positions_data[MIN]);
  servo_up.positions_data[CUR] = 0; //текущий угол поворота
//назначаем объекту класса servo контакт Arduino D5
  servo_up.servo_obj.attach(5);
//устанавливаем мотор на начальный угол поворота
  servo_up.servo_obj.write(servo_up.positions_data[INI]);
//задержка
  delay(delay_setup);

//Инициируем объект servo_front
  servo_front.positions_data[INI] = 90; //начальный угол поворота
  servo_front.positions_data[MAX] = 160; //максимальный угол поворота
  servo_front.positions_data[MIN] = 90; //минимальный угол поворот
  servo_front.positions_data[STP] = 1023/(servo_front.positions_data[MAX] - servo_front.positions_data[MIN]);
  servo_front.positions_data[CUR] = 0; //текущий угол поворота
//назначаем объекту класса servo контакт Arduino D6
  servo_front.servo_obj.attach(6);
//устанавливаем мотор на начальный угол поворота
  servo_front.servo_obj.write(servo_front.positions_data[INI]);
//задержка
  delay(delay_setup);

//Инициируем объект servo_hand
  servo_hand.positions_data[INI] = 40; //начальный угол поворота
  servo_hand.positions_data[MAX] = 40; //максимальный угол поворота
  servo_hand.positions_data[MIN] = 0; //минимальный угол поворот
  servo_hand.positions_data[STP] = 1023/(servo_hand.positions_data[MAX] - servo_hand.positions_data[MIN]);
  servo_hand.positions_data[CUR] = 0; //текущий угол поворота
//назначаем объекту класса servo контакт Arduino D7
  servo_hand.servo_obj.attach(7);
//устанавливаем мотор на начальный угол поворота
  servo_hand.servo_obj.write(servo_hand.positions_data[INI]);
//задержка
  delay(delay_setup);
}

/*стандартная функция с начальной инициализацией всех компонентов
программы*/
void setup()
{
//инициируем все объекты в отдельной функции
  InitServoObjects();
}

/*стандартная функция с реализацией программы, в которой все действия циклически повторяются*/
void loop(){
//считываем данные с первой рукоятки (переменного резистора)
  read_result_var = analogRead(Upravlen_servo_round);

/*осуществляем проверку, если значение превысило максимально возможное, либо если оно не изменилось по сравнению с предыдущим, то ничего делать не нужно*/
  if( (read_result_var < 1023)&&(read_result_var != servo_round.positions_data[CUR]) )
  {
//рассчитываем в какое положение привести мотор
    servo_round.positions_data[CUR] = read_result_var;
    read_result_var = servo_round.positions_data[MIN] + ( read_result_var/servo_round.positions_data[STP] );
    servo_round.servo_obj.write(read_result_var);
  }

//считываем данные со второй рукоятки (переменного резистора)
  read_result_var = analogRead(Upravlen_servo_up);
  if( (read_result_var < 1023)&&(read_result_var != servo_up.positions_data[CUR]) )
  {
//рассчитываем в какое положение привести мотор
    servo_up.positions_data[CUR] = read_result_var;
    read_result_var = servo_up.positions_data[MIN] + ( read_result_var/servo_up.positions_data[STP] );
    servo_up.servo_obj.write(read_result_var);
  }

//считываем данные с третьей рукоятки (переменного резистора)
  read_result_var = analogRead(Upravlen_servo_front);
  if( (read_result_var < 1023)&&(read_result_var != servo_front.positions_data[CUR]) )
  {
//рассчитываем в какое положение привести мотор
    servo_front.positions_data[CUR] = read_result_var;
    read_result_var = servo_front.positions_data[MIN] + ( read_result_var/servo_front.positions_data[STP] );
    servo_front.servo_obj.write(read_result_var);
  }

//считываем данные с четвёртой рукоятки (переменного резистора)
  read_result_var = analogRead(Upravlen_servo_hand);
  if( (read_result_var < 1023)&&(read_result_var != servo_hand.positions_data[CUR]) )
  {
//рассчитываем в какое положение привести мотор
    servo_hand.positions_data[CUR] = read_result_var;
    read_result_var = servo_hand.positions_data[MIN] + ( read_result_var/servo_hand.positions_data[STP] );
    servo_hand.servo_obj.write(read_result_var);
  }

//задержка
  delay(15);
}