
Одна из самых частых забав ретрогеймера-электроника — сделать что-нибудь со старыми джойстиками. Вот и сделаем! Это простейший материал для начинающих самоделкиных, на грани треш-контента и на радость одному Доктору, но с познавательными элементами, которые могут или не могут пригодиться на практике.
Будем курочить джойстики от старых игровых консолей, не приходя в сознание. Из двух проводных джойстиков сделаем один, другой джойстик лишим провода, а ещё пару просто подключим проводами. По сути это сразу три разных микро-проекта, связанные общей тематикой. Нет времени объяснять, приступаем!
▍ Делай раз: проводной на коленке
Допустим, вам, а на самом деле мне, зачем-то захотелось подключить набор простых кнопок к компьютеру или другому устройству через USB порт — проще говоря, сделать свой собственный нестандартный, «кастомный» джойстик.
Конечно, можно реализовать это с помощью соответствующего современного микроконтроллера. Но не спешите расчехлять свои Ардуины. Они пригодятся нам немного попозже. Всё можно сделать гораздо проще, по технологиям древних. Буквально на коленке, хотя и сродни удалению гланд с противоположной стороны.

Кто застал эпоху великого противостояния ZX Spectrum и Денди в середине 1990-х годов, наверняка помнит, как переделать джойстик от последней в джойстик для первого.
Готовые джойстики для ZX Spectrum’а никогда были не особо доступны. Как правило, они имели формат чёрной палки с красной кнопкой и не совпадающую ни с чем распиновку разъёма, а иногда и сам разъём тоже (вариации советского DIN с разным числом контактов). Джойстики же для Денди найти было гораздо проще, уже предварительно сломанные, идеально подходящие для переделки. Да и формат «геймпада», пульта с кнопками, был слегка поудобнее во многих типах игр.

Однако устроены джойстики для этих двух систем принципиально по-разному. У ZX Spectrum это просто пять пассивных кнопок с одним общим проводом, всего шесть проводов. У Денди же состояние восьми независимых кнопок передаётся активной электроникой (сдвиговым регистром) по примитивному, но вполне эффективному последовательному протоколу, всего по пяти проводам — питание, защёлкивание, строб, и линия данных.
Чтобы не городить лишнюю электронику, преобразующую эти сильно разные интерфейсы, применялся варварский, но действенный процесс переделки: обрезались дорожки, идущие к «капле» на плате геймпада, брался кабель с нужным количеством проводов (шесть) — часто это был плоский белый шлейф или косичка из проводов типа МГТФ. Проводки от кабеля припаивались к обрезанным дорожкам на плате и к разъёму нужного типа, и джойстик был готов.

Примерно по такой же технологии можно переделать и джойстик от одной приставки в джойстик для другой. Когда-то я проделывал подобное, помещая электронику от Super Nintendo в корпус джойстика от Sega, чтобы было комфортнее играть в местный Mortal Kombat. Как говорится, задолго до того, как это стало мейнстримом.
Для начала повторим подобную конструкцию: переделаем старый, крайне классический джойстик от Денди в умеренно классический проводной USB-джойстик для современного ПК. Для реализации задуманного потребуется два джойстика: один и другой.

В роли первого будет выступать один из самых узнаваемых джойстиков для Денди, а точнее, знаменитого в наших краях чуть более раннего клона Ending Man. То бишь, в переводе с китайского, Терминатор.
Не волнуйтесь, данный экземпляр не обладает коллекционной ценностью. Он был приобретён специально для учинения сего бесчинства на Авито (где же ещё), в несильно исправном и презентабельном состоянии. Однако за немалые для подобного предмета 614 рублей — теперь это уже не мусор, который раньше не задумываясь несли на помойку, а востребованная в узких кругах старина.

Вероятно, вы уже приметили разъём для наушников, и вам любопытно узнать, куда же он подключён, ведь обычно на джойстиках Денди ничего такого нет, а в кабеле всего пять проводов, лишних для передачи звука нет. Разбираем джойстик и смотрим на плату. Ответ: а никуда не подключён. Но может и быть: на плате написано, какие контакты для чего нужны, и для звука предусмотрен шестой контакт, который в данном случае не задействован.

В роли второго джойстика, он же донор платы, выступит любой ненужный USB-джойстик с достаточным количеством кнопок. К счастью, они пока не в цене, и можно заполучить их забесплатно или за сущие копейки. Совершенно новый подобный джойстик без лишних наворотов и аналоговых стиков был приобретён на Озоне за 246 рублей некоторое время назад.

Осуществимость затеи обеспечивает тот удобный факт, что обычно в джойстиках требуется поддержка одновременного нажатия любого количества кнопок, а значит, они не объединяются ни в какие хитрые матрицы, имеют один общий провод и по одной персональной дорожке от чипа до каждой кнопки.

Впрочем, в конкретно нашем случае у USB-донора есть некоторые сложности с крестовиной. Дело в том, что на ПК она традиционно, с самого начала была аналоговой, а не дискретной, как на игровых консолях. А значит, джойстики, имитирующие классическую дискретную крестовину, имитируют аналоговое подключение, и кнопки их стрелок направлений разведены соответствующим образом: два входа, X и Y, плюс и минус питания.

Нажатие кнопки вверх подключает вход Y к плюсу питания, кнопки вниз — к минусу, а одновременное нажатие замыкает плюс с минусом, и джойстик начинает нагревать крестовиной пальцы слишком ловкого игрока и немного дымить (на самом деле нет, токи очень маленькие). Это не является серьёзным препятствием, просто нужно чуть больше резни на плате адаптируемого джойстика, чтобы реализовать в нём такую же схему работы.
Для осуществления переделки понадобится паяльник, возможно, лобзик, умение паять и решать лабиринты из детских книжек. Процесс очень прост:
- Фотографируем плату USB-джойстика, чтобы знать, как проходили дорожки от кнопок к чипу.
- При необходимости вырезаем из донора область платы с чипом, а если место позволяет, то запихиваем плату целиком.
- Обрезаем дорожки на плате реципиента.
- Закрепляем плату (фрагмент) донора, чтобы она никуда не делась.
- Соединяем проводками дорожки донора с дорожками к кнопкам реципиента.
- ????
- PROFIT!
В моём случае плата хоть и (почти) помещалась целиком, но ничто не помешало слегка обрезать ей торчащие уши и выиграть немного места. Место было нужно для реализации дополнительной задумки.

Так как в оригинальном джойстике есть разъём наушников, я решил немного разбавить уныние этого упражнения и восстановить историческую справедливость, сделав разъём действующим. Реализовано это по той же самой методике. Приобрёл самую дешёвую USB-гарнитуру и USB-хаб, вытащил из них платы, соединил ожидаемым образом — джойстик и гарнитура подключаются к хабу, от хаба провод наружу — и кое-как запихал всё это внутрь джойстика. Итого четыре платы в одном корпусе! (и минус один такой же хаб за кадром).

Для серьёзных применений или серийного производства такой способ, разумеется, не годится. Но решить одиночную задачу прямо здесь и сейчас им вполне можно. К тому же, не потребовалось совсем никакого программирования — неплохо для эпохи, в которую только ленивый не пинает DIY за засилье Ардуин.

Моя собственная оценка этой самоделки — 200 полуляхов из 250. Формально работает, но и качество исходного джойстика было ниже плинтуса, и новая конструкция не сказать, что надёжная. Сомнительно, но окей.
▍ Делай два: беспроводной на ESP32
Допустим, теперь вам, но на самом деле снова мне, зачем-то захотелось сделать из классического джойстика от Денди (да, опять) беспроводной Bluetooth-джойстик для ПК и может быть телефона. Современные технологии и ассортимент китайских запчастей позволяют сделать это с лёгкостью, но не без проблем.
Для проекта был выбран самый странно выглядящий, а заодно и самый огромный в истории джойстик, которым когда-то комплектовалась приставка Dendy Classic II.

Не волнуйтесь, специально для проекта опять же был приобретён не имеющий большой коллекционной ценности экземпляр, и к тому же не от аутентичной Денди, а от её китайского клона IQ-502 (это ирония). Данные джойстики были весьма неплохи по качеству, не сильно распространены, и сейчас довольно-таки ценятся — мой экземпляр обошёлся мне в 1160 рублей на всё том же Авито.
Также потребовались следующие запчасти, часть из которых была извлечена из запасов, а часть приобретена по месту, на всё том же Озоне и в радиомагазине поблизости:
- ESP32-C3 Super Mini — 509 рублей.
- Аккумулятор 1100 мАч — 495 рублей.
- Модуль контроллера заряда TP4056 — из запасов, около 100 рублей.
- Движковый выключатель — из запасов, копейки.
- Светодиоды — из запасов, копейки.
Безусловно, и этот проект можно было бы сделать первым способом, раскурочив готовый Bluetooth-джойстик. Да, применение ESP32, вполне сопоставимой по вычислительной мощности с первым Pentium, для подобной затеи — как стрельба из двустволки по клопам. Но с другой стороны, в одном готовом модуле размером с большой ноготь большого пальца есть совершенно всё, что требуется для решения задачи, и стоит он относительно недорого, а проект штучный — так зачем же изобретать велосипед?

На самом деле, главная причина пойти этим путём была в том, что я хотел повозиться с контроллером ESP32-C3 с ядром RISC V и Bluetooth 5, в формате новой для меня платки SuperMini, которая как будто создана для подобных проектов. Вместо неё можно применить любую другую версию ESP32 с ядром Xtensa, кроме S2, в которой поддержка Bluetooth отсутствует. По той же причине, к сожалению, для проекта не подходит и классический ESP8266: он умеет только в WiFi.
Если хочется особых изысков, ровно то же самое можно проделать с Arduino Nano и Bluetooth-модулем типа HC-05, но по сути это будет замена одной платки двумя другими, которые в сумме стоят дороже, чем один ESP32.

Для начала подготовил железо. Первым делом разобрал джойстик, изучил особенности его устройства и возможности по размещению новой электроники. Удивительно, но несмотря на внушительный размер и пухлую форму, внутри джойстика свободного места просто нет. Плата занимает всю площадь корпуса, а на задней крышке под кнопками есть упоры. Сделано на совесть! Но для моих целей придётся переделать.
Ещё из интересного то, что кабель выполнен на внутреннем разъёме, а схема построена на специализированной корпусной микросхеме UM6582, а не на традиционной «капле». Максимально ремонтопригодно. Даже жаль портить это великолепие. Жаль, но надо: чтобы вместить новые компоненты, придётся убрать всё лишнее по максимуму. Собственно, по причине нехватки места я решил не делать вариант с преобразованием последовательного интерфейса и сохранением оригинальной электроники.

Впрочем, великолепие изрядно обветшало: часть кнопочных резинок порвалась прямо в руках. В частности, отпал один из контактных прыщей от крестовины. Это было типичной проблемой активно используемых джойстиков, когда они были не старыми. Пришлось провести восстановительные работы.
Одиночные резинки я подобрал из запасов. Подходящей резинки для крестовины в них не нашлось, но нашлась подходящая по расстоянию между площадок. Применил к ней дедовский лайфхак: отрезал оставшиеся контактные площадки, у новой резинки отрезал бортик, и просунул одну в другую. Не очень хорошее решение, так как увеличивает общую толщину резинки и делает нажатия жёстче, но вполне рабочий вариант. Позже я переставил резинку в чёрный джойстик Терминатора, а оттуда взял более живую, которая оказалась точно такой же по формату.
Внезапно, и этот джойстик тоже оказался оснащён гнездом для наушников. Вообще-то, это большая редкость для джойстиков Денди, а не их стандартная функция, просто так сложилось, что мне попалось сразу два джойстика с ней. И на этот раз она даже полностью реализована: кабель этого джойстика содержит шесть проводов, дополнительный провод идёт на второй контакт 15-контактного разъёма, куда на оригинальном Famicom действительно выводится звук.

На этот раз я решил учинить историческую несправедливость и лишить джойстик гнезда наушников, выведя на его место гнездо для зарядки. Немного подпилил модуль TP4056, чтобы разъём выступал посильнее, немного подрезал изначально круглую дырку в корпусе, и надёжно приклеил модуль суперклеем на оригинальную плату. При последующей проверке оказалось, что из десятка модулей в коробке я выбрал единственный неисправный. Пришлось отклеить и повторить процедуру.

Аккумулятор разместил на задней крышке джойстика. Без модификаций туда влезал только очень маленький, на 450 мАч, но путём фигурной обрезки штатных упоров для платы удалось разместить куда более крупный аккумулятор ёмкостью 1100 мАч. Закрепил его на задней стенке на тонком двухстороннем скотче и припаял разъём для удобства последующей сборки-разборки. В собранном состоянии аккумулятор упирается в выступы корпуса, и не имеет возможностей для случайного перемещения.

Стандартный движковый выключатель питания разместил в штатном квадратном отверстии, из которого раньше выходил круглый шнур. Корпус для установки выключателя дорабатывать не понадобилось, но доработал плату: сделал в ней вырез поглубже. Выключатель припаял на кусочек монтажной платы, а монтажную плату приклеил на плату джойстика. На неё же припаял пару светодиодов для индикации режимов работы. Пара тут не нужна, но квадратная дырка в корпусе слишком большая, и надо было её чем-то заполнить.

Также на оригинальную плату приклеил и модуль ESP32, сделав под него дополнительный вырез в упорах задней крышки. Все старые детали с платы удалил, включая микросхему, разъём и прочие резисторы. Соединил модуль с платой тонкими проводками. Для подключения к кнопкам использовал штатные отверстия на плате. Таким образом, вся новая электроника разместилась на старой плате и осталась полностью разборной. Перерезать на плате дорожки не понадобилось, так как после демонтажа старых элементов ненужных соединений не осталось.
Элементы дополнительно никак не закреплял. В этот раз я применял не традиционный суперклей из Fix Price, а Космофен CA500.200, и приклеились детали удивительно крепко (убедился, когда отклеивал модуль зарядки). Схема соединения компонентов получилась следующей:

Что касается кода, то изобретать ничего не нужно. Огромный плюс платформы Arduino, которая включая и поддержку плат на ESP32, заключается в том, что для всех типовых задач уже созданы практически готовые решения — нужные библиотеки и примеры, на основе которых можно быстро создать то, что нужно. В данном случае это библиотека ESP32-BLE-Gamepad и её пример Gamepad.
У этой медали есть и обратная сторона, проявившаяся в этом проекте: отсутствие понимания нижних уровней реализации и попадание в зависимость от третьих лиц, когда (а не если) что-то идёт не так. Причины никому не известны, что делать — непонятно.
Библиотека показала себя весьма ненадёжной и своенравной. В разные периоды времени я потратил немало часов, пытаясь найти работоспособную комбинацию версии самой библиотеки, версии нужной ей библиотеки NimBLE и пакета поддержки ESP32. Но даже заведомо работавшая в один момент времени комбинация версий на другой подход не работала: контроллер просто крашился на инициализации библиотеки.
Впрочем, в итоге библиотека кое-как заработала, хотя со странностями. В Windows 7 не определяется заданное мной имя устройства, а его тип считается Bluetooth Mouse. Windows 10 не сразу, но показывает имя, а устройство считается необычным видом джойстика. Телефон на Android устройство вообще не видит, хотя должен. Для моих целей пойдёт, а вообще обидно.
Код получился предельно простым, в кои-то веки его даже можно привести целиком:
#include <Arduino.h>
#include <BleGamepad.h>
BleGamepad bleGamepad("IQ-502 Bluetooth Gamepad", "shiru8bit", 100);
BleGamepadConfiguration bleGamepadConfig;
#define PIN_BTN_B 0
#define PIN_BTN_A 1
#define PIN_BTN_TURBO_B 2
#define PIN_BTN_TURBO_A 3
#define PIN_BTN_COMMON 4
#define PIN_BTN_START 5
#define PIN_BTN_SELECT 6
#define PIN_LED_ACTIVITY 8
#define PIN_LED_ONLINE 7
#define PIN_BTN_RIGHT 9
#define PIN_BTN_LEFT 10
#define PIN_BTN_DOWN 20
#define PIN_BTN_UP 21
const uint8_t pin_numbers[] = {
PIN_BTN_B,
PIN_BTN_A,
PIN_BTN_TURBO_B,
PIN_BTN_TURBO_A,
PIN_BTN_START,
PIN_BTN_SELECT,
PIN_BTN_RIGHT,
PIN_BTN_LEFT,
PIN_BTN_DOWN,
PIN_BTN_UP,
};
uint8_t pin_state[10];
uint32_t led_activity_count = 0;
int16_t x_axis = 32768 / 2;
int16_t y_axis = 32768 / 2;
void setup()
{
memset(pin_state, 0, sizeof(pin_state));
pinMode(PIN_BTN_UP, INPUT_PULLUP);
pinMode(PIN_BTN_DOWN, INPUT_PULLUP);
pinMode(PIN_BTN_LEFT, INPUT_PULLUP);
pinMode(PIN_BTN_RIGHT, INPUT_PULLUP);
pinMode(PIN_BTN_A, INPUT_PULLUP);
pinMode(PIN_BTN_B, INPUT_PULLUP);
pinMode(PIN_BTN_TURBO_A, INPUT_PULLUP);
pinMode(PIN_BTN_TURBO_B, INPUT_PULLUP);
pinMode(PIN_BTN_SELECT, INPUT_PULLUP);
pinMode(PIN_BTN_START, INPUT_PULLUP);
pinMode(PIN_BTN_COMMON, OUTPUT);
pinMode(PIN_LED_ACTIVITY, OUTPUT);
pinMode(PIN_LED_ONLINE, OUTPUT);
digitalWrite(PIN_BTN_COMMON, LOW);
digitalWrite(PIN_LED_ACTIVITY, HIGH);
digitalWrite(PIN_LED_ONLINE, HIGH);
bleGamepadConfig.setAutoReport(false);
bleGamepadConfig.setControllerType(CONTROLLER_TYPE_GAMEPAD);
bleGamepadConfig.setVid(0xe502);
bleGamepadConfig.setPid(0xabcd);
bleGamepadConfig.setTXPowerLevel(9);
bleGamepadConfig.setModelNumber("1.0");
bleGamepadConfig.setSoftwareRevision("Software Rev 1");
bleGamepadConfig.setSerialNumber("0000000001");
bleGamepadConfig.setFirmwareRevision("1.0");
bleGamepadConfig.setHardwareRevision("1.0");
bleGamepadConfig.setButtonCount(10); //only 6 working, just to match standard layout
bleGamepadConfig.setWhichAxes(1, 1, 0, 0, 0, 0, 0, 0); //XY axes only
bleGamepadConfig.setHatSwitchCount(0); //no hats
bleGamepadConfig.setAxesMin(0x0000);
bleGamepadConfig.setAxesMax(0x7FFF);
bleGamepad.begin(&bleGamepadConfig);
}
void set_button(uint8_t button, uint8_t down)
{
if (down) bleGamepad.press(button); else bleGamepad.release(button);
}
void loop()
{
while (1)
{
//check all pins, remember their state, set flag if the state has changed between the polls
for (uint8_t n = 0; n < sizeof(pin_numbers); ++n)
{
uint8_t pin = pin_numbers[n];
if (digitalRead(pin) == HIGH) //released
{
if (pin_state[n] & 1) pin_state[n] = 0x80;
}
else //pressed down
{
if (!(pin_state[n] & 1)) pin_state[n] = 0x81;
}
}
if (bleGamepad.isConnected())
{
//led indicates connection
digitalWrite(PIN_LED_ACTIVITY, LOW);
digitalWrite(PIN_LED_ONLINE, HIGH);
led_activity_count = 0;
uint8_t change = 0;
for (uint8_t n = 0; n < sizeof(pin_numbers); ++n)
{
if (pin_state[n] & 0x80)
{
change = 1;
pin_state[n] &= ~0x80;
uint8_t down = pin_state[n] & 1;
switch (pin_numbers[n])
{
case PIN_BTN_B: set_button(BUTTON_2, down); break;
case PIN_BTN_A: set_button(BUTTON_3, down); break;
case PIN_BTN_TURBO_B: set_button(BUTTON_1, down); break;
case PIN_BTN_TURBO_A: set_button(BUTTON_4, down); break;
case PIN_BTN_START: set_button(BUTTON_9, down); break;
case PIN_BTN_SELECT: set_button(BUTTON_10, down); break;
case PIN_BTN_RIGHT:
if (down) x_axis = 32767; else x_axis = 32768 / 2;
break;
case PIN_BTN_LEFT:
if (down) x_axis = 0; else x_axis = 32768 / 2;
break;
case PIN_BTN_DOWN:
if (down) y_axis = 32767; else y_axis = 32768 / 2;
break;
case PIN_BTN_UP:
if (down) y_axis = 0; else y_axis = 32768 / 2;
break;
}
}
}
if (change)
{
bleGamepad.setX(x_axis);
bleGamepad.setY(y_axis);
bleGamepad.sendReport(); //send current state
}
}
else
{
//led blinks indicating there is no connection
digitalWrite(PIN_LED_ACTIVITY, HIGH);
digitalWrite(PIN_LED_ONLINE, led_activity_count & 0x100 ? LOW : HIGH);
++led_activity_count;
memset(pin_state, 0, sizeof(pin_state));
}
delay(5);
}
}
Главной сложностью в этом проекте стал выбор пинов. Казалось бы, всё просто, подключил кнопки к пинам GPIO, и готово. И хотя у ESP32-C3 SuperMini вполне достаточно пинов, есть подвох с некоторыми из них: в момент включения притягивание их к земле загоняет контроллер в режим загрузки прошивки. Это поведение нежелательно, и лучше бы его избежать.
Я решил обойти эту проблему подключением общего провода всех кнопок не к земле, а к ещё одному пину GPIO — как раз оставался один лишний. Это позволяет физически запретить срабатывание кнопок, когда оно не нужно, например, при запуске. Тут, правда, есть второй подвох: состояние пинов при включении не определено, а значит, на управляющем пине вполне может быть низкий уровень, и вся эта защита становится бесполезной. Но можно избежать этого, подтянув пин к плюсу питания внешним резистором, что я и сделал.
Для управления одним из двух моих светодиодов я выбрал GPIO8, который управляет установленным на плате светодиодом. Мой дублирующий жёлтый диод мигает, когда соединение не установлено, а дополнительный зелёный светится, когда джойстик работает. Светодиоды оказались сверхъяркими, даже резистор на 390 ом не смог ослабить их яркость ниже уровня карманного фонарика.
Итоговая раскладка пинов получилась следующей:
GPIO 0 | B |
GPIO 1 | A |
GPIO 2 | Turbo B |
GPIO 3 | Turbo A |
GPIO 4 | Общий для кнопок, подтянут через 10К на плюс |
GPIO 5 | Start |
GPIO 6 | Select |
GPIO 7 | Светодиод работающего соединения |
GPIO 8 | Светодиод на плате, светодиод поиска соединения |
GPIO 9 | Вправо |
GPIO 10 | Влево |
GPIO 20 | Вниз |
GPIO 21 | Вверх |
Итоговое устройство выглядит так:

Не обошлось без проблем. После окончательной сборки джойстик вроде бы полностью заработал, но когда я попробовал на нём поиграть, оказалось, что через небольшое время начинается сильное отставание и пропуски нажатий, и через пару минут играть становится абсолютно невозможно. Опытным путём выяснилось, что если джойстик расположен близко к Bluetooth-адаптеру, на расстоянии десятка сантиметров, всё работает идеально, но уже на расстоянии 30-50 сантиметров начинаются подобные проблемы.

Причин для этого могло быть много: питание пониженным напряжением, отсутствие дополнительной фильтрации по питанию, огромный земляной полигон в непосредственной близости от модуля с керамической антенной. Также известно, что в ESP32-C3 SuperMini антенна сама по себе работает плохо — это первое, что я услышал, только-только узнав про существование этой платы. Многие делают модификацию, поворачивая чип антенны на 90 градусов. Я же последовал другому совету: просто заменил антенну на кусок провода длиной 3 сантиметра. Удивительно, но это помогло, и джойстик стал работать надёжно даже с другого конца комнаты.
Оценка самоделки — 75 полуляхов. После доработок функционирует адекватно, выглядит аккуратно, ощущается довольно прикольно. Пользоваться можно.
▍ Делай три: адаптер PlayStation на USB
Ну а теперь кое-что посложнее, хотя на самом деле нет. Возьмём уже наконец очередной микроконтроллер и сделаем своё собственное проводное USB HID устройство.
Подключать теперь будем не просто кнопки, а классический джойстик целиком, без каких-либо его переделок, преобразуя штатный коммуникационный протокол — вот тут-то действительно найдётся хоть какая-то работа для микроконтроллера. В роли классического джойстика у меня выступит коллектив из двух единиц — два джойстика для оригинальной первой Sony PlayStation. Впрочем, подойдут и от оригинальной PlayStation 2, они обратно совместимы.

По сути это будет повторение функционала самоделок DirectPad Pro, NTPAD или PSXPAD, популярных и крайне актуальных в начале 2000-х годов, когда на рынке наблюдался дефицит качественных геймпадов для ПК. Эти простейшие устройства и драйверы для них позволяли без каких-либо переделок подключать к порту принтера компьютера аутентичные джойстики от самых разных игровых консолей, от Денди до Sega Saturn и Nintendo 64.
Позже появились более продвинутые аналоги подобных самодельных адаптеров на микроконтроллерах, с поддержкой USB. Но былой популярности они уже не имели — собрать их мог далеко не каждый, ведь дело было задолго до появления Arduino, нужно было покупать детали по отдельности, изготавливать плату, прошивать контроллер программатором, и совершать ещё много сложносочинённых движений.

Когда-то сделал себе простой адаптер для подключения джойстиков к LPT-порту и я, использовав блок разъёмов от неисправной консоли и провод от принтера. Эта конструкция довольно долго приносила пользу и радость, подарив моим джойстикам из конца 90-х вторую жизнь. Со временем она утратила актуальность, и с тех пор собирала пыль в кладовке. Но теперь, имея под рукой мощь современных технологий, ничто, включая здравый смысл, не может помешать мне легко и ненапряжно дать этим джойстикам уже третью жизнь. Только сначала нужно их как следует отмыть.

Реализовать задуманное можно несколькими способами: довольно сложным на Arduino Nano, используя программный стек V-USB родом из 2008 года, или более простыми, используя микроконтроллеры, имеющие аппаратную поддержку USB.
В выборе микроконтроллера для взаимодействия с джойстиками PlayStation есть один тонкий момент. На большинстве сайтов утверждается, что их питающее напряжение составляет 5 вольт. Однако, это историческое заблуждение. Фактически вся система питается от двух линий, формируемых блоком питания: 3.48 вольт и 7.5 вольт (для моторчиков вибрации). Логические уровни в системе ближе стандартным к 3.3-вольтовым. По этой причине в самоделках с пятивольтовыми контроллерами часть джойстиков не работает вовсе, а часть работает, но (теоретически) подвержена риску выхода из строя из-за сильного превышения питающего напряжения.

Таким образом, если делать адаптер на классических 8-битных Arduino, будь это Nano с V-USB, или Pro Micro на ATmega32u4 с аппаратным USB, понадобится организовать правильное напряжение питания джойстика и конвертор уровней, хотя бы резистивными делителями, а лучше MOSFET'ами BSS138, либо специализированной микросхемой.
Другой вариант — взять изначально трёхвольтовый контроллер. Например, ESP32-S2 и S3 имеют набортный USB. Тогда никакие дополнительные детали не понадобятся, но, конечно, это будет очередное забивание микроскопа: слишком мощный контроллер с беспроводными интерфейсами, которые даже не будут задействованы.
Сначала я хотел применить Pro Micro, но потом прикинул, что S2 Mini подойдёт для моих целей лучше: не надо городить никаких лишних деталей. Дело в том, что у меня была дополнительная хотелка: светодиодная индикация нажатия кнопок. Наличие большого количества GPIO у S2 Mini сильно упростило её реализацию. И опять же, весомой причиной стало желание применить эту плату в практической конструкции, чтобы ознакомиться с ней поближе.

Схема устройства проста и снова по большей содержит одни провода. Протокол обмена проводных джойстиков PlayStation представляет собой разновидность шины SPI. Соответственно, есть линии, аналогичные MISO, MOSI и CLK, соединяемые параллельно для всех подключаемых джойстиков, а также персональный CS для каждого — называются они примерно DATA, CMD, CLK и ATN, хотя в разных источниках используются разные названия.
Помимо этих сигналов, есть неиспользуемый в коде признак готовности ACK и отдельное питание для вибромоторов, которое я не задействовал, так как мои джойстики их не имеют. Если вибрация нужна, можно поставить повышающий DC-DC преобразователь и реализовать поддержку команды вибрации в коде.
Входные со стороны микроконтроллера линии, в данном случае DATA и ACK подтягиваются к плюсу питания резистором на 10К. Это важный момент, иначе схема работать не будет! Но если в коде сигнал ACK не используется, его можно не подключать, и резистор, соответственно, не ставить.

Самым сложным элементом на схеме выглядит опциональная светодиодная индикация, содержащая 14 сверхярких белых светодиодов. Разумеется, это совершенно опциональная вещь, делать её не обязательно. Устроена она очень просто: отдельный провод от GPIO до каждого из светодиодов и один гасящий резистор на 330 Ом.
Думаю, любой начинающий радиолюбитель знает, что для каждого светодиода нужно ставить отдельный резистор. Почему же здесь он всего один? Дело в том, что 3.3-вольтовый стабилизатор S2 Mini и так дополнительно нагружен, обеспечивая питание джойстиков — они потребляют порядка 10 мА каждый. Светодиоды в традиционном включении и на полной яркости, с током 10-20 мА на каждый, потребовали бы ещё порядка 140-280 мА в пике, и это слишком много.
Чтобы обойтись без дополнительного стабилизатора и пачки резисторов, я реализовал динамическую индикацию: светодиоды быстро-быстро включаются по очереди, и в один момент времени всегда горит только один светодиод. Дополнительно я сильно занизил ток светодиода. Теперь максимальное потребление индикации составляет всего лишь 1 мА, при этом светодиоды светятся достаточно ярко.

Конструктивно мой адаптер выполнен на том же самом блоке разъёмов джойстиков и карт памяти, позаимствованном когда-то от неисправной PS1, который я применял в моём старом переходнике для LPT. Для новой конструкции я слегка облагородил его наружность: закрепил электронику на монтажных платах, винтиках и стойках, и склеил корпус из тонкого белого пластика.
Программирование снова сводится к применению готовых библиотек. К сожалению, применение платы S2 Mini ограничило выбор доступных решений: лучшая библиотека для работы с джойстиками PlayStation, PsxNewLib и лучшая библиотека для реализации USB HID устройств, Arduino Joystick Library, поддерживают только платы на базе ATmega32u4. Если вы решите повторить конструкцию на Arduino Leonardo или Pro Micro, это прекрасный выбор, в них даже есть готовый пример кода именно для подобной задачи. Можно применить их и для проекта на ESP32, но это потребует доработки библиотек, и это уже другой уровень сложности.
Я же решил пойти простым путём и взял две другие библиотеки: Joystick_ESP32S2 для реализации USB-устройства именно на используемой мной плате, и PsxLib для общения с джойстиком PlayStation. Они менее функциональны, чем упомянутые выше. В частности, PsxLib не поддерживает вибрацию, аналоговые стики и силу нажатия для джойстиков PlayStation 2 — только цифровые кнопки PlayStation 1 и режима обратной совместимости. Но для моих целей этого достаточно.
Осталось только написать связующую логику и индикацию нажатия кнопок. Получился следующий код:
#include <Joystick_ESP32S2.h>
#include "PsxLib.h"
#define JOYSTICK_COUNT 2
Joystick_ Joystick[JOYSTICK_COUNT] = {
Joystick_(0x03, JOYSTICK_TYPE_JOYSTICK, 10, 0, true, true, false, false, false, false, false, false, false, false, false),
Joystick_(0x04, JOYSTICK_TYPE_JOYSTICK, 10, 0, true, true, false, false, false, false, false, false, false, false, false)
};
Psx pad1;
Psx pad2;
//L1 L2 L U D R St Sl # ^ X O R1 R2
const uint8_t led_pins[14] = {
1, 2,
3, 5, 4, 6,
7, 8,
9, 11, 10, 13,
14, 12
};
uint8_t led_state[14];
const uint32_t btn_mask[14] = {
PSB_L1, PSB_L2,
PSB_PAD_LEFT, PSB_PAD_UP, PSB_PAD_DOWN, PSB_PAD_RIGHT,
PSB_SELECT, PSB_START,
PSB_SQUARE, PSB_TRIANGLE, PSB_CROSS, PSB_CIRCLE,
PSB_R1, PSB_R2,
};
const byte PIN_DAT = 37;
const byte PIN_CMD = 35;
const byte PIN_CLK = 36;
const byte PIN_ATT1 = 18;
const byte PIN_ATT2 = 34;
void setup() {
memset(led_state, 0, sizeof(led_state));
for (int i = 0; i < sizeof(led_pins); ++i)
{
digitalWrite(led_pins[i], HIGH);
pinMode(led_pins[i], OUTPUT);
}
pad1.setupPins(PIN_DAT, PIN_CMD, PIN_ATT1, PIN_CLK, 50);
pad2.setupPins(PIN_DAT, PIN_CMD, PIN_ATT2, PIN_CLK, 50);
USB.productName("PS1 to USB Converter");
USB.manufacturerName("shiru8bit");
USB.begin();
for (int id = 0; id < JOYSTICK_COUNT; ++id)
{
Joystick[id].setXAxisRange(-127, 127);
Joystick[id].setYAxisRange(-127, 127);
Joystick[id].begin(false);
}
}
void loop() {
while (1)
{
uint32_t pad_state[2] = {0, 0};
cli();
pad_state[0] = pad1.read();
pad_state[1] = pad2.read();
sei();
//led display prepare state
memset(led_state, 0, sizeof(led_state));
for (int index = 0; index < JOYSTICK_COUNT; index++)
{
for (int i = 0; i < sizeof(btn_mask) / sizeof(uint32_t); ++i)
{
uint32_t mask = btn_mask[i];
if (pad_state[index] & mask) led_state[i] = 1;
}
}
//led display update
for (int i = 0; i < sizeof(led_pins); ++i)
{
digitalWrite(led_pins[i], led_state[i] ? LOW : HIGH);
delayMicroseconds(500);
digitalWrite(led_pins[i], HIGH);
}
//USB state update
for (int id = 0; id < JOYSTICK_COUNT; ++id)
{
uint32_t state = pad_state[id];
Joystick[id].setButton(0, state & PSB_L1 ? 1 : 0);
Joystick[id].setButton(1, state & PSB_L2 ? 1 : 0);
Joystick[id].setButton(2, state & PSB_R1 ? 1 : 0);
Joystick[id].setButton(3, state & PSB_R2 ? 1 : 0);
Joystick[id].setButton(4, state & PSB_SQUARE ? 1 : 0);
Joystick[id].setButton(5, state & PSB_TRIANGLE ? 1 : 0);
Joystick[id].setButton(6, state & PSB_CROSS ? 1 : 0);
Joystick[id].setButton(7, state & PSB_CIRCLE ? 1 : 0);
Joystick[id].setButton(8, state & PSB_SELECT ? 1 : 0);
Joystick[id].setButton(9, state & PSB_START ? 1 : 0);
int x = 0;
int y = 0;
if (state & PSB_PAD_LEFT) x = -127;
if (state & PSB_PAD_RIGHT) x = 127;
if (state & PSB_PAD_UP) y = -127;
if (state & PSB_PAD_DOWN) y = 127;
Joystick[id].setXAxis(x);
Joystick[id].setYAxis(y);
Joystick[id].sendState();
}
}
}
Наконец, пора загрузить скетч в плату. Как и многие новые платы на ESP32, свежекупленная S2 Mini при подключении к ПК не подаёт совершенно никаких признаков жизни. Это её нормальное поведение. Чтобы чудо произошло, нужно перевести плату в загрузочный режим: нажать Reset, удерживая Button 0. Если после перехода в загрузочный режим драйвер платы в Windows не установился и COM-порт для загрузки скетча не появился, поможет программа Zadig. Нужно перевести плату в загрузочный режим, и с помощью программы установить драйвер USB Serial (CDC) на Interface 0 и WinUSB на Interface 2.

Скетч загрузился, всё работает! Правда, есть один момент: решения для USB HID на Arduino-совместимых платах не позволяют программно установить имя отдельных джойстиков в системе. Для этого требуется редактировать boards.txt, чем я заниматься не стал.

Пожалуй, это ровно 100 полуляхов. Работает чётко, пользоваться можно, но вышло не очень красиво. Если однажды захочется зарубиться в Tekken 3 на оригинальных джойстиках — пригодится. С другой стороны, светодиодная индикация помогла мне найти и исправить неисправность в самих джойстиках, так что пусть будет ровно 80 полуляхов.
▍ Но зачем?
Вот так с помощью нехитрых приспособлений можно превратить одни джойстики в другие джойстики. Понятия не имею, зачем — не хотите, не превращайте.
Если же без шуток, вы можете задать резонный вопрос — к чему вся эта мышиная возня во времена, когда можно в пару кликов мышью приобрести готовый джойстик на любой вкус, цвет и карман? Действительно, трудно с этим поспорить.
Трудно, но не невозможно: всё-таки не любой. Да, большинство популярных потребностей закрыто массовым производством. Но существуют ещё и специальные потребности, где с доступностью решений похуже, и эта та ниша, где подобные поделки могут оказаться полезными.
Например, вам хочется задействовать исключительно любимый джойстик из детства для аутентичных ощущений. Или собрать кастомный контроллер под одну конкретную игру, и вы хотите, чтобы кнопки работали как оси, а оси как кнопки. Или очень бюджетно и по-быстрому, буквально на коленке реализовать подключение джойстиков самодельного аркадного кабинета к мини-ПК, не дожидаясь прибытия готовой платы USB-интерфейса из Китая.
Другая возможная цель — подстройка под физические особенности конкретного пользователя. Допустим, он — левша, и ему почему-то критически неудобно играть на стандартном контроллере. Существует также такая благородная цель, как постройка устройств ввода для людей с ограниченными возможностями. Правда, я не знаю никого, кто занимался бы ей в наших краях, но она довольно развита за рубежом. В этом случае контроллер подстраивается под физические возможности конкретного пользователя: например, на возможность игры одной рукой на джойстике с двумя стиками (второй стик упирается в колено).
Ещё одно специфическое, чисто техническое, но полезное применение — стенд для контроля лага. Если вы настолько просветлённый разработчик игр, что уже знаете про распространение задержки ввода в системе, и не можете спать спокойно без средств её контроля, тогда вам может пригодиться контрольный стенд.
В простейшем виде он представляет собой россыпь светодиодов, подобных той, что я сделал в адаптере для PlayStation 1, но напрямую подключённых к каждой кнопке джойстика, до всех протоколов и пересылок по кабелю. Снимая видеокамерой экран и светодиоды джойстика одновременно, можно визуально контролировать фактический лаг. Такие приборы не делаются массово, но они востребованы в профессиональной среде.
▍ Заключение
На самом деле, конечно, главная причина, по которой я занялся вышеперечисленным безобразием — ознакомление с некоторыми решениями и подготовка базы для нескольких других, более сложных и занимательных проектов. Также у меня накопился целый ворох различных задач, связанных с разнообразными джойстиками. Когда они реализуются, обязательно расскажу о самых интересных из них!
© 2025 ООО «МТ ФИНАНС»
Telegram-канал со скидками, розыгрышами призов и новостями IT 💻
