В этой статье расскажу, как подключить геймпад от игровой приставки Sega Mega Drive к ПК, используя микроконтроллер в качестве переходника. Разберемся как приставка опрашивает геймпад и повторим эту логику на микроконтроллере. Сделаем, чтобы ПК видел микроконтроллер с подключенным геймпадом, как USB-клавиатуру или USB-геймпад.
В итоге получиться вот такой переходник на два геймпада:

Содержание:
Геймпад от игровой приставки Sega Mega Drive
У меня есть два вот таких геймпада от китайской реплики приставки Sega Mega Drive (она же Sega Genesis). Геймпады, естественно, тоже не оригинальные. Один старый (серый), ещё с того времени, когда у меня была сама приставка, а другой (черный) недавно купил в DNS, удивительно, что они до с��х пор продаются.

Компоновка геймпадов такая:
Крестовина: 4 направления + 4 диагонали
Шесть кнопок справа: A, B, C, x, y, z
Кнопка Start посредине
Кнопка Mode на правом торце

На старом геймпаде есть «читерский» переключатель Normal/Turbo/Slow. В режиме Turbo при зажатии кнопки A/B/C/x/y/z геймпад будет передавать последовательность нажатий/отпусканий соответствующей кнопки с высокой частотой. В режиме Slow геймпад постоянно передает нажатия на кнопку Start несколько раз в секунду. При этом игра несколько раз в секунду встает на паузу и получается что‑то вроде эффекта замедления. Переключатель работает аппаратно, на уровне самого геймпада, поэтому должен работать как с приставкой, так и с переходником.
Изначально у приставки Sega Mega Drive были геймпады только с тремя кнопками A, B, C, а геймпады с шестью кнопками появились позднее.

Большинство игр на Sega Mega Drive, рассчитанных на использование трехкнопочного геймпада могли работать и с шестикнопочным геймпадам, однако для некоторых игр нужно было включить режим обратной совместимости на геймпаде. Для этого на шестикнопочном геймпаде нужно было зажать кнопку Mode во время подключения к приставке, тогда геймпад распознавался приставкой, как трехкнопочный. Также некоторые игры использовали кнопку Mode как дополнительную кнопку в самой игре. Подробнее про разные типы геймпадов, обратную совместимость и кнопку Mode можно почитать в этой статье.
Итак, задача будет состоять в том, чтобы подключить эти геймпады к ПК и использовать их для игр со старых приставок, при этом не вносить изменения в конструкцию самих геймпадов. Значит нужно сделать переходник на основе микроконтроллера, который будет опрашивать геймпады, как это делала оригинальная приставка. При подключении к ПК микроконтроллер будет эмулировать клавиатуру или геймпад. Геймпада у меня два, поэтому будем делать переходник сразу для двух геймпадов.
Протокол опроса геймпада Sega Mega Drive
Посмотрим, как работает геймпад. Вот ссылки на описание протокола опроса геймпада:
Далее кратко опишу процесс опроса геймпада на основе этих источников.
Геймпад подключается к приставке с помощью разъема D-Sub DE-9 с девятью контактами.

Функции контактов:
pin 1, pin 2, pin 3, pin 4, pin 6, pin 9 — контакты для считывания значений кнопок.
pin 7 — контакт Select, на него приставка подает сигнал, который определяет, какие кнопки можно считать в данный момент.
pin 5 — напряжение питания (5 вольт, согласно документации).
pin 8 — земля (GND).
Начнем с протокола опроса трехкнопочного геймпада.
Для работы с геймпадом на контакт Select подаем попеременно высокий и низкий уровень сигнала и считываем значения нажатых кнопок с остальных контактов. При этом логика кнопок инвертированная: нажатой кнопке будет соответствовать низкий уровень напряжения (LOW).
В таблице ниже показано, значение каких кнопок можно считать с соответствующих контактов при подаче высокого и низкого уровня сигнала на к��нтакт Select (pin 7).

(*) - на контактах pin 3 и pin 4 будет значение LOW, когда контакт Select (pin 7) установлен в значение LOW. Таким образом можно определить, что контроллер подключен.
Для тестирования геймпада сделаем переходник из разъема D-Sub. Подключать будем к Arduino Leonardo (ATMEGA32U4), для тестирования подойдет любая Arduino с напряжением 5 вольт.


Напишем код для считывания значений с геймпада в соответствии с таблицей и выводом значений кнопок в последовательный порт.
Код тестирования трехкнопочного геймпада
#include <Arduino.h> const int pin1 = 1; const int pin2 = 2; const int pin3 = 3; const int pin4 = 4; const int pin6 = 5; const int pin7 = 6; const int pin9 = 7; const int pinSelect = pin7; const unsigned long delayBeforeReadMicros = 10; void setup() { Serial.begin(115200); pinMode(pin1, INPUT_PULLUP); pinMode(pin2, INPUT_PULLUP); pinMode(pin3, INPUT_PULLUP); pinMode(pin4, INPUT_PULLUP); pinMode(pin6, INPUT_PULLUP); pinMode(pin9, INPUT_PULLUP); pinMode(pinSelect, OUTPUT); digitalWrite(pinSelect, HIGH); } void loop() { // state 0 digitalWrite(pinSelect, LOW); delayMicroseconds(delayBeforeReadMicros); bool isConnected = !digitalRead(pin3) && !digitalRead(pin4); bool btnA = !digitalRead(pin6); bool btnStart = !digitalRead(pin9); // state 1 digitalWrite(pinSelect, HIGH); delayMicroseconds(delayBeforeReadMicros); bool btnUp = !digitalRead(pin1); bool btnDown = !digitalRead(pin2); bool btnLeft = !digitalRead(pin3); bool btnRight = !digitalRead(pin4); bool btnB = !digitalRead(pin6); bool btnC = !digitalRead(pin9); String outputString = String() + "isConnected:" + (int)isConnected + " Up:" + (int)btnUp + " Down:" + (int)btnDown + " Left:" + (int)btnLeft + " Right:" + (int)btnRight + " A:" + (int)btnA + " B:" + (int)btnB + " C:" + (int)btnC + " Start:" + (int)btnStart; Serial.println(outputString); delay(200); }
При нажатой кнопке сигнал на соответствующем контакте будет LOW, поэтому для входов Arduino можно включить встроенные подтягивающие резисторы (INPUT_PULLUP).
При подключенном геймпаде в мониторе порта увидим такую картину:
isConnected:1 Up:0 Down:0 Left:0 Right:0 A:0 B:0 C:0 Start:0 isConnected:1 Up:1 Down:0 Left:0 Right:0 A:0 B:0 C:0 Start:0 isConnected:1 Up:1 Down:0 Left:0 Right:0 A:0 B:0 C:0 Start:0 isConnected:1 Up:0 Down:0 Left:0 Right:0 A:0 B:0 C:0 Start:1 isConnected:1 Up:0 Down:0 Left:0 Right:0 A:0 B:0 C:0 Start:1 isConnected:1 Up:0 Down:0 Left:0 Right:0 A:1 B:0 C:0 Start:0 isConnected:1 Up:0 Down:0 Left:0 Right:0 A:1 B:1 C:0 Start:0 isConnected:1 Up:0 Down:0 Left:0 Right:0 A:0 B:1 C:0 Start:0
Для шестикнопочного геймпада нужно несколько раз подряд подать поочередно низкий и высокий уровень на контакт Select, чтобы считать значения всех кнопок. Затем нужно выдержать паузу перед следующим циклом опроса геймпада, чтобы геймпад вернулся к изначальному состоянию и начал заново выдавать значения кнопок в зависимости от значения на контакте Select.
График сигнала Select при опросе геймпада приведен на рисунке:

В описаниях протокола опроса геймпада не говорится, какие должны быть точные значения задержек между сменой сигнала на контакте Select и паузы между циклами опроса геймпада, поэтому на графике и в коде приведены задержки исходя из рекомендаций в исходных статьях, которые работают для моих геймпадов.
Для каждого значения контакта Select в процессе опроса геймпада в таблице указано, значения каких кнопок можно прочитать с соответствующих контактов. Также, как для трехкнопочного геймпада, здесь нажатой кнопке соответствует низкий уровень сигнала (LOW).

(*) - на контактах pin 3 и pin 4 будет значение LOW, когда контакт Select (pin 7) установлен в значение LOW первый и второй раз в процессе опроса геймпада (State 0 и State 2). Таким образом можно определить, что контроллер подключен.
(**) - на контактах pin 1 и pin 2 будет значение LOW, когда контакт Select (pin 7) установлен в значение LOW в третий раз (State 4). Таким образом можно определить, что подключен шестикнопочный геймпад.
В таблице видно, что State 0 и State 1 у шестикнопочного геймпада такие же, как у трехкнопочного. Таким образом обеспечивается обратная совместимость: если опрашивать шестикнопочный геймпад как трехкнопочный, то будут считаны точно такие же значения. Нужно только добавлять паузу после State 0 и State 1, чтобы шестикнопочный геймпад вернулся к изначальному состоянию, вот таким образом:

Однако не все игры на Sega Mega Drive добавляют эту паузу и логика опроса геймпада в таких играх ломается. Чтобы обеспечить совместимость с таким играми нужно подключить шестикнопочный геймпад к приставке с зажатой кнопкой Mode. Тогда геймпад будет работать, как трехкнопочный. В остальном кнопка Mode ведет себя как обычная кнопка. (Подробности про кнопку Mode и совместимость геймпадов с играми).
Код для тестирования шестикнопочного геймпада
#include <Arduino.h> const int pin1 = 1; const int pin2 = 2; const int pin3 = 3; const int pin4 = 4; const int pin6 = 5; const int pin7 = 6; const int pin9 = 7; const int pinSelect = pin7; const unsigned long delayBeforeReadMicros = 10; void setup() { Serial.begin(115200); pinMode(pin1, INPUT_PULLUP); pinMode(pin2, INPUT_PULLUP); pinMode(pin3, INPUT_PULLUP); pinMode(pin4, INPUT_PULLUP); pinMode(pin6, INPUT_PULLUP); pinMode(pin9, INPUT_PULLUP); pinMode(pinSelect, OUTPUT); digitalWrite(pinSelect, HIGH); } void loop() { // state 0 digitalWrite(pinSelect, LOW); delayMicroseconds(delayBeforeReadMicros); bool isConnected = !digitalRead(pin3) && !digitalRead(pin4); bool btnA = !digitalRead(pin6); bool btnStart = !digitalRead(pin9); // state 1 digitalWrite(pinSelect, HIGH); delayMicroseconds(delayBeforeReadMicros); bool btnUp = !digitalRead(pin1); bool btnDown = !digitalRead(pin2); bool btnLeft = !digitalRead(pin3); bool btnRight = !digitalRead(pin4); bool btnB = !digitalRead(pin6); bool btnC = !digitalRead(pin9); // state 2 digitalWrite(pinSelect, LOW); delayMicroseconds(delayBeforeReadMicros); // state 3 digitalWrite(pinSelect, HIGH); delayMicroseconds(delayBeforeReadMicros); // state 4 digitalWrite(pinSelect, LOW); delayMicroseconds(delayBeforeReadMicros); bool isSixBtns = !digitalRead(pin1) && !digitalRead(pin2); // state 5 digitalWrite(pinSelect, HIGH); delayMicroseconds(delayBeforeReadMicros); bool btnZ = !digitalRead(pin1); bool btnY = !digitalRead(pin2); bool btnX = !digitalRead(pin3); bool btnMode = !digitalRead(pin4); // state 6 digitalWrite(pinSelect, LOW); delayMicroseconds(delayBeforeReadMicros); // state 7 digitalWrite(pinSelect, HIGH); delayMicroseconds(delayBeforeReadMicros); String outputString = String() + "isConnected:" + (int)isConnected + " isSixBtns:" + (int)isSixBtns + " Up:" + (int)btnUp + " Down:" + (int)btnDown + " Left:" + (int)btnLeft + " Right:" + (int)btnRight + " A:" + (int)btnA + " B:" + (int)btnB + " C:" + (int)btnC + " x:" + (int)btnX + " y:" + (int)btnY + " z:" + (int)btnZ + " Start:" + (int)btnStart + " Mode:" + (int)btnMode; Serial.println(outputString); delay(200); }
В коде добавлена проверка того, какой подключен геймпад: трех- или шестикнопочный. Это можно видеть в мониторе порта (флаг isSixBtns):
isConnected:1 isSixBtns:1 Up:0 Down:0 Left:1 Right:0 A:0 B:0 C:0 x:0 y:0 z:0 Start:0 Mode:0 isConnected:1 isSixBtns:1 Up:0 Down:0 Left:1 Right:0 A:0 B:0 C:0 x:1 y:0 z:0 Start:1 Mode:0 isConnected:1 isSixBtns:1 Up:0 Down:0 Left:1 Right:0 A:0 B:0 C:0 x:1 y:0 z:0 Start:0 Mode:1 isConnected:1 isSixBtns:1 Up:0 Down:0 Left:0 Right:0 A:0 B:0 C:0 x:1 y:0 z:0 Start:0 Mode:1 isConnected:1 isSixBtns:1 Up:0 Down:0 Left:0 Right:0 A:0 B:0 C:0 x:0 y:1 z:1 Start:0 Mode:0 isConnected:1 isSixBtns:1 Up:1 Down:0 Left:0 Right:0 A:0 B:1 C:0 x:0 y:1 z:1 Start:0 Mode:0 isConnected:1 isSixBtns:1 Up:1 Down:0 Left:0 Right:0 A:1 B:1 C:0 x:0 y:0 z:0 Start:0 Mode:0 isConnected:1 isSixBtns:1 Up:1 Down:0 Left:0 Right:0 A:1 B:1 C:0 x:0 y:0 z:0 Start:0 Mode:0
Если подключить геймпад с зажатой кнопкой Mode, то можно видеть, что он распознается, как трехкнопочный (выводится значение isSixBtns:0):
isConnected:1 isSixBtns:0 Up:0 Down:0 Left:1 Right:0 A:0 B:0 C:0 x:0 y:0 z:0 Start:0 Mode:0 isConnected:1 isSixBtns:0 Up:0 Down:0 Left:1 Right:0 A:0 B:0 C:0 x:0 y:0 z:0 Start:0 Mode:0 isConnected:1 isSixBtns:0 Up:0 Down:0 Left:0 Right:0 A:0 B:0 C:0 x:0 y:0 z:0 Start:0 Mode:0 isConnected:1 isSixBtns:0 Up:0 Down:0 Left:0 Right:0 A:0 B:0 C:0 x:0 y:0 z:0 Start:0 Mode:0 isConnected:1 isSixBtns:0 Up:0 Down:1 Left:0 Right:0 A:0 B:0 C:0 x:0 y:0 z:0 Start:0 Mode:0 isConnected:1 isSixBtns:0 Up:0 Down:1 Left:0 Right:0 A:0 B:1 C:0 x:0 y:0 z:0 Start:0 Mode:0 isConnected:1 isSixBtns:0 Up:0 Down:0 Left:0 Right:0 A:1 B:1 C:0 x:0 y:0 z:0 Start:0 Mode:0 isConnected:1 isSixBtns:0 Up:0 Down:0 Left:0 Right:0 A:1 B:1 C:0 x:0 y:0 z:0 Start:0 Mode:0
Подключение двух геймпадов
Для переходника я использовал два разъема D-Sub DE-9 в пластиковом корпусе и Arduino Pro Micro в качестве контроллера. Pro Micro использует микроконтроллер ATmega32u4, подойдет любой аналогичный контроллер маленького размера с достаточным количеством портов ввода/вывода и возможностью эмуляции USB-клавиатуры. Для подключения двух геймпадов понадобиться 14 портов ввода/вывода.

Припаял проводами контакты разъемов к плате Arduino, положил два разъема один на другой, соединил их пластиковыми стяжками, сверху приклеил плату контроллера, получилась такая конструкция:

Затем приклеил сверху согнутую пластиковую карточку, чтобы закрыть провода:


Геймпада два, поэтому, чтобы не дублировать код опроса геймпада, завернем его в класс SegaGamepad. Класс опубликован в виде библиотеки на платформе PlatformIO и в репозитории на GitHub:
Там можно посмотреть исходный код и примеры. Здесь приведу объявление класса SegaGamepad:
SegaGamepad.h
#ifndef SegaGamepad_h #define SegaGamepad_h #include <Arduino.h> class SegaGamepad { public: SegaGamepad(uint8_t pin1, uint8_t pin2, uint8_t pin3, uint8_t pin4, uint8_t pin6, uint8_t pin7, uint8_t pin9, unsigned int delayBeforeReadMicros = 10, unsigned int delayBeforeNextUpdateMicros = 2000); void init(); void update(); bool isConnected = false; bool isSixBtns = false; bool btnUp = false; bool btnDown = false; bool btnLeft = false; bool btnRight = false; bool btnA = false; bool btnB = false; bool btnC = false; bool btnX = false; bool btnY = false; bool btnZ = false; bool btnStart = false; bool btnMode = false; private: uint8_t pin1; uint8_t pin2; uint8_t pin3; uint8_t pin4; uint8_t pin6; uint8_t pinSelect; uint8_t pin9; unsigned int delayBeforeReadMicros; unsigned int delayBeforeNextUpdateMicros; unsigned long previousUpdateTime = 0; }; #endif
Прошивка переходника будет опрашивать геймпады и передавать значения нажатых кнопок на ПК в виде нажатий кнопок клавиатуры. Для эмуляции клавиатуры будем использовать библиотеку <Keyboard.h>
Раскладка клавиатуры для геймпадов будет такая:

Код прошивки переходника для двух геймпадов
#include <Arduino.h> #include <Keyboard.h> #include "SegaGamepad.h" const bool serialPrintEnabled = true; unsigned long previousKeyUpdateTime = 0; const unsigned int delayBeforeReadMicros = 10; const unsigned int delayBeforeNextUpdateMicros = 2000; SegaGamepad segaGamepad1(A0, A1, A2, A3, 1, 0, 2, delayBeforeReadMicros, delayBeforeNextUpdateMicros); SegaGamepad segaGamepad2(9, 8, 7, 6, 5, 4, 3, delayBeforeReadMicros, delayBeforeNextUpdateMicros); const int keysCount = 12; const char* keysNames[keysCount] = { "btnUp", "btnDown", "btnLeft", "btnRight", "btnA", "btnB", "btnC", "btnX", "btnY", "btnZ", "btnStart", "btnMode" }; const uint8_t keysKeyboard1[keysCount] = { 'w', 's', 'a', 'd', 'j', 'k', 'l', 'u', 'i', 'o', KEY_RETURN, '\\' }; const uint8_t keysKeyboard2[keysCount] = { 't', 'g', 'f', 'h', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',' }; void printGamepadStatusOnSetup(SegaGamepad& segaGamepad, int gamepadIndex); void handleGamepad(SegaGamepad& segaGamepad, int gamepadIndex, const uint8_t keysKeyboard[]); void updateKeyboard(bool keys[], bool keysPrevious[], const uint8_t keysKeyboard[]); void printGamepadStatus(SegaGamepad& segaGamepad, int gamepadIndex, bool keys[], bool keysPrevious[], bool isConnectedPrevious, bool isSixButtonsPrevious); void initKeys(bool keys[], SegaGamepad& segaGamepad); void setup() { Serial.begin(115200); Keyboard.begin(); segaGamepad1.init(); segaGamepad2.init(); int gamepadReadigsToDiscard = 2; for (int i = 0; i < gamepadReadigsToDiscard + 1; i++) { segaGamepad1.update(); segaGamepad2.update(); } if (serialPrintEnabled) { delay(5000); printGamepadStatusOnSetup(segaGamepad1, 1); printGamepadStatusOnSetup(segaGamepad2, 2); } } void loop() { handleGamepad(segaGamepad1, 1, keysKeyboard1); handleGamepad(segaGamepad2, 2, keysKeyboard2); } void printGamepadStatusOnSetup(SegaGamepad& segaGamepad, int gamepadIndex) { if (segaGamepad.isConnected) { Serial.print("Gamepad "); Serial.print(gamepadIndex); Serial.println(" connected"); } else { Serial.print("Gamepad "); Serial.print(gamepadIndex); Serial.println(" disconnected"); } if (segaGamepad.isConnected && segaGamepad.isSixBtns) { Serial.print("Gamepad "); Serial.print(gamepadIndex); Serial.print(" type: "); if (segaGamepad.isSixBtns) { Serial.println("\"six buttons\""); } else { Serial.println("\"three buttons\""); } } } void handleGamepad(SegaGamepad& segaGamepad, int gamepadIndex, const uint8_t keysKeyboard[]) { bool keysPrevious[keysCount]; initKeys(keysPrevious, segaGamepad); bool isConnectedPrevious = segaGamepad.isConnected; bool isSixButtonsPrevious = segaGamepad.isSixBtns; segaGamepad.update(); bool keys[keysCount]; initKeys(keys, segaGamepad); updateKeyboard(keys, keysPrevious, keysKeyboard); if (serialPrintEnabled) { printGamepadStatus(segaGamepad, gamepadIndex, keys, keysPrevious, isConnectedPrevious, isSixButtonsPrevious); } } void updateKeyboard(bool keys[], bool keysPrevious[], const uint8_t keysKeyboard[]) { for (int i = 0; i < keysCount; i++) { if (keys[i] && !keysPrevious[i]) { Keyboard.press(keysKeyboard[i]); } if (!keys[i] && keysPrevious[i]) { Keyboard.release(keysKeyboard[i]); } } } void printGamepadStatus(SegaGamepad& segaGamepad, int gamepadIndex, bool keys[], bool keysPrevious[], bool isConnectedPrevious, bool isSixButtonsPrevious) { unsigned long currentTime = millis(); for (int i = 0; i < keysCount; i++) { if (keys[i] && !keysPrevious[i]) { Serial.print("+ "); Serial.print(currentTime - previousKeyUpdateTime); Serial.print(" ms "); Serial.print(keysNames[i]); Serial.print(" pressed on gamepad "); Serial.println(gamepadIndex); previousKeyUpdateTime = currentTime; } if (!keys[i] && keysPrevious[i]) { Serial.print("+ "); Serial.print(currentTime - previousKeyUpdateTime); Serial.print(" ms "); Serial.print(keysNames[i]); Serial.print(" released on gamepad "); Serial.println(gamepadIndex); previousKeyUpdateTime = currentTime; } } if (segaGamepad.isConnected && !isConnectedPrevious) { Serial.print("Gamepad "); Serial.print(gamepadIndex); Serial.println(" connected"); } if (!segaGamepad.isConnected && isConnectedPrevious) { Serial.print("Gamepad "); Serial.print(gamepadIndex); Serial.println(" disconnected"); } if (segaGamepad.isConnected && segaGamepad.isSixBtns != isSixButtonsPrevious) { Serial.print("Gamepad "); Serial.print(gamepadIndex); Serial.print(" type changed to "); if (segaGamepad.isSixBtns) { Serial.println("\"six buttons\""); } else { Serial.println("\"three buttons\""); } } } void initKeys(bool keys[], SegaGamepad& segaGamepad) { keys[0] = segaGamepad.btnUp; keys[1] = segaGamepad.btnDown; keys[2] = segaGamepad.btnLeft; keys[3] = segaGamepad.btnRight; keys[4] = segaGamepad.btnA; keys[5] = segaGamepad.btnB; keys[6] = segaGamepad.btnC; keys[7] = segaGamepad.btnX; keys[8] = segaGamepad.btnY; keys[9] = segaGamepad.btnZ; keys[10] = segaGamepad.btnStart; keys[11] = segaGamepad.btnMode; }
Далее кратко опишу работу прошивки.
Для каждого геймпада создается глобальный экземпляр класса SegaGamepad:
const unsigned int delayBeforeReadMicros = 10; const unsigned int delayBeforeNextUpdateMicros = 2000; SegaGamepad segaGamepad1(A0, A1, A2, A3, 1, 0, 2, delayBeforeReadMicros, delayBeforeNextUpdateMicros); SegaGamepad segaGamepad2(9, 8, 7, 6, 5, 4, 3, delayBeforeReadMicros, delayBeforeNextUpdateMicros);
В конструктор передаются номера портов контроллера, к которым подключен геймпад и значения задержек между сменами значения на контакте Select (значение delayBeforeReadMicros) и задержки перед следующим опросом геймпада (значение delayBeforeNextUpdateMicros). Эти значения задержек минимальные, которые стабильно работают с моими геймпадами, для других геймпадов возможно их придется увеличить.
В процедуре setup() инициализируются порты, к которым подключены геймпады, вызовом метода SegaGamepad::init(). Далее считываем несколько раз данные с геймпадов и выводим статус геймпадов в последовательный порт:
segaGamepad1.init(); segaGamepad2.init(); int gamepadReadigsToDiscard = 2; for (int i = 0; i < gamepadReadigsToDiscard + 1; i++) { segaGamepad1.update(); segaGamepad2.update(); } if (serialPrintEnabled) { delay(5000); printGamepadStatusOnSetup(segaGamepad1, 1); printGamepadStatusOnSetup(segaGamepad2, 2); }
При старте программы в последовательный порт будут выведены следующие данные:
Gamepad 1 connected Gamepad 1 type: "six buttons" Gamepad 2 connected Gamepad 2 type: "six buttons"
В цикле loop() для каждого геймпада вызываем процедуру handleGamepad():
void loop() { handleGamepad(segaGamepad1, 1, keysKeyboard1); handleGamepad(segaGamepad2, 2, keysKeyboard2); }
В этой процедуре заполняем массив keysPrevious значениями кнопок с предыдущей итерации, обновляем состояние геймпада и заполняем массив keys текущими значениями кнопок.
Процедура initKeys() для заполнения массива keys значениями кнопок геймпада:
void initKeys(bool keys[], SegaGamepad& segaGamepad) { keys[0] = segaGamepad.btnUp; keys[1] = segaGamepad.btnDown; keys[2] = segaGamepad.btnLeft; keys[3] = segaGamepad.btnRight; keys[4] = segaGamepad.btnA; keys[5] = segaGamepad.btnB; keys[6] = segaGamepad.btnC; keys[7] = segaGamepad.btnX; keys[8] = segaGamepad.btnY; keys[9] = segaGamepad.btnZ; keys[10] = segaGamepad.btnStart; keys[11] = segaGamepad.btnMode; }
Раскладка клавиатуры хранится в массивах keysKeyboard1 и keysKeyboard2:
const uint8_t keysKeyboard1[keysCount] = { 'w', 's', 'a', 'd', 'j', 'k', 'l', 'u', 'i', 'o', KEY_RETURN, '\\' }; const uint8_t keysKeyboard2[keysCount] = { 't', 'g', 'f', 'h', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',' };
Расположение кнопок в массиве совпадает с расположением кнопок в массиве keys, заполняемом в процедуре initKeys().
Далее в процедуре handleGamepad() вызываем процедуры updateKeyboard() и printGamepadStatus() для обновления состояния кнопок виртуальной клавиатуры и вывода в последовательный порт сообщения о нажатиях кнопок. Обновление состояния кнопок клавиатуры и вывод сообщения в последовательный порт происходит только если состояние кнопок геймпада изменилось.
Процедура updateKeyboard():
void updateKeyboard(bool keys[], bool keysPrevious[], const uint8_t keysKeyboard[]) { for (int i = 0; i < keysCount; i++) { if (keys[i] && !keysPrevious[i]) { Keyboard.press(keysKeyboard[i]); } if (!keys[i] && keysPrevious[i]) { Keyboard.release(keysKeyboard[i]); } } }
Ниже пример вывода данных в последовательный порт. В начале каждой строки выводится время, прошедшее с предыдущего нажатия/отпускания кнопки:
+ 7896 ms btnRight pressed on gamepad 1 + 191 ms btnRight released on gamepad 1 + 2406 ms btnUp pressed on gamepad 1 + 1367 ms btnUp released on gamepad 1 + 1548 ms btnStart pressed on gamepad 1 + 1097 ms btnStart released on gamepad 1 + 2747 ms btnA pressed on gamepad 1 + 62 ms btnA released on gamepad 1 + 1891 ms btnB pressed on gamepad 1 + 2772 ms btnB released on gamepad 1
Переключатель Normal/Turbo/Slow тоже работает с этим переходником. Ниже вывод в последовательный порт для режима Slow. видно, что автоматически нажимается кнопка Start:
+ 27 ms btnStart pressed on gamepad 1 + 28 ms btnStart released on gamepad 1 + 27 ms btnStart pressed on gamepad 1 + 29 ms btnStart released on gamepad 1 + 26 ms btnStart pressed on gamepad 1 + 27 ms btnStart released on gamepad 1 + 28 ms btnStart pressed on gamepad 1 + 27 ms btnStart released on gamepad 1
И зажатая кнопка A в режиме Turbo:
+ 29 ms btnA released on gamepad 1 + 26 ms btnA pressed on gamepad 1 + 29 ms btnA released on gamepad 1 + 26 ms btnA pressed on gamepad 1 + 27 ms btnA released on gamepad 1 + 29 ms btnA pressed on gamepad 1 + 21 ms btnA released on gamepad 1
Устранение дребезга кнопки Mode
Получившаяся прошивка работает, но при нажатии кнопки Mode наблюдается такая картина:
+ 3718 ms btnMode pressed on gamepad 1 // Кнопка нажата + 1 ms btnMode pressed on gamepad 1 // Ложные срабатывания (дребезг контактов) + 2 ms btnMode released on gamepad 1 + 515 ms btnMode released on gamepad 1 // Кнопка отпущена + 2 ms btnMode pressed on gamepad 1 // Ложные срабатывания (дребезг контактов) + 21 ms btnMode released on gamepad 1 + 2 ms btnMode pressed on gamepad 1 + 15 ms btnMode released on gamepad 1
При нажатии этой кнопки происходит много ложных срабатываний. Остальные кнопки геймпада имеют контакты из токопроводящей резины, а кнопка Mode это обычная тактовая кнопка с металлическими контактами. Такие кнопки подвержены дребезгу контактов.

Для устранения этого явления воспользуемся классом ButtonDebounce:
ButtonDebounce.h
#ifndef ButtonDebounce_h #define ButtonDebounce_h #include <Arduino.h> class ButtonDebounce { public: ButtonDebounce(unsigned long debounceDelayMillis = 100); bool isBtnPressed = false; bool isBtnReleased = false; bool btnState = false; void updateState(bool btnStateInput); private: bool debounceDelayPassed = false; unsigned long debounceDelayMillis; unsigned long previousStateInternalChangeTime = 0; bool btnStateInternal = false; }; #endif
В этом классе фильтруется состояние кнопки в поле ButtonDebounce::btnState. Значение ButtonDebounce::btnState изменится только после того, как прошел таймаут debounceDelayMillis с момента последнего изменения состояния кнопки, которое передается в метод ButtonDebounce::updateState(bool btnStateInput) в каждой итерации цикла loop().
Изменения в коде прошивки переходника будут небольшие. Сначала добавим глобальные объекты класса ButtonDebounce для обоих контроллеров. В конструктор передадим параметр debounceDelayMillis:
const unsigned long debounceDelayMillis = 50; ButtonDebounce modeButtonDebounce1(debounceDelayMillis); ButtonDebounce modeButtonDebounce2(debounceDelayMillis);
И в массив значений кнопок геймпада будем передавать отфильтрованное значение состояния кнопки Mode из поля ButtonDebounce::btnState:
modeButtonDebounce.updateState(segaGamepad.btnMode); keys[11] = modeButtonDebounce.btnState;
Полный текст прошивки доступен на GitHub:
Код прошивки с устранением дребезга кнопки Mode
#include <Arduino.h> #include <Keyboard.h> #include "SegaGamepad.h" #include "ButtonDebounce.h" const bool serialPrintEnabled = true; unsigned long previousKeyUpdateTime = 0; const unsigned int delayBeforeReadMicros = 10; const unsigned int delayBeforeNextUpdateMicros = 2000; SegaGamepad segaGamepad1(A0, A1, A2, A3, 1, 0, 2, delayBeforeReadMicros, delayBeforeNextUpdateMicros); SegaGamepad segaGamepad2(9, 8, 7, 6, 5, 4, 3, delayBeforeReadMicros, delayBeforeNextUpdateMicros); const unsigned long debounceDelayMillis = 50; ButtonDebounce modeButtonDebounce1(debounceDelayMillis); ButtonDebounce modeButtonDebounce2(debounceDelayMillis); const int keysCount = 12; const char* keysNames[keysCount] = { "btnUp", "btnDown", "btnLeft", "btnRight", "btnA", "btnB", "btnC", "btnX", "btnY", "btnZ", "btnStart", "btnMode" }; const uint8_t keysKeyboard1[keysCount] = { 'w', 's', 'a', 'd', 'j', 'k', 'l', 'u', 'i', 'o', KEY_RETURN, '\\' }; const uint8_t keysKeyboard2[keysCount] = { 't', 'g', 'f', 'h', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',' }; void printGamepadStatusOnSetup(SegaGamepad& segaGamepad, int gamepadIndex); void handleGamepad(SegaGamepad& segaGamepad, int gamepadIndex, ButtonDebounce& modeButtonDebounce, const uint8_t keysKeyboard[]); void updateKeyboard(bool keys[], bool keysPrevious[], const uint8_t keysKeyboard[]); void printGamepadStatus(SegaGamepad& segaGamepad, int gamepadIndex, bool keys[], bool keysPrevious[], bool isConnectedPrevious, bool isSixButtonsPrevious); void initKeys(bool keys[], SegaGamepad& segaGamepad, ButtonDebounce& modeButtonDebounce); void setup() { Serial.begin(115200); Keyboard.begin(); segaGamepad1.init(); segaGamepad2.init(); int gamepadReadigsToDiscard = 2; for (int i = 0; i < gamepadReadigsToDiscard + 1; i++) { segaGamepad1.update(); segaGamepad2.update(); } if (serialPrintEnabled) { delay(5000); printGamepadStatusOnSetup(segaGamepad1, 1); printGamepadStatusOnSetup(segaGamepad2, 2); } } void loop() { handleGamepad(segaGamepad1, 1, modeButtonDebounce1, keysKeyboard1); handleGamepad(segaGamepad2, 2, modeButtonDebounce2, keysKeyboard2); } void printGamepadStatusOnSetup(SegaGamepad& segaGamepad, int gamepadIndex) { if (segaGamepad.isConnected) { Serial.print("Gamepad "); Serial.print(gamepadIndex); Serial.println(" connected"); } else { Serial.print("Gamepad "); Serial.print(gamepadIndex); Serial.println(" disconnected"); } if (segaGamepad.isConnected && segaGamepad.isSixBtns) { Serial.print("Gamepad "); Serial.print(gamepadIndex); Serial.print(" type: "); if (segaGamepad.isSixBtns) { Serial.println("\"six buttons\""); } else { Serial.println("\"three buttons\""); } } } void handleGamepad(SegaGamepad& segaGamepad, int gamepadIndex, ButtonDebounce& modeButtonDebounce, const uint8_t keysKeyboard[]) { bool keysPrevious[keysCount]; initKeys(keysPrevious, segaGamepad, modeButtonDebounce); bool isConnectedPrevious = segaGamepad.isConnected; bool isSixButtonsPrevious = segaGamepad.isSixBtns; segaGamepad.update(); bool keys[keysCount]; initKeys(keys, segaGamepad, modeButtonDebounce); updateKeyboard(keys, keysPrevious, keysKeyboard); if (serialPrintEnabled) { printGamepadStatus(segaGamepad, gamepadIndex, keys, keysPrevious, isConnectedPrevious, isSixButtonsPrevious); } } void updateKeyboard(bool keys[], bool keysPrevious[], const uint8_t keysKeyboard[]) { for (int i = 0; i < keysCount; i++) { if (keys[i] && !keysPrevious[i]) { Keyboard.press(keysKeyboard[i]); } if (!keys[i] && keysPrevious[i]) { Keyboard.release(keysKeyboard[i]); } } } void printGamepadStatus(SegaGamepad& segaGamepad, int gamepadIndex, bool keys[], bool keysPrevious[], bool isConnectedPrevious, bool isSixButtonsPrevious) { unsigned long currentTime = millis(); for (int i = 0; i < keysCount; i++) { if (keys[i] && !keysPrevious[i]) { Serial.print("+ "); Serial.print(currentTime - previousKeyUpdateTime); Serial.print(" ms "); Serial.print(keysNames[i]); Serial.print(" pressed on gamepad "); Serial.println(gamepadIndex); previousKeyUpdateTime = currentTime; } if (!keys[i] && keysPrevious[i]) { Serial.print("+ "); Serial.print(currentTime - previousKeyUpdateTime); Serial.print(" ms "); Serial.print(keysNames[i]); Serial.print(" released on gamepad "); Serial.println(gamepadIndex); previousKeyUpdateTime = currentTime; } } if (segaGamepad.isConnected && !isConnectedPrevious) { Serial.print("Gamepad "); Serial.print(gamepadIndex); Serial.println(" connected"); } if (!segaGamepad.isConnected && isConnectedPrevious) { Serial.print("Gamepad "); Serial.print(gamepadIndex); Serial.println(" disconnected"); } if (segaGamepad.isConnected && segaGamepad.isSixBtns != isSixButtonsPrevious) { Serial.print("Gamepad "); Serial.print(gamepadIndex); Serial.print(" type changed to "); if (segaGamepad.isSixBtns) { Serial.println("\"six buttons\""); } else { Serial.println("\"three buttons\""); } } } void initKeys(bool keys[], SegaGamepad& segaGamepad, ButtonDebounce& modeButtonDebounce) { keys[0] = segaGamepad.btnUp; keys[1] = segaGamepad.btnDown; keys[2] = segaGamepad.btnLeft; keys[3] = segaGamepad.btnRight; keys[4] = segaGamepad.btnA; keys[5] = segaGamepad.btnB; keys[6] = segaGamepad.btnC; keys[7] = segaGamepad.btnX; keys[8] = segaGamepad.btnY; keys[9] = segaGamepad.btnZ; keys[10] = segaGamepad.btnStart; modeButtonDebounce.updateState(segaGamepad.btnMode); keys[11] = modeButtonDebounce.btnState; }
После обновления прошивки, кнопка Mode работает как надо. Три нажатия на кнопку Mode и никаких ложных срабатываний:
+ 2448 ms btnMode pressed on gamepad 1 + 3579 ms btnMode released on gamepad 1 + 2638 ms btnMode pressed on gamepad 1 + 2406 ms btnMode released on gamepad 1 + 1579 ms btnMode pressed on gamepad 1 + 4229 ms btnMode released on gamepad 1
Добавление режима эмуляции USB-геймпада
Через некоторое время использования переходника захотелось сделать, чтобы ПК видел его как геймпад, а не как клавиатуру. Это упростит маппинг кнопок в некоторых играх и освободит клавиатуру, которую можно использовать, например для маппинга кнопок для ещё одного игрока. При этом хотелось бы сохранить режим клавиатуры. Кнопки для переключения режимов на переходнике нет, поэтому будем испо��ьзовать кнопки самого геймпада.
Для эмуляции геймпада при подключении к ПК будем использовать библиотеку <Joystick.h>.
На GitHub выложены две версии прошивки:
Эмуляция только виртуальных геймпадов
#include <Arduino.h> #include <Joystick.h> #include "SegaGamepad.h" #include "ButtonDebounce.h" const bool serialPrintEnabled = true; unsigned long previousKeyUpdateTime = 0; Joystick_ joystick1(JOYSTICK_DEFAULT_REPORT_ID, JOYSTICK_TYPE_GAMEPAD, 8, 1, false, false, false, false, false, false, false, false, false, false, false); Joystick_ joystick2(JOYSTICK_DEFAULT_REPORT_ID + 1, JOYSTICK_TYPE_GAMEPAD, 8, 1, false, false, false, false, false, false, false, false, false, false, false); const unsigned int delayBeforeReadMicros = 10; const unsigned int delayBeforeNextUpdateMicros = 2000; SegaGamepad segaGamepad1(A0, A1, A2, A3, 1, 0, 2, delayBeforeReadMicros, delayBeforeNextUpdateMicros); SegaGamepad segaGamepad2(9, 8, 7, 6, 5, 4, 3, delayBeforeReadMicros, delayBeforeNextUpdateMicros); const unsigned long debounceDelayMillis = 50; ButtonDebounce modeButtonDebounce1(debounceDelayMillis); ButtonDebounce modeButtonDebounce2(debounceDelayMillis); const int keysCount = 12; const char* keysNames[keysCount] = { "btnUp", "btnDown", "btnLeft", "btnRight", "btnA", "btnB", "btnC", "btnX", "btnY", "btnZ", "btnStart", "btnMode" }; const uint8_t keysJoystick[keysCount] = { 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7 }; void printGamepadStatusOnSetup(SegaGamepad& segaGamepad, int gamepadIndex); void handleGamepad(SegaGamepad& segaGamepad, int gamepadIndex, ButtonDebounce& modeButtonDebounce, Joystick_& joystick); void updateJoystick(SegaGamepad& segaGamepad, bool keys[], bool keysPrevious[], Joystick_& joystick); void printGamepadStatus(SegaGamepad& segaGamepad, int gamepadIndex, bool keys[], bool keysPrevious[], bool isConnectedPrevious, bool isSixButtonsPrevious); void initKeys(bool keys[], SegaGamepad& segaGamepad, ButtonDebounce& modeButtonDebounce); void setup() { Serial.begin(115200); joystick1.begin(); joystick2.begin(); segaGamepad1.init(); segaGamepad2.init(); int gamepadReadigsToDiscard = 2; for (int i = 0; i < gamepadReadigsToDiscard + 1; i++) { segaGamepad1.update(); segaGamepad2.update(); } if (serialPrintEnabled) { delay(5000); printGamepadStatusOnSetup(segaGamepad1, 1); printGamepadStatusOnSetup(segaGamepad2, 2); } } void loop() { handleGamepad(segaGamepad1, 1, modeButtonDebounce1, joystick1); handleGamepad(segaGamepad2, 2, modeButtonDebounce2, joystick2); } void printGamepadStatusOnSetup(SegaGamepad& segaGamepad, int gamepadIndex) { if (segaGamepad.isConnected) { Serial.print("Gamepad "); Serial.print(gamepadIndex); Serial.println(" connected"); } else { Serial.print("Gamepad "); Serial.print(gamepadIndex); Serial.println(" disconnected"); } if (segaGamepad.isConnected && segaGamepad.isSixBtns) { Serial.print("Gamepad "); Serial.print(gamepadIndex); Serial.print(" type: "); if (segaGamepad.isSixBtns) { Serial.println("\"six buttons\""); } else { Serial.println("\"three buttons\""); } } } void handleGamepad(SegaGamepad& segaGamepad, int gamepadIndex, ButtonDebounce& modeButtonDebounce, Joystick_& joystick) { bool keysPrevious[keysCount]; initKeys(keysPrevious, segaGamepad, modeButtonDebounce); bool isConnectedPrevious = segaGamepad.isConnected; bool isSixButtonsPrevious = segaGamepad.isSixBtns; segaGamepad.update(); bool keys[keysCount]; initKeys(keys, segaGamepad, modeButtonDebounce); updateJoystick(segaGamepad, keys, keysPrevious, joystick); if (serialPrintEnabled) { printGamepadStatus(segaGamepad, gamepadIndex, keys, keysPrevious, isConnectedPrevious, isSixButtonsPrevious); } } void updateJoystick(SegaGamepad& segaGamepad, bool keys[], bool keysPrevious[], Joystick_& joystick) { for (int i = 0; i < keysCount; i++) { if (keys[i] && !keysPrevious[i]) { if (i >= 4) { joystick.pressButton(keysJoystick[i]); } } if (!keys[i] && keysPrevious[i]) { if (i >= 4) { joystick.releaseButton(keysJoystick[i]); } } } bool isArrowChanged = false; for (int i = 0; i < 4; i++) { isArrowChanged = isArrowChanged || (keys[i] != keysPrevious[i]); } if (isArrowChanged) { if (segaGamepad.btnUp && segaGamepad.btnRight) { joystick.setHatSwitch(0, 45); } else if (segaGamepad.btnRight && segaGamepad.btnDown) { joystick.setHatSwitch(0, 135); } else if (segaGamepad.btnDown && segaGamepad.btnLeft) { joystick.setHatSwitch(0, 225); } else if (segaGamepad.btnLeft && segaGamepad.btnUp) { joystick.setHatSwitch(0, 315); } else if (segaGamepad.btnUp) { joystick.setHatSwitch(0, 0); } else if (segaGamepad.btnRight) { joystick.setHatSwitch(0, 90); } else if (segaGamepad.btnDown) { joystick.setHatSwitch(0, 180); } else if (segaGamepad.btnLeft) { joystick.setHatSwitch(0, 270); } else { joystick.setHatSwitch(0, -1); } } } void printGamepadStatus(SegaGamepad& segaGamepad, int gamepadIndex, bool keys[], bool keysPrevious[], bool isConnectedPrevious, bool isSixButtonsPrevious) { unsigned long currentTime = millis(); for (int i = 0; i < keysCount; i++) { if (keys[i] && !keysPrevious[i]) { Serial.print("+ "); Serial.print(currentTime - previousKeyUpdateTime); Serial.print(" ms "); Serial.print(keysNames[i]); Serial.print(" pressed on gamepad "); Serial.println(gamepadIndex); previousKeyUpdateTime = currentTime; } if (!keys[i] && keysPrevious[i]) { Serial.print("+ "); Serial.print(currentTime - previousKeyUpdateTime); Serial.print(" ms "); Serial.print(keysNames[i]); Serial.print(" released on gamepad "); Serial.println(gamepadIndex); previousKeyUpdateTime = currentTime; } } if (segaGamepad.isConnected && !isConnectedPrevious) { Serial.print("Gamepad "); Serial.print(gamepadIndex); Serial.println(" connected"); } if (!segaGamepad.isConnected && isConnectedPrevious) { Serial.print("Gamepad "); Serial.print(gamepadIndex); Serial.println(" disconnected"); } if (segaGamepad.isConnected && segaGamepad.isSixBtns != isSixButtonsPrevious) { Serial.print("Gamepad "); Serial.print(gamepadIndex); Serial.print(" type changed to "); if (segaGamepad.isSixBtns) { Serial.println("\"six buttons\""); } else { Serial.println("\"three buttons\""); } } } void initKeys(bool keys[], SegaGamepad& segaGamepad, ButtonDebounce& modeButtonDebounce) { keys[0] = segaGamepad.btnUp; keys[1] = segaGamepad.btnDown; keys[2] = segaGamepad.btnLeft; keys[3] = segaGamepad.btnRight; keys[4] = segaGamepad.btnA; keys[5] = segaGamepad.btnB; keys[6] = segaGamepad.btnC; keys[7] = segaGamepad.btnX; keys[8] = segaGamepad.btnY; keys[9] = segaGamepad.btnZ; keys[10] = segaGamepad.btnStart; modeButtonDebounce.updateState(segaGamepad.btnMode); keys[11] = modeButtonDebounce.btnState; }
Эмуляция виртуальной клавиатуры и виртуальных геймпадов с возможностью выбора режима работы
#include <Arduino.h> #include <Keyboard.h> #include <Joystick.h> #include <EEPROM.h> #include "SegaGamepad.h" #include "ButtonDebounce.h" bool serialPrintEnabled = false; unsigned long previousKeyUpdateTime = 0; const int outputModesCount = 2; enum OutputMode { keyboard = 0, joystick = 1 }; const char* outputModeNames[outputModesCount] = { "keyboard", "joystick" }; OutputMode outputMode = keyboard; int outputModeStorageAddress = 24; Joystick_ joystick1(JOYSTICK_DEFAULT_REPORT_ID, JOYSTICK_TYPE_GAMEPAD, 8, 1, false, false, false, false, false, false, false, false, false, false, false); Joystick_ joystick2(JOYSTICK_DEFAULT_REPORT_ID + 1, JOYSTICK_TYPE_GAMEPAD, 8, 1, false, false, false, false, false, false, false, false, false, false, false); const unsigned int delayBeforeReadMicros = 10; const unsigned int delayBeforeNextUpdateMicros = 2000; SegaGamepad segaGamepad1(A0, A1, A2, A3, 1, 0, 2, delayBeforeReadMicros, delayBeforeNextUpdateMicros); SegaGamepad segaGamepad2(9, 8, 7, 6, 5, 4, 3, delayBeforeReadMicros, delayBeforeNextUpdateMicros); const unsigned long debounceDelayMillis = 50; ButtonDebounce modeButtonDebounce1(debounceDelayMillis); ButtonDebounce modeButtonDebounce2(debounceDelayMillis); const int keysCount = 12; const char* keysNames[keysCount] = { "btnUp", "btnDown", "btnLeft", "btnRight", "btnA", "btnB", "btnC", "btnX", "btnY", "btnZ", "btnStart", "btnMode" }; const uint8_t keysKeyboard1[keysCount] = { 'w', 's', 'a', 'd', 'j', 'k', 'l', 'u', 'i', 'o', KEY_RETURN, '\\' }; const uint8_t keysKeyboard2[keysCount] = { 't', 'g', 'f', 'h', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',' }; const uint8_t keysJoystick[keysCount] = { 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7 }; void initSerialPrintEnableFlag(); void initOutputMode(); void printOutputModeInfo(); void printGamepadStatusOnSetup(SegaGamepad& segaGamepad, int gamepadIndex); void handleGamepad(SegaGamepad& segaGamepad, int gamepadIndex, ButtonDebounce& modeButtonDebounce, const uint8_t keysKeyboard[], Joystick_& joystick); void updateJoystick(SegaGamepad& segaGamepad, bool keys[], bool keysPrevious[], Joystick_& joystick); void updateKeyboard(bool keys[], bool keysPrevious[], const uint8_t keysKeyboard[]); void printGamepadStatus(SegaGamepad& segaGamepad, int gamepadIndex, bool keys[], bool keysPrevious[], bool isConnectedPrevious, bool isSixButtonsPrevious); void initKeys(bool keys[], SegaGamepad& segaGamepad, ButtonDebounce& modeButtonDebounce); void setup() { segaGamepad1.init(); segaGamepad2.init(); delay(2000); int gamepadReadigsToDiscard = 2; for (int i = 0; i < gamepadReadigsToDiscard + 1; i++) { segaGamepad1.update(); segaGamepad2.update(); } initSerialPrintEnableFlag(); initOutputMode(); if (serialPrintEnabled) { printOutputModeInfo(); } switch (outputMode) { case OutputMode::keyboard: Keyboard.begin(); break; case OutputMode::joystick: joystick1.begin(); joystick2.begin(); break; } if (serialPrintEnabled) { printGamepadStatusOnSetup(segaGamepad1, 1); printGamepadStatusOnSetup(segaGamepad2, 2); } } void loop() { handleGamepad(segaGamepad1, 1, modeButtonDebounce1, keysKeyboard1, joystick1); handleGamepad(segaGamepad2, 2, modeButtonDebounce2, keysKeyboard2, joystick2); } void printOutputModeInfo() { Serial.println("Press Start+A on first gamepad during startup to change output mode to keyboard"); Serial.println("Press Start+B on first gamepad during startup to change output mode to joystick"); Serial.print("Current output mode: "); Serial.println(outputModeNames[outputMode]); Serial.println(); } void initOutputMode() { if (segaGamepad1.btnStart && (segaGamepad1.btnA || segaGamepad1.btnB)) { if (segaGamepad1.btnA) outputMode = OutputMode::keyboard; if (segaGamepad1.btnB) outputMode = OutputMode::joystick; EEPROM.put(outputModeStorageAddress, outputMode); } else { EEPROM.get(outputModeStorageAddress, outputMode); outputMode = (OutputMode)(outputMode % outputModesCount); } } void initSerialPrintEnableFlag() { if (segaGamepad1.btnStart) { serialPrintEnabled = true; Serial.begin(115200); delay(5000); Serial.println(); Serial.println("Please stand by..."); delay(1000); Serial.println(); Serial.println("Enabled serial output by pressing Start on first gamepad during startup"); } else { serialPrintEnabled = false; } } void printGamepadStatusOnSetup(SegaGamepad& segaGamepad, int gamepadIndex) { if (segaGamepad.isConnected) { Serial.print("Gamepad "); Serial.print(gamepadIndex); Serial.println(" connected"); } else { Serial.print("Gamepad "); Serial.print(gamepadIndex); Serial.println(" disconnected"); } if (segaGamepad.isConnected && segaGamepad.isSixBtns) { Serial.print("Gamepad "); Serial.print(gamepadIndex); Serial.print(" type: "); if (segaGamepad.isSixBtns) { Serial.println("\"six buttons\""); } else { Serial.println("\"three buttons\""); } } Serial.println(); } void handleGamepad(SegaGamepad& segaGamepad, int gamepadIndex, ButtonDebounce& modeButtonDebounce, const uint8_t keysKeyboard[], Joystick_& joystick) { bool keysPrevious[keysCount]; initKeys(keysPrevious, segaGamepad, modeButtonDebounce); bool isConnectedPrevious = segaGamepad.isConnected; bool isSixButtonsPrevious = segaGamepad.isSixBtns; segaGamepad.update(); bool keys[keysCount]; initKeys(keys, segaGamepad, modeButtonDebounce); switch (outputMode) { case OutputMode::keyboard: updateKeyboard(keys, keysPrevious, keysKeyboard); break; case OutputMode::joystick: updateJoystick(segaGamepad, keys, keysPrevious, joystick); break; } if (serialPrintEnabled) { printGamepadStatus(segaGamepad, gamepadIndex, keys, keysPrevious, isConnectedPrevious, isSixButtonsPrevious); } } void updateKeyboard(bool keys[], bool keysPrevious[], const uint8_t keysKeyboard[]) { for (int i = 0; i < keysCount; i++) { if (keys[i] && !keysPrevious[i]) { Keyboard.press(keysKeyboard[i]); } if (!keys[i] && keysPrevious[i]) { Keyboard.release(keysKeyboard[i]); } } } void updateJoystick(SegaGamepad& segaGamepad, bool keys[], bool keysPrevious[], Joystick_& joystick) { for (int i = 0; i < keysCount; i++) { if (keys[i] && !keysPrevious[i]) { if (i >= 4) { joystick.pressButton(keysJoystick[i]); } } if (!keys[i] && keysPrevious[i]) { if (i >= 4) { joystick.releaseButton(keysJoystick[i]); } } } bool isArrowChanged = false; for (int i = 0; i < 4; i++) { isArrowChanged = isArrowChanged || (keys[i] != keysPrevious[i]); } if (isArrowChanged) { if (segaGamepad.btnUp && segaGamepad.btnRight) { joystick.setHatSwitch(0, 45); } else if (segaGamepad.btnRight && segaGamepad.btnDown) { joystick.setHatSwitch(0, 135); } else if (segaGamepad.btnDown && segaGamepad.btnLeft) { joystick.setHatSwitch(0, 225); } else if (segaGamepad.btnLeft && segaGamepad.btnUp) { joystick.setHatSwitch(0, 315); } else if (segaGamepad.btnUp) { joystick.setHatSwitch(0, 0); } else if (segaGamepad.btnRight) { joystick.setHatSwitch(0, 90); } else if (segaGamepad.btnDown) { joystick.setHatSwitch(0, 180); } else if (segaGamepad.btnLeft) { joystick.setHatSwitch(0, 270); } else { joystick.setHatSwitch(0, -1); } } } void printGamepadStatus(SegaGamepad& segaGamepad, int gamepadIndex, bool keys[], bool keysPrevious[], bool isConnectedPrevious, bool isSixButtonsPrevious) { unsigned long currentTime = millis(); for (int i = 0; i < keysCount; i++) { if (keys[i] && !keysPrevious[i]) { Serial.print("+ "); Serial.print(currentTime - previousKeyUpdateTime); Serial.print(" ms "); Serial.print(keysNames[i]); Serial.print(" pressed on gamepad "); Serial.println(gamepadIndex); previousKeyUpdateTime = currentTime; } if (!keys[i] && keysPrevious[i]) { Serial.print("+ "); Serial.print(currentTime - previousKeyUpdateTime); Serial.print(" ms "); Serial.print(keysNames[i]); Serial.print(" released on gamepad "); Serial.println(gamepadIndex); previousKeyUpdateTime = currentTime; } } if (segaGamepad.isConnected && !isConnectedPrevious) { Serial.print("Gamepad "); Serial.print(gamepadIndex); Serial.println(" connected"); } if (!segaGamepad.isConnected && isConnectedPrevious) { Serial.print("Gamepad "); Serial.print(gamepadIndex); Serial.println(" disconnected"); } if (segaGamepad.isConnected && segaGamepad.isSixBtns != isSixButtonsPrevious) { Serial.print("Gamepad "); Serial.print(gamepadIndex); Serial.print(" type changed to "); if (segaGamepad.isSixBtns) { Serial.println("\"six buttons\""); } else { Serial.println("\"three buttons\""); } } } void initKeys(bool keys[], SegaGamepad& segaGamepad, ButtonDebounce& modeButtonDebounce) { keys[0] = segaGamepad.btnUp; keys[1] = segaGamepad.btnDown; keys[2] = segaGamepad.btnLeft; keys[3] = segaGamepad.btnRight; keys[4] = segaGamepad.btnA; keys[5] = segaGamepad.btnB; keys[6] = segaGamepad.btnC; keys[7] = segaGamepad.btnX; keys[8] = segaGamepad.btnY; keys[9] = segaGamepad.btnZ; keys[10] = segaGamepad.btnStart; modeButtonDebounce.updateState(segaGamepad.btnMode); keys[11] = modeButtonDebounce.btnState; }
Конфигурация виртуальных геймпадов будет такая:

Так же, как с клавиатурой, создаем массив с раскладкой кнопок, в котором будут храниться номера кнопок виртуального геймпада для соответствующих кнопок геймпада Sega Mega Drive. Для кнопок крестовины будет отдельная обработка, поэтому для них вместо номеров кнопок указано значение 0:
const uint8_t keysJoystick[keysCount] = { 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7 };
В глобальных переменных создаем два объекта виртуальных геймпадов, передавая в конструкторы конфигурацию:
Joystick_ joystick1(JOYSTICK_DEFAULT_REPORT_ID, JOYSTICK_TYPE_GAMEPAD, 8, 1, false, false, false, false, false, false, false, false, false, false, false); Joystick_ joystick2(JOYSTICK_DEFAULT_REPORT_ID + 1, JOYSTICK_TYPE_GAMEPAD, 8, 1, false, false, false, false, false, false, false, false, false, false, false);
Далее нужно инициализировать геймпады в процедуре setup() и обновлять их состояния при изменении состояния кнопок, как это делали с виртуальной клавиатурой. Восемь кнопок виртуальных геймпадов обновляются аналогично клавиатуре:
for (int i = 0; i < keysCount; i++) { if (keys[i] && !keysPrevious[i]) { if (i >= 4) { joystick.pressButton(keysJoystick[i]); } } if (!keys[i] && keysPrevious[i]) { if (i >= 4) { joystick.releaseButton(keysJoystick[i]); } } }
А point of view hat обновляется отдельной процедурой, в которой в зависимости от нажатых направлений на крестовине задается угол направления point of view hat. Угол направления point of view hat задается в градусах по часовой стрелке. Значение -1 соответствует не нажатой point of view hat:
bool isArrowChanged = false; for (int i = 0; i < 4; i++) { isArrowChanged = isArrowChanged || (keys[i] != keysPrevious[i]); } if (isArrowChanged) { if (segaGamepad.btnUp && segaGamepad.btnRight) { joystick.setHatSwitch(0, 45); } else if (segaGamepad.btnRight && segaGamepad.btnDown) { joystick.setHatSwitch(0, 135); } else if (segaGamepad.btnDown && segaGamepad.btnLeft) { joystick.setHatSwitch(0, 225); } else if (segaGamepad.btnLeft && segaGamepad.btnUp) { joystick.setHatSwitch(0, 315); } else if (segaGamepad.btnUp) { joystick.setHatSwitch(0, 0); } else if (segaGamepad.btnRight) { joystick.setHatSwitch(0, 90); } else if (segaGamepad.btnDown) { joystick.setHatSwitch(0, 180); } else if (segaGamepad.btnLeft) { joystick.setHatSwitch(0, 270); } else { joystick.setHatSwitch(0, -1); } }
В Windows работа point of view hat выглядит так:

В коде видно, что для определения направления point of view hat достаточно обработать четыре нажатия отдельных кнопок Up/Down/Left/Right и четыре комбинации из двух кнопок соответствующие диагоналям. Остальные комбинации обрабатывать не нужно т. к. нажать больше двух кнопок или зажать одновременно две кнопки противоположных направлений на крестовине физически невозможно из-за конструкции самой крестовины.
У крестовины снизу есть выступ, который упирается в печатную плату геймпада, поэтому крестовина может только наклонятся в сторону при нажатии, утопить её полностью, чтобы нажать все кнопки одновременно не получится.

Другой вариант конструкции геймпада показан в этой статье.
Здесь выступ крестовины проходит через печатную плату насквозь и упирается в нижнюю часть корпуса геймпада. А при нажатии крестовина замыкает контакты с обратной стороны печатной платы. Это позволяет сделать геймпад меньше по высоте.

К слову, у геймпадов XBox 360 используется первый вариант конструкции крестовины, с упором в печатную плату, а сама крестовина очень высокая, что дает большой люфт при нажатии и делает очень неудобным её использование в играх, четко нажать диагональ при такой конструкции почти невозможно.

Не знаю, как на Sega Mega Drive, но на китайских клонах NES (Денди) у геймпадов крестовина всё-таки прожималась полностью с возможностью одновременно нажать кнопки противоположных направлений. Например, в игре Battletoads это приводило к багу, блокирующему прохождение игры (подробности здесь).
В эмуляторе Gens/GS есть даже функция блокировки одновременного нажатия противоположных направлений крестовины, чтобы случайно не нажать их, играя, скажем, на клавиатуре.

Теперь вернемся к коду.
Процедура initOutputMode() вызывается при включении микроконтроллера и отвечает за инициализацию режима работы переходника. Если при включении микроконтроллера зажать на первом подключенном геймпаде кнопки Start+A то переходник будет эмулировать клавиатуру. Если зажать кнопки Start+B то переходник будет эмулировать виртуальные геймпады. Эта процедура записывает выбранный режим в постоянную память микроконтроллера, так что при повторном включении переходника будет активирован последний выбранный режим. Код процедуры initOutputMode():
void initOutputMode() { if (segaGamepad1.btnStart && (segaGamepad1.btnA || segaGamepad1.btnB)) { if (segaGamepad1.btnA) outputMode = OutputMode::keyboard; if (segaGamepad1.btnB) outputMode = OutputMode::joystick; EEPROM.put(outputModeStorageAddress, outputMode); } else { EEPROM.get(outputModeStorageAddress, outputMode); outputMode = (OutputMode)(outputMode % outputModesCount); } }
Далее в коде происходит инициализация виртуальной клавиатуры или виртуальных геймпадов в зависимости от переменной outputMode, в которой хранится выбранный режим работы:
switch (outputMode) { case OutputMode::keyboard: Keyboard.begin(); break; case OutputMode::joystick: joystick1.begin(); joystick2.begin(); break; }
В цикле опроса геймпадов в зависимости от переменной outputMode обновляется виртуальная клавиатура или виртуальный геймпад:
switch (outputMode) { case OutputMode::keyboard: updateKeyboard(keys, keysPrevious, keysKeyboard); break; case OutputMode::joystick: updateJoystick(segaGamepad, keys, keysPrevious, joystick); break; }
Ещё при старте контроллера инициализируется флаг serialPrintEnabled, включающий вывод информации о подключенных геймпадах в последовательный порт. Чтобы включить вывод этой информации, нужно включить переходник с зажатой кнопкой Start на первом подключенном геймпаде. Процедура инициализации флага serialPrintEnabled:
void initSerialPrintEnableFlag() { if (segaGamepad1.btnStart) { serialPrintEnabled = true; Serial.begin(115200); delay(5000); Serial.println(); Serial.println("Please stand by..."); delay(1000); Serial.println(); Serial.println("Enabled serial output by pressing Start on first gamepad during startup"); } else { serialPrintEnabled = false; } }
Пример выводимых в последовательный порт данных:
Please stand by... Enabled serial output by pressing Start on first gamepad during startup Press Start+A on first gamepad during startup to change output mode to keyboard Press Start+B on first gamepad during startup to change output mode to joystick Current output mode: joystick Gamepad 1 connected Gamepad 1 type: "six buttons" Gamepad 2 disconnected + 8006 ms btnStart released on gamepad 1 + 1447 ms btnZ pressed on gamepad 1 + 771 ms btnZ released on gamepad 1 + 1232 ms btnB pressed on gamepad 1 + 1017 ms btnB released on gamepad 1 + 1510 ms btnMode pressed on gamepad 1 + 564 ms btnMode released on gamepad 1 + 1504 ms btnUp pressed on gamepad 1 + 696 ms btnUp released on gamepad 1
Заключение
Окончательная версия кода прошивки переходника по итогам статьи:
Для сборки использовать плагин PlatformIO для Visual Studio Code.
Для режима USB-клавиатуры зажать кнопки Start+A на первом геймпаде при включении.
Для режима USB-геймпадов зажать кнопки Start+B на первом геймпаде при включении.
Для вывода отладочной информации в последовательный порт зажать кнопку Start на первом геймпаде при включении.
Напоследок, ещё одно видео с демонстрацией работы переходника, на этот раз с эмулятором PlayStation 1, игра Fighting Force.
Источники и полезные ссылки:
Исходный код примеров в статье на GitHub.
Для работы с проектами нужно установить плагин PlatformIO для Visual Studio Code.Ещё одна библиотека (здесь отличается обработка задержки между циклами опроса геймпада).
Описание протокола геймпада:
http://web.archive.org/web/20171229105419/http://www.cs.cmu.edu/~chuck/infopg/segasix.txt
https://github.com/jonthysell/SegaController/wiki/How-To-Read-Sega-Controllers
