Stick-IO Arcade stick – HID Keyboard emulator

Stick-IO Arcade stick – HID Keyboard emulator

Come molti sapranno ci piace giocare, soprattutto con i vecchi giochi arcade che un tempo si vedevano solo nelle sale giochi e che per goderteli dovevi fare lunghe file fra curiosi, veterani e super campioni.
Adesso quelle sale giochi non ci sono più, ma la passione per quei giochi è rimasta.

Mossi da questa passione, abbiamo pensato di realizzare un nostro controller arcade: Stick-IO.

A bordo sono presenti tre bottoni superiori per accedere ai vari menù, una leva direzionale e 8 bottoni generici per il cui funzionamento varia da emulatore ad emulatore.
Sul retro è presente inoltre uno switch che permette il passaggio rapido da giocatore uno a giocatore due, rimappando quindi tutti i tasti come se ci fossimo materialmente spostati alla postazione del secondo giocatore.

Il controller al momento è compatibile solo con PC e una volta collegato viene visto come una tastiera.
All’interno è presente una scheda Arduino-compatibile basata sulla Leonardo che monta un microcontrollore Atmel 32U4.

Dal punto di vista elettrico il funzionamento è molto semplice: i pin sono configurati in modo che normalmente si trovino allo stato logico alto (in questo caso 5V) e, una volta premuto il relativo pulsante, vari in stato logico basso (0V).

Per quanto riguarda il software è stato previsto un meccanismo che ciclicamente controlli lo stato di ogni pulsante, ne verifichi lo stato istantaneo e quindi l’avvenuta variazione rispetto all’ultimo stato memorizzato; in caso di variazione di stato viene eseguita l’istruzione di pressione o di rilascio del carattere ad esso collegato.

Essendo però i pulsanti un sistema prettamente meccanico – con tutti i difetti del caso – è doveroso ricordarsi che al momento della pressione (e del rilascio), il pulsante commuta più di una volta, ad una frequenza molto elevata e questo può causare un disturbo sulla lettura dello stato da parte del microcontrollore. Per ovviare a questo difetto generalmente si usano sistemi “anti-rimbalzo” hardware e/o software, che fanno si che il microcontrollore veda una sola variazione di stato. In questo caso abbiamo optato per un più economico e veloce sistema software, che non fa altro che memorizzare il momento in cui è avvenuta l’ultima variazione di stato e ignorare le successive che avvengono in un certo intervallo di tempo.
Qui ci viene in aiuto la funzione millis() del core Arduino che, una volta richiamata, restituisce il numero di millisecondi trascorsi dall’accensione del microcontrollore.
Per adattarla al nostro utilizzo, alla pressione di un pulsante, è sufficiente confrontare il valore restituito da millis() con il valore restituito dalla stessa all’ultima lettura: se la differenza fra questi due valori è minore o uguale ad un certo valore – memorizzato nella variabile debounce – la variazione di stato del pulsante viene ignorata, altrimenti viene ritenuta valida.

oscillostickioIn prima battuta non abbiamo utilizzato questo meccanismo, infatti abbiamo notato che alla pressione di un pulsante, il computer riceva più di un carattere, coerentemente con quanto appena spiegato.
Una volta introdotto questo sistema, abbiamo subito riscontrato un miglioramento che abbiamo analizzato con un oscilloscopio e che vedete nell’immagine:

La traccia in giallo rappresenta la variazione di stato di un pulsante senza nessun “filtraggio”, quella in azzurro è il segnale che viene ritenuto valido dal microcontrollore: è evidente come tutte le commutazioni successive alla prima vengano ignorate.

Per la rappresentazione di un pulsante è stato previsto un nuovo tipo di dato strutturato chiamato button che contiene al suo interno il numero del pin al quale è collegato, l’ultimo stato logico letto, i codici dei tasti da simulare per entrambi i giocatori e l’ultimo istante in cui è stato premuto il pulsante (millisecondi restituiti dalla funzione millis()).

typedef struct button
{
    unsigned char pin; //Button's Pin
    unsigned char state; // Button's current State (H or L)
    unsigned char p1; // Keyboard code to emulate for 1P
    unsigned char p2; // Keyboard code to emulate for 2P
    long lastMillis; // Last press time
}button;

Per “forzare” lo stato logico al livello alto è stato necessario attivare la resistenza di pull-up interna di ogni singolo pin:

// set button's pins as input with pull-ups..     
    for (int i = 0; i<N_PIN; i++)
        pinMode(pulsanti[i].pin, INPUT_PULLUP);
// ... and 1P-2P switch too.
    pinMode(pSet, INPUT_PULLUP);

Nella funzione loop() si trova il ciclo di funzioni che si occupano di interrogare tutti i pulsanti, verificare che lo stato logico sia cambiato rispetto l’ultimo controllo ed eventualmente simulare la pressione o il rilascio del relativo tasto.

Alla riga 161 troviamo il controllo sullo switch di selezione del giocatore. In caso di cambio di giocatore, viene simulato il rilascio di tutti i pulsanti del giocatore precedente: questo è necessario perché altrimenti, se provassimo a cambiare giocatore mantenendo premuto un qualsiasi pulsante del controller, il computer continuerebbe a considerare premuto il tasto del giocatore precedentemente selezionato.

Per quanto riguarda la configurazione dei tasti, ci siamo basati su quella di un controller molto diffuso, il X Arcade Dual Joystick.
In questo modo abbiamo reso il nostro controller retrocompatibile: all’occorrenza, direttamente dallo sketch, si possono modificare i valori dei singoli tasti da simulare, agendo sulle variabili p1 e p2 di ogni pulsante.
Da notare che nel X Arcade, alcuni bottoni sono mappati come tasti del pad numerico della tastiera. Su Arduino, per simulare un tasto del pad numerico è sufficiente sommare al valore ASCII del numero scelto il valore numerico 176.
Esempio:

pulsanti[4].p1 = '8'+ 176; // "+176"= Bloc Num ON

In questi giorni pubblicheremo un aggiornamento riguardante l’uso di una nuova scheda Arduino – molto interessante – basata sulla Leonardo.

Di seguito trovate il codice sorgente completo, in fondo trovato il link per scaricare l’archivio.

//  -----------------------------------------------------------------------
//  Stick-IO Arcade stick - HID Keyboard emulator (Arduino Leonardo) v1.0
//  Project page: https://fablabmessina.it/progetto/stick-io/
//  Copyright (C) 2016 FabLab Messina <info@fablabmessina.it>
//  -----------------------------------------------------------------------
//  This programm is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  any later version
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this file.  If not, see <http://www.gnu.org/licenses/>.
//  -----------------------------------------------------------------------

//Button Layout:
//
// ABC
// 1  2  3  7
// 4  5  6  8


#include <Keyboard.h>
#define N_PIN 15
#define uchar unsigned char

const  uchar pSet = A3; //switch Player1(1P)-Player2(2P)
const long debounce = 5; //debounce (ms)



typedef struct button
{
    unsigned char pin; //Button's Pin
    unsigned char state; // Button's current State (H or L)
    unsigned char p1; // Keyboard code to emulate for 1P
    unsigned char p2; // Keyboard code to emulate for 2P
    long lastMillis; // Last press time
}button;

button pulsanti[N_PIN]; //button's array


void setup() {
  
  
    delay(5000);

// Button A
    pulsanti[0].pin = 12;
    pulsanti[0].p1 = '3';
    pulsanti[0].p2 = '3';
    pulsanti[0].state = 1;
    pulsanti[0].lastMillis = 0;

// Button C
    pulsanti[1].pin = A1;
    pulsanti[1].p1 = '1';
    pulsanti[1].p2 = '2';
    pulsanti[1].state = 1;
    pulsanti[1].lastMillis = 0;

// Button 1    
    pulsanti[2].pin = 7;
    pulsanti[2].p1 = 128;
    pulsanti[2].p2 = 'a';
    pulsanti[2].state = 1;
    pulsanti[2].lastMillis = 0;
// Tasto 2
    pulsanti[3].pin = 2;
    pulsanti[3].p1 = 130;
    pulsanti[3].p2 = 's';
    pulsanti[3].state = 1;
    pulsanti[3].lastMillis = 0;
// Dir UP   
    pulsanti[4].pin = 10;
    pulsanti[4].p1 = '8' + 176; // "+176"= Bloc Num ON
    pulsanti[4].p2 = 'r';
    pulsanti[4].state = 1;
    pulsanti[4].lastMillis = 0;

// Dir DOWN 
    pulsanti[5].pin = 13;
    pulsanti[5].p1 = '2' + 176;
    pulsanti[5].p2 = 'f';
    pulsanti[5].state = 1;
    pulsanti[5].lastMillis = 0;
    
// Dir RIGHT
    pulsanti[6].pin = A0;
    pulsanti[6].p1 = '6' + 176;
    pulsanti[6].p2 = 'g';
    pulsanti[6].state = 1;
    pulsanti[6].lastMillis = 0;

// DIR LEFT   
    pulsanti[7].pin = 11;
    pulsanti[7].p1 = '4' + 176;
    pulsanti[7].p2 = 'd';
    pulsanti[7].state = 1;
    pulsanti[7].lastMillis = 0;

// Button B    
    pulsanti[8].pin = A2;
    pulsanti[8].p1 = '4';
    pulsanti[8].p2 = '4';
    pulsanti[8].state = 1;
    pulsanti[8].lastMillis = 0;

// Button 3
    pulsanti[9].pin = 9;
    pulsanti[9].p1 = ' ';
    pulsanti[9].p2 = 'q';
    pulsanti[9].state = 1;
    pulsanti[9].lastMillis = 0;

// Button 4    
    pulsanti[10].pin = 3;
    pulsanti[10].p1 = 129;
    pulsanti[10].p2 = 'w';
    pulsanti[10].state = 1;
    pulsanti[10].lastMillis = 0;

// Button 5
    pulsanti[11].pin = 8;
    pulsanti[11].p1 = 'z';
    pulsanti[11].p2 = 'e';
    pulsanti[11].state = 1;
    pulsanti[11].lastMillis = 0;

// Button 6  
    pulsanti[12].pin = 4;
    pulsanti[12].p1 = 'x';
    pulsanti[12].p2 = '[';
    pulsanti[12].state = 1;
    pulsanti[12].lastMillis = 0;

// Button 7    
    pulsanti[13].pin = 5;
    pulsanti[13].p1 = 'c';
    pulsanti[13].p2 = ']';
    pulsanti[13].state = 1;
    pulsanti[13].lastMillis = 0;

// Button 8
    pulsanti[14].pin = 6;
    pulsanti[14].p1 = '5';
    pulsanti[14].p2 = '6';
    pulsanti[14].state = 1;
    pulsanti[14].lastMillis = 0;

// set button's pins as input with pull-ups..     
    for (int i = 0; i<N_PIN; i++)
        pinMode(pulsanti[i].pin, INPUT_PULLUP);
// ... and 1P-2P switch too.
    pinMode(pSet, INPUT_PULLUP);
    
  

}


uchar temp; //Button momentary state
long currentMillis; // state time
uchar p = 1; //1P-2P momentary state
uchar lastP = 1; //1P-2P last state
void loop() {
  
  if((p=digitalRead(pSet))!=lastP) // if state of 1P-2P change:
  {
      Keyboard.releaseAll(); //release all previous buttons
      lastP = p; //save 1P-2P state
  }
  
  
  for (uchar i = 0; i<N_PIN; i++)
    {
        currentMillis=millis();
        temp = digitalRead(pulsanti[i].pin);
        if (temp!=pulsanti[i].state && currentMillis-pulsanti[i].lastMillis > debounce)
        {
            if(pulsanti[i].state && !temp)
            {
                Keyboard.press(lastP?pulsanti[i].p1:pulsanti[i].p2);
                pulsanti[i].state = 0;
            }
            else if (!pulsanti[i].state && temp)
            {
                Keyboard.release(lastP?pulsanti[i].p1:pulsanti[i].p2);
                pulsanti[i].state = 1;
            }
                
            
            pulsanti[i].lastMillis = currentMillis;

        }
        
    }

    
}

Ideato da: unshe
Test di funzionalità ed ergonomia: Alborto
Articolo e sketch di: Dario Gogliandolo