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.
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).
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:
- Envia um pulso alto de 12µs (microsegundos) no pino 3.
- Espera 6µs.
- Se o pino 4 estiver baixo, o botão B está pressionado.
- Envia um pulso baixo de 6µs seguido por um pulso alto de 6µs no pino 2.
- 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.
- 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:
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 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.