Дистанционное управление роботом "Малыш" с помощью радиомодуля HC-12

Введение:

В этом уроке мы дополним робота «Малыш» радиомодулем HC-12, а так же построим пульт дистанционного управления на основе этого же модуля. При подаче питания на робота и включении пульта управления устройства будут соединяться самостоятельно на установленной частоте.

Видео:

Редактируется...

Нам понадобится:

Робот "Малыш":

Пульт:

Для реализации проекта нам необходимо установить библиотеки:

  • Библиотека SoftwareSerial входит в базовый набор Arduino IDE и не требует установки.

О том, как устанавливать библиотеки, Вы можете ознакомиться на странице Wiki - Установка библиотек в Arduino IDE.

Схема подключения модуля для предварительной настройки:

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

  • Установите на Arduino / Piranha UNO плату Trema Shield;
  • Подключите к Trema Shield модуль HC-12 как показано ниже:
  • Радиомодуль HC-12Trema Shield
    TXD8
    RXD9
    SD13
    V5V
    GGND
  • Загрузите в плату следующий скетч:
#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 и установив аналогичные скорость и канал.

    После того, как все подготовительные работы выполнены, можно переходить непосредственно к сборке проекта.

    Схема подключения робота «Малыш»:

    Соберём механическую и электрическую части, как это описано в проектах № 1-8 в книге "Набор "Малыш"". Далее, на верхнюю стенку установим модуль HC-12, который подключается к шине UART (в примере используется программная шина UART).

    BluetoothMotor Shield
    Радиомодуль HC-12вывод RXцифровой вывод D9
    вывод TXцифровой вывод D8
    вывод S (Key)цифровой вывод D13
    вывод V (Vcc)любой вывод 5V на красной колодке
    вывод G (GND)любой вывод GND на чёрной колодке

    Воспользуемся схемой сборки из проекта №9 книги "Набор "Малыш"", но, вместо Trema-модуля ИК-приёмник, установим в корпус Trema-модуль Радиомодуль HC-12 (подключается к выводам D13, D8 и D9).

    Схема сборки пульта управления:

    Соберём блок пульта управления. Для этого вам понадобится Arduino / Piranha UNO:

    Теперь установим Battery Shield на Arduino / Piranha UNO:

    Во время установки Battery Shield должен быть в выключенном состоянии!

    Далее на Battery Shield установите Trema Shield для удобного подключения модулей:

    Теперь вновь воспользуемся модулем HC-12, который подключим к Trema Shield вместе с 5 Trema-кнопками:

    Кнопка "ВПРАВО"Trema Shield
    SD2
    V5V
    GGND
    Кнопка "ВЛЕВО"Trema Shield
    SD3
    V5V
    GGND
    Кнопка "НАЗАД"Trema Shield
    SD4
    V5V
    GGND
    Кнопка "ВПЕРЁД"Trema Shield
    SD5
    V5V
    GGND
    Кнопка "СВЕТОДИОДЫ"Trema Shield
    SD6
    V5V
    GGND
    Радиомодуль HC-12Trema Shield
    SD13
    TXD8
    RXD9
    V5V
    GGND

    Код программы для пульта:

    #include "SoftwareSerial.h"                     //  Подключаем библиотеку для общения с модулем по шине UART
    SoftwareSerial mySerial(8, 9);                  //  Создаём объект mySerial указывая выводы RX, TX
    
    #define FRAME_HEADER  0xAA                      //  Задаём заголовок пакета
    #define FRAME_FOOTER  '\n'                      //  Задаём конец пакета
    #define button_led    6                         //  Вывод для кнопки управления светодиодами
    #define button_up     5                         //  Вывод для кнопки "Вперёд"
    #define button_down   4                         //  Вывод для кнопки "Назад"
    #define button_left   3                         //  Вывод для кнопки "Влево"
    #define button_right  2                         //  Вывод для кнопки "Вправо"
    #define radio_pin     13                        //  Определяем номер вывода S радио модуля
    byte data[5];                                   //  Массив для получения данных с пульта
    bool LedFlag;                                   //  Переменная-флаг
    int SIZE = sizeof(data)/sizeof(data[0]);        //  Переменная, равная размерности массива с данными
    
    void setup() {
      mySerial.begin(9600);                         //  инициируем работу с монитором последовательного порта
      pinMode(button_up,    INPUT );                //  настраиваем вывод на работу в режиме ВХОДА
      pinMode(button_down,  INPUT );                //  настраиваем вывод на работу в режиме ВХОДА
      pinMode(button_left,  INPUT );                //  настраиваем вывод на работу в режиме ВХОДА
      pinMode(button_right, INPUT );                //  настраиваем вывод на работу в режиме ВХОДА
    }
    
    void loop() {
      if (digitalRead(button_up))     data[0] = 1;  //  Если нажата кнопка ВПЕРЁД, то устанавливаем значение бита в 1
      else                            data[0] = 0;  //  Если кнопка отпущена, то устанавливаем значение бита в 0
                                      
      if (digitalRead(button_down))   data[1] = 1;  //  Если нажата кнопка НАЗАД, то устанавливаем значение бита в 1
      else                            data[1] = 0;  //  Если кнопка отпущена, то устанавливаем значение бита в 0
                                      
      if (digitalRead(button_left))   data[2] = 1;  //  Если нажата кнопка ВЛЕВО, то устанавливаем значение бита в 1
      else                            data[2] = 0;  //  Если кнопка отпущена, то устанавливаем значение бита в 0
                                      
      if (digitalRead(button_right))  data[3] = 1;  //  Если нажата кнопка ВПРАВО, то устанавливаем значение бита в 1
      else                            data[3] = 0;  //  Если кнопка отпущена, то устанавливаем значение бита в 0
                                      
      if (digitalRead(button_led) && LedFlag){      //  Если кнопка НАЖАТА, и ФЛАГ УСТАНОВЛЕН, то 
        LedFlag = false;                            //  сбрасываем флаг
        data[4] = 1;                                //  устанавиваем в 4 ячейке массива значение 1
      }
      if (!digitalRead(button_led)){                //  Если кнопка ОТПУЩЕНА, то
        LedFlag = true;                             //  устанавливаем флаг и 
        data[4] = 0;                                //  меняем значение в 4 ячейке массива на 0
      }
      mySerial.write(FRAME_HEADER);                 //  Передаём заголовок пакета в последовательный порт
      for (int i = 0; i < SIZE; i++){               //  Побайтово передаём данные
        mySerial.write(data[i]);                    //  в последовательный порт
      }
      mySerial.write(FRAME_FOOTER);                 //  Выводим байт конца пакета в последовательный порт
      data[4] = 0;                                  //  обнуляем значение в 4 ячейке массива
      delay(50);                                    //  Ждём 50 миллисекунд
    }

    Код программы для «Малыша»:

    //
    // ПОСЛЕ ЗАГРУЗКИ СКЕТЧА ОТКЛЮЧИ НА МАЛЫШЕ ВЫВОД TX, ИДУЩИЙ К ВЫВОДУ RX НА МОДУЛЕ!!!
    //
    #include "SoftwareSerial.h"                                                   //  Подключаем библиотеку SoftwareSerial для общения с модулем по программной шине UART
    SoftwareSerial mySerial(8, 9);                                                //  Создаём объект mySerial указывая выводы RX, TX
    #define FRAME_HEADER 0xAA                                                     //  Определяем заголовок пакета
    #define FRAME_FOOTER '\n'                                                     //  Определяем конец пакета
    #define radio_pin     13                                                      //  Определяем номер вывода S радио модуля
    
    uint8_t  pinShield_H2 = 4;                                                    //  Вывод, подключенный к драйверу, для задания направления вращения левым мотором
    uint8_t  pinShield_E2 = 5;                                                    //  Вывод ШИМ, подключенный к драйверу, для задания скорости левого мотора
    uint8_t  pinShield_E1 = 6;                                                    //  Вывод ШИМ, подключенный к драйверу, для задания скорости правого мотора
    uint8_t  pinShield_H1 = 7;                                                    //  Вывод, подключенный к драйверу, для задания направления вращения правым мотором
    uint8_t  pinLED_RED   = 11;                                                   //  Вывод с красным светодиодом
    uint8_t  pinLED_BLUE  = 10;                                                   //  Вывод с синим светодиодом
    uint16_t time_period  = 200;                                                  //  Частота мигания светодиодов (в миллисекундах)
    uint8_t  valSpeed     = 255;                                                  //  Максимальная скорость ШИМ (число от 0 до 255)
    bool     arrRoute[2]  = {1, 1};                                               //  Направление движения для каждого мотора ([0]- правый мотор, [1] - левый мотор)
    byte     b            = 0;                                                    //  Создаём переменную для хранения текущего байта последовательного порта
    byte     data[5];                                                             //  Создаём массив для хранения данных пакета
    uint16_t arrSpeed[2];                                                         //  Скорость для каждого мотора ([0]- правый мотор, [1] - левый мотор)
    uint32_t tmrLED;                                                              //  Время  последнего включения светодиодов
    uint32_t flgTime;                                                             //  Флаг для задания времени принятия пакетов от Bluetooth телефона
    uint8_t  flg;                                                                 //  Флаг кнопок
    uint32_t tmrWait;                                                             //  Время до начала сопряжения с новыми устройствами
    bool     flg_LED;                                                             //  Флаг включения светодиодов
    uint32_t FindTimer;                                                           //  Таймер получения команды
    
    void setup() {
        // ПОРТЫ
        mySerial.begin(9600);                                                     //  инициируем работу с програмным монитором последовательного порта на скорости 9600 БОД
        Serial.begin(9600);                                                       //  инициируем работу с аппаратным монитором последовательного порта на скорости 9600 БОД
        // МОТОРЫ
        pinMode(pinShield_H2, OUTPUT);                                            //  Конфигурируем вывод pinShield_H2 как выход (направление вращения левого мотора)
        pinMode(pinShield_E2, OUTPUT);                                            //  Конфигурируем вывод pinShield_E2 как выход (скорость вращения левого мотора, ШИМ)
        pinMode(pinShield_E1, OUTPUT);                                            //  Конфигурируем вывод pinShield_E1 как выход (скорость вращения правого мотора, ШИМ)
        pinMode(pinShield_H1, OUTPUT);                                            //  Конфигурируем вывод pinShield_H1 как выход (направление вращения правого мотора)
        // СВЕТОДИОДЫ
        pinMode(pinLED_RED,OUTPUT);                                               //  Конфигурируем вывод pinLED_RED как выход 
        pinMode(pinLED_BLUE,OUTPUT);                                              //  Конфигурируем вывод pinLED_BLUE как выход
        tmrLED = millis();                                                        //  Устанавливаем таймер светодиодов равным millis()
        flg_LED = 0;                                                              //  Сбрасываем флаг светодиодов
    }
    void loop() {
      if (mySerial.available() && mySerial.read() == 0xAA){                       //  Если получен какой-то пакет данных, начинающийся с 0xAA, то
        delay(10);                                                                //  Ждём 10 мс для получения всего пакета данных
        FindTimer = millis();                                                     //  обновляем таймер получения команды
        for (int i = 0; (b = mySerial.read()) != FRAME_FOOTER; i++) data[i] = b;  //  выполняем цикл получения и записи полученных битов в массив пока не получим бит конца пакета
        if (data[0] == 1){                                                        //  Проверяем, если первый элемент массива равен 0, то нажата кнопка "ВВЕРХ". После этого
          if(data[2] == 1){flgTime = millis(); flg = 1;} else                     //  проверяем, нажата ли кнопка ВЛЕВО и если да, то устанавливаем флаг 1 (Кнопка "стрелка вверх-влево")
          if(data[3] == 1){flgTime = millis(); flg = 3;} else                     //  проверяем, нажата ли кнопка ВПРАВО и если да, то устанавливаем флаг 3 (Кнопка "стрелка вверх-вправо")
                          {flgTime = millis(); flg = 2;}                          //  Иначе устанавливаем флаг 2 (Кнопка "стрелка вверх")
        } else
        if (data[1] == 1){                                                        //  Проверяем, если второй элемент массива равен 1, то нажата кнопка "НАЗАД". После этого
          if(data[2] == 1){flgTime = millis(); flg = 7;} else                     //  проверяем, нажата ли кнопка ВЛЕВО и если да, то устанавливаем флаг 7 (Кнопка "стрелка вниз-влево")
          if(data[3] == 1){flgTime = millis(); flg = 9;} else                     //  проверяем, нажата ли кнопка ВПРАВО и если да, то устанавливаем флаг 9 (Кнопка "стрелка вниз-вправо")
                          {flgTime = millis(); flg = 8;}                          //  Иначе устанавливаем флаг 8 (Кнопка "стрелка вниз")
        } else                                                                    //  Иначе проверяем
        if(data[2] == 1)  {flgTime = millis(); flg = 4;} else                     //  если нажата только кнопка "ВЛЕВО", то устанавливаем флаг 4
        if(data[3] == 1)  {flgTime = millis(); flg = 6;} else                     //  если нажата только кнопка "ВПРАВО", то устанавливаем флаг 5
                          {flgTime = millis(); flg = 5;}                          //  Иначе если ничего не нажато, то устанавливаем флаг 5
        if(data[4] == 1)  {flg_LED = !flg_LED;}                                   //  Если нажата кнопка включения светодиодов, то меняем значение флага на противоположное
        //  ========================================================================================================================================
        
        switch (flg){
        //          Направ.вращ.лев.мотора  Направ.вращ.прав.мотора    Скорость лев.мотора              Скорость прав.мотора
          case 1:       arrRoute[1] = 1;        arrRoute[0] = 1;     arrSpeed[1] = (valSpeed / 2);   arrSpeed[0] = valSpeed;       break; //  С-З
          case 2:       arrRoute[1] = 1;        arrRoute[0] = 1;     arrSpeed[1] = valSpeed;         arrSpeed[0] = valSpeed;       break; //  С
          case 3:       arrRoute[1] = 1;        arrRoute[0] = 1;     arrSpeed[1] = valSpeed;         arrSpeed[0] = (valSpeed / 2); break; //  С-В
          case 4:       arrRoute[1] = 1;        arrRoute[0] = 1;     arrSpeed[1] = 0;                arrSpeed[0] = valSpeed;       break; //  З
          case 5:       arrRoute[1] = 1;        arrRoute[0] = 1;     arrSpeed[1] = 0;                arrSpeed[0] = 0;              break; //  Стоп
          case 6:       arrRoute[1] = 1;        arrRoute[0] = 1;     arrSpeed[1] = valSpeed;         arrSpeed[0] = 0;              break; //  В
          case 7:       arrRoute[1] = 0;        arrRoute[0] = 0;     arrSpeed[1] = (valSpeed / 2);   arrSpeed[0] = valSpeed;       break; //  Ю-З
          case 8:       arrRoute[1] = 0;        arrRoute[0] = 0;     arrSpeed[1] = valSpeed;         arrSpeed[0] = valSpeed;       break; //  Ю
          case 9:       arrRoute[1] = 0;        arrRoute[0] = 0;     arrSpeed[1] = valSpeed;         arrSpeed[0] = (valSpeed / 2); break; //  Ю-В
        }}
      // ===========================================================================================================================================
      if (flg_LED) {                                                                 //  Если флаг установлен (была нажата кнопка включения фары)
        if (millis() - tmrLED > time_period) {                                       //  мигаем светодиодами с заданной частотой
          tmrLED = millis();                                                         //  сохраняем время
          digitalWrite(pinLED_RED, digitalRead(pinLED_BLUE));                        //  управляем питанием красного светодиода
          digitalWrite(pinLED_BLUE, !digitalRead(pinLED_BLUE));                      //  управляем питанием синего светодиода
          }
        } else {                                                                     //  если флаг сброшен, то
          digitalWrite(pinLED_RED, LOW);                                             //  гасим светодиоды
          digitalWrite(pinLED_BLUE, LOW);                                            //  
          }
      if(FindTimer + 1000 > millis()){                                               //  Если в течение секунды был установлен флаг движения, то
        digitalWrite(pinShield_H2, arrRoute[1]);                                     //  тогда задаем направление вращения правого мотора
        digitalWrite(pinShield_H1, arrRoute[0]);                                     //  и левого мотора
        analogWrite(pinShield_E2, arrSpeed[1]);                                      //  Задаём скорость вращения для правого мотора
        analogWrite(pinShield_E1, arrSpeed[0]);                                      //  и для левого мотора
      } else {                                                                       //  Если сигнала к движению нет или связь с пультом утеряна, то
        digitalWrite(pinShield_H2, 1);                                               //  тогда задаем направление вращения правого мотора
        digitalWrite(pinShield_H1, 1);                                               //  и левого мотора
        analogWrite(pinShield_E2, 0);                                                //  Задаём скорость вращения для правого мотора
        analogWrite(pinShield_E1, 0);                                                //  и для левого мотора
      }
    }

    В данном коде управление роботом осуществляется в три основных этапа:

    • получение данных с модуля;
    • установка флага движения, соответствующего одному из полученных значений;
    • подача питания на моторы и задание направления их вращения.

    Так же в коде присутствуют дополнительный блок включения/выключения светодиодов.

    Подключение:

    После подачи питания на робот и на пульт они автоматически соединятся и можно переходить непосредственно к управлению!

    Ссылки:

    Обсуждение