В этой статье расскажу, как подключить геймпад от игровой приставки 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
Посмотрим, как работает геймпад. Вот ссылки на описание протокола опроса геймпада:
-
Очень старая статья 1996 года, на которую многие другие статьи ссылаются, как на первоисточник:
http://web.archive.org/web/20171229105419/http://www.cs.cmu.edu/~chuck/infopg/segasix.txt -
Ещё одно описание протокола на GitHub:
https://github.com/jonthysell/SegaController/wiki/How-To-Read-Sega-Controllers -
Статья с исследованием протокола опроса геймпада на самой приставке с помощью логического анализатора:
https://www.raspberryfield.life/2019/03/25/sega-mega-drive-genesis-6-button-xyz-controller/#SMD6-protocol-overview
Далее кратко опишу процесс опроса геймпада на основе этих источников.
Геймпад подключается к приставке с помощью разъема D-Sub DE-9 с девятью контактами.

Функции контактов:
-
pin 1, pin 2, pin 3, pin 4, pin 6, pin 9 - контакты для считывания значений кнопок.
-
pin 7 - контакт Select, на него приставка подает сигнал, который определяет, какие кнопки можно считать в данный момент.
-
pin 5 - земля (GND).
-
pin 8 - напряжение питания (5 вольт, согласно документации).
Начнем с протокола опроса трехкнопочного геймпада.
Для работы с геймпадом на контакт Select подаем попеременно высокий и низкий уровень сигнала и считываем значения нажатых кнопок с остальных контактов. При этом логика кнопок инвертированная: нажатой кнопке будет соответствовать низкий уровень напряжения (LOW).
В таблице ниже показано, значение каких кнопок можно считать с соответствующих контактов при подаче высокого и низкого уровня сигнала на контакт Select (pin 7).
Select (pin 7) |
pin 1 |
pin 2 |
pin 3 |
pin 4 |
pin 6 |
pin 9 |
LOW |
- |
- |
LOW(*) |
LOW(*) |
A |
Start |
HIGH |
Up |
Down |
Left |
Right |
B |
C |
(*) - на контактах 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).
State |
Select (pin 7) |
pin 1 |
pin 2 |
pin 3 |
pin 4 |
pin 6 |
pin 9 |
Idle |
HIGH |
- |
- |
- |
- |
- |
- |
State 0 |
LOW |
- |
- |
LOW(*) |
LOW(*) |
A |
Start |
State 1 |
HIGH |
Up |
Down |
Left |
Right |
B |
C |
State 2 |
LOW |
- |
- |
LOW(*) |
LOW(*) |
A |
Start |
State 3 |
HIGH |
Up |
Down |
Left |
Right |
B |
C |
State 4 |
LOW |
LOW(**) |
LOW(**) |
- |
- |
- |
- |
State 5 |
HIGH |
z |
y |
x |
Mode |
- |
- |
State 6 |
LOW |
- |
- |
- |
- |
- |
- |
State 7 |
HIGH |
- |
- |
- |
- |
- |
- |
Idle |
HIGH |
- |
- |
- |
- |
- |
- |
(*) - на контактах pin 3 и pin 4 будет значение LOW, когда контакт Select (pin 7) установлен в значение LOW первый и второй раз в процессе опроса геймпада (State 0 и State 2). Таким образом можно определить, что контроллер подключен.
(**) - на контактах pin 3 и pin 4 будет значение 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
Заключение
Окончательная версия кода прошивки переходника по итогам статьи:
https://github.com/IvoryRubble/sega_gamepad_usb_adapter/blob/master/SegaGamepad_two_gamepads_keyboard_and_joystick/src/main.cpp
-
Для сборки использовать плагин PlatformIO для Visual Studio Code.
-
Для режима USB-клавиатуры зажать кнопки Start+A на первом геймпаде при включении.
-
Для режима USB-геймпадов зажать кнопки Start+B на первом геймпаде при включении.
-
Для вывода отладочной информации в последовательный порт зажать кнопку Start на первом геймпаде при включении.
Напоследок, ещё одно видео с демонстрацией работы переходника, на этот раз с эмулятором PlayStation 1, игра Fighting Force.
Источники и полезные ссылки:
-
Исходный код примеров в статье на GitHub:
https://github.com/IvoryRubble/sega_gamepad_usb_adapter
Для работы с проектами нужно установить плагин PlatformIO для Visual Studio Code. -
Библиотека для работы с геймпадом из этой статьи:
https://registry.platformio.org/libraries/ivoryrubble/SegaGamepad -
Ещё одна библиотека (здесь отличается обработка задержки между циклами опроса геймпада):
https://github.com/jonthysell/SegaController -
Описание протокола геймпада:
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 -
Список эмуляторов Sega Mega Drive:
https://emulation.gametechwiki.com/index.php/Sega_Genesis_emulators
Автор: IvoryRubble