Im WWW Suchen

Benutzerdefinierte Suche

Tasten auswerten in Assembler


Tasten sind  wahrscheinlich immer noch das Eingabemedium Nr.1 in der Mikrocontrollerwelt. An einen AVR kann man einen Taster an einen beliebigen Portpin gegen GND anschließen, den internen Pullup Widerstand einschalten und schon können wir mit einem Tastendruck unserem AVR sagen das wir eine bestimmte Funktion ausführen wollen. Normale Taster haben jedoch einen Nachteil. Taster prellen. Wird ein Taster betätigt, federt der der mechanische Kontakt noch einige male zurück bis er dann zum Stillstand kommt und unseren Portpin sauber auf GND zieht. Während des prellens, hängt der Kontakt Zeitweise in der Luft und unser Pullup zieht unser Portpin auf Highpegel.  Daraus folgt, dass unser Mikrocontroller mehrere Tastendrücke registriert, obwohl der Anwender nur einmal drückt. Da der MC durchaus schnell genug ist um einzelne Preller einzufangen müssen wir die Tasten entprellen. Einen guten Artikel zu diesem Thema gibt es auf http://www.mikrocontroller.net/articles/AVR-Tutorial:_Tasten. Hier findet man auch eine ausgeklügelte Entprellroutine u.a. in Assembler welche bis zu 8 Tasten gleichzeitig, durch geschickte logische Verknüpfungen entprellt. Der Grund, warum ich mich da noch selbst mit der Tastenentprellung beschäftigt habe, liegt darin, dass die Routine immerhin 5 (untere) Register belegt und ich noch nie mehr als 3 Tasten in einer meiner Schaltungen benötigt habe. Wenn ich also jemals mehr als 4 Tasten in einer Schaltung brauche, werde ich sicher auf dieser Routine aufbauen.

Natürlich kann man Tasten auch über Hardware mit R-C Gliedern, Logikgatter usw. entprellen. Darum soll es aber hier nicht gehen.

Im Folgenden geht es um Tastenentprellung über Software in AVR-Assembler.

Das Prinzip ist einfach. In den meisten Programmen wird dazu die Taste in Intervallen ausgelesen. Dazu wird ein Zähler benutzt, der bei nicht gedrückter Taste zurückgesetzt wird und bei Überlauf die gedrückte Taste meldet.
Ich habe eine für mich einfachere Art für das Entpellen entdeckt. Anstatt einen Zähler hochzuzählen, schiebe ich. So kann ich, zumindest in ASM leichter einen Überlauf feststellen und muss keinen Zähler zurücksetzen. Die ältesten Bits werden einfach herausgeschubst.

Darüber hinaus, werde ich zeigen, wie ich kurze, lange und Doppelklicks unterscheide und auswerte. Außerdem zeige ich, wie man einen Tastendruck auf 2 Tasten gleichzeitig auswertet und stelle eine Autorepeat Funktion vor, die den Artikel abschließt. Die folgenden Routinen sind durchaus Praxistauglich. In allen Projekten, in denen ich Tasten eingesetzt habe, kommen die Entprellroutinen ( evtl. mit leichten Anpassungen) vor.


Es liegt mir übrigens fern, mit diesem Beitrag Diskussionen zu starten. In den Foren werden zur Tastenentprellung  wahre Glaubenskriege ausgefochten. Dem möchte ich mich nicht anschließen. Ich zeige hier nur, wie ich es mache. Nicht mehr und nicht weniger.

Die Testschaltung

Alle folgenden Beispiele, lassen sich mit dieser Schaltung nachvollziehen. Ich habe die Schaltung auf einem Steckboard aufgebaut. Die Fuses des ATTiny2313 sind im Originalzustand außer dass CKDIV8 deaktiviert wurde. Der Tiny läuft also mit 8 MHz. Bitte sucht eure ältesten und ausgeleiertesten Tasten aus der Bastelkiste, damit man sieht, ob die Entprellung über Software Funktioniert.

Tasten in Assembler

Einfaches Entprellen..

.. geschieht am einfachsten und besten durch Polling in einem Timerinterrupt. Man liest den Portpin, an dem der Taster angeschlossen ist in Intervallen von etwa 2..10 Millisekunden aus und überprüft, ob der Taster über längere Zeit, den gleichen logischen Pegel liefert.  Da in fast jedem Programm schon ein Timerinterrupt läuft, baut man da am besten eine kleine Routine mit ein, die den Pin ausliest und die Daten für das Hauptprogramm bereithält. Sollte ein Timerinterrupt im Programm nicht sowieso schon vorhanden sein, ist eine Tastenentprellung ein guter Grund, einen solchen hinzuzufügen .

Beispiel1:

Im ersten Beispielprogramm, wird der Timer (0) so programmiert, das alle 8 Millisekunden ein Interrupt erzeugt wird.


;Beispielroutine 1 zur Tastenentprellung by juergen@avr-projekte.de
.include "tn2313def.inc"
;------------------------------------------------------
;Register
;------------------------------------------------------
.def    stat    =   r2
.def    taste   =   r3
.def    temp1   =   r16
;------------------------------------------------------
;Sprungvektoren
;------------------------------------------------------
.org    0                    ;Reset
        rjmp    init
.org    OVF0addr            ;Timer0 Overflow Interrupt
;------------------------------------------------------
;Timer Interrupt
;------------------------------------------------------
        in        stat,SREG            ;Status sichern
        ;----------------------------------------------
        ;Taste Entprellen
        ;----------------------------------------------
        lsl     taste       ;Eine Null in Taste schieben
        sbic    pind,0      ;Wenn die Taste gedrückt ist, überspringen
        inc     taste       ;Ansonsten die Null in eine 1 wandeln
        ;----------------------------------------------
        ;Interrupt Ende
        ;----------------------------------------------
        out        SREG,stat            ;str wieder zurück
        reti
;------------------------------------------------------
;Init
;------------------------------------------------------
init:    ldi     temp1, RAMEND            ;Stack
        out     SPL, temp1
        ;----------------------------------------------
        ;Ports init
        ;----------------------------------------------
        sbi     DDRD,6                  ;LEDanschluss auf Ausgang
        sbi     PortD,0                 ;Pullup für Taste einschalten       
        ;----------------------------------------------
        ;Timer init
        ;----------------------------------------------
        ldi        temp1,1<<cs02        ;TC0/256, ISR ca. alle 8mS
        out        TCCR0,temp1   
        ldi        temp1,1<<TOIE0        ;Timer0 overflow interrupt
        out        TIMSK,temp1
        sei
;------------------------------------------------------
;Main
;------------------------------------------------------
        ;----------------------------------------------
        ;Taste pollen
        ;----------------------------------------------
main:   tst     taste           ;
        brne    main
        ;----------------------------------------------
        ;Reaktion wenn Taste gedrückt
        ;----------------------------------------------
        sbi     pind,6          ;In diesem Fall, LED toggeln
        ;----------------------------------------------
        ;Warten, bis die Taste Losgelassen wird
        ;----------------------------------------------
main1:  tst     taste           ;
        breq    main1
        rjmp    main
;------------------------------------------------------
;Ende
;------------------------------------------------------

Nach dem üblichen sichern des Statusregister sind Folgende 3 Befehle für die Entprellung zuständig.

        lsl     taste  
        sbic    pind,0
        inc     taste

Jedes mal, wenn diese Befehlsfolge Durchlaufen wird (alle 8 mS), wird der Zustand der Taste an PinD0 in das Register taste geschoben. Da das Register taste mit dem unteren Register r3 definiert wurde, kann der Befehl sbr oder ori nicht benutzt werden um Bit0 zu setzten. Deshalb wird Bit0 mit inc gesetzt. Das kann man so programmieren, wenn man weiß, dass Bit0 eine 0 enthält.
Das Hauptprogramm kann nun durch Testen des Registers taste, abhängig vom Zustand der Letzten 8 ISRs (der letzten 8*8=64 mS) verzweigen. Ist der Inhalt von taste =0, war die Taste vor 64mS gedrückt und für diese Zeit gehalten. Jeder Tastenpreller wird eine 1 in taste schieben und so das Ergebnis zu <>0 (Taste nicht gedrückt) machen. Ebenso kann getestet werden, ob die Taste losgelassen wurde. Dann enthält taste -1 (255, $ff,0b11111111). Die Verzögerungszeit von 64mS ist in der Paxis nicht bemerkbar, deshalb können ruhig alle 8 Bit auf Null getestet werden. Ist es einmal nötig einen Timerinterrupt mit 20mS oder mehr zu programmieren, kommt man auf 160 Millisekunden Verzögerung . Hier könnte man noch das obere Nibble durch eine AND Verknüpfung ausmaskieren. Die unteren 4 Bit würden für eine saubere Entprellung auch ausreichen.

Diese Entprellung Funktioniert eigentlich sehr zuverlässig. Wenn man sich jedoch das Hauptprogramm ansieht, ist das ganze etwas unkomfortabel, da man am Ende warten muss, bis der Anwender die Taste wieder loslässt. Außerdem würde ein zweiter Tastendruck, der während des ausführen der eigentlichen Routine (hier nur sbi pind,6) verloren gehen. Besser währe es, nur die negative Flanke auszuwerten. Das ist der Moment, wenn der Zustand am Portpin von high (Taster offen) zu low (Taster geschlossen) wechselt.

Entprellung mit Flankenerkennung

Hinzugekommen ist das Flagregister flags (r23) welches die Flanke key1 in einem beliebigen Bit (hier Bit 0) speichert. Die restlichen 7 Bits, können für weitere Programmflags oder Tasten hergenommen werden.

Beispiel 2

;Beispielroutine 2 zur Tastenentprellung by juergen@avr-projekte.de
.include "tn2313def.inc"
;------------------------------------------------------
;Register
;------------------------------------------------------
.def    stat    =   r2
.def    taste   =   r3
.def    temp1   =   r16
 
.def    flags   =   r23
.equ    key1    =   0
;------------------------------------------------------
;Sprungvektoren
;------------------------------------------------------
.org    0                    ;Reset
        rjmp    init
.org    OVF0addr            ;Timer0 Overflow Interrupt
;------------------------------------------------------
;Timer Interrupt, alla 8 mS
;------------------------------------------------------
        in        stat,SREG            ;Status sichern
        ;----------------------------------------------
        ;Taste Entprellen
        ;----------------------------------------------
        lsl     taste       ;Eine Null in Taste schieben
        sbic    pind,0      ;Wenn die Taste gedrückt ist, überspringen
        inc     taste       ;Ansonsten die Null in eine 1 wandeln
        brne    is_end      ;Wenn nicht die letzten 8 ISRs gedrückt war
        brcc    is_end      ;Wenn vor 9 ISRs gedrückt war, Sprung
        sbr     flags,1<<key1   ;Tastenflag (Flanke) setzen
        ;----------------------------------------------
        ;Interrupt Ende
        ;----------------------------------------------
is_end: out        SREG,stat            ;Status wieder zurück
        reti
;------------------------------------------------------
;Init
;------------------------------------------------------
init:   ldi     temp1, RAMEND            ;Stack
        out     SPL, temp1
        ;----------------------------------------------
        ;Ports init
        ;----------------------------------------------
        sbi     DDRD,6                  ;LEDanschluss auf Ausgang
        sbi     PortD,0                 ;Pullup für Taste einschalten       
        ;----------------------------------------------
        ;Timer init
        ;----------------------------------------------
        ldi        temp1,1<<cs02        ;TC0/256, ISR ca. alle 8mS
        out        TCCR0,temp1   
        ldi        temp1,1<<TOIE0        ;Timer0 overflow interrupt
        out        TIMSK,temp1
        sei
;------------------------------------------------------
;Main
;------------------------------------------------------
        ;----------------------------------------------
        ;Tastenflag abfragen
        ;----------------------------------------------
main:   sbrc    flags,key1          ;Solange überspringen bis Tastenflag gesetzt ist
        rcall   toggle_LED
        ;Weitere Tasten, oder was anderes
        ...
        rjmp    main
        ;----------------------------------------------
        ;Reaktion wenn Taste gedrückt
        ;----------------------------------------------
Toggle_LED:
        cbr     flags,1<<key1       ;Tastenflag löschen
        sbi     pind,6              ;LED toggeln
        ret
;------------------------------------------------------
;Ende
;------------------------------------------------------


Die Entprellung sieht nun so aus:

        lsl     taste
        sbic    pind,0
        inc     taste
        brne    is_end
        brcc    is_end
        sbr     flags,1<<key1

Für die Flankenerkennung sind die letzten 3 Befehle zuständig. Brne prüft erstmal, ob die Taste für 64mS gehalten wurde und springt zum Interruptende wenn dem nicht so ist. Da das Flankenbit nur einmal pro Tastendruck gesetzt werden soll, wird das älteste (herausgeschobene) Bit mit brcc geprüft. Ist dieses gesetzt (high), war vor 9 Interrupts die Taste noch nicht gedrückt und wir haben eine negative Flanke die wir mit sbr  flags,1<<key1 speichern. 

Würde man PD0 an ein DSO anschließen, bekämen wir in etwa ein Bild wie unten (PIND) angezeigt.

Tastenentprellung Diagramm

So etwa, könnte die Bitfolge aussehen, wie sie die Entprellroutine sieht. Die Bits die alle 8 mS an PinD0 abgetastet werden, wandern im Gänsemarsch durch das Register taste und zuletzt ins Carrybit. Der blaue Rahmen, können wir in Gedanken nach rechts und links verschieben.
Links in der Bitfolge ist der Taster noch offen und PIND0 liefert einsen. Danach folgt die Prellphase mit einem zufälligen 0/1 Muster welches es ja hier zu eliminieren gilt. Der blaue Rahmen zeigt die Stelle, an welcher die Entprellroutine greift und das Flankenbit setzt. Schiebt man den Rahmen durch die Bitfolge und spielt die Routine im Kopf durch, wird man feststellen, daß das Flag nur an dieser Stelle gesetzt werden kann. Da das key1 Flag erst gesetzt wird, nachdem die Taste 8 mal hintereinander 0 liefert, ensteht eine kurze Verzögerungszeit von 64 Millisekunden, welche aber vernachlässigt werden kann.
Im Hauptprogramm (siehe Beispiel 2 unter Main) wird nun nur noch das Flankenbit key1 getestet und entsprechend verzweigt. Das Unterprogramm löscht als erstes das Flankenbit. Die ISR ist danach sofort wieder bereit, neue Tastendrücke aufzunehmen

.

Auch diese Entprellroutine könnte man durch eine logische AND Verknüpfung des Register taste mit $f, auf 4 Bit reduzieren. Brcc würde man dann durch brhc (Branch if Half Carry Flag is Cleared) ersetzen. In der Skizze oben, würde das bedeuten, das Taste nur noch 4 Bit breit ist und C dur H ersetzt würde. Die Verzögerungszeit würde sich in diesem Fall auf die hälfte reduzieren und somit schon nach 32 mS liefern. Das ist aber eigentlich nur von Nutzen, wenn die Intervalle in der ISR so groß werden, das die Verzögerung vom Anwender bemerkt wird. Für den langsamen Menschen ist sowohl 32 wie auch 64 Millisekunden, sofort .


Es liegt nahe, 2 Tasten mit 4 Bit, in nur einem Register, auf diese Art zu Entprellen. Dazu ist aber mehr Code und ein hohes Register (r16 - r31) für taste notwendig, so das es kürzer ist, für jede Taste ein eigenes Register zu definieren. Trotzdem hier der Code.

Beispiel 3

Im Programm wurde zusätzlich key2 definiert.

;Beispielroutine 3 zur Tastenentprellung by juergen@avr-projekte.de
.include "tn2313def.inc"
;------------------------------------------------------
;Register
;------------------------------------------------------
.def    stat    =   r2
.def    tasten  =   r22
.def    temp1   =   r16
 
.def    flags   =   r23
.equ    key1    =   0
.equ    key2    =   1
;------------------------------------------------------
;Sprungvektoren
;------------------------------------------------------
.org    0                    ;Reset
        rjmp    init
.org    OVF0addr            ;Timer0 Overflow Interrupt
;------------------------------------------------------
;Timer Interrupt, alle 8 mS
;------------------------------------------------------
        in        stat,SREG            ;Status sichern
        push    temp1
        ;----------------------------------------------
        ;2 Tasten (4 Bit) in einem Register Entprellen
        ;----------------------------------------------
        rol     tasten              ;Bit3 ins Halfcarry, 7 ins Carry
        andi    tasten,~(1<<0|1<<4) ;Null ins unterste Bit jedes Nibbles
        sbic    pind,0              ;Überspringen, wenn Taste gedrückt
        ori     tasten,1<<0 ;In 1 Wandeln, wenn Taste nicht gedrückt
        sbic    pind,1
        ori     tasten,1<<4
t1:     brhc    t2                  ;Taste1 im unteren Nibble testen   
        mov     temp1,tasten        ;Nibble auf Null testen
        andi    temp1,$0f           ;Dazu oberes Nibble ausmaskieren
        brne    t2                  ;Sprung, wenn ein Bit gesetzt ist
        sbr     flags,1<<key1       ;Ansonsten Flankenbit setzen
t2:     brcc    is_end              ;Taste2 ist im oberen Nibble
        mov     temp1,tasten        ;dito
        andi    temp1,$f0
        brne    is_end
        sbr     flags,1<<key2
        ;----------------------------------------------
        ;Interrupt Ende
        ;----------------------------------------------
is_end: pop     temp1
        out        SREG,stat            ;str wieder zurück
        reti
;------------------------------------------------------
;Init
;------------------------------------------------------
init:    ldi     temp1, RAMEND            ;Stack
        out     SPL, temp1
        ;----------------------------------------------
        ;Ports init
        ;----------------------------------------------
        ldi     temp1,1<<pd6|1<<pd5|1<<pd4
        out     DDRD,temp1              ;LED Anschlüsse auf Ausgang
        ori     temp1,1<<pd0|1<<pd1     ;Pullups dazuodern
        out     PortD,temp1             ;Alle LEDs erstmal aus, Pullups ein
        ;----------------------------------------------
        ;Timer init
        ;----------------------------------------------
        ldi        temp1,1<<cs02            ;TC0/256, ISR ca. alle 8mS
        out        TCCR0,temp1   
        ldi        temp1,1<<TOIE0          ;Timer0 overflow interrupt
        out        TIMSK,temp1
        sei
;------------------------------------------------------
;Main
;------------------------------------------------------
        ;----------------------------------------------
        ;Tastenflags abfragen
        ;----------------------------------------------
main:   sbrc    flags,key1
        rcall   Tast1
        sbrc    flags,key2
        rcall   Tast2
        rjmp    main

Tast1:  cbr     flags,1<<key1
        sbi     pind,5
        ret

Tast2:  cbr     flags,1<<key2
        sbi     pind,6
        ret
;------------------------------------------------------
;Ende
;------------------------------------------------------

Man könnte die Routine noch verkürzen wenn man beim Label t1: die beiden Befehle mov  temp1,tasten und andi temp1,$0f durch cpi tasten,$f0 (und bei t2: tasten,$0f) ersetzen würde, könnte dann aber keine 2 Tasten gleichzeitig detektieren. Auch bräuchte das Temp1 Register in diesem Fall nicht gepusht zu werden.
Die Routine oben benötigt zum Entprellen von 2 Taster 16 (oder eben 14) Befehle, ein (hohes, teures) Register und 2 Bits im Flagregister. Dem gegenüber steht der Code aus Beispiel 2 mit 6 Befehlen für eine Taste, also 12 Befehle für 2 Tasten und 2  (niedrige) Register. Hier kann man also abwägen, welche Variante für das jeweilige Programm besser passt. Für sehr langsame Timerinterrupts kann der Entprellcode nochmals verkürzt werden, indem nur 3 Bits auf null getestet werden.

;Beispielroutine 3.1 zur Tastenentprellung by juergen@avr-projekte.de
        ;----------------------------------------------
        ;2 Tasten (3 Bit) in einem Register Entprellen
        ;----------------------------------------------
        rol     tasten              ;Bit3 ins Halfcarry, 7 ins Carry
        andi    tasten,~(1<<0|1<<4) ;Jeweils eine Null ins unterste Bit der Nibbles
        sbic    pind,0              ;Überspringen, wenn Taste gedrückt
        ori     tasten,1<<0         ;In 1 Wandeln, wenn Taste nicht gedrückt
        sbic    pind,1
        ori     tasten,1<<4
t1:     cpi     tasten,0b11111000
        brne    t2
        sbr     flags,1<<key1
t2:     cpi     tasten,0b10001111
        brne    is_end
        sbr     flags,1<<key2

Hier wird auf die Tests der Carry Flags verzichtet. Man erkennt die negative Flanke, wenn man die Nibbles bei den CPI Befehlen separat betrachtet. Bei dieser Variante reichen wieder 12 Befehle, außerdem braucht temp1 nicht gepusht werden. Die Routine geht davon aus, das der Anwender nur eine Taste gleichzeitig drückt. Das muss nicht unbedingt schlecht sein. Drückt man also beide Tasten gleichzeitig, passiert gar nichts.
Die 3 und 4 Bit Varianten Funktionieren zwar auch sehr sicher, jedoch habe ich in meinen Projekten immer die 8 Bit Variante mit separatem Register pro Taste bevorzugt. Einer der Gründe ist die sehr einfache Auswertung von kurzen, langen und doppelten Tastendrücken, die ich gerne für spezielle Programmfunktionen benutze. Beispiel: Drehgebertaster wird durch langen Tastendruck zum Menüaufruf Uhr stellen benutzt.


Kurzer, langer, doppelter Tastendruck


Einen Nachteil, beim auswerten eines Doppelclicks möchte ich nicht verschweigen. Der kurze Tastendruck wird um die Delayzeit des doppelten Tastendrucks verzögert. Egal, ob man die Auswertung in der ISR vornimmt und ein Flag setzt, oder (wie ich) im Hauptprogramm. Will ich sofort auf einen kurzen Tastendruck reagieren, kann ich keinen Doppelclick detektieren. Zumindest nicht ohne vorher die Routine für den kurzen Tastendruck aufzurufen. Deshalb ist die Delayzeit immer ein Kompromiss zwischen der Verzögerung des kurzen Tastendrucks und der bequemen Bedienbarkeit des Doppelclick.


Neu hinzugekommen ist das Register tick, welches in der ISR heruntergezählt wird. Dieses wird für das Delay  der langen Tastendrücke benötigt.

Beispiel 4

;Beispielroutine 4 zur Tastenentprellung by juergen@avr-projekte.de
.include "tn2313def.inc"
;------------------------------------------------------
;Register
;------------------------------------------------------
.def    stat    =   r2
.def    taste   =   r3
.def    tick    =   r4
.def    temp1   =   r16
 
.def    flags   =   r23
.equ    key1    =   0
;------------------------------------------------------
;Sprungvektoren
;------------------------------------------------------
.org    0                    ;Reset
        rjmp    init
.org    OVF0addr            ;Timer0 Overflow Interrupt
;------------------------------------------------------
;Timer Interrupt, alla 8 mS
;------------------------------------------------------
        in        stat,SREG            ;Status sichern
        dec     tick                ;Jede ISR herunterzählen
        ;----------------------------------------------
        ;Taste Entprellen
        ;----------------------------------------------
        lsl     taste               ;Eine Null in Taste schieben
        sbic    pind,0              ;Wenn die Taste gedrückt ist, überspringen
        inc     taste               ;Ansonsten die Null in eine 1 wandeln
        brne    is_end
        brcc    is_end
        sbr     flags,1<<key1
        ;----------------------------------------------
        ;Interrupt Ende
        ;----------------------------------------------
is_end:    out        SREG,stat            ;str wieder zurück
        reti
;------------------------------------------------------
;Init
;------------------------------------------------------
init:    ldi     temp1, RAMEND            ;Stack
        out     SPL, temp1
        ;----------------------------------------------
        ;Ports init
        ;----------------------------------------------
        ldi     temp1,1<<pd6|1<<pd5|1<<pd4
        out     DDRD,temp1              ;LED Anschlüsse auf Ausgang
        out     PortD,temp1             ;Alle LEDs erstmal aus
        sbi     PortD,0                 ;Pullup für Taste einschalten       
        ;----------------------------------------------
        ;Timer init
        ;----------------------------------------------
        ldi        temp1,1<<cs02            ;TC0/256, ISR ca. alle 8mS
        out        TCCR0,temp1   
        ldi        temp1,1<<TOIE0          ;Timer0 overflow interrupt
        out        TIMSK,temp1
        sei
;------------------------------------------------------
;Main
;------------------------------------------------------
        ;----------------------------------------------
        ;Tastenflag abfragen
        ;----------------------------------------------
main:   sbrc    flags,key1            ;Überspringen, wenn nicht gedrückt
        rjmp    kld                   ;Sprung zu kurz lang doppel Auswertung
        rjmp    main
        ;----------------------------------------------
        ;Test auf kurz, lang oder Doppelklick
        ;----------------------------------------------
kld:    cbr     flags,1<<key1         ;Tastenflag löschen
        rcall   wait                  ;300mS warten
        sbrc    flags,key1            ;Wenn innerhalb 300mS das Flag gesetzt ist..
        rjmp    doppel                ;.. war es ein Doppelklick
        tst     taste                 ;Wenn kein Doppelclick und Taste = 0, wurde die Taste gehalten
        breq    lang
        rjmp    kurz                  ;Ansonsten war kurz gedrückt
        ;----------------------------------------------
        ;Reaktion wenn Taste 2* kurz gedrückt
        ;----------------------------------------------
doppel: cbr     flags,1<<key1         ;Tastenflag löschen
        sbi     pind,4                ;LED an pd4 toggeln
        rjmp    main
        ;----------------------------------------------
        ;Reaktion wenn Taste lang gedrückt
        ;----------------------------------------------
lang:   sbi     pind,5                ;LED an pd5 toggeln
        rjmp    main
        ;----------------------------------------------
        ;Reaktion wenn Taste 1* kurz gedrückt
        ;----------------------------------------------
kurz:   sbi     pind,6                ;LED an pd6 toggeln
        rjmp    main
        ;----------------------------------------------
        ;Delay
        ;----------------------------------------------
wait:   ldi     temp1,300/8         ;300 mS warten  
        mov     tick,temp1          ;Tick laden und in der ISR herunterzählen lassen
w1:;     tst     taste               ;Beim loslassen der Taste, Delay verlassen
;        breq    w2                  ;Bei Doppelklickauswertung nicht verwenden
;        ret
w2:     tst     tick               
        brne    w1                  ;Warten bis Tick =0
        ret
;------------------------------------------------------
;Ende
;------------------------------------------------------


Ich habe oben in Beispiel 4 eine Verzögerung von 300 Millisekunden angesetzt. In dieser Zeit kann ich noch ohne Anstrengung einen Doppelklick ausführen. 300 mS sind zwar recht kurz, aber bei einem kurzen Tastendruck doch noch bemerkbar. Außerdem kommt es darauf an, wie die Taste später montiert werden soll. An einer Frontplatte die Vertikal bedient wird, kann ein Doppelklick in 300 mS nur schwer ausgeführt werden. Hier hilft nur Ausprobieren.

Im Hauptprogramm.

Zuerst wird wie üblich auf einen kurzen Tastendruck geprüft. Ist dieser erfolgt, springt das Programm zu Label kld wo nach löschen des Flankenbit key1 300 Millisekunden gewartet wird. Danach wird key1 erneut geprüft. Ist dieses gesetzt, wurde die Taste innerhalb der 300 mS erneut gedrückt und es erfolgt ein Sprung zum Label Doppel, in welcher zur Kontrolle, die LED an PD4 getoggelt wird.
Wurde der Sprung nicht ausgeführt, wird das Register Taste auf Null getestet. War das Flag also Null UND die 8 Bit im Register taste sind alle 0 handelt es sich um einen langen Tastendruck und es wird entsprechend verzweigt.
Trifft beides nicht zu, war es ein einzelner, kurzer Tastendruck und wir haben die 300 mS umsonst vertrödelt .

Nur kurzer und langer Tastendruck

Benötigt man nur einen kurzen und langen Tastendruck, kann in der Delayroutine durch hinzufügen der 3 Auskommentierten Befehle, die Verzögerung für den kurzen Tastendruck minimiert werden. Hier wird einfach auf das Loslassen der Taste geprüft und bei einem kurzen Tastendruck die Delayroutine vorzeitig verlassen. In diesem Fall, kann der lange Tastendruck auch ruhig mit einer Sekunde oder mehr definiert werden. Hierzu einfach ldi temp1,1000/8 für eine Sekunde eintragen.

2 Tasten gleichzeitig drücken

Eines vorweg. Für den µC gibt es kein gleichzeitig. Für den Anwender unserer Hardware schon. Um dem Tiny beizubringen, was für uns das gleichzeitige drücken von 2 Tasten bedeutet, soll es hier gehen.
So können mit 2 Tasten, 3 Programmfunktionen aufgerufen werden, wie Z.B eine Uhr stellen mit Plus und Minustaste und beide Tasten gleichzeitig zum übernehmen

Beispiel 5

Im Interrupt werden die schon bekannten Entprellroutinen, hintereinander für jede Taste separat ausgeführt.


;Beispielroutine 5 zur Tastenentprellung by juergen@avr-projekte.de
.include "tn2313def.inc"
;------------------------------------------------------
;Register
;------------------------------------------------------
.def    stat    =   r2
.def    taste1  =   r3
.def    taste2  =   r4
.def    tick    =   r5
.def    temp1   =   r16
 
.def    flags   =   r23
.equ    key1    =   0
.equ    key2    =   1
;------------------------------------------------------
;Sprungvektoren
;------------------------------------------------------
.org    0                    ;Reset
        rjmp    init
.org    OVF0addr            ;Timer0 Overflow Interrupt
;------------------------------------------------------
;Timer Interrupt, alle 8 mS
;------------------------------------------------------
        in      stat,SREG           ;Status sichern
        dec     tick                ;Zeit - 8mS
        ;----------------------------------------------
        ;2 Tasten Entprellen
        ;----------------------------------------------
        lsl     taste1    ;Eine Null in Taste schieben
        sbic    pind,0    ;Wenn die Taste gedrückt ist, überspringen
        inc     taste1    ;Ansonsten die Null in eine 1 wandeln
        brne    t2   ;Wenn nicht die letzten 8 ISRs gedrückt war, Sprung
        brcc    t2   ;Wenn vor 9 ISRs gedrückt war, Sprung
        sbr     flags,1<<key1 ;Tastenflag (Flanke) setzen

t2:     lsl     taste2
        sbic    pind,1
        inc     taste2
        brne    is_end
        brcc    is_end
        sbr     flags,1<<key2
        ;----------------------------------------------
        ;Interrupt Ende
        ;----------------------------------------------
is_end: out        SREG,stat            ;Status wieder zurück
        reti
;------------------------------------------------------
;Init
;------------------------------------------------------
init:   ldi     temp1, RAMEND        ;Stack
        out     SPL, temp1
        ;----------------------------------------------
        ;Ports init
        ;----------------------------------------------
        ldi     temp1,1<<pd6|1<<pd5|1<<pd4
        out     DDRD,temp1           ;LEDs auf Ausgang
        ldi     temp1,1<<pd6|1<<pd5|1<<pd4|1<<pd1|1<<pd0
        out     PortD,temp1      ;LEDs aus, Pullups ein                
        ;----------------------------------------------
        ;Timer init
        ;----------------------------------------------
        ldi        temp1,1<<cs02        ;TC0/256, ISR ca. alle 8mS
        out        TCCR0,temp1   
        ldi        temp1,1<<TOIE0       ;Timer0 overflow interrupt
        out        TIMSK,temp1
        sei
        clr     flags
;------------------------------------------------------
;Main
;------------------------------------------------------
        ;----------------------------------------------
        ;Tastenflags abfragen
        ;----------------------------------------------
main:   mov     temp1,flags          ;Alle Flags nach temp1 holen
        andi    temp1,1<<key1|1<<key2;Nur Tasten stehen lassen
        breq    main            ;Sprung, wenn keine Taste gedrückt wurde
        rcall   wait            ;Dem User kurz Zeit lassen, gleichzeitig                                  2 Tasten zu drücken ;-)
        mov     temp1,flags           ;Flags updaten
        andi    temp1,1<<key1|1<<key2 ;Nur Tasten stehen lassen
        cbr     flags,1<<key1|1<<key2 ;Flags löschen
        cpi     temp1,1<<key1
        breq    toggle_LED1           ;Sprung wenn Taste 1 gedrückt
        cpi     temp1,1<<key2
        breq    toggle_LED2           ;Sprung wenn Taste 2 gedrückt
        rjmp    toggle_LED3           ;Können nur noch beide Tasten sein
        ;----------------------------------------------
        ;Reaktion wenn Taste gedrückt
        ;----------------------------------------------
Toggle_LED1:
        sbi     pind,4              ;LED toggeln
        rjmp    main
Toggle_LED2:
        sbi     pind,5              ;LED toggeln
        rjmp    main
Toggle_LED3:
        sbi     pind,6              ;LED toggeln
        rjmp    main
        ;----------------------------------------------
        ;Delay
        ;----------------------------------------------
wait:   ldi     temp1,100/8         ;100 mS vertrödeln
        mov     tick,temp1          ;In der ISR herunterzählen lassen
w1:     tst     tick
        brne    w1                  ;
        ret
;------------------------------------------------------
;Ende
;------------------------------------------------------

Am Ende des Codes befindet sich das Unterprogramm wait, welches beim Aufruf 100mS wartet. Damit ist auch gleich festgelegt das 2 Tasten innerhalb 100mS gedrückt, als gleichzeitig gelten sollen. In der main wird zunächst gewartet, bis irgendeine Taste gedrückt ist. Ist das geschehen, werden nochmals 100mS gewartet und die Tastenflags erneut gelesen. Nach dem Ausmaskieren der unrelevanten Flags, stehen nun die Tastenflags in temp1 bereit . Sind beide Flags gesetzt (Inhalt von Temp1=3) wird zur Kontrolle die LED an PORTD3 getoggelt.

Tasten mit Autorepeat

Die Repeatfunktion einer PC-Tastatur kennt wohl jeder. Man drückt in einem Texteditor z.B. die Leertaste und ein Leerzeichen wird sofort eingefügt. Hält man die Taste für eine gewisse Zeit gedrückt, erscheinen in kurzen Intervallen solange Leerzeichen, bis man die Taste wieder loslässt. Sowas wollen wir für unsere Hardwarebasteleien natürlich auch haben . Bestes Beispiel ist das Stellen einer Uhr mit einer Plus und Minustaste über LCD oder LED- Display.
Im Gegensatz zu der Abfrage von langen Tastendrücken, läuft der  Autorepeat vollständig in der ISR. Die Routine benötigt 2 Zeitvorgaben die am Programmanfang mit .equ zugewiesen werden. Dabei handelt es sich um rep_lang und rep_kurz. Rep_lang ist die Zeit die bei niedergedrückten Taste vergeht, bis der Autorepeat aktiv wird. Hier habe ich 600 Millisekunden eingetragen. Bei rep_kurz, wird die Zeit der Intervalle festgelegt, in der nach aktivierter Repeatfuktion, das Flankenbit gesetzt wird. 

Beispiel 6

Neu hinzugekommen ist der Zähler repeat den ich hier mit r17 definiert habe. Man könnte auch ein niederes Register hernehmen, müsste dann aber den ldi Befehl umgehen indem man temp1 pusht oder die Zeitwerte als Konstante in der Init in andere niedere Register schreibt und diese dann in der ISR mit mov überträgt.
Der Zähler repeat wird in der ISR bei gedrückter Taste heruntergezählt. Ist er bei Null angekommen wird er neu geladen und ein Tastendruck generiert. Ist keine Taste gedrückt wird er mit der langen Zeit geladen.

;Beispielroutine 6 zur Tastenentprellung by juergen@avr-projekte.de
.include "tn2313def.inc"
;------------------------------------------------------
;Register
;------------------------------------------------------
.def    stat    =   r2
.def    taste   =   r3
.def    temp1   =   r16
.def    repeat  =   r17
;------------------------------------------------------
;Flagregister
;------------------------------------------------------ 
.def    flags   =   r23
.equ    key1    =   0
.equ    rep     =   7
;------------------------------------------------------
;Repeatzeiten bei einer ISR alle 8 mS
;------------------------------------------------------
.equ    rep_lang    =   600/8   ;600mS
.equ    rep_kurz    =   120/8   ;120mS
;------------------------------------------------------
;Sprungvektoren
;------------------------------------------------------
.org    0                    ;Reset
        rjmp    init
.org    OVF0addr            ;Timer0 Overflow Interrupt
;------------------------------------------------------
;Timer Interrupt, alle 8 mS
;------------------------------------------------------
        in        stat,SREG            ;Status sichern
        ;----------------------------------------------
        ;Tastenabfrage mit Repeat
        ;----------------------------------------------
        lsl     taste      ;Eine Null in Taste schieben
        sbic    pind,0     ;Wenn Taste gedrückt, die Null stehen lassen
        inc     taste      ;Ansonsten die Null in eine 1 wandeln
        brne    taste2     ;Sprung, wenn Taste nicht gedrückt
        sbr     flags,1<<rep ;Repeat am Ende der ISR nicht zurücksetzen
        dec     repeat     ;Repeat herunterzählen
        brne    rp1    ;Wenn Repeatzeit <0, auf neuen Tastendruck testen
        ldi     repeat,rep_kurz ;Repeatzeit abgelaufen, kurz laden
        sec                     ;und Taste durch Repeat drücken
rp1:    brcc    Taste2
        sbr     flags,1<<key1       ;Taste gedrückt, Flag setzen
        ;----------------------------------------------
        ;Weitere Tasten (oder anderes)
        ;----------------------------------------------
Taste2: nop
        ;----------------------------------------------
        ;Interrupt Ende
        ;----------------------------------------------
is_end: sbrs    flags,rep          ;Wurde Repeat runtergezählt?
        ldi        repeat,rep_lang ;Wenn nicht, Repeatzeit lang laden..
        cbr     flags,1<<rep   ;Repeatflag für die nächste Runde löschen
        out        SREG,stat       ;Statusregister wieder zurück
        reti
;------------------------------------------------------
;Init
;------------------------------------------------------
init:   ldi     temp1, RAMEND            ;Stack
        out     SPL, temp1
        ;----------------------------------------------
        ;Ports init
        ;----------------------------------------------
        ldi     temp1,1<<pd6|1<<pd5|1<<pd4
        out     DDRD,temp1              ;LEDs auf Ausgang
        ldi     temp1,1<<pd5|1<<pd4|1<<pd0
        out     PortD,temp1  ;Pullup für Taster, LED an PD6 ein.
        ;----------------------------------------------
        ;Timer init
        ;----------------------------------------------
        ldi        temp1,1<<cs02           ;TC0/256, ISR ca. alle 8mS
        out        TCCR0,temp1   
        ldi        temp1,1<<TOIE0          ;Timer0 overflow interrupt
        out        TIMSK,temp1
        sei
;------------------------------------------------------
;Main
;------------------------------------------------------
        ;----------------------------------------------
        ;Tastenflag abfragen
        ;----------------------------------------------
main:   sbrs    flags,key1         ;Wenn Taste gedrückt, überspringen
        rjmp    main
        ;----------------------------------------------
        ;Reaktion wenn Taste gedrückt
        ;----------------------------------------------
        cbr     flags,1<<key1           ;Tastenflag löschen
        sbi     pind,6                  ;LED an pd6 und 5 toggeln
        sbi     pind,5                  ;Da PD6 in der Init gesetzt wird, blinken die LEDs abwechselnd
        rjmp    main                    ;Neue Runde
;------------------------------------------------------
;Ende
;------------------------------------------------------

In der ISR sind in der Tastenabfrage die ersten 4 Befehle schon aus Beispiel 2 bekannt. Ist der Tiny an sbr flags,1<<rep angelangt, ist die Taste gedrückt und es wird der Repeat aktiviert. Das setzten des rep Flags bewirkt am Ende der ISR, das der Repeatzähler nicht mit der langen Reapeatzeit (600mS) geladen wird.
Durch dec repeat, werden 8 mS vom Zähler abgezogen. Ist der Zähler abgelaufen, wird dieser gleich mit rep_kurz (120 mS) neu geladen. Danach wird mit sec das Carry gesetzt um den nachfolgenden brcc Befehl auszuhebeln. Natürlich könnte hier auch ein rjmp stehen um das Flankenbit zu setzten. Am Ende der ISR, wird durch das rep Flag geprüft, ob irgend eine Taste, das rep Flag gesetzt hat. Danach wird das Repeatflag für die nächste ISR zurückgesetzt. Das könnte auch am den Anfang der ISR passieren.
Im Hauptprogramm wird die Autorepeat-Entprellroutine genauso behandelt, wie die Routinen mit Flankenerkennung (ab Beispiel 2).

Tipp am Schluss

Alle Entprelloutinen in den bisherigen Beispielen, fangen mit den ersten 3 Befehlen so an:

lsl     taste
sbic    pind,0
inc     taste

Mit dieser Befehlsfolge können mehrere Taster sowohl an beliebigen Pins als auch an verschiedenen Ports abgefragt werden. Hat man jedoch mehrere Taster in einer Reihenfolge am selben Port, so bietet sich an, den Port in ein Register zu lesen und direkt über das Carryflag in die Tastenregister zu schieben. Das würde pro Taste nochmals einen Befehl sparen. Temp1 muss natürlich gepusht werden, es sei den man hat ein Arbeitsregister für die ISR reserviert.

Beispiel mit 3 Taster an Portd 0..2

    in     temp1,pind

    lsr    temp1        ;Taste an PinD0 ins Carry
    rol    Taste1       ;und in Bit0 von Taste1 schieben
    brne   t2           ;Wenn nicht die letzten 8 ISRs gedrückt war, Sprung
    brcc   t2           ;Wenn vor 9 ISRs gedrückt war, Sprung
    sbr    flags,1<<key1;Tastenflag (Flanke) setzen

t2: lsr    temp1         ;Taste an PinD1 ins Carry
    rol    Taste2
    brne   t3
    brcc   t3
    sbr    flags,1<<key2

t3: lsr    temp1        ;Taste an PinD2 ins Carry
    rol    Taste3
    ...
    ...

Im Beispiel oben, wird PIND nur noch einmal gelesen und temp1 verteilt dann die gelesenen Bits, in die richtigen Tastenregister.
Nach dem übertragen von PIND nach temp1, wird PIND0 durch den lsr Befehl ins Carry transportiert. Der nachfolgende rol Befehl  schiebt die Taste weiter in Bit 0 von Taste1. Gleichzeitig wird Bit 7 (das älteste Bit) ins Carry geschoben, so das wir wie in den Beispielen oben, weiter verfahren können.
Das ganze würde auch anders herum Funktionieren, wenn z.B. 3 Tasten an PortD 5..7 angeschlossen währen. Anstatt lsr temp1 würde man dann lsl oder rol temp1 verwenden und zunächst Taste3 bedienen.
Wichtig ist im Endeffekt nur, das die Pins an denen die Taster angeschlossen sind, später ins richtige Register geschoben werden.

Ich wünsche euch eine prellfreie Zeit,
Jürgen