martes, 9 de junio de 2015

Arduino - Interfaz para Joystick de Family Game

  Buscando un control para un par de proyectos con Arduino que estoy desarrollando encontré una par de Joystick's de un viejo Clon Family (Dynacom en rigor de verdad) y me parecieron adecuados para la aplicación.
  Tratando de encontrar en internet la forma de conexionado y una librería para el manejo me encontré con que no coincidía la información existente con el PinOut del controlador que tenía en mi poder.


  Después de desarmar, analizar un poco el circuito (mas detalles abajo para los interesados) y de hacer un "tanteo" logré dar con el PinOut y el protocolo para la lectura de teclas.
  Desarrollé una librería (si bien hay alguna que se podría llegar a adaptar me gusta perder tiempo ;)) simple para el manejo.

A los bifes

PinOut
  Los muchachos que clonaron al NES no se gastaron mucho con respecto; Si bien el PinOut y el conector es distinto, comparten señales con los Joystick clásicos de NES / SNES (supongo empezaron haciendo Joystick's genéricos para las NES :P).


  El conector utilizado es un clásico DB9 utilizando solo cinco pines de los nueve; dos para alimentación y tres para señal.
  La alimentación (VCC + GND) es de 5V (probé con 3V3 y funcionó perfectamente).
  Con respecto a los pines de señal:
    Latch: Entrada digial que toma el estado de cada botón del controlador (pueden haber varios apretados a la vez) y genera un registro de tantos bits como botones (8).
    Clock: Entrada digital de reloj que produce el desplazamiento del registro de 8bits con el estado de cada botón (1 bit por botón).
    Data: Salida digital. Bit a bit (desplazamiento) del registro de estado de cada botón.

Conexionado
  Para el conexionado se pueden utilizar cualquiera de las E/S del Arduino (inclusive las analógicas).
  Para el ejemplo utilicé las E/S 49 para Data, 51 para Latch y 53 para el Clock (los pines utilizados se pasan como parámetros de la rutina de inicialización de la librería).
Protocolo
  El protocolo utilizado es el de los Joystick's NES:
    60 veces por segundo, el CPU del NES genera un pulso de 12uS sobre el pin Latch. Esta señal toma el estado de cada botón y el mismo se almacena a razón de 1 bit por botón en un registro de 8 bits.
    6 uS después el CPU envía un tren de 8 pulsos de 12uS de período al 50% de duty (6uS de estado bajo + 6uS de estado alto) por el pin Clock.
    Sincronizado con el flanco descendiente del pin clock se realiza un desplazamiento del registro de 8 bits por el pin Data. El CPU del NES captura el estado de cada botón leyendo el estado del pin Data en cada flanco ascendente del pin Clock.
    Cada botón tiene su "bit" específico.

  Lo anteriormente mencionado puede apreciarse en la siguiente figura:


  Notas
    Es posible presionar múltiples teclas al mismo tiempo sin ningún inconveniente (salida con tantos bits 1 como botones apretados).
    Si bien el Joystick tiene 10 botones, 2 de los mismos son "virtuales". A y B turbo equivalen a la pulsación repetida de los botones A y B respectivamente.

  La rutina de "lectura" de teclas es de implementación extremadamente sencilla:

unsigned char CheckButtons(void){
  unsigned char buttons = 0;
  unsigned char i;
  digitalWrite(_LatchPin, 1);
  delayMicroseconds(12);
  digitalWrite(_LatchPin, 0);
  for (i = 0 ; i < BUTTON_COUNT ; i++){
    delayMicroseconds(6);
    buttons <<= 1;
    buttons |= digitalRead(_DataPin) ? 0 : 1;
    digitalWrite(_ClockPin, 1);
    delayMicroseconds(6);
    digitalWrite(_ClockPin, 0);
  } 
  return buttons;
}

Librería
  La librería de control es bien simple; Se trata de una única clase que verifica las teclas pulsadas / presionadas y genera los eventos asociados.
  Puede descargarse del repositorio de blog desde aquí
  Características generales:
    No utiliza interrupciones (usa Polling).
    Cuenta con Buffer circular de los últimos botones presionados (últimos 16).
    Se pueden asociar Callback's a los eventos de teclas presionadas y pulsadas.
    Permite la utilización de múltiples instancias (más de 1 Joystick).

Clase pública
class FamilyGameJoystick {
  public:
    void Init(unsigned char DataPin, unsigned char LatchPin, unsigned char ClockPin);
    void Release(void);
    unsigned char Poll(void);
    bool ButtonDown(unsigned char button);
    bool ButtonPressed(void);
    unsigned char GetButton(void);
    void onButtonDown(void (*function)(unsigned char));
    void onButtonPress(void (*function)(unsigned char));
};

Constantes utilizadas
  BUTTON_RIGHT
BUTTON_LEFT
BUTTON_DOWN
BUTTON_UP
BUTTON_START
BUTTON_SELECT
BUTTON_B
BUTTON_A

Descripción de los métodos

void Init(unsigned char DataPin, unsigned char ClockPin, unsigned char LatchPin);
Rutina de inicialización de la librería.
Se pasan como parámetros los pines del Arduino conectados a los pines Data (pin 4 del DB9), Clock (pin 2 del DB9) y Latch (pin 3 del DB9) del Joystick.

void Release(void);
Libera los recursos asociados a la librería (puertos).

unsigned char Poll(void);
Rutina de Polling.
Interroga el Joystick en busca de teclas pulsadas y genera los eventos asociados.
Debe ser llamada periódicamente desde el bucle principal de la aplicación para el correcto funcionamiento del resto de las rutinas.
Puede ser llamada desde una interrupción.
Retorna máscara de 8 bits con botones actualmente pulsados utilizando las constantes [BUTTON_RIGHT .. BUTTON_A].

bool ButtonDown(unsigned char button);
Retorna [true] si el / los botones pasados en el parámetro [button] están pulsados.
Se entiende "pulsado" como el botón apretado que se mantiene en ese estado mientras se realiza el chequeo.
El parámetro [button] es una máscara de 8 bits utilizando las constantes [BUTTON_RIGHT .. BUTTON_A].

bool ButtonPressed(void);
Retorna [true] si hay al menos un botón en el buffer de botones presionados
Se entiende como "presionado" a la acción de pulsar y liberar cualquiera de los botones.

unsigned char GetButton(void);
Retorna el primer botón del buffer de botones presionados.
El valor retornado se encuentra en el rango [BUTTON_RIGHT .. BUTTON_A].

void onButtonDown(void (*function)(unsigned char));
Registra "callback" de tecla pulsada.
La rutina [function] pasada como parámetro se "ejecuta" mientras se mantenga pulsada una o varias teclas.
El parámetro pasado a la función se encuentra en el rango [BUTTON_RIGHT .. BUTTON_A].

void onButtonPress(void (*function)(unsigned char));
Registra "callback" de tecla presionada.
La rutina [function] pasada como parámetro se "ejecuta" cada vez que se presiona (pulsa y libera) una tecla.
El parámetro pasado a la función se encuentra en el rango [BUTTON_RIGHT .. BUTTON_A].

Instalación / Utilización
  La instalación es la "clásica" del IDE Arduino:
    Menú Programa -> Include Library -> Add .ZIP Library
  Para más información consultar la documentación oficial: Installing Additional Arduino Libraries

  Una vez instalada la librería en el IDE, en un Sketch nuevo o existente se debe proceder con la inclusión de la librería: Programa -> Include Library -> FamilyGameJoystick.

Sketch de ejemplo
  A continuación un ejemplo simple sin la utilización de eventos.
  Este y otros ejemplos se adjuntan con la librería y pueden ser abiertos como Sketch's de ejemplo de la librería (Archivo -> Ejemplos -> FamilyGameJoystick).

#include <FamilyGameJoystick.h>

//variable global de acceso al Joystick
FamilyGameJoystick Joystick;

void setup() {
  // inicialización del terminal serial
  Serial.begin(9600);
  // rutina de inicialización del Joystick
  // E/S 49 de arduino a pin Data del Joystick 
  // E/S 51 de arduino a pin Latch del Joystick 
  // E/S 53 de arduino a pin Clock del Joystick   
  Joystick.Init(49, 51, 53);
}

//bucle principal de la aplicación
void loop() {
  // se llama a la rutina de Polling
  // la misma establece comunicaciones con el joystick, detecta pulsación de botones, 
  // genera los eventos asociados, etc.
  Joystick.Poll();
  
  //si se presionó un botón (buffer de hasta 16 botones)
  if (Joystick.ButtonPressed()){
    Serial.print("Botón presionado: ");
    // escribimos máscara del botón presionado
    Serial.println(Joystick.GetButton());
  }
  
  //si el botón "izquierda" está siendo pulsado
  if (Joystick.ButtonDown(BUTTON_LEFT)){
    //decrementar la posición X del personaje   
    Serial.println("Izquierda");
  }
    
  //si el botón "derecha" está siendo pulsado
  if (Joystick.ButtonDown(BUTTON_RIGHT)){
    //incrementar la posición X del personaje
    Serial.println("Derecha");    
  }
}

En lo próximo
    Generaré otros ejemplos de aplicación (tengo un PacMan a punto de salir del horno).
    Implementaré sistema "anti-rebotes" (estimé estaría implementado en el controlador del Joystick / a nivel eléctrico pero me equivoqué).
    Corregiré pequeños detalles, documentaré el código fuente, puliré la documentación, etc.
    Trataré de conseguir Joystick's de otras marcas / modelos (temo que el PinOut difiera).

Espero el contenido haya sido de utilidad.
Consultas / comentarios son siempre bienvenidos.
Nos vemos en la próxima entrada, Saludos!