Joystik для ПК на базе Arduino

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Идея

Недавно купил длинный HDMI-кабель для просмотра киношек на телевизоре с комфортной кровати, а не сидя за ПК в углу.

Возникла проблема: часто вставать и ходить до ПК, чтобы поставить видео на паузу или изменить настройки плеера (качество, звук, озвучка). Захотелось чего-то компактного и простого, наподобие «Плей‑Пауза» пульта. Был вариант просто купить дешёвый Bluetooth‑комплект мышь‑клавиатура или только беспроводную мышь, Но это показалось мне неинтересным и простым.

Было принято решение разработать специфическое устройство для этих задач.

Проектирование

Во-первых, определим, какие функции и, как следствие, устройства ввода необходимы.

  1. В качестве устройства, позволяющего не только выполнять разовый «клик» по кнопке паузы, но и имеющего возможность передвигать курсор мыши, был выбран стик KY-023, наподобие тех, что применяются в контроллерах DualShock для PS4.

    Joystick KY-023
    Joystick KY-023

    KY-023 имеет:

    1) 2 аналоговых выхода: Ось X, Ось Y;
    2) 1 дискретный выход: Кнопка самого «стика».

    Т. к. проект в процессе разработки стал расширяться и усложняться, приходя к полноценному и большему функционалу, чем у компьютерной мыши, было решено добавить второй «стик».

    Таким образом получили конструкцию со следующим функционалом: правый «стик» должен отвечать за перемещение курсора (подобно как правый стик джойстика от «плойки» управляет обзором персонажа в играх); Левый стик должен будет управлять громкостью плеера и перемоткой видео (пока не реализован, в процессе разработки функций); внутренние кнопки «стиков» копируют ЛКМ и ПКМ.

  2. Т. к. только начинаю заниматься программированием, было решено использовать как контроллер плату Arduino. Почитав про особенности «камней», применяемых в «разношерстных» платформах, я выбрал это:

    Arduino pro mini на базе «камня» AtMega 168 (5V, 16MHz) в роли передатчика;

    Arduino pro micro на базе «камня» AtMega 23u4 в роли приемника и устройства, имеющего свойство двустороннего общения с ПК, определяется как HID-устройство (клавиатура, мышь).

  3. Необходимо было устройство приема‑передачи информации между платами Arduino. Выбрал радиомодули NRF24L01, работающие в диапазоне частот 2.4–2.5 ГГц. Также в связи с особенностями питания «камня» NRF потребовался адаптер со стабилизатором напряжения, способный использовать внешнее питание от 4.8V до 12V и подавать на плату NRF 3.3V.

    NRF24L01
    NRF24L01

    Немного характеристик:
    Напряжение питания: 1,9В – 3,6В;
    Интерфейс обмена данными: SPI;
    Частота приёма и передачи: 2,4 ГГц;
    Количество каналов: 128 с шагом 1МГц;
    Тип модуляции: GFSK;

    NRF24L01
    NRF24L01

    Скорость передачи данных: 250kbps, 1Mbps и 2Mbps;
    Чувствительность приёмника: -82 dBm;
    Расстояние приёма/передачи данных: 100м — прямая видимость; 30м — помещение;
    Коэффициент усиления антенны: 2dBm;
    Диапазон рабочей температуры: -40оС…+85оС;
    Организация сети на одном канале: 7 модулей (1 приёмник и 6 передатчиков).

Электросхема

На схеме не указан адаптер, но распиновка NRF и адаптера идентичные, и все необходимые сигналы прописаны в литографии на маске плат.

TX
TX
RX
RX

Программа

  1. Перво-наперво необходимо было научиться считывать и обрабатывать сигналы со стиков и управлять курсором напрямую с 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);
    
    }
  2. Учимся «дружить» 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]);
      }
    }
  3. А теперь самое интересное! Объединить эти два кода. Не обошлось и без плясок с бубном, и убегающим в «самоволку» курсором.

    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). Загоняем все в широкую термоусадку и фиксируем.

Для передатчика припаиваем «стики» на удобном вам расстоянии и также все фиксируем термоусадкой.

RX
RX
RX
RX
TX (Joystick)
TX (Joystick)
TX (Joystick)
TX (Joystick)

Итог

Получился рабочий прототип GamePad, похожий на DualShock.

Баги

ВАЖНО! Сначала подключать к питанию передатчик, после — приемник к ПК. Т.к. имеется баг в виде убегающего курсора при обратном порядке подключения, но он сразу же подчиняется, как только подаёшь питание на передатчик.

Высокая чувствительность стиков и резкое перемещение курсора (сложно попадать по кнопкам диалоговых окон).

Дальнейшее развитие

  1. Сейчас в разработке корпус для данного прототипа.

  2. Также требуется создать систему питания на основе аккумулятора 18 650 с модулями заряда и преобразователя напряжения до 5V.

  3. В планах добавить энкодер для имитации колёсика мыши; ряд тактовых кнопок с различными функциями, расширить код — придать действия для осей левого стика.

Заметки

Буду рад вашим предложениям по улучшению и развитию функционала данного девайса. В частности, идеям исправления скорости курсора, придания ему плавности.

Источник: https://habr.com/ru/articles/749656/


Интересные статьи

Интересные статьи

Создатели дистрибутивов Linux предлагают пользователям пригодные для работы без установки образы операционных систем, однако универсальные сборки плохо подходят для задач хостинга. Рассказываем, как м...
Технология поиска «VideoColor» заключается в том, что каждый кадр в видео рассматривается как отдельное изображение по которому может вестись поиск. Индексируемое, а затем и искомое изображение, дели...
Отечественные компании продолжают развиваться: в июле 2021 года компания UR-LI добилась получения статуса стартапа города Иннополис в Республике Татарстан. Новый статус дает компании возможность мас...
WebAssembly позволяет запускать код на разных языках программирования внутри браузера, и сейчас это не только способ разрабатывать веб-приложения не на JS, но и возможность использова...
В этой части статьи мы перейдем к самому интересному - будем разбирать музыкальный модуль Dizzy IV по винтиками и воспроизводить мелодию сначала на Windows, а потом и на ...