Как-то несколько лет назад, когда я только начинал программировать, вышел на меня человек и попросил написать прошивку для "Прикормочного кораблика". Потом он куда-то пропал и так и не ответил, заработало это, как он хотел, или нет. Я же сам полную схему не собирал, только проверял управление, работает нормально или нет. У меня была договоренность, что позже я имею право выложить эту разработку в общий доступ.
Думаю эта безделушка должна увидеть свет. По инету их много, но, я считаю, что мой вариант имеет право на жизнь. Суть ТЗ вкратце такая:
- Основная функция "Прикормочного кораблика" - доставка прикормки в необходимую точку и сброс её по команде.
- Бункеров два.
- Регулировка оборотов ходового двигателя должна производится плавно.
- Управление рулём с помощью сервопривода, плавное.
- Управление бункерами с помощью сервопривода, состояние открыто/закрыто.
Основные модули кораблика:
- Привод ходового винта, двигатель 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/