Modificando um controle de Super Nintendo (SNES) para funcionar via USB com Arduino

Encontrei um artigo onde o autor decidiu modificar um controle de Super Nintendo (SNES) para funcionar no computador dele, via USB.

O artigo é curto, e eu diria que o foco está mais na parte de programação do que de modificação do hardware (solda, eletrônica etc.), mas para quem tem interesse em aprender como algum hardware funciona ou como é programar em Arduino, vale essa breve leitura, podendo expandir o que aprender aqui para experimentar em outras coisas. Além disso, o texto também é uma boa lembrança de como a programação e o hardware estão conectados.

Como o SNES é um console de 1990, o controle dele é bem mais simples do que os controles de hoje em dia, então eu diria que o artigo é de fácil entendimento mesmo para quem não tem conhecimento sobre o assunto. Boa leitura!

O hardware do controle do SNES

O primeiro passo para conseguir modificar o controle é descobrir como o hardware dele informa quais botões estão sendo pressionados. Para isso, o autor encontrou um documento que descreve o hardware.

Plug original do controle SNES ao lado da documentação explicando o singificado de cada pino.

A imagem acima mostra, do lado esquerdo, o plug original do controle SNES, e do lado direito a documentação, explicando o propósito de cada pino.

E, na imagem abaixo, temos a outra ponta do cabo do controle, que está conectada à placa do controle. É possível ver que existem apenas cinco fios, como explicado na documentação (apesar do controle ter sete pinos, dois não possuem fios).

Controle SNES aberto

Pino Propósito Cor do fio
1 Linha de energia +5v Branco
2 Relógio de dados Amarelo
3 Trava de dados Laranja
4 Dados seriais Vermelho
7 Terra Marrom

(talvez minha tradução para o "propósito" acima esteja errada, já que não conheço esses termos, mas a tabela acima foi feita com base na documentação da primeira imagem)

Os pinos 2 e 3 são controlados pelo console, e o 4 é controlado pelo controle. Para identificar qual botão está sendo pressionado, o console segue um algoritmo assim:

  1. Envia um pulso alto de 12µs (microsegundos) no pino 3.
  2. Espera 6µs.
  3. Se o pino 4 estiver baixo, o botão B está pressionado.
  4. Envia um pulso baixo de 6µs seguido por um pulso alto de 6µs no pino 2.
  5. Repete as 2 etapas anteriores para todos os botões restantes em ordem (Y, Selecionar, Iniciar, Cima, Baixo, Esquerda, Direita, A, X, L, R) e depois 4 vezes extras sem nenhum botão correspondente.
  6. Repete todo o processo a cada 16,667ms (60Hz)

Agora que descobrimos como o hardware funciona, podemos seguir com a modificação.

Programando o Arduino

Para executar as etapas acima e transmitir o resultado ao computador conectado, o autor escolheu uma pequena placa baseada no chip ATmega32U4, pois era pequena o suficiente para caber dentro do controle e poderia alimentar o controle na voltagem certa. Ele fez as seguintes conexões:

Pino original Propósito Cor do fio Pino do Arduino
1 Linha de energia +5v Branco VCC
2 Relógio de dados Amarelo 14
3 Trava de dados Laranja 15
4 Dados seriais Vermelho 16
7 Terra Marrom GND

A placa instalada no controlador SNES modificado:

Placa com os fios conectados

O código para escanear o botão pressionado no Arduino ficou assim:

#define CLOCK_PIN 14
#define LATCH_PIN 15
#define DATA_PIN 16

const uint8_t num_buttons = 16;

void setup() {
  pinMode(CLOCK_PIN, OUTPUT);
  pinMode(LATCH_PIN, OUTPUT);
  pinMode(DATA_PIN, INPUT);

  digitalWrite(CLOCK_PIN, HIGH);
}

void loop() {
  // Collect button state info from controller.
  // Send data latch.
  digitalWrite(LATCH_PIN, HIGH);
  delayMicroseconds(12);
  digitalWrite(LATCH_PIN, LOW);

  delayMicroseconds(6);

  bool button_states[num_buttons];

  for (uint8_t id = 0; id < num_buttons; id++) {
    // Sample the button state.
    int button_pressed = digitalRead(DATA_PIN) == LOW;
    button_states[id] = button_pressed;

    digitalWrite(CLOCK_PIN, LOW);
    delayMicroseconds(6);
    digitalWrite(CLOCK_PIN, HIGH);
    delayMicroseconds(6);
  }

  delay(16);
}

O autor mencionou como observação que, como a pesquisa pelo botão com o código acima leva cerca de 210µs (12µs + 6µs antes do laço, e 12µs * 16µs dentro do laço), atrasar 16ms após cada pesquisa significa que o controle modificado pesquisa um pouco mais rápido do que 60Hz (61.69Hz), mas que estava próximo o suficiente.

Conectando ao computador

O Arduino já sabe quais botões estão pressionados (na variável button_states), mas agora precisa passar essa informação para o computador.

Periféricos como teclados, mouses e gamepads se comunicam com o computador ao qual estão conectados por meio do protocolo HID. O autor usou a biblioteca do Arduino HID Project para programar o Arduino como um HID de gamepad.

Perto do topo do código, ele importou a biblioteca, definiu uma constante para o índice de cada botão SNES no array button_states e criou um mapeamento de cada botão SNES para o botão do HID do gamepad ao qual queria que correspondesse:

#include <HID-Project.h>

#define SNES_BUTTON_B 0
#define SNES_BUTTON_Y 1
#define SNES_BUTTON_SELECT 2
#define SNES_BUTTON_START 3
#define SNES_BUTTON_UP 4
#define SNES_BUTTON_DOWN 5
#define SNES_BUTTON_LEFT 6
#define SNES_BUTTON_RIGHT 7
#define SNES_BUTTON_A 8
#define SNES_BUTTON_X 9
#define SNES_BUTTON_L 10
#define SNES_BUTTON_R 11
#define SNES_BUTTON_UNDEF_1 12
#define SNES_BUTTON_UNDEF_2 13
#define SNES_BUTTON_UNDEF_3 14
#define SNES_BUTTON_UNDEF_4 15

// Map SNES buttons to HID joypad buttons.
const uint8_t snes_id_to_hid_id[] = { 2, 4, 7, 8, 0, 0, 0, 0, 1, 3, 5, 6, 10, 11, 12, 13 };

Além disso, dentro da função setup, ele inicializou a bibliioteca com Gamepad.begin();. Após cada ciclo de escaneamento dos botões, ele atualiza o estado da biblioteca com base nos valores na matriz button_states (com uma lógica especial para o D-pad) e depois relata esses valores ao computador com Gamepad.write():

// Report button states over HID.
void reportButtons(bool button_states[num_buttons]) {
  // D-Pad.
  int8_t dpad_status = GAMEPAD_DPAD_CENTERED;

  if (button_states[SNES_BUTTON_UP]) {
    dpad_status = GAMEPAD_DPAD_UP;
    if (button_states[SNES_BUTTON_LEFT]) {
      dpad_status = GAMEPAD_DPAD_UP_LEFT;
    } else if (button_states[SNES_BUTTON_RIGHT]) {
      dpad_status = GAMEPAD_DPAD_UP_RIGHT;
    }
  } else if (button_states[SNES_BUTTON_DOWN]) {
    dpad_status = GAMEPAD_DPAD_DOWN;
    if (button_states[SNES_BUTTON_LEFT]) {
      dpad_status = GAMEPAD_DPAD_DOWN_LEFT;
    } else if (button_states[SNES_BUTTON_RIGHT]) {
      dpad_status = GAMEPAD_DPAD_DOWN_RIGHT;
    }
  } else if (button_states[SNES_BUTTON_LEFT]) {
    dpad_status = GAMEPAD_DPAD_LEFT;
  } else if (button_states[SNES_BUTTON_RIGHT]) {
    dpad_status = GAMEPAD_DPAD_RIGHT;
  }

  Gamepad.dPad1(dpad_status);
  Gamepad.dPad2(dpad_status);

  for (uint8_t snes_id = 0; snes_id < num_buttons; snes_id++) {
    if (snes_id >= 4 && snes_id <= 7) {
      // D-Pad.
      continue;
    }

    if (button_states[snes_id]) {
      Gamepad.press(snes_id_to_hid_id[snes_id]);
    } else {
      Gamepad.release(snes_id_to_hid_id[snes_id]);
    }
  }
}

void loop() {
  ...

  // Update HID button states.
  reportButtons(button_states);
  Gamepad.write();

  delay(16);
}

Finalizando

O autor realizou a solda dos fios apenas após confirmar que o reconhecimento dos botões estava funcionando corretamente no computador, ep recisou adaptar um pouco o molde interno do controle para colocar a placa Arduino e o cabo novo. A parte de trás do controle (por dentro) ficou assim:

O cabo e a placa novos ocupam um espaço significativamente maior do que os originais, mas couberam dentro do controle.

O autor disse que jogou Super Mario Kart com esse controle, então tudo funcionou como esperado, e o processo foi bem mais simples do que eu imaginava.

Para quem tem interesse em hardware, me parece que usar dispositivos antigos para aprender sobre hardware é mais fácil do que ir direto para os novos, que são bem mais complexos e com cada parte bem menor.

Parece que fiz uma viagem no tempo e estou lendo uma revista antiga de games ensinando sobre isso kkk ta aí algo que não esperava ler hj kkkk muito bom, vou arrumar um tempo para testar