Costruire un tastierino numerico con Arduino - prima parte

La mia bellissima tastiera meccanica, di cui ho parlato in un post precedente, è comoda per mille ragioni. Una di queste è il fatto che ha dimensioni inferiori a una tastiera standard grazie alla rinuncia al tastierino numerico: ogni giorno la devo infilare nello zaino per andare al lavoro, e una tastiera full sarebbe scomoda. La mancanza del tastierino numerico non è un problema per il mio lavoro, nel 95% delle situazioni. Accade però che ogni tanto capiti qualche situazione in cui devo fare un pochino di data entry, e in quel caso un tastierino numerico mi farebbe comodo.

Ovviamente la soluzione più sana è quella di spendere letteralmente due spiccioli e comprare un tastierino numerico qualunque su Amazon. Altrettanto ovviamente la soluzione più sana non è mai stata nel mio stile. Inoltre sono masochista, come tutti i nerd. Ho quindi deciso di provare, con molta calma, a costruirne uno da zero.

Il primo passo su cui ragionare è l'interfacciamento con il computer: mi serve un dispositivo USB che sia riconosciuto dal PC come una periferica di input (HID) e che sia programmabile. La scelta più semplice è un sistema basato su Arduino, comodo da programmare, ricco di tutorial e supporto pubblico, e con un sacco di librerie già pronte (tra cui Keyboard.h, che guarda caso serve appunto ad emulare una tastiera). Se scegliamo una scheda Arduino basata su chip ATmega32U4 abbiamo anche il vantaggio del controller USB integrato, che consente alla scheda di agire autonomamente come dispositivo USB riconosciuto dal computer (in assenza di questa funzionalità, l'USB è usato per alimentare la scheda e programmarla da pc ma non può funzionare come dispositivo USB autonomo). La soluzione migliore è procurarsi un Arduino Pro Micro, che occupa poco spazio e ha tutte le caratteristiche richieste. E costa 5-6 euro su Aliexpress.

Il secondo passo è capire come collegare i pulsanti alla scheda. L'Arduino Pro Micro ha una buona selezione di pin utilizzabili come ingressi o uscite digitali (indicati in azzurro nello schema qui sotto).

Avendo 18 pin utilizzabili, potrei collegarne ciascuno a un pulsante diverso e avrei spazio per 18 pulsanti diversi. Per esempio, potrei configurare i pin come input con pullup, e collegare l'altro capo del pulsante a gnd: se il pulsante NON è premuto ho un "1" logico sul pin (grazie al pullup), se è premuto allora il valore letto è "0".

Questo andrebbe bene, ma non è particolarmente efficiente. In particolar modo se un domani volessi costruire qualcosa con più pulsanti. Il meccanismo che si usa normalmente per collegare pulsanti (ad esempio in una tastiera) è una matrice: i tasti sono disposti su righe e colonne, e ogni riga e ogni colonna sono collegate al microcontrollore. In questo modo, per esempio, con 8 (4+4) pin posso controllare 4x4=16 pulsanti, con 10 (5+5) pin ben 25 pulsanti.

Il collegamento è qualcosa del genere:

Il funzionamento è intuitivo: si collegano le righe a uscite digitali, e le colonne a ingressi digitali. Posso dare tensione (cioè mettere un "1" logico) alla prima riga e misurare le colonne, per verificare lo stato dei primi tre tasti. Poi tolgo tensione alla prima riga, metto tensione sulla seconda e misuro ancora le colonne per studiare lo stato del secondo gruppo di tasti. E così via, continuamente.

L'unica aggiunta necessaria per arrivare a un circuito funzionante è mettere un diodo in serie a ciascun pulsante, per evitare che la pressione contemporanea di più tasti attivi per sbaglio colonne che non deve, inviando informazioni errate.

Tanto per fare una prima prova, ho recuperato una manciata di led e pulsanti economici e ho provato a realizzare una matrice 3x3. Ho disegnato questo schema in EasyEDA:

E poi mi sono messo a giocare con il saldatore.

Non è bellissimo (si, lo è :-) ) ma è sufficiente a fare un primo esperimento. A questo punto mi sono messo a scrivere il programma per Arduino.

All'inizio del programma ho definito un pò di costanti per indicare i pin delle righe e delle colonne, nonchè un array destinato a contenere lo stato dei pulsanti (letto a ogni ciclo) e un array con il valore corrispondente da mandare al pc.


#include <Keyboard.h>

// ROWS (inputs)
byte rows[] = {7, 6, 5};
const int rowCount = sizeof(rows)/sizeof(rows[0]);

// COLUMNS (outputs)
byte cols[] = {2, 4, 3};
const int colCount = sizeof(cols)/sizeof(cols[0]);

// Current status of switches
byte keys[colCount][rowCount];

// Keyboard value for each switch
char keyValues[3][3] = {
  {'1', '2', '3'},
  {'4', '5', '6'},
  {'7', '8', '9'}
};

Nel metodo "setup", che è eseguito al boot del firmware, inizializzo la libreria Keyboard e configuro tutti i pin. Le righe (input) sono configurate come input con pullup. Le colonne saranno output, ma per fare si che non siano attive le configuro temporaneamente come input.

void setup() {
  
  Keyboard.begin();

  // Set cols (outputs) as inputs
  for(int x=0; x<rowCount; x++) {
    pinMode(rows[x], INPUT);
  }

  // Set rows as inputs with pullup
  for (int x=0; x<colCount; x++) {
    pinMode(cols[x], INPUT_PULLUP);
  }
    
}

Il metodo "loop", che è quello che verrà eseguito ripetutamente dopo il boot, è semplicemente una lettura dello stato dei pulsanti seguito dall'invio dei corrispondenti tasti alla tastiera:

void loop() {
  readMatrix();
  printKeyboard();
  delay(100);
}

Per leggere lo stato degli interruttori, seleziono una colonna alla volta e la attivo mettendola a "zero". Dopodichè leggo tutte le righe, per studiare lo stato dei corrispettivi pulsanti: se il pulsante è premuto sarà connesso alla colonna e quindi sarà a "zero", se il pulsante non è premuto non sarà connesso a nulla e quindi sarà messo a "uno" grazie al pullup interno. Poi disattivo la colonna (rimettendola in configurazione input) e passo alla colonna successiva. Il risultato di questa analisi è scritto in un array bidimensionale.

void readMatrix() {
  
  // iterate the columns
  for (int colIndex=0; colIndex < colCount; colIndex++) {
    
    // enable col: set to output and low
    byte curCol = cols[colIndex];
    pinMode(curCol, OUTPUT);
    digitalWrite(curCol, LOW);

    // row: interate through the rows
    for (int rowIndex=0; rowIndex < rowCount; rowIndex++) {
      
      byte rowCol = rows[rowIndex];
      
      pinMode(rowCol, INPUT_PULLUP);
      keys[colIndex][rowIndex] = !digitalRead(rowCol);
      pinMode(rowCol, INPUT);
      
    }
    
    // disable the col (set as input)
    pinMode(curCol, INPUT);
    
  }
  
}

Per inviare i pulsanti al computer non devo fare altro che analizzare l'array bidimensionale con due cicli annidati, e se trovo un valore attivo simulo, attraverso la libreria Keyboard, la pressione del tasto, viceversa ne simulo il rilascio.

void printKeyboard() {
  for (int rowIndex=0; rowIndex < rowCount; rowIndex++) {
    for (int colIndex=0; colIndex < colCount; colIndex++) { 
      if (keys[rowIndex][colIndex])
        Keyboard.press(keyValues[rowIndex][colIndex]);
      else
        Keyboard.release(keyValues[rowIndex][colIndex]);
    } 
  }
}

A questo punto mi trovo con un accrocchio con nove pulsanti in grado di mandare i numeri da 1 a 9 al computer. Funziona! I prossimi passi sono fare qualche esperimento con questo prototipo (voglio provare ad aggiungere un encoder e magari qualche led), e quando sarò soddisfatto proverò a progettare e farmi realizzare il PCB in grado di ospitare dei veri tasti meccanici. Ma un pò alla volta.

Alla prossima!