- PVSM.RU - https://www.pvsm.ru -

Подключаем геймпад от Денди (NES) к ПК

У меня есть старый геймпад от игровой приставки Денди (клон NES). Задача: подключить его к ПК, чтобы играть в старые игры на “оригинальном” геймпаде. Я уже публиковал статью [1]про подключение геймпада Sega Mega Drive к ПК. Теперь точно так же подключим геймпад от Денди. Изменений в конструкцию геймпада вносить не будем, вместо этого будем опрашивать геймпад точно так же, как это делала сама игровая приставка.
Вот что должно получиться в итоге:

Переходник с подключенным геймпадом, подключенный через Micro-USB к ПК

Переходник с подключенным геймпадом, подключенный через Micro-USB к ПК
Переходник с подключенным геймпадом

Переходник с подключенным геймпадом

Содержание

Геймпад от игровой приставки NES

Оригинальная игровая приставка называлась Nintendo Entertainment System (NES) на рынке США и Европы или Famicom (Family Computer) в Японии. В России в основном были неофициальные клоны этой приставки с разными названиями, обычно их все называли Денди (Dendy) по названию самого известного клона. Подробнее в Википедии в соответствующих статьях про NES [9] и Dendy [10].

От какой именно приставки у меня геймпад и откуда он взялся я не помню, известно только, что это какой-то клон NES.

Мой геймпад от Денди

Мой геймпад от Денди

Компоновка геймпада стандартная:

  • Крестовина слева

  • Кнопки Start и Select посредине

  • Кнопки B и A (именно в таком порядке) справа. Над ними дублирующие кнопки с режимом Turbo (зажатую кнопку приставка воспринимает как серию быстрых нажатий)

Дальше рассмотрим протокол опроса геймпада и обязательно проверим работу Turbo-кнопок в конце статьи.

Подключение и протокол опроса геймпада NES

У разных версий приставки были разные разъемы для подключения геймпадов. У оригинальной Famicom (версия NES для Японии) геймпады вообще не имели внешнего разъема для подключения и не отсоединялись от консоли.

У Famicom не было внешнего разъема для подключения геймпадов (источник)

У Famicom не было внешнего разъема для подключения геймпадов (источник [11])

У оригинальной NES был 7-контактный разъем своей собственной конструкции, а у клонов приставки 9 или 15-контактные разъемы, представляющие собой стандартные разъемы D-Sub: DB-9 и DA-15.  Соответственно к ним можно купить стандартные гнезда для подключения и подключать геймпад оригинальным разъемом.
У моего геймпада 15-контактный разъем, на фото ниже он справа.

Варианты разъемов геймпадов NES

Варианты разъемов геймпадов NES

На все разъемы выведены одни и те же контакты геймпада:

  • Питание: +5 В и GND.

  • Latch

  • Pulse (Clock)

  • Data

Распиновка разъемов геймпадов NES

Распиновка разъемов геймпадов NES

Для считывания значений кнопок нужно подать импульс высокого уровня сигнала на контакт Latch, а затем последовательно считывать состояния кнопок геймпада с контакта Data, после считывания каждого значения подавая импульс высокого уровня сигнала на контакт Pulse (Clock).
Последовательность считывания кнопок такая: A, B, Select, Start, Up, Down, Left, Right.
Сигналы при опросе геймпада приведены на графике ниже.

Опрос геймпада NES

Опрос геймпада NES

Длина импульса на контакте Latch равна 12 микросекунд, импульсы на контакте Pulse (Clock) должны быть длиной 6 микросекунд с паузами между импульсами тоже по 6 микросекунд. 
Приставка повторяет опрос геймпада каждый кадр (50-60 Гц), но можно повторять опрос с максимальной частотой, добавляя задержку 6 микросекунд после каждого опроса. 

Вот код для проверки геймпада, в котором реализован алгоритм опроса и вывод значений кнопок в последовательный порт:

Код для проверки работы геймпада

https://github.com/IvoryRubble/ArduinoNesGamepadLibrary/blob/master/examples/NesGamepad_test_without_lib/NesGamepad_test_without_lib.ino [12]

// No actually a NesGamepad library example but just reading buttons from NES gamepad and print to serial port
// also blink led

const int latchPin = A0;
const int pulsePin = A1;
const int dataPin = A2;

const unsigned int delayTimeMicroseconds = 6;

const int btnsCount = 8;
bool btns[8]; 

const char* btnNames[8] = {
  "A",
  "B",
  "Select",
  "Start",
  "Up",
  "Down",
  "Left",
  "Right"
};

void setup() {
  Serial.begin(115200);

  pinMode(latchPin, OUTPUT);
  pinMode(pulsePin, OUTPUT);
  digitalWrite(latchPin, LOW);
  digitalWrite(pulsePin, LOW);

  pinMode(dataPin, INPUT_PULLUP);
}

void loop() {
  digitalWrite(pulsePin, LOW);
  digitalWrite(latchPin, HIGH);
  delayMicroseconds(delayTimeMicroseconds * 2);
  digitalWrite(latchPin, LOW);
  delayMicroseconds(delayTimeMicroseconds);

  for (int i = 0; i < btnsCount; i++) {
    btns[i] = !digitalRead(dataPin);
    digitalWrite(pulsePin, HIGH);
    delayMicroseconds(delayTimeMicroseconds);
    digitalWrite(pulsePin, LOW);
    delayMicroseconds(delayTimeMicroseconds);
  }

  String s = String();
  for (int i = 0; i < btnsCount; i++) {
    s = s + btnNames[i] + ":" + btns[i] + " ";
  }
  Serial.println(s);

  digitalWrite(LED_BUILTIN, LOW);
  delay(100);
  digitalWrite(LED_BUILTIN, HIGH);
  delay(200);
}

Пример данных, выводимых в последовательный порт:

A:0 B:0 Select:0 Start:0 Up:0 Down:0 Left:0 Right:0 
A:1 B:0 Select:0 Start:0 Up:0 Down:0 Left:0 Right:0 
A:1 B:1 Select:0 Start:0 Up:0 Down:0 Left:0 Right:0 
A:1 B:1 Select:0 Start:0 Up:0 Down:0 Left:0 Right:0 
A:0 B:0 Select:0 Start:0 Up:1 Down:0 Left:0 Right:0 
A:0 B:0 Select:0 Start:1 Up:1 Down:0 Left:0 Right:0 
A:0 B:0 Select:0 Start:1 Up:0 Down:0 Left:0 Right:0 
A:0 B:0 Select:0 Start:1 Up:0 Down:0 Left:0 Right:0

Сборка переходника

Для переходника я использовал разъем D-Sub DA-15 в пластиковом корпусе и Arduino Pro Micro (ATmega32u4).

Всё готово для сборки

Всё готово для сборки
Припаял разъем

Припаял разъем
В процессе сборки

В процессе сборки
Готово

Готово
Подключенный переходник. Обернул его в термоусадку

Подключенный переходник. Обернул его в термоусадку

Библиотека NesGamepad

Для удобства работы с геймпадом завернул логику опроса в библиотеку NesGamepad. Код библиотеки опубликовал на GitHub [13], на PlatformIO [14] и в Arduino library-registry (в менеджере библиотек Arduino IDE ввести в поиске “Nes Gamepad”).

Для использования библиотеки нужно создать экземпляр класса NesGamepad. В конструктор передать порты контроллера, к которым подключены контакты геймпада Latch, Pulse (Clock) и Data. Также можно передать значение длины импульсов при опросе геймпада (по умолчанию 6 миллисекунд для импульсов Pulse и 6*2=12 миллисекунд для импульса Latch).

const int latchPin = A0;
const int pulsePin = A1;
const int dataPin = A2;
const unsigned int delayBeforeReadMicros = 6;
NesGamepad gamepad(latchPin, pulsePin, dataPin, delayBeforeReadMicros); 

Далее в процедуре setup() вызвать метод NesGamepad::init() для инициализации портов контроллера.

gamepad.init();

После этого в цикле loop() нужно вызывать метод NesGamepad::update() и читать состояния кнопок геймпада из полей объекта gamepad.

gamepad.update();
Serial.println(gamepad.btnA);
Serial.println(gamepad.btnStart);
Serial.println(gamepad.btnUp);

Прошивка переходника

Для переходника сделал две версии прошивки: с эмуляцией USB клавиатуры и с переключением режимов USB клавиатура/USB геймпад.
Код прошивок выложил в отдельный репозиторий на GitHub [15].

Для сборки нужно использовать Arduino IDE установленными библиотеками NesGamepad [13] и ArduinoJoystickLibrary [16] для версии с переключением режимов.
Также в коде используется класс ButtonDebounce [17] для фильтрации ложных срабатываний кнопок при замыкании/размыкании контактов.

Код прошивки переходника с эмуляцией USB клавиатуры

https://github.com/IvoryRubble/nes_gamepad_usb_adapter/blob/master/NesGamepad_keyboard/NesGamepad_keyboard.ino [18]

// Press Start on gamepad during startup to enable serial output 

#include <Keyboard.h>
// Install NesGamepad lib from here: https://github.com/IvoryRubble/ArduinoNesGamepadLibrary
#include <NesGamepad.h>
#include "ButtonDebounce.h"

bool serialPrintEnabled = false;
unsigned long previousBtnUpdateTime = 0;

const int latchPin = A0;
const int pulsePin = A1;
const int dataPin = A2;
const unsigned int delayBeforeReadMicros = 6;
NesGamepad gamepad(latchPin, pulsePin, dataPin, delayBeforeReadMicros);

unsigned long debounceDelay = 25;
ButtonDebounce btnDebouces[gamepad.btnsCount] = {
  {debounceDelay},
  {debounceDelay},
  {debounceDelay},
  {debounceDelay},
  {debounceDelay},
  {debounceDelay},
  {debounceDelay},
  {debounceDelay}
};

const char* btnNames[gamepad.btnsCount] = {
  "A",
  "B",
  "Select",
  "Start",
  "Up",
  "Down",
  "Left",
  "Right"
};

const uint8_t keysKeyboard[gamepad.btnsCount] = {
  'k',
  'j',
  '\',
  KEY_RETURN,
  'w',
  's',
  'a',
  'd'
};

void setup() {
  gamepad.init();

  delay(2000);
  int gamepadReadigsToDiscard = 2;
  for (int i = 0; i < gamepadReadigsToDiscard + 1; i++) {
    gamepad.update();
  }

  initSerialPrintEnableFlag();

  Keyboard.begin();
}

void loop() {
  gamepad.update();
  btnDebouces[0].updateState(gamepad.btnA);
  btnDebouces[1].updateState(gamepad.btnB);
  btnDebouces[2].updateState(gamepad.btnSelect);
  btnDebouces[3].updateState(gamepad.btnStart);
  btnDebouces[4].updateState(gamepad.btnUp);
  btnDebouces[5].updateState(gamepad.btnDown);
  btnDebouces[6].updateState(gamepad.btnLeft);
  btnDebouces[7].updateState(gamepad.btnRight);

  updateKeyboard();

  if (serialPrintEnabled) {
    printGamepadStatus();
  }
}

void initSerialPrintEnableFlag() {
  if (gamepad.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 gamepad during startup");
  } else {
    serialPrintEnabled = false;
  }
}

void updateKeyboard() {
  for (int i = 0; i < gamepad.btnsCount; i++) {
    if (btnDebouces[i].isBtnPressed) {
      Keyboard.press(keysKeyboard[i]);
    }
    if (btnDebouces[i].isBtnReleased) {
      Keyboard.release(keysKeyboard[i]);
    }
  }
}

void printGamepadStatus() {
  unsigned long currentTime = millis();
  unsigned long longDelayTimeout = 1000;
  for (int i = 0; i < gamepad.btnsCount; i++) {
    if (btnDebouces[i].isBtnPressed) {
      if (currentTime - previousBtnUpdateTime > longDelayTimeout) Serial.println();
      Serial.print("+ "); Serial.print(currentTime - previousBtnUpdateTime); Serial.print(" ms "); Serial.print(btnNames[i]); Serial.println(" pressed");
      previousBtnUpdateTime = currentTime;
    }
    if (btnDebouces[i].isBtnReleased) {
      if (currentTime - previousBtnUpdateTime > longDelayTimeout) Serial.println();
      Serial.print("+ "); Serial.print(currentTime - previousBtnUpdateTime); Serial.print(" ms "); Serial.print(btnNames[i]); Serial.println(" released");
      previousBtnUpdateTime = currentTime;
    }
  }
}
Код прошивки переходника с эмуляцией переключением режимов USB клавиатура/USB геймпад

https://github.com/IvoryRubble/nes_gamepad_usb_adapter/blob/master/NesGamepad_keyboard_and_joystick/NesGamepad_keyboard_and_joystick.ino [19] 

// Press Start on gamepad during startup to enable serial output  
// Press Start+A on gamepad during startup to change output mode to keyboard
// Press Start+B on gamepad during startup to change output mode to joystick

#include <Keyboard.h>
// Install Joystick lib from here: https://github.com/MHeironimus/ArduinoJoystickLibrary
#include <Joystick.h>
#include <EEPROM.h>
// Install NesGamepad lib from here: https://github.com/IvoryRubble/ArduinoNesGamepadLibrary
#include <NesGamepad.h>
#include "ButtonDebounce.h"

bool serialPrintEnabled = false;
unsigned long previousBtnUpdateTime = 0;

const int outputModesCount = 2;
enum OutputMode {
  keyboardOutputMode = 0,
  joystickOutputMode = 1
};
const char* outputModeNames[outputModesCount] = { "keyboard", "joystick" };

OutputMode outputMode = keyboardOutputMode;
int outputModeStorageAddress = 24;

const int latchPin = A0;
const int pulsePin = A1;
const int dataPin = A2;
const unsigned int delayBeforeReadMicros = 6;
NesGamepad gamepad(latchPin, pulsePin, dataPin, delayBeforeReadMicros);

Joystick_ joystick(JOYSTICK_DEFAULT_REPORT_ID, JOYSTICK_TYPE_GAMEPAD, 4, 1, false, false, false, false, false, false, false, false, false, false, false);

unsigned long debounceDelay = 25;
ButtonDebounce btnDebouces[gamepad.btnsCount] = {
  {debounceDelay},
  {debounceDelay},
  {debounceDelay},
  {debounceDelay},
  {debounceDelay},
  {debounceDelay},
  {debounceDelay},
  {debounceDelay}
};

const char* btnNames[gamepad.btnsCount] = {
  "A",
  "B",
  "Select",
  "Start",
  "Up",
  "Down",
  "Left",
  "Right"
};

enum ButtonIndex {
  btnUpIndex = 4,
  btnDownIndex = 5,
  btnLeftIndex = 6,
  btnRightIndex = 7
};

const uint8_t keysKeyboard[gamepad.btnsCount] = {
  'k',
  'j',
  '\',
  KEY_RETURN,
  'w',
  's',
  'a',
  'd'
};

const uint8_t keysJoystick[gamepad.btnsCount] = {
  0,
  1,
  2,
  3,
  0,
  0,
  0,
  0
};

void setup() {
  gamepad.init();

  delay(2000);
  int gamepadReadigsToDiscard = 2;
  for (int i = 0; i < gamepadReadigsToDiscard + 1; i++) {
    gamepad.update();
  }

  initSerialPrintEnableFlag();
  initOutputMode();

  if (serialPrintEnabled) {
    printOutputModeInfo();
  }

  switch (outputMode) {
    case OutputMode::keyboardOutputMode:
      Keyboard.begin();
      break;
    case OutputMode::joystickOutputMode:
      joystick.begin();
      break;
  }
}

void loop() {
  gamepad.update();
  btnDebouces[0].updateState(gamepad.btnA);
  btnDebouces[1].updateState(gamepad.btnB);
  btnDebouces[2].updateState(gamepad.btnSelect);
  btnDebouces[3].updateState(gamepad.btnStart);
  btnDebouces[4].updateState(gamepad.btnUp);
  btnDebouces[5].updateState(gamepad.btnDown);
  btnDebouces[6].updateState(gamepad.btnLeft);
  btnDebouces[7].updateState(gamepad.btnRight);

  switch (outputMode) {
    case OutputMode::keyboardOutputMode:
      updateKeyboard();
      break;
    case OutputMode::joystickOutputMode:
      updateJoystick();
      break;  
  }

  if (serialPrintEnabled) {
    printGamepadStatus();
  }
}

void initSerialPrintEnableFlag() {
  if (gamepad.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 gamepad during startup");
  } else {
    serialPrintEnabled = false;
  }
}

void initOutputMode() {
  if (gamepad.btnStart && (gamepad.btnA || gamepad.btnB)) {
    if (gamepad.btnA) outputMode = OutputMode::keyboardOutputMode;
    if (gamepad.btnB) outputMode = OutputMode::joystickOutputMode;
    EEPROM.put(outputModeStorageAddress, outputMode);
  } else {
    EEPROM.get(outputModeStorageAddress, outputMode);
    outputMode = (OutputMode)(abs(outputMode) % outputModesCount);
  }
}

void printOutputModeInfo() {
  Serial.println("Press Start+A on gamepad during startup to change output mode to keyboard");
  Serial.println("Press Start+B on gamepad during startup to change output mode to joystick");
  Serial.print("Current output mode: ");
  Serial.println(outputModeNames[outputMode]);
  Serial.println();
}

void updateKeyboard() {
  for (int i = 0; i < gamepad.btnsCount; i++) {
    if (btnDebouces[i].isBtnPressed) {
      Keyboard.press(keysKeyboard[i]);
    }
    if (btnDebouces[i].isBtnReleased) {
      Keyboard.release(keysKeyboard[i]);
    }
  }
}

void updateJoystick() {
  for (int i = 0; i < 4; i++) {
    if (btnDebouces[i].isBtnPressed) {
      joystick.pressButton(keysJoystick[i]);
    }
    if (btnDebouces[i].isBtnReleased) {
      joystick.releaseButton(keysJoystick[i]);
    }
  }

  bool isArrowChanged = false;
  for (int i = 4; i < gamepad.btnsCount; i++) {
    isArrowChanged = isArrowChanged || (btnDebouces[i].isBtnPressed || btnDebouces[i].isBtnReleased);
  }
  if (isArrowChanged) {
    if (btnDebouces[btnUpIndex].btnState && btnDebouces[btnRightIndex].btnState) {
      joystick.setHatSwitch(0, 45);
    } else if (btnDebouces[btnRightIndex].btnState && btnDebouces[btnDownIndex].btnState) {
      joystick.setHatSwitch(0, 135);
    } else if (btnDebouces[btnDownIndex].btnState && btnDebouces[btnLeftIndex].btnState) {
      joystick.setHatSwitch(0, 225);
    } else if (btnDebouces[btnLeftIndex].btnState && btnDebouces[btnUpIndex].btnState) {
      joystick.setHatSwitch(0, 315);
    } else if (btnDebouces[btnUpIndex].btnState) {
      joystick.setHatSwitch(0, 0);
    } else if (btnDebouces[btnRightIndex].btnState) {
      joystick.setHatSwitch(0, 90);
    } else if (btnDebouces[btnDownIndex].btnState) {
      joystick.setHatSwitch(0, 180);
    } else if (btnDebouces[btnLeftIndex].btnState) {
      joystick.setHatSwitch(0, 270);
    } else {
      joystick.setHatSwitch(0, -1);
    }
  }
}

void printGamepadStatus() {
  unsigned long currentTime = millis();
  unsigned long longDelayTimeout = 1000;
  for (int i = 0; i < gamepad.btnsCount; i++) {
    if (btnDebouces[i].isBtnPressed) {
      if (currentTime - previousBtnUpdateTime > longDelayTimeout) Serial.println();
      Serial.print("+ "); Serial.print(currentTime - previousBtnUpdateTime); Serial.print(" ms "); Serial.print(btnNames[i]); Serial.println(" pressed");
      previousBtnUpdateTime = currentTime;
    }
    if (btnDebouces[i].isBtnReleased) {
      if (currentTime - previousBtnUpdateTime > longDelayTimeout) Serial.println();
      Serial.print("+ "); Serial.print(currentTime - previousBtnUpdateTime); Serial.print(" ms "); Serial.print(btnNames[i]); Serial.println(" released");
      previousBtnUpdateTime = currentTime;
    }
  }
}

Для включения вывода данных в последовательный порт нужно зажать кнопку Start на геймпаде при подключении переходника к ПК. Для переключения режимов нужно зажать кнопки Start+A (режим USB клавиатуры) или Start+B (режим USB геймпада) при подключении переходника к ПК.

Пример выводимых в последовательный порт данных:

Please stand by...

Enabled serial output by pressing Start on gamepad during startup
Press Start+A on gamepad during startup to change output mode to keyboard
Press Start+B on gamepad during startup to change output mode to joystick
Current output mode: keyboard

+ 2623 ms Up pressed
+ 465 ms Up released
+ 1170 ms Start pressed
+ 196 ms Start released
+ 32 ms B pressed
+ 32 ms B released
+ 32 ms B pressed
+ 31 ms B released
+ 32 ms A pressed
+ 31 ms A released
+ 32 ms A pressed
+ 31 ms A released
+ 32 ms A pressed
+ 32 ms A released

В конце лога видно, как нажаты Turbo-кнопки B и A. Кнопки работают, период нажатия составляет примерно 60 миллисекунд (30 мс кнопка нажата и 30 мс - отпущена).

Раскладка виртуальной USB клавиатуры для геймпада

Раскладка виртуальной USB клавиатуры для геймпада
Конфигурация виртуального USB геймпада

Конфигурация виртуального USB геймпада

Заключение

Теперь можно играть в игры с Денди с “оригинальным” геймпадом

Теперь можно играть в игры с Денди с “оригинальным” геймпадом

Библиотека NesGamepad:
https://github.com/IvoryRubble/ArduinoNesGamepadLibrary [13]
https://registry.platformio.org/libraries/ivoryrubble/NesGamepad [14]

Репозиторий прошивок для переходника:
https://github.com/IvoryRubble/nes_gamepad_usb_adapter [15]

Источники и полезные ссылки

Старая статья с описанием протокола работы геймпада NES:
https://tresi.github.io/nes/ [20] 
https://web.archive.org/web/20150829043041/https://www.mit.edu/~tarvizo/nes-controller.html [21] 

Исследование протокола опроса геймпада NES на оригинальной консоли с помощью логического анализатора:
https://www.raspberryfield.life/2018/09/01/nespi-project-part-4-the-nes-controller-protocol/ [22] 

Другие статьи на Хабре по использованию геймпада NES:
https://habr.com/ru/articles/147356/ [23] 
https://habr.com/ru/articles/191936/ [24] 

Эмулятор NES, который я использую:
https://github.com/punesemu/puNES [25] 

Другие эмуляторы NES:
https://emulation.gametechwiki.com/index.php/Nintendo_Entertainment_System_emulators [26]

Автор: IvoryRubble

Источник [27]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/arduino/418643

Ссылки в тексте:

[1] статью : https://habr.com/p/900300

[2] vkvideo.ru: https://vkvideo.ru/video-228936534_456239021%20

[3] Геймпад от игровой приставки NES: #NES_Gamepad

[4] Подключение и протокол опроса геймпада NES: #NES_Gamepad_pinout

[5] Сборка переходника: #NES_adapter_assembly

[6] Библиотека NesGamepad: #NesGamepad_lib

[7] Прошивка переходника: #NES_adapter_firmware

[8] Заключение: #useful_links

[9] NES: https://ru.wikipedia.org/wiki/Nintendo_Entertainment_System

[10] Dendy: https://ru.wikipedia.org/wiki/Dendy

[11] источник: https://commons.wikimedia.org/wiki/File:Nintendo-Famicom-Console-BR.jpg

[12] https://github.com/IvoryRubble/ArduinoNesGamepadLibrary/blob/master/examples/NesGamepad_test_without_lib/NesGamepad_test_without_lib.ino: https://github.com/IvoryRubble/ArduinoNesGamepadLibrary/blob/master/examples/NesGamepad_test_without_lib/NesGamepad_test_without_lib.ino

[13] GitHub: https://github.com/IvoryRubble/ArduinoNesGamepadLibrary

[14] PlatformIO: https://registry.platformio.org/libraries/ivoryrubble/NesGamepad

[15] GitHub: https://github.com/IvoryRubble/nes_gamepad_usb_adapter

[16] ArduinoJoystickLibrary: https://github.com/MHeironimus/ArduinoJoystickLibrary

[17] ButtonDebounce: https://gist.github.com/IvoryRubble/4eb6355ca0a115232f57452e6ee2bdb1

[18] https://github.com/IvoryRubble/nes_gamepad_usb_adapter/blob/master/NesGamepad_keyboard/NesGamepad_keyboard.ino: https://github.com/IvoryRubble/nes_gamepad_usb_adapter/blob/master/NesGamepad_keyboard/NesGamepad_keyboard.ino

[19] https://github.com/IvoryRubble/nes_gamepad_usb_adapter/blob/master/NesGamepad_keyboard_and_joystick/NesGamepad_keyboard_and_joystick.ino: https://github.com/IvoryRubble/nes_gamepad_usb_adapter/blob/master/NesGamepad_keyboard_and_joystick/NesGamepad_keyboard_and_joystick.ino

[20] https://tresi.github.io/nes/: https://tresi.github.io/nes/

[21] https://web.archive.org/web/20150829043041/https://www.mit.edu/~tarvizo/nes-controller.html: https://web.archive.org/web/20150829043041/https://www.mit.edu/~tarvizo/nes-controller.html

[22] https://www.raspberryfield.life/2018/09/01/nespi-project-part-4-the-nes-controller-protocol/: https://www.raspberryfield.life/2018/09/01/nespi-project-part-4-the-nes-controller-protocol/

[23] https://habr.com/ru/articles/147356/: https://habr.com/ru/articles/147356/

[24] https://habr.com/ru/articles/191936/: https://habr.com/ru/articles/191936/

[25] https://github.com/punesemu/puNES: https://github.com/punesemu/puNES

[26] https://emulation.gametechwiki.com/index.php/Nintendo_Entertainment_System_emulators: https://emulation.gametechwiki.com/index.php/Nintendo_Entertainment_System_emulators

[27] Источник: https://habr.com/ru/articles/906662/?utm_campaign=906662&utm_source=habrahabr&utm_medium=rss