Сборка прикормочного кораблика на радиоуправлении начиналась в рамках моего первого студенческого проекта на arduino. Я жил далеко от городской суеты, поэтому приходилось в основном использовать только те компоненты, которые были на руках. Задача была проста - создать кораблик, который сможет разносить корм для рыбы с полезной нагрузкой около двух килограмм. Что бы достичь своих целей я должен был решить список следующих задач:
Сделать корпус и определить габариты.
Сделать движители.
Выбрать двигатель.
Обеспечить радиоуправление.
Обеспечить автономность питания.
Сборка всего этого.
Следующим шагом разбил все это дело на электронную часть и на механическую. Механическая часть должна была решать передвижение кораблика по воде, а электронная должна была управлять двигателями и обеспечить связь блоком управления и пультом. По такой логике была нарисована простая структурная схема.

Механическая часть
Для изготовления корпуса скачал себе около десяти книг по судостроению и судомоделированию и начал их изучать. В итоге понял, что технически не смогу сделать те вещи, которые были написаны в этих книгах. Для изготовления шпангоутов нужен был ЧПУ фрезер или 3D принтер. Их, к сожалению, у меня не было. Руками все это делать из дерева - тоже нелегкая задача. В конце остановился на варианте с катамараном, так как в интернете нашел много аналогов. Решил все это сделать из подручных средств. Искал что-то обтекаемое, нетяжелое и нашел решение у себя в гараже. Да, это были канистры из-под масла и чемоданчик от набора инструментов. Поклеил их термоклеем, для надежности усилил деревом и фиксировал саморезом. Покрасил в разные цвета чтобы видно было издалека.

Далее были неудачные попытки расчета гидродинамических характеристик на FlowVision CFD. В итоге остановился на этапе 3D моделирования =)


Движитель выбрал основываясь на показателе КПД и самым лучшим в этом плане, конечно же, был подводный винт. Винт состоит из ступицы и радиально закреплённых к нему лопастей. Обычно используется 3-4 лопастные винты. Все размеры винтов были рассчитаны по книге "Юный моделист-кораблестроитель". Винт был изготовлен из стальной пластины, дейдвут из трубы пвх, а внутренность залита обычным солидолом.

С двигателем аналогичная ситуация. Бесколлекторных у меня не было, да и с Китая они шли очень долго. Нашел где-то и установил коллекторные 20-ваттные двигатели от печки ВАЗ МЭ 255.

Для движения механизма сброса корма использовал двухпроводной электропривод Starline SL-2 от замка машины.

Электронная часть
Перейдем к электронной части. Мозгом всего этого был выбран китайский Arduino Nano 3.0 на базе Atmega328P, так как его вычислительных мощностей хватало с запасов и прототипировать на нем тоже легко. Далее искал в интернете похожие проекты для разных задач и в youtube нашел интересный проект блока управления: часть кода и схем заимствовал оттуда и адаптировал под свои задачи. Радиомодуль использовал NRF24L01+PA+LNA. В открытой местности с ним удалось отправлять пакеты на 700м. Драйвером электродвигателя использовался L298N правда он сильно грелся из-за того, что не рассчитан на эти моторы(лучше брать с запасом), пришлось усилить их радиаторы радиаторами из старых телевизоров. Пульт управления работал на той же связке плюс 2 модуля KY-023.
Пульт управления





Основной блок









Последовательные диоды нужн�� были для понижения напряжения для полезной нагрузки R8, так как у меня не было подходящего стабилизатора в диапазоне меньше 14,8-5V, Q2 можно убрать если вам он не нужен. R8, R6, R9 это любая полезная нагрузка. Но нужно учитывать мощность транзисторов и напряжение стабилизатора.




Код для основного блока (rx.ino)
#include <SPI.h> // Подключаем библиотеку для работы с SPI #include <nRF24L01.h> // Подключаем библиотеку для работы с радиомодулем nRF24L01 #include <RF24.h> // Подключаем библиотеку для работы с радиомодулем nRF24L01 const uint64_t pipe = 0xE8E8F0F0E1LL; // Указываем адрес конфигурации радиомодуля для обмена данными RF24 radio(2, 9); // Инициализируем объект радиомодуля на пинах 2 и 9 int data[4]; // Создаем массив для хранения полученных данных int reserve=0; // Инициализируем переменную для хранения резервной кнопки, использовать при необходимости unsigned long motorOnTime; // Инициализируем переменную для хранения времени включения двигателя byte count = 0; // Инициализируем переменную счетчик для механизма сброса // В функции setup инициализируются различные пины как входы или выходы, устанавливается канал радио, скорость передачи данных и мощность. void setup() { delay(50); // Небольшая задержка перед началом работы radio.begin(); // начало работы с радиомодулем radio.setChannel(9); // установка радиоканала radio.setDataRate(RF24_250KBPS); // Установка минимальной скорости; radio.setPALevel(RF24_PA_HIGH); // Установка максимальной мощности; radio.openReadingPipe(1,pipe); // Открытие канала для чтения данных radio.startListening(); // Начало прослушивания канала pinMode(10, OUTPUT); // Установка пина 10 на вывод digitalWrite(10, LOW); // Установка пина 10 в низкий уровень pinMode(17, OUTPUT); // Установка пина 17 на вывод pinMode(18, OUTPUT); // Установка пина 18 на вывод pinMode(19, OUTPUT); // Установка пина 19 на вывод pinMode(16, OUTPUT); // Установка пина 16 на вывод digitalWrite(16, LOW); // Установка пина 16 в низкий уровень } void loop() { if ( radio.available() ){ // Если доступны новые данные от радиомодуля bool done = false; while (!done){ // Читаем данные, пока все данные не будут прочитаны done = radio.read(data, sizeof(data)); // Считываем данные в массив data размером sizeof(data) // Проверка полученных данных от левого джойстика (data[0]), управление левым двигателем if(data[0]>450 && data[0]<598){ // Если значение элемента массива в заданном диапазоне analogWrite(5, 0); // Отключение левого двигателя analogWrite(6, 0); // Отключение левого двигателя digitalWrite(17,LOW); // Установка пина 17 в низкий уровень digitalWrite(10,LOW); // Установка пина 10 в низкий уровень digitalWrite(18,LOW); // Установка пина 18 в низкий уровень digitalWrite(19,LOW); // Установка пина 19 в низкий уровень } if(data[0]>598) { analogWrite(5,255); // Включение левого двигателя analogWrite(6,255); // Включение левого двигателя digitalWrite(17,LOW); // Установка пина 17 в низкий уровень digitalWrite(10,HIGH); // Установка пина 10 в верхний уровень, направление движения вперед digitalWrite(18,HIGH); // Установка пина 10 в верхний уровень, направление движения вперед digitalWrite(19,LOW); // Установка пина 19 в низкий уровень } if(data[0]< 450) { analogWrite(5,255); // Включение левого двигателя analogWrite(6,255); // Включение левого двигателя digitalWrite(17,HIGH); // Установка пина 17 в верхний уровень, направление движения назад digitalWrite(10,LOW); // Установка пина 10 в низкий уровень digitalWrite(18,LOW); // Установка пина 18 в низкий уровень digitalWrite(19,HIGH); // Установка пина 19 в верхний уровень, направление движения назад } // Проверка полученных данных от правого джойстика (data[1]), управление правым двигателем if(data[1]>450 && data[1]<598){ // Если значение первого элемента массива в заданном диапазоне digitalWrite(3, LOW); // Отключение правого двигателя digitalWrite(8, LOW); // Отключение правого двигателя digitalWrite(4,LOW); // Установка пина 4 в низкий уровень digitalWrite(7,LOW); // Установка пина 7 в низкий уровень digitalWrite(14,LOW); // Установка пина 14 в низкий уровень digitalWrite(15,LOW); // Установка пина 15 в низкий уровень } if(data[1]>598) { digitalWrite(3, HIGH); // Включение правого двигателя digitalWrite(8,HIGH); // Включение правого двигателя digitalWrite(4,LOW); // Установка пина 4 в низкий уровень digitalWrite(7,HIGH); // Установка пина 7 в верхний уровень, направление движения вперед digitalWrite(14,HIGH); // Установка пина 14 в верхний уровень, направление движения вперед digitalWrite(15,LOW); // Установка пина 15 в низкий уровень } if(data[1]< 450) { digitalWrite(3,HIGH); // Включение правого двигателя digitalWrite(8,HIGH); // Включение правого двигателя digitalWrite(4,HIGH); // Установка пина 4 в верхний уровень, направление движения назад digitalWrite(7,LOW); // Установка пина 7 в низкий уровень digitalWrite(14,LOW); // Установка пина 14 в низкий уровень digitalWrite(15,HIGH); // Установка пина 15 в верхний уровень, направление движения назад } // Проверка полученных данных от кнопки правого джойстика (data[2]), управление механизмом сброса if(data[2] == 1){ // Если равно 0 if(count < 1){ // Счетчик меньше единицы digitalWrite(16, HIGH); // Замыкаем реле запускаем механизм сброса delay(500); Ждем 500 миллисекунд digitalWrite(16, LOW); // Отключаем count++; // Увеличиваем счетчик на единицу } } else{ if(count >= 1){ // Если счетчик равно или больше единицы digitalWrite(16, HIGH); // Замыкаем реле запускаем механизм сброса delay(500); Ждем 500 миллисекунд digitalWrite(16, LOW); // Отключаем count--; // Уменьшаем счетчик на единицу } } reserve = !data[3]; // Резервная кнопка правого джойстика для свободного пользования } } }
Код для пульта управления (tx.ino)
#include <SPI.h> // Подключаем библиотеку для работы с SPI #include <nRF24L01.h> / Подключаем библиотеку для работы с радиомодулем nRF24L01 #include <RF24.h> / Подключаем библиотеку для работы с радиомодулем nRF24L01 const uint64_t pipe = 0xE8E8F0F0E1LL; // Указываем адрес конфигурации радиомодуля для обмена данными RF24 radio(9,10);// vмодуль на пинах 9 и 10 // Инициализируем объект радиомодуля на пинах 2 и 9 byte pinLeftJoystickX = 14; // Указываем номер пина, на который подключен левый джойстик X byte pinRightJoystickX = 15; // Указываем номер пина, на который подключен правый джойстик X byte pinLeftJoystickSwitch = 4; // Указываем номер пина, на который подключена кнопка левого джойстика byte pinRightJoystickSwitch = 3; // Указываем номер пина, на который подключена кнопка правого джойстика boolean leftJoystickSwitch = 0; // Объявляем переменную для хранения состояния кнопки левого джойстика boolean rightJoystickSwitch = 0; // Объявляем переменную для хранения состояния кнопки правого джойстика boolean leftJoystickSwitchState; // Объявляем переменную для хранения текущего состояния кнопки левого джойстика boolean rightJoystickSwitchState; // Объявляем переменную для хранения текущего состояния кнопки правого джойстика int transmitData[4]; // Массив, хранящий передаваемые данные int latestData[4]; // Массив, хранящий последние переданные данные boolean flag = 0; // Флаг, указывающий на необходимость отправки данных по радио boolean leftJoystickSwitchFlag = 0; // Флаг, указывающий на изменение состояния кнопки левого джойстика boolean rightJoystickSwitchFlag = 0; // Флаг, указывающий на изменение состояния кнопки правого джойстика unsigned long lastPressLeftJoystickSwitch; // Переменная, хранящая время последнего нажатия кнопки левого джойстика unsigned long lastPressRightJoystickSwitch; // Переменная, хранящая время последнего нажатия кнопки правого джойстика void setup() { pinMode(pinLeftJoystickSwitch, INPUT_PULLUP); // Устанавливаем пин, на который подключена кнопка левого джойстика, в режим входа с подтяжкой к питанию pinMode(pinRightJoystickSwitch, INPUT_PULLUP); // Устанавливаем пин, на который подключена кнопка правого джойстика, в режим входа с подтяжкой к питанию radio.begin(); // Активировать модуль radio.openWritingPipe(pipe); // Мы - труба 0, открываем канал для передачи данных radio.setChannel(9); // Выбираем канал (в котором нет шумов!) radio.setPALevel (RF24_PA_MAX); // Уровень мощности передатчика. На выбор RF24_PA_MIN, RF24_PA_LOW, RF24_PA_HIGH, RF24_PA_MAX radio.setDataRate (RF24_250KBPS); // Скорость обмена. На выбор RF24_2MBPS, RF24_1MBPS, RF24_250KBPS // Должна быть одинакова на приёмнике и передатчике! // При самой низкой скорости имеем самую высокую чувствительность и дальность!! radio.powerUp(); // Начать работу radio.stopListening(); // Не слушаем радиоэфир, мы передатчик } void loop() { leftJoystickSwitchState = !digitalRead(pinLeftJoystickSwitch); // Считать состояние переключателя левого джойстика rightJoystickSwitchState = !digitalRead(pinRightJoystickSwitch); // Считать состояние переключателя правого джойстика if(leftJoystickSwitchState == 1 && leftJoystickSwitchFlag == 0 && millis() - lastPressLeftJoystickSwitch > 50){ // Если нажат переключатель левого джойстика и флаг еще не поднят, и прошло более 50 миллисекунд после последнего нажатия leftJoystickSwitchFlag = 1; // Поднять флаг переключения левого джойстика leftJoystickSwitch = !leftJoystickSwitch; // Поменять состояние переключателя левого джойстика lastPressLeftJoystickSwitch = millis(); // Запомнить время последнего нажатия переключателя левого джойстика } if(leftJoystickSwitchState == 0 && leftJoystickSwitchFlag == 1){ // Если переключатель левого джойстика отпущен и флаг поднят leftJoystickSwitchFlag = 0; // Опустить флаг переключения левого джойстика } if(rightJoystickSwitchState == 1 && rightJoystickSwitchFlag == 0 && millis() - lastPressRightJoystickSwitch > 50){ // Если нажат переключатель правого джойстика и флаг еще не поднят, и прошло более 50 миллисекунд после последнего нажатия rightJoystickSwitchFlag = 1; // Поднять флаг переключения правого джойстика rightJoystickSwitch = !rightJoystickSwitch; // Поменять состояние переключателя правого джойстика lastPressRightJoystickSwitch = millis(); // Запомнить время последнего нажатия переключателя правого джойстика } if(rightJoystickSwitchState == 0 && rightJoystickSwitchFlag == 1){ // Если переключатель правого джойстика отпущен и флаг поднят rightJoystickSwitchFlag = 0; // Опустить флаг переключения правого джойстика } transmitData[0] = analogRead(pinLeftJoystickX); // Записать данные с левого джойстика в массив для передачи transmitData[1] = analogRead(pinRightJoystickX); // Записать данные с правого джойстика в массив для передачи transmitData[2] = leftJoystickSwitch; // Записать состояние переключателя левого джойстика в массив для передачи transmitData[3] = rightJoystickSwitch; // Записать состояние переключателя правого джойстика for (int i = 0; i < 4; i++) { // В цикле от 0 до числа каналов if (transmitData[i] != latestData[i]) { // Если есть изменения в transmit_data flag = 1; // Поднять флаг отправки по радио latestData[i] = transmitData[i]; // Запомнить последнее изменение } } if (flag == 1) { radio.powerUp(); // Включить передатчик radio.write(&transmitData, sizeof(transmitData)); // Отправить по радио flag = 0; // Опустить флаг radio.powerDown(); // Выключить передатчик } }
В заключении тесты показали, что кораблик развивает скорость 1м/c с нагрузкой в тихую погоду. Радиоуправление работает до 700м в открытой местности. Сильно перегреваются драйвера двигателей, их нужно заменить на более мощные. Периодически протекают деидвуды при заднем ходе. Лучше купить готовые решения. Не хватает оборотов у двигателей, нужны более оборотистые. Плюс нужен рефакторинг всего кода.
