Как-то несколько лет назад, когда я только начинал программировать, вышел на меня человек и попросил написать прошивку для "Прикормочного кораблика". Потом он куда-то пропал и так и не ответил, заработало это, как он хотел, или нет. Я же сам полную схему не собирал, только проверял управление, работает нормально или нет. У меня была договоренность, что позже я имею право выложить эту разработку в общий доступ.
Думаю эта безделушка должна увидеть свет. По инету их много, но, я считаю, что мой вариант имеет право на жизнь. Суть ТЗ вкратце такая:
- Основная функция "Прикормочного кораблика" - доставка прикормки в необходимую точку и сброс её по команде.
- Бункеров два.
- Регулировка оборотов ходового двигателя должна производится плавно.
- Управление рулём с помощью сервопривода, плавное.
- Управление бункерами с помощью сервопривода, состояние открыто/закрыто.
Основные модули кораблика:
- Привод ходового винта, двигатель RS-540H. Используется такой драйвер.
- Привод руля MG996R.
- Привод контейнеров MG996R.
- Приёмопередатчик nRF24L01 с усилителем и антенной.
- Ходовые огни с управлением от пульта.
- Прожектор с управлением от пульта.
- Блок контроллера питания. Используются 4 литиевых аккумулятора.
Основные модули пульта:
- Приёмопередатчик nRF24L01 с усилителем и антенной.
- Джойстик управления "вперёд/назад", "вправо/влево", аналоговый.
- Индикация коннекта с кораблём.
- Индикация питания.
- Две кнопки открытия бункеров.
- Управление прожектором.
- Управление ходовыми огнями.
- Корпус пульта.
- Блок контроллера питания и зарядки. Один литий ионный аккумулятор.
Вся схема собирается на готовых модулях с Али. Один из вариантов соединений показан в файлах Pult.pdf и Receiver.pdf. Ничего особенного там нет, смысла описывать не вижу. Сами файлы также находятся в архиве, прикреплённом в конце статьи.
Заказчиком был выбран вариант: один двигатель, рулевая машинка на сервоприводе, сброс производится сервоприводами. Схема легко переделывается на два двигателя и на два двигателя без рулевого управления.
Программная часть построена на стандартных библиотеках. Многие буфер передачи делают на массиве, но мне такое решение не понравилось, так как есть переменные, занимающие один бит, и отводить под них целую ячейку нецелесообразно. Можно одну переменную обрабатывать с помощью маски и выделять необходимые биты, но мне это тоже не понравилось. Исходя из этого я создал структуру, в которой и находятся все переменные:
struct { int16_t Turn; // Управление поворотом int16_t Speed; // Управление ходом unsigned Bunker_1 :1; // Бункер 1 unsigned Bunker_2 :1; // Бункер 2 unsigned RunFires :1; // Ходовые огни unsigned SearchLight :1; // Прожектор unsigned Joystick :1; // Инверсия джойстиков unsigned Free :3; // Пока не занятые переменные } SendBuff = {0,0,0};
Переменные Turn
и Speed
целые со знаком. Сделано это для того, чтобы передавать скорость и направление в одну сторону как положительное число, в другую как отрицательное.
Пульт.
При включении пульта значения с джойстиков считываются и запоминаются как константы среднего положения:
CalibrTurn_1 = analogRead(JOY_1_X); CalibrSpeed_1 = analogRead(JOY_1_Y); CalibrTurn_2 = analogRead(JOY_2_X); CalibrSpeed_2 = analogRead(JOY_2_Y);
При занесении значения положения джойстиков в структуру из текущего значения вычитается значение нулевого положения:
SendBuff.Turn = analogRead(JOY_1_X) - CalibrTurn_1; // Левый поворот, правый скорость SendBuff.Speed = analogRead(JOY_2_Y) — CalibrSpeed_2;
Сама прошивка передатчика проста. При включении в setup()
производится инициализация всего и вся. Создание объектов перед setup()
. Для кнопок и отсчёта времени используются библиотеки Гайвера, для nRF24L01 стандартная библиотека.
// Описываем кнопки GButton Key_Bunker_1(BUNKER_1); GButton Key_Bunker_2(BUNKER_2); GButton Key_RunFires(RUN_FIRES); GButton Key_SearchLight(SEARCH_LIGHT); GButton Key_SwapJoy(JOY_SWAP); // Таймер посылок на передатчик GTimer_ms TimerSend; // Подключение радиомодуля RF24 radio(10, 9);
Инициализация джойстиков:
// Инициализируем входы джойстиков pinMode(JOY_1_X, INPUT); pinMode(JOY_1_Y, INPUT); pinMode(JOY_2_X, INPUT); pinMode(JOY_2_Y, INPUT); // Считываем средние положения джойстиков и заносим в калибровочные константы CalibrTurn_1 = analogRead(JOY_1_X); CalibrSpeed_1 = analogRead(JOY_1_Y); CalibrTurn_2 = analogRead(JOY_2_X); CalibrSpeed_2 = analogRead(JOY_2_Y);
Инициализация кнопок, таймера и модуля передатчика:
// Инициализация входов кнопок Key_Bunker_1.setTickMode(AUTO); Key_Bunker_2.setTickMode(AUTO); Key_RunFires.setTickMode(AUTO); Key_SearchLight.setTickMode(AUTO); Key_SwapJoy.setTickMode(AUTO); // Инициализация индикации соединения pinMode(LED_CONNECT, OUTPUT); digitalWrite(LED_CONNECT, LOW); // Инициализация таймера TimerSend.setInterval(50); // Считываем установки с EEPROM EEPROM.get(0, SendBuff); // Инициализация радиомодуля radio.begin(); radio.setChannel(200); radio.setDataRate (RF24_2MBPS); radio.setPALevel (RF24_PA_MIN); radio.openWritingPipe(0xAABBCCDD11LL); radio.setAutoAck(true); // Включаем радомодуль radio.powerUp();
В loop()
производится сканирование всех датчиков, занесение их значений в структуру и раз в 50 миллисекунд передача в эфир. Если приёмник ответил, зажигается светодиод установления нормальной связи.
void loop() { if(Key_Bunker_1.isClick()) // Была нажата кнопка "Бункер 1" { if (SendBuff.Bunker_1) SendBuff.Bunker_1 = false; else SendBuff.Bunker_1 = true; } // И так далее для всех переменных // Когда сработал таймер, мы считываем положение джойстиков, забиваем их в переменные и передаём if (TimerSend.isReady()) { // Считываем и запоминаем положение джойстиков if (SendBuff.Joystick) { SendBuff.Turn = analogRead(JOY_1_X) - CalibrTurn_1; // Левый поворот, правый скорость SendBuff.Speed = analogRead(JOY_2_Y) - CalibrSpeed_2; } else { SendBuff.Turn = analogRead(JOY_2_X) - CalibrTurn_2; // Левый скорость, правый поворот SendBuff.Speed = analogRead(JOY_1_Y) - CalibrSpeed_1; } if (radio.write(&SendBuff, sizeof(SendBuff))) digitalWrite(LED_CONNECT, HIGH); else digitalWrite(LED_CONNECT, LOW); } }
Приёмник.
В программе приёмника тоже ничего особенного - ходовой двигатель управляется ШИМ-сигналом. Создаём объекты таймеров, приёмника и серводвигателей:
// Таймеры GTimer_ms TimerLostSignal; GTimer_ms TimerServo; // Подключение радиомодуля RF24 radio(10, 9); // Подключение серводвигателей Servo ServoBunker_1; Servo ServoBunker_2; Servo ServoTurn;
В setup()
производится инициализация всех переменных и исполнительных устройств, серводвигатели:
// Инициализация серводвигателей и ходового двигателя ServoBunker_1.attach(SERVO_BUNKER_1); // Управление 1-м бункером ServoBunker_2.attach(SERVO_BUNKER_2); // Управление 2-м бункером ServoTurn.attach(SERVO_TURN); // Управление рулём ServoBunker_1.write(AngleBunker_1); // Начальная установка 1-го бункера ServoBunker_2.write(AngleBunker_2); // Начальная установка 2-го бункера ServoTurn.write(AngleTurn); // Начальная установка руля
Серводвигатель руля ставится на 90 градусов при описании переменных. Так как такое положение может оказаться неправильным для движения прямо, нужно его калибровать. В данной прошивке это не реализовано. Инициализация исполнительных устройств и таймеров:
// Инициализируем выходы управления устройствами pinMode(PIN_RUN_FIRES, OUTPUT); // Пин ходовых огней pinMode(PIN_SEARCH_LIGHT, OUTPUT); // Пин прожектора pinMode(SPEED_KEY_1, OUTPUT); // Выходы управления направлением pinMode(SPEED_KEY_2, OUTPUT); // Инициализация выходов digitalWrite(PIN_RUN_FIRES, LOW); digitalWrite(PIN_SEARCH_LIGHT, LOW); analogWrite(SPEED_ENABLE, 0); digitalWrite(SPEED_KEY_1, HIGH); digitalWrite(SPEED_KEY_2, LOW); // Инициализация таймера TimerLostSignal.setInterval(300); TimerServo.setInterval(3);
Инициализация радиомодуля:
radio.begin(); radio.setChannel(200); radio.setDataRate(RF24_2MBPS); radio.setPALevel(RF24_PA_MIN); radio.openReadingPipe (1, 0xAABBCCDD11LL); radio.setAutoAck(true); radio.startListening();
В loop()
производится приём массива и проверка каждой переменной, если переменная изменилась, в действие приводится исполнительный механизм. В случае потери связи с пультом, катер останавливается.
Сама функция loop()
разделёна на 2 части - модуль сортировки и модуль управления. В первом из них, в случае приёма данных, сбрасывается флаг ошибки приёма, согласно считанным заполняются переменные, хранящие положения исполнительных устройств, и производится управление ходовыми огнями и прожектором:
// ----------------------------------------------------------------- // Модуль сортировки принятых данных // ----------------------------------------------------------------- if (radio.available()) { // При успешном приёме сбрасываем флаг потери сигнала TimerLostSignal.reset(); // и считываем данные radio.read(&ReceiveBuff, sizeof(ReceiveBuff)); // Проверяем изменение положения руля ---------------------------- if(ReceiveBuff.Turn >= 0) AngleTurn = map(ReceiveBuff.Turn, 0, 530, 90, 180); else AngleTurn = map(-ReceiveBuff.Turn, 0, 530, 90, 0); // Проверяем состояние 1-го бункера ------------------------------- if(ReceiveBuff.Bunker_1) AngleBunker_1 = 180; else AngleBunker_1 = 0; // Проверяем состояние 2-го бункера ------------------------------- if(ReceiveBuff.Bunker_2) AngleBunker_2 = 180; else AngleBunker_2 = 0; // Управление ходовым двигателем --------------------------------- if(ReceiveBuff.Speed >= 0) { SpeedMain = map(ReceiveBuff.Speed, 0, 530, 0, 255); Direction = true; } else { SpeedMain = map(-ReceiveBuff.Speed, 0, 530, 0, 255); Direction = false; } // Включаем/выключаем ходовые огни ------------------------------- digitalWrite(PIN_RUN_FIRES, ReceiveBuff.RunFires); // Включаем/выключаем прожектор ---------------------------------- digitalWrite(PIN_SEARCH_LIGHT, ReceiveBuff.SearchLight); }
В модуле управления, согласно переменным, устройства устанавливаются в нужное положение, поворачивается руль - но не сразу на нужную величину, а постепенно - временная переменная увеличивается и передаётся серводвигателю, делается это до тех пор, пока временная переменная не сравняется с принятой. То же самое и с серводвигателями для бункеров:
// Крутим руль if(AngleTurnLast != AngleTurn) { if(AngleTurnLast < AngleTurn) AngleTurnLast++; if(AngleTurnLast > AngleTurn) AngleTurnLast--; ServoTurn.write(AngleTurnLast); } // Окрываем/закрываем 1-й бункер if(AngleBunker_1_Last != AngleBunker_1) { if(AngleBunker_1_Last < AngleBunker_1) AngleBunker_1_Last++; if(AngleBunker_1_Last > AngleBunker_1) AngleBunker_1_Last--; ServoBunker_1.write(AngleBunker_1_Last); } // Открываем/закрываем второй бункер if(AngleBunker_2_Last != AngleBunker_2) { if(AngleBunker_2_Last < AngleBunker_2) AngleBunker_2_Last++; if(AngleBunker_2_Last > AngleBunker_2) AngleBunker_2_Last--; ServoBunker_2.write(AngleBunker_2_Last); }
Далее управляем ходовым двигателем, сброс оборотов и ускорение также производится плавно:
// Управляем двигателем if(SpeedMainLast != SpeedMain) { if(Direction == DirectionLast) { if(SpeedMainLast < SpeedMain) SpeedMainLast++; if(SpeedMainLast > SpeedMain) SpeedMainLast--; } else { if(SpeedMainLast > 0) SpeedMainLast--; else { DirectionLast = Direction; if(DirectionLast) { digitalWrite(SPEED_KEY_1, LOW); digitalWrite(SPEED_KEY_2, HIGH); } else { digitalWrite(SPEED_KEY_1, HIGH); digitalWrite(SPEED_KEY_2, LOW); } } } analogWrite(SPEED_ENABLE, SpeedMainLast); }
И последняя строка - проверяется таймер потери сигнала, если он сработал, скорость выставляется в 0:
if(TimerLostSignal.isReady()) { SpeedMain = 0; }
Этот проект не получил продолжения и модернизации, можно использовать его как стартовый набор для воплощения идеи. Сейчас ищу готовый корпус - хочу попробовать воплотить, только уже на STM32.
Все исходники находятся в архиве - BaitBoat. Проект сделан под PlatformIO, переделать под ArduinoIDE весьма просто.
Проект получил продолжение.
Приступил к разработке.
Будут предусмотрены дополнительные функции.
GPS на корабле и видеокамера.
Создал тему на форуме
https://microtechnics.ru/community/vashi-razrabotki/prikormochnyj-korablik/