Идея
Недавно купил длинный HDMI-кабель для просмотра киношек на телевизоре с комфортной кровати, а не сидя за ПК в углу.
Возникла проблема: часто вставать и ходить до ПК, чтобы поставить видео на паузу или изменить настройки плеера (качество, звук, озвучка). Захотелось чего-то компактного и простого, наподобие «Плей‑Пауза» пульта. Был вариант просто купить дешёвый Bluetooth‑комплект мышь‑клавиатура или только беспроводную мышь, Но это показалось мне неинтересным и простым.
Было принято решение разработать специфическое устройство для этих задач.
Проектирование
Во-первых, определим, какие функции и, как следствие, устройства ввода необходимы.
-
В качестве устройства, позволяющего не только выполнять разовый «клик» по кнопке паузы, но и имеющего возможность передвигать курсор мыши, был выбран стик KY-023, наподобие тех, что применяются в контроллерах DualShock для PS4.
KY-023 имеет:
1) 2 аналоговых выхода: Ось X, Ось Y;
2) 1 дискретный выход: Кнопка самого «стика».Т. к. проект в процессе разработки стал расширяться и усложняться, приходя к полноценному и большему функционалу, чем у компьютерной мыши, было решено добавить второй «стик».
Таким образом получили конструкцию со следующим функционалом: правый «стик» должен отвечать за перемещение курсора (подобно как правый стик джойстика от «плойки» управляет обзором персонажа в играх); Левый стик должен будет управлять громкостью плеера и перемоткой видео (пока не реализован, в процессе разработки функций); внутренние кнопки «стиков» копируют ЛКМ и ПКМ.
-
Т. к. только начинаю заниматься программированием, было решено использовать как контроллер плату Arduino. Почитав про особенности «камней», применяемых в «разношерстных» платформах, я выбрал это:
Arduino pro mini на базе «камня» AtMega 168 (5V, 16MHz) в роли передатчика;
Arduino pro micro на базе «камня» AtMega 23u4 в роли приемника и устройства, имеющего свойство двустороннего общения с ПК, определяется как HID-устройство (клавиатура, мышь).
-
Необходимо было устройство приема‑передачи информации между платами Arduino. Выбрал радиомодули NRF24L01, работающие в диапазоне частот 2.4–2.5 ГГц. Также в связи с особенностями питания «камня» NRF потребовался адаптер со стабилизатором напряжения, способный использовать внешнее питание от 4.8V до 12V и подавать на плату NRF 3.3V.
Немного характеристик:
Напряжение питания: 1,9В – 3,6В;
Интерфейс обмена данными: SPI;
Частота приёма и передачи: 2,4 ГГц;
Количество каналов: 128 с шагом 1МГц;
Тип модуляции: GFSK;Скорость передачи данных: 250kbps, 1Mbps и 2Mbps;
Чувствительность приёмника: -82 dBm;
Расстояние приёма/передачи данных: 100м — прямая видимость; 30м — помещение;
Коэффициент усиления антенны: 2dBm;
Диапазон рабочей температуры: -40оС…+85оС;
Организация сети на одном канале: 7 модулей (1 приёмник и 6 передатчиков).
Электросхема
На схеме не указан адаптер, но распиновка NRF и адаптера идентичные, и все необходимые сигналы прописаны в литографии на маске плат.
Программа
-
Перво-наперво необходимо было научиться считывать и обрабатывать сигналы со стиков и управлять курсором напрямую с Arduino Pro Micro.
#include <Mouse.h> const int X1_Pin = A0; const int Y1_Pin = A1; const int X2_Pin = A2; const int Y2_Pin = A3; const int SW1_Pin = 3; const int SW2_Pin = 2; int SW1_Stage; int SW2_Stage; int SW1; int SW2; const int Sp = 5; void setup() { Serial.begin(9600); Mouse.begin(); pinMode(SW1_Pin, INPUT_PULLUP); pinMode(SW2_Pin, INPUT_PULLUP); pinMode(8, OUTPUT); pinMode(9, OUTPUT); } void loop() { //Доп пины питания digitalWrite(8, HIGH); digitalWrite(9, HIGH); //Чтение портов int x1 = analogRead(X1_Pin); int y1 = analogRead(Y1_Pin); int x2 = analogRead(X2_Pin); int y2 = analogRead(Y2_Pin); SW1_Stage = digitalRead(SW1_Pin); SW2_Stage = digitalRead(SW2_Pin); int x1pos, y1pos; int x2pos, y2pos; //Фильтр XY1 if (x1 > 450 and x1 < 550) x1pos = 0; if (x1 >= 550) x1pos = map(x1, 550, 1023, 0, Sp); if (x1 <= 450) x1pos = map(x1, 450, 0, 0, -Sp); if (y1 > 450 and y1 < 550) y1pos = 0; if (y1 >= 550) y1pos = map(y1, 550, 1023, 0, Sp); if (y1 <= 450) y1pos = map(y1, 450, 0, 0, -Sp); //Обработка кнопки ЛКМ if (SW1_Stage == LOW) SW1 = 1; else SW1 = 0; //Фильтр XY2 if (x2 > 450 and x2 < 550) x2pos = 0; if (x2 >= 550) x2pos = map(x2, 550, 1023, 0, Sp); if (x2 <= 450) x2pos = map(x2, 450, 0, 0, -Sp); if (y2 > 450 and y2 < 550) y2pos = 0; if (y2 >= 550) y2pos = map(y2, 550, 1023, 0, Sp); if (y2 <= 450) y2pos = map(y2, 450, 0, 0, -Sp); //Обработка кнопки ПКМ if (SW2_Stage == LOW) SW2 = 1; else SW2 = 0; //Управление курсором Mouse.move(x1pos, y1pos); if (SW1) { Mouse.press(MOUSE_LEFT); } else { Mouse.release(MOUSE_LEFT); } if (SW2) { Mouse.press(MOUSE_RIGHT); } else { Mouse.release(MOUSE_RIGHT); } /*/Отладка Serial.print(x1pos); Serial.print(":"); Serial.print(y1pos); Serial.print(":"); Serial.println(SW1_Stage); Serial.print(":"); Serial.print(x2pos); Serial.print(":"); Serial.print(y2pos); Serial.print(":"); Serial.println(SW2_Stage); /*/ delay(10); }
-
Учимся «дружить» NRF‑ки и моргать лампочкой по нажатию кнопки.
TX:
#include <SPI.h> #include "nRF24L01.h" #include "RF24.h" RF24 radio(9, 10); byte address[][6] = {"1Node", "2Node", "3Node", "4Node", "5Node", "6Node"}; byte button = 3; byte transmit_data[1]; byte latest_data[1]; boolean flag; void setup() { Serial.begin(9600); pinMode(button, INPUT_PULLUP); radio.begin(); radio.setAutoAck(1); radio.setRetries(0, 15); radio.enableAckPayload(); radio.setPayloadSize(32); radio.openWritingPipe(address[0]); radio.setChannel(0x60); radio.setPALevel (RF24_PA_MAX); radio.setDataRate (RF24_250KBPS); radio.powerUp(); radio.stopListening(); } void loop() { transmit_data[0] = !digitalRead(button); for (int i = 0; i < 3; i++) { if (transmit_data[i] != latest_data[i]) { flag = 1; latest_data[i] = transmit_data[i]; } } if (flag == 1) { radio.powerUp(); radio.write(&transmit_data, sizeof(transmit_data)); flag = 0; radio.powerDown(); } }
RX:
#include <SPI.h> #include "nRF24L01.h" #include "RF24.h" #include <Servo.h> RF24 radio(9, 10); byte recieved_data[1]; byte L = 13; byte address[][6] = {"1Node", "2Node", "3Node", "4Node", "5Node", "6Node"}; void setup() { Serial.begin(9600); pinMode(L, OUTPUT); radio.begin(); radio.setAutoAck(1); radio.setRetries(0, 15); radio.enableAckPayload(); radio.setPayloadSize(32); radio.openReadingPipe(1, address[0]); radio.setChannel(0x60); radio.setPALevel (RF24_PA_MAX); radio.setDataRate (RF24_250KBPS); radio.powerUp(); radio.startListening(); } void loop() { byte pipeNo; while ( radio.available(&pipeNo)) { radio.read(&recieved_data, sizeof(recieved_data)); digitalWrite(L, recieved_data[0]); } }
-
А теперь самое интересное! Объединить эти два кода. Не обошлось и без плясок с бубном, и убегающим в «самоволку» курсором.
TX:
#include <SPI.h> #include "nRF24L01.h" #include "RF24.h" RF24 radio(9, 10); //Создать модуль на пинах 9 и 10 byte address[][6] = {"1Node", "2Node", "3Node", "4Node", "5Node", "6Node"}; //возможные номера труб const int XL_Pin = A0; //Аналоговый вход левого стика ось X const int YL_Pin = A1; //Аналоговый вход левого стика ось Y const int XR_Pin = A2; //Аналоговый вход правого стика ось X const int YR_Pin = A3; //Аналоговый вход правогоо стика ось Y byte LBM_Pin = 2; //Цифровой вход ЛКМ byte RBM_Pin = 3; //Цифровой вход ПКМ byte transmit_data[6]; //Массив, хранящий передаваемые данные byte latest_data[6]; //Массив, хранящий последние переданные данные boolean flag; //Флаг отправки данных void setup() { Serial.begin(9600); //Открываем порт для связи с ПК pinMode(XL_Pin, INPUT); //Настройка порта левого стика ось X pinMode(YL_Pin, INPUT); //Настройка порта левого стика ось Y pinMode(XR_Pin, INPUT); //Настройка порта правого стика ось X pinMode(YR_Pin, INPUT); //Настройка порта правого стика ось Y pinMode(LBM_Pin, INPUT_PULLUP); //Настройка порта ЛКМ pinMode(RBM_Pin, INPUT_PULLUP); //Настройка порта ЛКМ pinMode(7, OUTPUT); //Доп питание pinMode(8, OUTPUT); //Доп питание digitalWrite(7, HIGH); //Доп пин питания левого стика digitalWrite(8, HIGH); //Доп пин питания правого стика radio.begin(); //Активировать модуль radio.setAutoAck(1); //Режим подтверждения приёма, 1 вкл 0 выкл radio.setRetries(0, 15); //Время между попыткой достучаться, число попыток radio.enableAckPayload(); //Разрешить отсылку данных в ответ на входящий сигнал radio.setPayloadSize(32); //Размер пакета, в байтах radio.openWritingPipe(address[0]); //Труба 0, открыть канал для передачи данных radio.setChannel(0x70); //Выбираем канал (в котором нет шумов!) 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() { //Чтение портов transmit_data[0] = map(analogRead(XL_Pin), 0, 1023, 0, 255); //Записать значение XL на 1 место в массиве transmit_data[1] = map(analogRead(YL_Pin), 0, 1023, 0, 255); //Записать значение YL на 2 место в массиве transmit_data[2] = map(analogRead(XR_Pin), 0, 1023, 0, 255); //Записать значение XR на 3 место в массиве transmit_data[3] = map(analogRead(YR_Pin), 0, 1023, 0, 255); //Записать значение YR на 4 место в массиве transmit_data[4] = !digitalRead(LBM_Pin); //Записать сигнал ЛКМ на 5 место в массиве transmit_data[5] = !digitalRead(RBM_Pin); //Записать сигнал ПКМ на 5 место в массиве radio.powerUp(); // включить передатчик radio.write(&transmit_data, sizeof(transmit_data)); // Отправить по радио for (int i = 0; i < 6; i++) { // В цикле от 0 до числа каналов if (transmit_data[i] != latest_data[i]) { // Если есть изменения в transmit_data flag = 1; // Поднять флаг отправки по радио latest_data[i] = transmit_data[i]; // Запомнить последнее изменение } } if (flag == 1) { radio.powerUp(); // Включить передатчик radio.write(&transmit_data, sizeof(transmit_data)); // Отправить по радио flag = 0; // Опустить флаг radio.powerDown(); // Выключить передатчик } /*/Отладка, проверка сигнала на A0,A1,A2,A3 Serial.print(analogRead(XL_Pin)); Serial.print(":"); Serial.print(analogRead(YL_Pin)); Serial.print(":"); Serial.println(!digitalRead(2)); Serial.print("n"); Serial.print(analogRead(XR_Pin)); Serial.print(":"); Serial.print(analogRead(YR_Pin)); Serial.print(":"); Serial.println(!digitalRead(3)); delay(10); */ } //Список занятых пинов: A0,A1,A2,A3,1,2,3,4,7,8,9,10,11,12,13 //Список передаваемых пинов: A0,A1,A2,A3,2,3 //Список доп пинов питания 7,8 (В итоговой версии необходимо объединить на 5V, Gnd объединить)
RX:
#include <SPI.h> #include "nRF24L01.h" #include "RF24.h" #include <Mouse.h> RF24 radio(9, 10); //Создать модуль на пинах 9 и 10 byte recieved_data[6]; //Массив принятых данных byte XLP; //Значения левого стика ось X byte YLP; //Значения левого стика ось Y byte XRP; //Значения правого стика ось X byte YRP; //Значения правого стика ось Y int LBMP; //Значения ЛКМ int RBMP; //Значения ПКМ const int Sp = 30; //Скорость курсора (10,20,30,40,50,60,70) чем больше, тем медленне byte address[][6] = {"1Node", "2Node", "3Node", "4Node", "5Node", "6Node"}; //возможные номера труб void setup() { Serial.begin(9600); //Открываем порт для связи с ПК Mouse.begin(); radio.begin(); //Активировать модуль radio.setAutoAck(1); // Режим подтверждения приёма, 1 вкл 0 выкл radio.setRetries(0, 15); // Время между попыткой достучаться, число попыток) radio.enableAckPayload(); // Разрешить отсылку данных в ответ на входящий сигнал radio.setPayloadSize(32); // Размер пакета, в байтах radio.openReadingPipe(1, address[0]); // Слушаем трубу 0 radio.setChannel(0x70); // Выбираем канал (в котором нет шумов!) 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 должна быть одинакова на приёмнике и передатчике! Gри самой низкой скорости имеем самую высокую чувствительность и дальность!! radio.powerUp(); // Начать работу radio.startListening(); // Начинаем слушать эфир, мы приёмный модуль } void loop() { byte pipeNo; while ( radio.available(&pipeNo)) { // Есть входящие данные radio.read(&recieved_data, sizeof(recieved_data)); // Читаем входящий сигнал XLP = recieved_data[0]; //Читаем входящие данные оси X левого стика и записываем значение YLP = recieved_data[1]; //Читаем входящие данные оси Y левого стика и записываем значение XRP = recieved_data[2]; //Читаем входящие данные оси X правого стика и записываем значение YRP = recieved_data[3]; //Читаем входящие данные оси Y правого стика и записываем значение LBMP = recieved_data[4]; //Читаем входящие данные ЛКМ и записываем значение RBMP = recieved_data[5]; //Читаем входящие данные ПКМ и записываем значение } int xLpos, yLpos; int xRpos, yRpos; //Фильтр XYL if (XLP > 120 and XLP < 130) xLpos = 0; if (XLP >= 130) xLpos = map(XLP, 130, 255, 0, 80); if (XLP <= 120) xLpos = map(XLP, 120, 0, 0, -80); if (YLP > 120 and YLP < 130) yLpos = 0; if (YLP >= 130) yLpos = map(YLP, 130, 255, 0, 80); if (YLP <= 120) yLpos = map(YLP, 120, 0, 0, -80); //Фильтр XYR if (XRP > 120 and XRP < 130) xRpos = 0; if (XRP >= 130) xRpos = map(XRP, 130, 255, 0, 80); if (XRP <= 120) xRpos = map(XRP, 120, 0, 0, -80); if (YRP > 120 and YRP < 130) yRpos = 0; if (YRP >= 130) yRpos = map(YRP, 130, 255, 0, 80); if (YRP <= 120) yRpos = map(YRP, 120, 0, 0, -80); //Управление курсором Mouse.move(xRpos / Sp, yRpos / Sp); if (LBMP) { Mouse.press(MOUSE_LEFT); } else { Mouse.release(MOUSE_LEFT); } if (RBMP) { Mouse.press(MOUSE_RIGHT); } else { Mouse.release(MOUSE_RIGHT); } /*//Отладка A0,A1,A2,A3 Serial.print(XLP); Serial.print(":"); Serial.print(YLP); Serial.print(":"); Serial.println(LBMP); Serial.print("n"); Serial.print(XRP); Serial.print(":"); Serial.print(YRP); Serial.print(":"); Serial.println(RBMP); Serial.print("n"); Serial.print(xRpos); Serial.print(":"); Serial.print(yRpos); delay(5); */ } //Список занятых пинов: 9,10,14,15,16 //Список передаваемых пинов: A0,A1,A2,A3,2,3 //LBM-LeftButtonMouse(ЛКМ) RBM-RightButtonMouse(ПКМ)
Прототипирование
В обоих случаях последовательность действий при подключении радиомодулей одинакова: от адаптера питания NRF отпаиваем колодку 6pin(F) и припаиваем напрямую NRF гребёнкой 6pin(M), короткими проводами припаиваемся к платам Arduino. Аккуратно и компактно складываем и сжимаем «бутерброд». ПОЛЕЗНО! Вокруг антенны для каждого модуля NRF намотать пару‑тройку витков монтажного провода (в моём случае 0.2mm^2). Загоняем все в широкую термоусадку и фиксируем.
Для передатчика припаиваем «стики» на удобном вам расстоянии и также все фиксируем термоусадкой.
Итог
Получился рабочий прототип GamePad, похожий на DualShock.
Баги
ВАЖНО! Сначала подключать к питанию передатчик, после — приемник к ПК. Т.к. имеется баг в виде убегающего курсора при обратном порядке подключения, но он сразу же подчиняется, как только подаёшь питание на передатчик.
Высокая чувствительность стиков и резкое перемещение курсора (сложно попадать по кнопкам диалоговых окон).
Дальнейшее развитие
-
Сейчас в разработке корпус для данного прототипа.
-
Также требуется создать систему питания на основе аккумулятора 18 650 с модулями заряда и преобразователя напряжения до 5V.
-
В планах добавить энкодер для имитации колёсика мыши; ряд тактовых кнопок с различными функциями, расширить код — придать действия для осей левого стика.
Заметки
Буду рад вашим предложениям по улучшению и развитию функционала данного девайса. В частности, идеям исправления скорости курсора, придания ему плавности.
Автор:
SamDrankRaven