Введение:
В этом уроке мы дополним робота «Quadruped» радиомодулем HC-12, а так же построим пульт дистанционного управления на основе этого же модуля. При подаче питания на робота и включении пульта управления устройства будут соединяться самостоятельно на установленной частоте.
Видео:
Редактируется...
Нам понадобится:
Робот "Quadruped"
- Робот-платформа «QUADRUPED» x1 шт.
- Arduino / Piranha UNO x1 шт.
- Trema Shield x1 шт.
- Trema-модуль Радиомодуль HC-12 x1 шт.
- Battery Shield x1 шт. либо 1 аккумулятор Ni-MH размером крона и переходник под DC-jack.
Пульт
- Arduino / Piranha UNO x1 шт.
- Trema Shield x1 шт.
- Trema-модуль Радиомодуль HC-12 x1 шт.
- Trema-модуль кнопка х4 шт.
- Trema-модуль потенциометр x1 шт.
- Battery Shield x1 шт. либо 1 аккумулятор Ni-MH размером крона и переходник под DC-jack.
Для реализации проекта нам необходимо установить библиотеки:
- Библиотеки SoftwareSerial и Servo входят в базовый набор Arduino IDE и не требуют установки.
О том как устанавливать библиотеки, Вы можете ознакомиться на странице Wiki - Установка библиотек в Arduino IDE.
Схема подключения модуля для предварительной настройки:
Прежде, чем собирать весь проект, необходимо произвести предварительную настройку модулей. Для этого воспользуйтесь следующей инструкцией.
- Установите на Arduino / Piranha UNO плату Trema Shield;
- Подключите к Trema Shield модуль HC-12 как показано ниже:
- Загрузите в плату следующий скетч:
Радиомодуль HC-12 | Trema Shield |
---|---|
TX | D8 |
RX | D9 |
S | D13 |
V | 5V |
G | GND |
#include "SoftwareSerial.h" // подключаем библиотеку для работы с программным UART SoftwareSerial mySerial(8, 9) // Указываем выводы Arduino, к которым подключены (TX, RX) выводы МОДУЛЯ #define S 13 // Указываем, к какому выводу подключен вывод S модуля HC-12 void setup() { Serial.begin(9600); // Инициируем аппаратный последовательный порт mySerial.begin(9600); // Инициируем программный последовательный порт pinMode(S, OUTPUT); // Переводим вывод S модуля в режим выход digitalWrite(S, LOW); // Назначаем выводу уровень логического нуля delay(40); // Ждём пока модуль войдёт в режим AT команд } void loop() { if(mySerial.available()){ // Если в буфере программного последовательного порта есть данные Serial.write(mySerial.read()); // Перенаправляем их в аппаратный последовательный порт } if(Serial.available()){ // Если в буфере аппаратного последовательного порта есть данные mySerial.write(Serial.read()); // Перенаправляем их в программный последовательный порт } }
- Откройте монитор последовательного порта и в открывшемся окне введите следующие команды:
- AT+B**** - для задания скорости передачи данных;
- AT+C*** - для задания канала (частоты) работы модуля;
- Подробнее про AT-команды вы можете прочесть в статье, посвящённой модулю HC-12;
После того, как все необходимые манипуляции с модулем закончены - повторите всё тоже самое со вторым модулем, аналогично подключив его к той же плате Arduino и установив аналогичные скорость и канал.
После того, как все подготовительные работы выполнены, можно переходить непосредственно к сборке проекта.
Схема сборки пульта управления:
Соберём блок пульта управления. Для этого вам понадобится Arduino / Piranha UNO:
Теперь установим Battery Shield на Arduino / Piranha UNO:
Во время установки Battery Shield должен быть в выключенном состоянии!
Далее на Battery Shield установите Trema Shield для удобного подключения модулей:
Теперь вновь воспользуемся модулем HC-12, который подключим к Trema Shield вместе с 5 Trema-кнопками:
Кнопка "ВПРАВО" | Trema Shield |
---|---|
S | D2 |
V | 5V |
G | GND |
Кнопка "ВЛЕВО" | Trema Shield |
---|---|
S | D3 |
V | 5V |
G | GND |
Кнопка "НАЗАД" | Trema Shield |
---|---|
S | D4 |
V | 5V |
G | GND |
Кнопка "ВПЕРЁД" | Trema Shield |
---|---|
S | D5 |
V | 5V |
G | GND |
Потенциометр | Trema Shield |
---|---|
S | A0 |
V | 5V |
G | GND |
Радиомодуль HC-12 | Trema Shield |
---|---|
S | D13 |
TX | D8 |
RX | D9 |
V | 5V |
G | GND |
Схема сборки робота QUADRUPED:
Соберите механику, подключите Trema Shield, сервоприводы и откалибруйте робота, как это описано на странице Сборка QUADRUPED. Электроника + Калибровка. Далее на боковую панель установите модуль HC-12, который подключается к выводам D13, D8 и D9 (в примере используется программная шина UART).
Сервоприводы | Trema Shield | |
---|---|---|
1 конечность | Горизонтальный сустав (№ 0) | вывод 4 на белой колодке |
Вертикальный сустав (№ 1) | вывод 5 на белой колодке | |
2 конечность | Горизонтальный сустав (№ 2) | вывод 6 на белой колодке |
Вертикальный сустав (№ 3) | вывод 7 на белой колодке | |
3 конечность | Горизонтальный сустав (№ 4) | вывод A0 на белой колодке |
Вертикальный сустав (№ 5) | вывод A1 на белой колодке | |
4 конечность | Горизонтальный сустав (№ 6) | вывод 10 на белой колодке |
Вертикальный сустав (№ 7) | вывод 11 на белой колодке |
Вы можете изменить выводы 4-11 для подключения сервоприводов на любые другие, указав их в скетче при объявлении массива объектов pinServo[8].
Подключите радиомодуль к Trema Shield на роботе так, как показано на рисунке ниже:
Радиомодуль HC-12 | Trema Shield |
---|---|
S | D13 |
TX | D8 |
RX | D9 |
V | 5V |
G | GND |
Трехпроводные шлейфы сервоприводов устанавливаются следующим образом:
- Оранжевый провод подключается к выводу на белой колодке.
- Красный провод подключается к выводу на красной колодке.
- Коричневый провод подключается к выводу на чёрной колодке.
Код программы для пульта дистанционного управления:
#include "SoftwareSerial.h" // Подключаем библиотеку Servo для работы с сервоприводами SoftwareSerial mySerial(8, 9); // Создаём объект mySerial указывая выводы RX, TX #define FRAME_HEADER 0xAA // Определяем заголовок пакета #define FRAME_FOOTER '\n' // Определяем конец пакета #define radio_pin 13 // Определяем номер вывода S радио модуля #define button_up 5 // Вывод, к которому подключена кнопка ВПЕРЁД #define button_down 4 // Вывод, к которому подключена кнопка НАЗАД #define button_left 3 // Вывод, к которому подключена кнопка ВЛЕВО #define button_right 2 // Вывод, к которому подключена кнопка ВПРАВО #define LevelRegulationPin A0 // Вывод, к которому подключен ПОТЕНЦИОМЕТР uint16_t LevelRegulation; // Переменная для хранения значения с потенциометра int8_t arrData[4]; // Массив для хранения отправляемых данных void setup() { mySerial.begin(9600); // инициируем работу с монитором последовательного порта pinMode(button_up, INPUT ); // настраиваем вывод на работу в режиме ВХОДА pinMode(button_down, INPUT ); // настраиваем вывод на работу в режиме ВХОДА pinMode(button_left, INPUT ); // настраиваем вывод на работу в режиме ВХОДА pinMode(button_right, INPUT ); // настраиваем вывод на работу в режиме ВХОДА pinMode(LevelRegulationPin,INPUT ); // настраиваем вывод на работу в режиме ВХОДА } void loop() { arrData[3] = 0; // обнуляем значение контрольной суммы всех элементов массива LevelRegulation = analogRead(LevelRegulationPin); // считываем значение с потенциометра if (digitalRead(button_up)) {arrData[0] = 1;} else // если нажата кнопка ВПЕРЁД, то записываем в массив значение 1 if (digitalRead(button_down)) {arrData[0] = -1;} else // если нажата кнопка НАЗАД, то записываем в массив значение -1 {arrData[0] = 0;} // иначе передаём 0 if (digitalRead(button_right)) {arrData[1] = 1;} else // если нажата кнопка ВПРАВО, то записываем в массив значение 1 if (digitalRead(button_left)) {arrData[1] = -1;} else // если нажата кнопка ВЛЕВО, то записываем в массив значение -1 {arrData[1] = 0;} // иначе передаём 0 arrData[2] = map(LevelRegulation, 0, 1023, 0, 100); // переопределяем диапазон значений потенциометра for(int i = 0; i < 3; i++){ // в цикле arrData[3] += arrData[i]; // вычисляем общую сумму всех элементов массива } mySerial.write(FRAME_HEADER); // Передаём заголовок пакета в последовательный порт for (int i = 0; i < 4; i++) { // Побайтово передаём данные mySerial.write(arrData[i]); // в последовательный порт } mySerial.write(FRAME_FOOTER); // Передаём байт конца пакета в последовательный порт delay(70); // Ждём 70 миллисекунд }
Код программы для робота QUADRUPED:
#define FRAME_HEADER 0xAA // Задаём заголовок пакета #define FRAME_FOOTER '\n' // Задаём конец пакета #define radio_pin 13 // Определяем номер вывода S радио модуля // Подключаем библиотеки: #include "SoftwareSerial.h" // Подключаем библиотеку для общения с модулем по шине UART SoftwareSerial mySerial(8, 9); // Создаём объект mySerial указывая выводы RX, TX #include "Servo.h" // Подключаем библиотеку Servo, для работы с сервоприводами. Servo objServo[8]; /* 0 1 2 3 4 5 6 7 */ // Создаём массив, каждый элемент которого является объектом библиотеки Servo. Цифры в комментарии указывают на № сервопривода // Определяем сервы: Г В Г В Г В Г В // Буквы в комментарии указывают на плоскость вращения сервопривода (Горизонтальная / Вертикальная) const uint8_t pinServo[8] = { 4, 5, 6, 7, A0, A1, 10, 11 }; // Определяем массив хранящий номера выводов к которым подключены сервоприводы (можно менять) // ЗАПОЛНЯЕМ МАССИВ ЗНАЧЕНИЯМИ ПОСЛЕ КАЛИБРОВКИ const int cenAngle[8] = { 72, 103, 120, 84, 125, 111, 93, 76 }; // Определяем массив хранящий углы в градусах, при которых сервоприводы находятся в центральном положении (КОРРЕКТИРУЕТСЯ В КАЛИБРОВОЧНОМ СКЕТЧЕ) const int minAngle[8] = { 50, 30, 50, 30, 50, 30, 50, 30 }; // Определяем массив хранящий углы в градусах, на которые отклоняются сервоприводы от центрального положения назад или вниз const int maxAngle[8] = { 50, 40, 50, 40, 50, 40, 50, 40 }; // Определяем массив хранящий углы в градусах, на которые отклоняются сервоприводы от центрального положения вперёд или вверх // Объявляем функции: void funLimbMove ( uint8_t, bool, int8_t ); // Объявляем Функцию установки одного сустава в значение от -100 до 100 (конечность 1-4, сустав 0/1, положение -100...+100) void funLimbStep ( uint8_t, uint8_t, int, int ); // Объявляем Функцию установки одной конечности в положение от 0 до 255 (конечность 1-4, положение 0...255, ограничение по горизонтали -100...+100, ограничение по вертикали 0...100%) void funLimbGait ( uint8_t, uint8_t, int, int ); // Объявляем Функцию установки всех конечностей в положение от 0 до 255 (тип походки 0-4, положение 0...255, ограничение по горизонтали -100...+100, ограничение по вертикали 0...100%) void funLimbFree ( void ); // Объявляем Функцию ослабления всех суставов (без параметров) void funLimbCent ( void ); // Объявляем Функцию установки всех суставов в центральное положение (без параметров) // Определяем дополнительные переменные: uint8_t varPosition = 0; // Текущая позиция походки от 0 до 255 (чем быстрее меняется значение тем выше скорость, если значение увеличивается - значит вперёд, если значение уменьшается - значит назад) int8_t tempData[5]; // Промежуточный массив, куда записываются полученные данные int8_t arrData[4]; // Массив, куда записываются команды для робота int8_t temp_sum; // Переменная, в которую записывается сумма всех полученных от пульта значений uint32_t FindTimer; // Таймер для отслеживания наличия связи с пультом void setup(){ mySerial.begin(9600); // инициируем работу с монитором последовательного порта funLimbFree(); // Выполняем функцию освобождения всех конечностей funLimbCent(); // Устанавливаем все конечности в центральное положение } void loop(){ if(FindTimer + 3000 < millis()){ // Если с последнего обновления таймера прошло больше 3 секунд, то arrData[0] = 0; // сбрасываем значения массива данных arrData[1] = 0; // arrData[3] = 0; // } if( mySerial.available() && mySerial.read() == 0xAA){ // Если получен пакет данных, начинающийся с 0xAA, то delay(10); // Ждём 10 мс для получения всего пакета данных FindTimer = millis(); // Обновляем таймер for (int i = 0; i < 5; i++){ // Выполняем цикл получения и записи 4 полученных битов в промежуточный массив tempData[i] = mySerial.read(); // } if(tempData[4] == FRAME_FOOTER){ // Проверяем, что последний полученный бит равен биту конца пакета и если да, то temp_sum = 0; // обнуляем переменную for (int j = 0; j < 3; j++){ // в цикле выполняем сложение всех значений массива и temp_sum += tempData[j]; // полученную сумму записываем в переменную temp_sum } if(tempData[3] == temp_sum){ // Проверяем, что полученная сумма равна значению суммы всех элементов массива, который нам отправил пульт и если всё верно, то for (int k = 0; k < 3; k++){ // в цикле выполняем arrData[k] = tempData[k]; // перезапись элементов промежуточного массива в постоянный массив } } } } if(arrData[0] != 0){ // Если нажата кнопка "ВПЕРЁД" или "НАЗАД", то if(arrData[0] > 0){varPosition+=2;} // для кнопки ВПЕРЁД увеличиваем позицию походки varPosition else {varPosition-=2;} // для кнопки НАЗАД уменьшаем позицию походки varPosition if(arrData[1] > 0){funLimbGait(1, varPosition, 50, arrData[2]);} else // Проверяем, нажата ли кнопка ВПРАВО или ВЛЕВО, и если нажата кнопка ВПРАВО, то указываем значение поворота = 50 if(arrData[1] < 0){funLimbGait(1, varPosition, -50, arrData[2]);} else // для кнопки ВЛЕВО указываем значение поворота -50 {funLimbGait(1, varPosition, 0, arrData[2]);} // Иначе робот идёт прямо } else if(arrData[1] != 0){ // Если нажата кнопка "ВПРАВО" или "ВЛЕВО", то if(arrData[1] > 0){varPosition+=2;} // для кнопки ВПРАВО увеличиваем позицию походки varPosition. else {varPosition-=2;} // для кнопки ВЛЕВО уменьшаем позицию походки varPosition. funLimbGait(0, varPosition, 0, arrData[2]); // Вызываем функцию походки с заданными условиями (поворот на месте в сторону нажатой кнопки) } else{ // Если ни одна кнопка не нажата, то funLimbMove(1, 1, map(arrData[2],0,100,+100,-100)); // устанавливаем для первой конечности значения высоты в простое (меняется потенциометром) funLimbMove(2, 1, map(arrData[2],0,100,+100,-100)); // устанавливаем для второй конечности значения высоты в простое (меняется потенциометром) funLimbMove(3, 1, map(arrData[2],0,100,+100,-100)); // устанавливаем для третьей конечности значения высоты в простое (меняется потенциометром) funLimbMove(4, 1, map(arrData[2],0,100,+100,-100)); // устанавливаем для четвёртой конечности значения высоты в простое (меняется потенциометром) } } // Функция установки одного сустава конечности в значение от -100% (самое нижнее положение) до +100% (самое верхнее положение): void funLimbMove(uint8_t num, bool joint, int8_t pos){ // Аргументы функции: «num» - номер конечности от 1 до 4 , «joint» - тип сустава 0 (горизонтальный) или 1 (вертикальный) , «pos» - положение сустава от -100 (внизу-сзади) до +100 (вверху-спереди) uint8_t i = (num-1) * 2 + joint; // Определяем № сервопривода (i) по № конечности (num-1) и типу сустава (joint) int k = 0; // Определяем переменную (k) для хранения экстремума int j = pos? cenAngle[i]:0; // Определяем переменную (j) для хранения экстремума if(pos>0){ k = +100; // Находим (k) - максимально допустимое значение для аргумента (pos) if( num%2==0||i%2==1 ){j-=maxAngle[i];} else // Находим (j) - максимально допустимый угол в градусах для чётной конечности или сустава {j+=maxAngle[i];} // Находим (j) - максимально допустимый угол в градусах для нечётной конечности или сустава } if(pos<0){ k = -100; // Находим (k) - минимально допустимое значение для аргумента (pos) if( num%2==1&&i%2==0 ){j-=minAngle[i];} else // Находим (j) - минимально допустимый угол в градусах для чётной конечности {j+=minAngle[i];}} // Находим (j) - минимально допустимый угол в градусах для нечётной конечности if(!objServo[i].attached()){objServo[i].attach(pinServo[i]);} // Подключаем объект (objServo) работающий с сервоприводом (i) к выводу (pinServo[i]) objServo[i].write(map(pos, 0, k, cenAngle[i], j)); // Устанавливаем сервопривод (i) в угол находящийся между центром (cenAngle[I]) и экстремумом (j) } // Функция установки одной конечности в положение от 0 до 255: void funLimbStep(uint8_t num, uint8_t pos, int hor, int ver){ // Аргументы функции: «num» - номер конечности от 1 до 4 , «pos» - позиция от 0 до 255 , «hor» - ограничение поворота горизонтального сустава от -100 до +100 , «ver» - ограничение высоты вертикального сустава от 0 до 100% int i; // Объявляем переменные (i) int j; // Объявляем переменные (j) if(pos < 225){i = map(pos, 0, 212, +100, -100); }else // Сустав конечности поворачивается назад (+100 >>> -100) {i = map(pos, 225, 255, -100, +100); } // Сустав конечности поворачивается вперёд (-100 >>> +100) if(pos < 225){j = -100; }else // Сустав конечности опущен (-100) if(pos < 235){j = map(pos, 225, 235, -100, +100); }else // Сустав конечности поднимается вверх (-100 >>> +100) if(pos < 245){j = +100; }else // Сустав конечности поднят (+100) {j = map(pos, 245, 255, +100, -100); } // Сустав конечности опускается вниз (+100 >>> -100) if(hor<0 && num%2==1){ i = map(i, -100, +100, -(100+hor) , 100+hor);} // Ограничиваем угол (i) горизонтального сустава левых конечностей (поворот влево) if(hor>0 && num%2==0){ i = map(i, -100, +100, -(100-hor) , 100-hor);} // Ограничиваем угол (i) горизонтального сустава правых конечностей (поворот вправо) j = map(j, -100, +100, ver*(-2)+100, 100 ); // Ограничиваем угол (j) вертикального сустава любых конечностей (высота хексапода) funLimbMove(num, 0, i); // Устанавливаем угол (i) для горизонтального (0) сустава конечности (num) funLimbMove(num, 1, j); // Устанавливаем угол (j) для вертикального (1) сустава конечности (num) } // Функция выполнения походки по одному из возможных вариантов i // void funLimbGait(uint8_t num, uint8_t pos, int hor, int ver){ // Аргументы функции: (num) - номер походки от 0 до 2 , (pos) - позиция от 0 до 255 , «hor» - ограничение поворота горизонтальных суставов от -100 до +100 , «ver» - ограничение высоты вертикального сустава от 0 до 100% switch(num){ // // Походка номер 0: разворот на месте: // pos = 0 63 127 191 255 case 0: // | | | | | funLimbStep(1, 31+pos, hor, ver); // L | > > > > | > > > > | > > > > | <<< > > | конечность №1 выполняет полный цикл движений (от 255 до 0) быстро возвращаясь в последней тетраде funLimbStep(2, 0-pos, hor, ver); // R | >>> < < | < < < < | < < < < | < < < < | конечность №2 выполняет полный цикл движений (от 0 до 255) быстро возвращаясь в первой тетраде funLimbStep(3, 95+pos, hor, ver); // L | > > > > | > > > > | <<< > > | > > > > | конечность №3 выполняет полный цикл движений (от 255 до 0) быстро возвращаясь в предпоследней тетраде funLimbStep(4, 63-pos, hor, ver); // R | < < < < | >>> < < | < < < < | < < < < | конечность №4 выполняет полный цикл движений (от 0 до 255) быстро возвращаясь во второй тетраде break; // | | | | | // Походка номер 1: движение вперёд или назад: // pos = 0 63 127 191 255 case 1: // | | | | | funLimbStep(1, 0+pos, hor, ver); // L | > > > > | > > > > | > > > > | > > <<< | конечность №1 выполняет полный цикл движений (от 0 до 255) быстро возвращаясь в последней тетраде funLimbStep(2, 127+pos, hor, ver); // R | > > > > | > > <<< | > > > > | > > > > | конечность №2 выполняет полный цикл движений (от 0 до 255) быстро возвращаясь во второй тетраде funLimbStep(3, 63+pos, hor, ver); // L | > > > > | > > > > | > > <<< | > > > > | конечность №3 выполняет полный цикл движений (от 0 до 255) быстро возвращаясь в предпоследней тетраде funLimbStep(4, 191+pos, hor, ver); // R | > > <<< | > > > > | > > > > | > > > > | конечность №4 выполняет полный цикл движений (от 0 до 255) быстро возвращаясь в первой тетраде break; // | | | | | // Походка номер 2: плывёт вперёд или назад: // pos = 0 63 127 191 255 case 2: // | | | | | funLimbStep(1, 0+pos, hor, ver); // L | > > > > | > > > > | > > > > | > > <<< | конечность №1 выполняет полный цикл движений (от 0 до 255) быстро возвращаясь в последней тетраде funLimbStep(2, 0+pos, hor, ver); // R | > > > > | > > > > | > > > > | > > <<< | конечность №2 выполняет полный цикл движений (от 0 до 255) быстро возвращаясь в последней тетраде funLimbStep(3, 0+pos, hor, ver); // L | > > > > | > > > > | > > > > | > > <<< | конечность №3 выполняет полный цикл движений (от 0 до 255) быстро возвращаясь в последней тетраде funLimbStep(4, 0+pos, hor, ver); // R | > > > > | > > > > | > > > > | > > <<< | конечность №4 выполняет полный цикл движений (от 0 до 255) быстро возвращаясь в последней тетраде break; // | | | | | } } // Функция освобождения конечностей: void funLimbFree(void){ for(uint8_t i=0;i<8;i++){ // выполняем в цикле objServo[i].detach(); // отвязку всех сервоприводов digitalWrite(pinServo[i],LOW); // и отключаем питание на них } } // Функция установки всех суставов конечностей в центральное положение: void funLimbCent(void){ for(uint8_t i=1;i<=4;i++){ // выполняем в цикле funLimbMove(i,0,0); // устанавливаем все суставы (горизонтальные) в центральное положение funLimbMove(i,1,0); // устанавливаем все суставы (вертикальные) в центральное положение } }
Управление:
После подачи питания на робота и на пульт они автоматически соединятся и можно переходить непосредственно к управлению! Нажатие кнопок будет заставлять робота двигаться в выбранном направлении.
Если скорость движения кажется вам слишком большой или, наоборот, слишком малой, то измените величину, на которую изменяется переменнаяvarPosition
в скетче выше!
Обсуждение