Top.Mail.Ru

Прикормочный кораблик. Зародыш кораблика для подкормки.

Как-то несколько лет назад, когда я только начинал программировать, вышел на меня человек и попросил написать прошивку для "Прикормочного кораблика". Потом он куда-то пропал и так и не ответил, заработало это, как он хотел, или нет. Я же сам полную схему не собирал, только проверял управление, работает нормально или нет. У меня была договоренность, что позже я имею право выложить эту разработку в общий доступ.

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

  • Основная функция "Прикормочного кораблика" - доставка прикормки в необходимую точку и сброс её по команде.
  • Бункеров два.
  • Регулировка оборотов ходового двигателя должна производится плавно.
  • Управление рулём с помощью сервопривода, плавное.
  • Управление бункерами с помощью сервопривода, состояние открыто/закрыто.

Основные модули кораблика:

  • Привод ходового винта, двигатель 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 весьма просто.

Подписаться
Уведомить о
guest

2 комментариев
Старые
Новые
Межтекстовые Отзывы
Посмотреть все комментарии
2
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x