Scansione Statica per Tastiere a Matrice



È una soluzione, alternativa al metodo classico di scansione, che mi è venuta in mente molto tempo fa, ma ho sempre pensato che qualcun altro potesse averla già concepita prima di me, senza che la cosa mi fosse giunta, e che quindi non costituisse una vera novità.
Qualche tempo fa, però, ho letto su una rivista un interessante articolo sulla gestione delle tastiere a matrice, in cui non si faceva riferimento alcuno a tale tecnica, quindi ho deciso di ritagliare un po’ di tempo per scrivere il presente.

Anni fa, avevo appena abbandonato i vecchi, lenti e costosissimi ST6 ed avevo acquistato il mio PICStart Plus, stavo realizzando un radiocomando con una tastiera 4x4 ed un PIC facente le funzioni sia di decoder tastiera che di encoder comandi.
Il tutto serviva per ridurre le dimensioni di quello già esistente basato su componenti discreti (74C922 + MC145026).
Dato che era alimentato da una micro batteria da 12V (di quelle da 6mm, di diametro inferiore alle classiche per radiocomandi, quindi di minor capacità), era essenziale ridurre al minimo i consumi, specialmente quelli a riposo: la CPU doveva fermarsi quando non utilizzata!
La cosa è facile, l’istruzione sleep serve a questo, ed a questo serve anche il risveglio sulla variazione dello stato di un pin d’ingresso.
Solo però, che in questo caso l’ingresso era una tastiera a matrice, non un semplice pulsante.
Come potevo fare a risvegliare il PIC qualunque fosse il tasto premuto?
Nella scansione di una tastiera a matrice, se ad esempio si scandisce per colonne, una sola colonna alla volta verrà portata al livello “attivo” (supponiamo che il livello attivo sia quello alto, mentre le altre colonne sono a livello basso).
Se si arresta il micro e si preme un tasto che corrisponde ad un’altra colonna, non avremo alcuna variazione sugli ingressi di riga, e, di conseguenza, questo non si risveglierà dallo sleep.
La prima soluzione che mi sia venuta in mente è stata quindi quella di portare tutte le colonne in stato attivo subito prima dello sleep, per poi ricominciare la scansione normalmente dopo il risveglio.
Ed è stato proprio a questo punto che mi è venuta un’idea di quelle che fanno pensare “È così ovvio!”.
Se ho tutte le colonne allo stato attivo, controllando, con una semplice lettura, quale ingresso di riga è a livello alto, so già su quale riga si trova il tasto premuto.
A questo punto potrei riavviare la scansione per colonne…ma è molto più semplice intervenire in un altro modo…
Scambio gli ingressi con le uscite, cosa che sicuramente chi ha inventato le tastiere a matrice non può aver pensato di fare, ma che è facilissima sulla maggior parte dei microcontrollori, PIC inclusi.
A questo punto le righe sono delle uscite, e vengono portate tutte a livello alto, mentre le colonne, che erano uscite, sono diventate degli ingressi.
Dato che so già su quale riga si trova il pulsante premuto, leggendo quale ingresso di colonna è a livello alto so anche quale sia la colonna interessata, in maniera velocissima. Tutto questo senza aver effettuato alcuna operazione fino alla pressione di un tasto, cosa indispensabile per una sospensione della MCU!
Chiaramente andranno implementate tutte le classiche tecniche antirimbalzo, come il calcolo della media su diversi campionamenti consecutivi, ma ciò che conta è l’aver sostituito la classica scansione della tastiera con questo metodo quasi statico, che richiede una sola istruzione per il controllo dello stato della tastiera e che funziona perfettamente anche con la CPU in sleep.
Dico una sola istruzione perché, poste tutte le colonne come uscite attive, basterà leggere lo stato del registro associato al port d’ingresso per sapere se qualche tasto è premuto, e quindi passare alla sua decodifica come descritto, oppure continuare con le normali operazioni (resto del programma).
Facciamo un esempio proprio su un PIC.

Schema di Esempio

Ci riferiremo allo schema di figura, e si adotterà la convenzione di considerare le uscite (siano esse le righe o le colonne) attive al livello basso, anziché a quello alto.
Innanzitutto dobbiamo sapere cosa sono le resistenze di pull-up
Supponiamo di voler collegare un pulsante ad un ingresso logico ed il negativo, per generare un ingresso basso premendo il pulsante: quando questo è premuto, rimbalzi a parte, l’ingresso sarà a livello basso, ma quando il tasto è aperto chi stabilisce il valore logico?
Per tale scopo si inserisce una resistenza (detta di pull-up, perché “tira” il valore d’ingresso verso il positivo di alimentazione e quindi l’uno logico) tre l’ingresso ed il positivo: a pulsante aperto la resistenza è più che sufficiente a portare ad uno logico l’ingresso, senza consumare corrente (si considera l’impedenza d’ingresso infinita, cosa verosimile con ingressi C-Mos).
Se avessimo voluto inserire il pulsante tra il positivo e l’ingresso avremmo dovuto inserire la resistenza verso massa (pull-down).
Nel nostro caso, avendo scelto di considerare le uscite attive a livello basso, dovremo inserire delle resistenze tra gli ingressi ed il positivo di alimentazione.
In questo ci viene incontro l’hardware interno del PIC, sui cui piedini impostati come ingressi è possibile abilitare delle resistenze di pull-up che serviranno allo scopo, senza la necessità di componenti esterni.
In più, dato che noi scambieremo ingressi ed uscite, una serie di pin è commutata come uscita per il 99.999% del tempo (anche quando la CPU è in sleep) e si troveranno a livello basso. Se avessimo inserito delle resistenze esterne tra tali pin ed il positivo, queste costituirebbero un’inutile carico e relativo spreco di energia per tutto il tempo in cui non servono. Questo non sarebbe molto adatto ad un circuito alimentato a batteria!
Con le resistenze di pull-up interne, invece, il problema non si pone: nel momento in cui le abiliteremo via firmware, esse saranno attive solo sui pin impostati come ingresso, e si spegneranno automaticamente quando un pin diventerà uscita.

Le resistenze che restano sugli ingressi non costituiscono invece un carico, almeno finché non si preme un tasto, ma questo riguarda solo una piccolissima frazione del tempo di lavoro dell’apparato, ed è una condizione necessaria.
Se invece si utilizza un micro privo di resistenze di pull-up, oppure si ha l’impossibilità di attivarle (magari perché darebbero fastidio ad altri ingressi sullo stesso port) è possibile usare un piccolo stratagemma: si inseriscono le resistenze esternamente, ma per quanto riguarda quelle relative ai pin che sono uscite per la maggior parte del tempo, anziché collegarle tra il pin ed il positivo, si collega il capo comune ad un altro pin del micro.
Questo deve essere impostato come uscita, e deve essere a livello basso per tutto il periodo in cui i relativi pin sono uscite, commutando a livello alto (e quindi inserendo i pull-up esterni) solo un attimo prima che questi diventino ingressi.
Questa tecnica, a fronte dello spreco di un (talvolta preziosissimo) pin d’uscita comporta l’eliminazione di uno spreco di corrente che, seppur minimo, mal si adatta ad un circuito alimentato da una così piccola batteria.
La cosa è molto più semplice, soprattutto a livello di sbroglio del PCB, se si utilizza una rete resistiva SIL con pin comune: si utilizza come uscita di controllo il pin affianco a quelli delle colonne, rendendo il PCB semplicissimo.
Nello schema si vede una rete resistiva inserita sui pin relativi alla parte bassa del PortB, che sono uscite a libello basso per la maggior parte del tempo. Questa è collegata con delle linee tratteggiate perché inutile col micro in uso.


Analizziamo ora il semplicissimo listato.
Per prima cosa abbiamo il Main, in cui si attivano le resistenze di Pull-UP interne del PIC16F84 relative al PortB e si configurano le porte di ingresso e uscita.
Segue, in Inizio Ciclo, parte lettura dei tasti premuti.

;*****************************************************************************************
;************** Inizio Ciclo **********************************************************
;*****************************************************************************************
CICLO MOVF PORTB,F ;Serve per poter disabilitare il flag delle interruzioni su PortB
BCF RBIF ;Disabilito il Flag Interruzioni PortB4-7
BSF RBIE ;Abilito Interrupt su variazione PortB4-7
BSF GIE

;*****************************************************************************************
;************** Sospensione del Micro ****************************************************
;*****************************************************************************************
DORMI SLEEP ;Spengo l'oscillatore interno: la MCU consuma pochissimo.
;Dopo il risveglio verrà eseguita la procedura fittizzia
;di gestione delle interruzioni, che non farà altro che
;ritornare all'istruzione successiva allo sleep.


Sapendo che solo i pin relativi alla parte alta del Port B (PB4, PB5, PB6 e PB7) possono generare un’interruzione se modificati (e quindi risvegliare il micro dallo sleep), si impostano questi ultimi come ingressi, lasciando gli altri quattro pin dello stesso port come uscite a livello basso.
Devo ora abilitare le interruzioni su variazione di un pin, ma prima devo disattivare il relativo flag, per evitare che, appena abilitate le interruzioni, se ne generi una inesistente.
Per poterlo fare è indispensabile azzerare il bit RBIF (nel registro IntCon), ma non prima di aver eliminato le incongruenze avviando una lettura del PortB (come descritto nel DataSheer della MCU alla sezione “4.2 PORTB and TRISB Registers”).
Si abilitano quindi anche globalmente le interruzioni (senza che ciò ne generi una immotivata) e si addormenta il micro.
Una volta eseguita l’istruzione di Sleep il PIC fermerà l’oscillatore, assorbendo (secondo il grafico nel Datasheet) meno di 0.2A a 5V, e nessuna corrente fluirà nelle resistenze di Pull-up interne finché non premeremo un tasto.
Quando ciò avviene, collegheremo uno dei quattro pin impostati come ingresso (PortB 4-7) con uno dei quattro impostati come uscite (PortB 0-3) che si trovano a livello basso.
Questo porterà ad una variazione del pin d’ingresso interessato, che era mantenuto al livello alto dalla resistenza di Pull-up interna, nonché al risveglio del micro, essendo tali ingressi abilitati allo scopo.
Una volta risvegliato, il micro eseguirà un salto alla procedura di gestione delle interruzioni, dopo aver inserito nello stack l’attuale contenuto del Program Counter, ovvero l’indirizzo dell’istruzione successiva allo sleep.
Il vettore di interrupt si limità a disabilitare ulteriori interruzioni provenienti dal PortB e ad uscire, tornando al normale ciclo di programma e riattivando il GIE (l'istruzione RetFIE setta GIE).

;*****************************************************************************************
;************** Gestore Interruzioni *****************************************************
;*****************************************************************************************
ORG 0004h
BCF RBIE ;Disabilito le interruzioni sui pin PortB4-7
RETFIE ;RetFIE riabilita il GIE.

Segue quindi una procedura antirimbalzo alquanto rudimentale la molto efficace.
Una variabile, CONT1, funge da contatore di ciclo per il conto alla rovescia, e viene inizializzata a zero.
Si ricordi che nei PIC l’istruzione DECFSZ (Decrementa un File e Salta se ottieni Zero) effettua il controllo DOPO aver decrementato, e che quindi eseguirla su una variabile che vale già zero non provoca il salto.
Nel ciclo si confronta il dato letto dal PortB con il suo precedente valore: se sono diversi si riazzera Cont1, riavviando il ciclo. Se il dato risulta invece stabile per 256 cicli consecutivi il contatore riuscirà ad essere decrementato fino a zero, forzando un’uscita dal ciclo e l’esecuzione delle istruzioni successive.

;*****************************************************************************************
;************** Antirimbalzo **********************************************************
;*****************************************************************************************
ANTIR1 MOVF PORTB,W ;leggo il PortB, sapendo che la parte bassa è irrilevante, dato che a zero
XORWF VDATO,F ;Confronto con la vecchia lettura
EXECCLR ZERO ;Se i dati sono diversi, a causa di un falsocontatto
CLRF CONT1 ;azzero il contatore e ricomincio l'attesa.
MOVWF VDATO ;Salvo l'attuale come vecchio valore per la prox
DECFSZ CONT1,F ;Per poter valere zero dopo il decremento, Cont1
;deve valere uno. Se è stato azzerato varrà zero
;e si decrementerà a 255 ricominciando il conteggio
;senza attivare il salto condizionato.
GOTO ANTIR1 ;Continua regolarmente il conto alla rovescia.

Attenzione però: aver avuto 256 letture consecutive identiche implica che il dato sia stabile, ma non che sia valido. Se un disturbo elettromagnetico riuscisse a risvegliare il PIC dallo sleep, difficilmente riuscirà anche a superare il filtro appena descritto, che si libererà solo alla scomparsa del disturbo. Ma a questo punto il ciclo constaterà che gli ingressi sono si stabili, ma anche tutti a livello alto.
Si effettua quindi un ulteriore controllo: se il dato letto dopo il ciclo è quello relativo allo stato di riposo, in cui tutti gli ingressi sono a livello alto, si ritorna all’inizio della procedura di scansione, riaddormentando il micro.

;*****************************************************************************************
;************** Controllo tasto effettivamente premuto **********************************
;*****************************************************************************************
;A questo punto il dato è stabile, ma non è detto che sia valido
XORLW 0F0h ;Se i quattro ingressi sono tutti alti in W vrò 1111 0000,
EXECSET ZERO ;ovvero nessun tasto è premuto, ma si è trattato di un disturbo.
GOTO CICLO ;In tal caso ri-addormento il micro.

Una volta letto in maniera stabile lo stato degli ingressi, dovremo calcolare a quanto equivale tale stato.
Per far questo dobbiamo calcolare il logaritmo in base due del complemento ad uno del solo nibble relativo agli ingressi, eventualmente swappato…
Più semplice a farsi che a dirsi: dato che gli ingrassi sono nel nibble alto (bit 4..7 del byte letto) dobbiamo scambiare i due nibble con un’istruzione di swapf di VDato su se stesso.
Azzero quindi la variabile Dato, che conterrà l’uscita ed in cui calcolerò il logaritmo.
Farò poi scorrere il byte a destra finché non ne esce uno zero (che finisce nel Carry); ogni volta che ciò non accade incremento Dato.

;*****************************************************************************************
;************** Calcolo logaritmo di Riga ******************************************
;*****************************************************************************************
CLRF DATO ;Azzero Dato
SWAPF VDATO,F ;Scambio il nibble alto di VDATO con quello basso
;per avere in quest'ultimo il dato relativo alla colonna.
QUALEC RRF VDATO,F ;Per avere il valore della colonna, devo calcolare
EXECCLR CARRY ;il logaritmo in base due del contenuto di VDATO shiftandolo
GOTO HOCOL ;finché non trovo il bit basso (che so essere tra i bit 0..3)
INCF DATO,F ;Fintanto incremento DATO
GOTO QUALEC ;e ciclo

Se, ad esempio, ho letto 00001011, shifterò la prima volta, ed uscirà un 1; incremento Dato (1) e shifto una seconda volta, ne uscirà il secondo 1; incremento Dato (2) e shifto una terza volta, ma stavolta esce uno 0, quindi non incremento ed esco dal ciclo di calcolo del logaritmo.
In questo caso Dato conterrà 2, ed infatti quello a zero è il bit numero due, che è il valore della colonna su cui si trova il tasto.
Scegliendo di utilizzare questo come parte più significativa del risultato, lo moltiplicheremo per quattro con due operazioni di shift a sinistra (rlf): si noti che con tali operazioni a destra entrerà il contenuto del Carry, ma con il primo shift sappiamo già contenere zero (ha provocato l’uscita dal ciclo logaritmico), mentre il secondo shift inserirà lo zero che è uscito dalla testa della variabile (bit 7) col primo.

;*****************************************************************************************
;************** Moltiplicazione per Quattro ******************************************
;*****************************************************************************************
HOCOL RLF DATO,F ;Col primo shift ciò che esce è uno Zero, quindi Carry è pronto
RLF DATO,F ;per il secondo shift senza necessità di azzeramento

Ora avviene lo scambio di ingressi ed uscite.
Da notare che, se non avessimo usato le resistenze di pull-up interne per le colonne, ma avessimo dovuto inserirne delle esterne, in questo punto del programma avremmo dovuto portare a livello alto l’uscita di controllo della rete resistiva, inserendo una resistenza tra ogni pin delle colonne ed il livello alto. Questo non è il nostro caso.

;*****************************************************************************************
;************** Scambio ingressi-uscite **************************************************
;*****************************************************************************************
MOVLW b'00001111' ;PortB Alto come Uscite (righe) e basso come Ingressi (colonne)
;Se non avessi i Pull-UP interni, quì dovrei abilitare il pin di controllo della
;rete resistiva esterna per garantire il Pull-UP alle colonne quando sono ingressi.
BSF RP0 ;/BANCO\
MOVWF PORTB ;Scambio ingressi ed uscite
BCF RP0 ;\BANCO/
CLRF PORTB ;Porto bassi i pin del PortB che sono uscite, ovvero le righe

Dato che ho già eseguito il controllo antirimbalzo, e che so che il tasto è stabile e premuto, posso semplicemente leggere il PortB ed eseguire il logaritmo come prima (senza swap).

;*****************************************************************************************
;************** Lettura Colonna **********************************************************
;*****************************************************************************************
;Leggendo la colonna non ho bisogno di un nuovo controllo antirimbalzo, il dato è già stabile
MOVF PORTB,W ;Ora ciò che mi serve è la sola parte bassa del PortB,
;che so già contenere uno ZERO.


;*****************************************************************************************
;************** Calcolo logaritmo di Colonna ******************************************
;*****************************************************************************************
MOVWF VDATO ;Come prima userò VDATO come variabile di Shift
;per il calcolo deel logaritmo
QUALER RRF VDATO,F ;Per avere il valore della riga, devo calcolare
EXECCLR CARRY ;il logaritmo in base due del contenuto di VDATO shiftandolo
GOTO HORIG ;finché non trovo il bit basso (che so essere tra i bit 0..3)
INCF DATO,F ;Fintanto incremento DATO
GOTO QUALER ;e ciclo

Subito dopo reimposto il PortB nella sua configurazione di riposo (disattivando l’eventuale pull-up esterno) per risparmiare energia, ma non riattivo ancora l’interrupt della tastiera.

;*****************************************************************************************
;************** Reimpostazione PIN *******************************************************
;*****************************************************************************************
MOVLW b'11110000' ;PortB Alto come ingresso (righe) e basso come uscita (colonne)
BSF RP0 ;/BANCO\
MOVWF PORTB ;Solo i pin PortB4-PortB7 possono generare interrupt e risvegliare il micro.
BCF RP0 ;\BANCO/
CLRF PORTB ;Azzerando tutto il PortB porto a livello basso

Ottengo quindi in Dato il valore del tasto premuto, che, per puri motivi didattici, invio al PortA. A questo sarà possibile collegare quattro led o un display esadecimale con decodifica integrata (tipo il TIL311 della Texas Instruments).

;*****************************************************************************************
;************** Fine Lettura **********************************************************
;*****************************************************************************************
;Ora DATO contiene il valore del tasto premuto, dove la colonna dà i due bit più
;significativi e la riga quelli meno significativi.
HORIG MOVF DATO,W ;Trasferisco il valore del tasto sul PortA per poterlo
MOVWF PORTA ;visualizzare su un display esadecimale o dei LED.

Dopo di che, dopo aver eseguito anche altre operazioni col dato acquisito, il ciclo ricomincia reimpostando l’interrupt della tastiera e mandando nuovamente il PIC in sleep.

;*****************************************************************************************
;************** Uso dei dati (P.E. Trasmissione) *****************************************
;*****************************************************************************************
;Quì userò il dato letto
;istruzioni...
;istruzioni...
;istruzioni...

;*****************************************************************************************
;************** Ritorno al ciclo d'attesa e Sleep **********************************
;*****************************************************************************************
GOTO CICLO



Quella descritta è una tecnica che ben si adatta a sostituire la normale scansione della tastiera per righe o per colonne.
Nata per il progetto di un telecomando (vedi Telecomando 16 Canali con PIC), in cui si abbandonò l’originale PIC16C84 (che ora è fuori produzione da anni) perché troppo costoso (15.000L) in favore di un 16C54, un paio d’anni fa l’ho efficacemente usata per realizzare una tastiera PS/2 ridotta (con meno di una sessantina di tasti) con un PIC16C57 privo di interruzioni (lo sleep non era necessario, essendo il circuito alimentato dal PC cui si collegava) e di pull-up interni.
È molto adatta anche a sistemi in cui la CPU deve svolgere molti compiti contemporaneamente e non ha il tempo di scandire velocemente la tastiera. In molti cellulari capita che questa risponda lentamente alla pressione dei tasti, rendendo l’utente impossibilitato ad immettere correttamente la sempre crescente quantità di dati che le nostre povere dita si sono dovute abituare ad inserire in un tempo sempre calante.
Capita poi che alcuni telefoni abbassino la velocità di scansione durante il riposo. Risultato: se digitate velocemente un numero non apparirà la prima cifra (come accade con gli odiosi chordless forniti da fastweb…il mio l’ho sfracellato contro un muro all’ennesima volta in cui l’ha fatto!).


Ora, se la tecnica esisteva già, leggete qui ed imparatela anche voi, se non esisteva…ricordatela pure come “algoritmo di Marcantonio per la scansione passiva delle tastiere a matrice” (viva la modestia!)…poi, ovviamente, imparatela anche voi ed usatela dove più vi può essere utile.


[Sorgente Assembler per PIC16F84]  [Schema formato CadSoft Eagle]