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.
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]