Введение:
В этом уроке мы дополним робота «Малыш» радиомодулем HC-12, а так же построим пульт дистанционного управления на основе этого же модуля. При подаче питания на робота и включении пульта управления устройства будут соединяться самостоятельно на установленной частоте.
Видео:
Редактируется...
Нам понадобится:
Робот "Малыш":
- Робот «Малыш» в сборе x1 шт.
- Trema-модуль Радиомодуль HC-12 x1 шт.
Пульт:
- Arduino / Piranha UNO x1 шт.
- Trema Shield x1 шт.
- Trema-модуль Радиомодуль HC-12 x1 шт.
- Trema-модуль кнопка х5 шт.
- Battery Shield x1 шт.
Для реализации проекта нам необходимо установить библиотеки:
- Библиотека SoftwareSerial входит в базовый набор 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 и установив аналогичные скорость и канал.
После того, как все подготовительные работы выполнены, можно переходить непосредственно к сборке проекта.
Схема подключения робота «Малыш»:
Соберём механическую и электрическую части, как это описано в проектах № 1-8 в книге "Набор "Малыш"". Далее, на верхнюю стенку установим модуль HC-12, который подключается к шине UART (в примере используется программная шина UART).
| Bluetooth | Motor 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 |
|---|---|
| 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 | D6 |
| V | 5V |
| G | GND |
| Радиомодуль HC-12 | Trema Shield |
|---|---|
| S | D13 |
| TX | D8 |
| RX | D9 |
| V | 5V |
| G | GND |
Код программы для пульта:
#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); // и для левого мотора
}
}
В данном коде управление роботом осуществляется в три основных этапа:
- получение данных с модуля;
- установка флага движения, соответствующего одному из полученных значений;
- подача питания на моторы и задание направления их вращения.
Так же в коде присутствуют дополнительный блок включения/выключения светодиодов.
Подключение:
После подачи питания на робот и на пульт они автоматически соединятся и можно переходить непосредственно к управлению!

Обсуждение