AVR-Tutorial: Tasten
Bisher beschränkten sich die meisten Programme auf reine Ausgabe an einem Port. Möchte man Eingaben machen, so ist der Anschluss von Tasten an einen Port unumgänglich. Dabei erheben sich aber 2 Probleme
- Wie kann man erreichen, dass ein Tastendruck nur einmal ausgewertet wird?
- Tasten müssen entprellt werden
Erkennung von Flanken am Tasteneingang
Möchte man eine Taste auswerten, bei der eine Aktion nicht ausgeführt werden soll, solange die Taste gedrückt ist, sondern nur einmal beim Drücken einer Taste, dann ist eine Erkennung der Schaltflanke der Weg zum Ziel. Anstatt eine gedrückte Taste zu erkennen, wird bei einer Flankenerkennung der Wechsel des Zustands des Eingangspins detektiert.
Dazu vergleicht man in regelmäßigen Zeitabständen den momentanen Zustand des Eingangs mit dem Zustand zum vorhergehenden Zeitpunkt. Unterscheiden sich die beiden, so hat man eine Schaltflanke erkannt und kann darauf reagieren. Solange sich der Tastenzustand nicht ändert, egal ob die Taste gedrückt oder losgelassen ist, unternimmt man nichts.
Die Erkennung des Zustandswechsels kann am einfachsten mit einer XOR (Exklusiv Oder) Verknüpfung durchgeführt werden.
A | B | Ergebnis |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
Nur dann, wenn sich der Zustand A vom Zustand B unterscheidet, taucht im Ergebnis eine 1 auf. Sind A und B gleich, so ist das Ergebnis 0.
A ist bei uns der vorhergehende Zustand eines Tasters, B ist der jetzige Zustand so wie er vom Port Pin eingelesen wurde. Verknüpft man die beiden mit einem XOR, so bleiben im Ergebnis genau an jenen Bitpositionen 1en übrig, an denen sich der jetzige Zustand vom vorhergehenden unterscheidet.
Nun ist bei Tastern aber nicht nur der erkannte Flankenwechsel interessant, sondern auch in welchen Zustand die Taste gewechselt hat:
- Ist dieser 0, so wurde die Taste gedrückt.
- Ist dieser 1, so wurde die Taste losgelassen.
Eine einfache UND Verknüpfung der Tastenflags mit dem XOR Ergebnis liefert diese Information
Das folgende Programm soll bei jedem Tastendruck eines Tasters am Port D (egal welcher Pin) eine LED am Port B0 in den jeweils anderen Zustand umschalten:
.include "m8def.inc"
.def key_old = r3
.def key_now = r4
.def temp1 = r17
.def temp2 = r18
.equ key_pin = PIND
.equ key_port = PORTD
.equ key_ddr = DDRD
.equ led_port = PORTB
.equ led_ddr = DDRB
.equ LED = 0
ldi temp1, 1<<LED
out led_ddr, temp1 ; den LED Port auf Ausgang
ldi temp1, $00 ; den Key Port auf Eingang schalten
out key_ddr, temp1
ldi temp1, $FF ; die Pullup Widerstände aktivieren
out key_port, temp1
mov key_old, temp1 ; bisher war kein Taster gedrückt
loop:
in key_now, key_pin ; den jetzigen Zustand der Taster holen
mov temp1, key_now ; und in temp1 sichern
eor key_now, key_old ; mit dem vorhergehenden Zustand XOR
mov key_old, temp1 ; und den jetzigen Zustand für den nächsten
; Schleifendurchlauf als alten Zustand merken
breq loop ; Das Ergebnis des XOR auswerten:
; wenn keine Taste gedrückt war -> neuer Schleifendurchlauf
and temp1, key_now ; War das ein 1->0 Übergang, wurde der Taster also
; gedrückt (in key_now steht das Ergebnis vom XOR)
brne loop ;
in temp1, led_port ; den Zustand der LED umdrehen
com temp1
out led_port, temp1
rjmp loop
Probiert man diese Implementierung aus, so stellt man fest: Sie funktioniert nicht besonders gut. Es kann vorkommen, dass bei einem Tastendruck die LED zwar kurzzeitig umschaltet aber gleich darauf wieder ausgeht. Genauso gut kann es passieren, dass die LED beim Loslassen einer Taste ebenfalls wieder den Zustand wechselt. Die Ursache dafür ist: Taster prellen.
Prellen
Das Prellen entsteht in der Mechanik der Tasten: Eine Kontaktfeder wird durch das Drücken des Tastelements auf einen anderen Kontakt gedrückt. Wenn die Kontaktfeder das Kontaktfeld berührt, federt sie jedoch nach. Dies kann soweit gehen, dass die Feder wieder vom Feld abhebt und den elektrischen Kontakt kurzzeitig wieder unterbricht. Auch wenn diese Effekte sehr kurz sind, sind sie für einen Mikrocontroller viel zu lang. Für ihn sieht die Situation so aus, dass beim Drücken der Taste eine Folge von: Taste geschlossen, Taste offen, Taste geschlossen, Taste offen Ereignissen am Port sichtbar sind, die sich dann nach einiger Zeit auf den Zustand Taste geschlossen einpendelt. Beim Loslassen der Taste dann dasselbe Spielchen in der umgekehrten Richtung.
Nun kann es natürlich sein, dass ein neuer Taster zunächst überhaupt nicht prellt. Ist der Taster vom Hersteller nicht explizit als 'prellfreier Taster' verkauft worden, besteht aber kein Grund zur Freude. Auch wenn der Taster heute noch nicht prellt, irgendwann wird er es tun. Dann nämlich, wenn die Kontaktfeder ein wenig ihrer Spannung verliert und ausleiert, bzw. wenn sich die Kontaktflächen durch häufige Benutzung abgewetzt haben.
Entprellung
Aus diesem Grund müssen Tasten entprellt werden. Im Prinzip kann eine Entprellung sehr einfach durchgeführt werden. Ein 'Tastendruck' wird nicht bei der Erkennung der ersten Flanke akzeptiert, sondern es wird noch eine zeitlang gewartet. Ist nach Ablauf dieser Zeitdauer die Taste immer noch gedrückt, dann wird diese Flanke als Tastendruck akzeptiert und ausgewertet.
.include "m8def.inc"
.def key_old = r3
.def key_now = r4
.def temp1 = r17
.def temp2 = r18
.equ key_pin = PIND
.equ key_port = PORTD
.equ key_ddr = DDRD
.equ led_port = PORTB
.equ led_ddr = DDRB
.equ LED = 0
ldi temp1, 1<<LED
out led_ddr, temp1 ; den Led Port auf Ausgang
ldi temp1, $00 ; den Key Port auf Eingang schalten
out key_ddr, temp1
ldi temp1, $FF ; die Pullup Widerstände aktivieren
out key_port, temp1
mov key_old, temp1 ; bisher war kein Taster gedrückt
loop:
in key_now, key_pin ; den jetzigen Zustand der Taster holen
mov temp1, key_now ; und in temp1 sichern
eor key_now, key_old ; mit dem vorhergehenden Zustand XOR
mov key_old, temp1 ; und den jetzigen Zustand für den nächsten
; Schleifendurchlauf als alten Zustand merken
breq loop ; Das Ergebnis des XOR auswerten:
; wenn keine Taste gedrückt war -> neuer Schleifendurchlauf
and temp1, key_now ; War das ein 1->0 Übergang, wurde der Taster also
; gedrückt (in key_now steht das Ergebnis vom XOR)
brne loop ;
ldi temp1, $FF ; ein bisschen warten ...
wait1:
ldi temp2, $FF
wait2:
dec temp2
brne wait2
dec temp1
brne wait1
; ... und nachsehen, ob die Taste immer noch gedrückt ist
in temp1, key_pin
and temp1, key_now
brne loop
in temp1, led_port ; den Zustand der LED umdrehen
com temp1
out led_port, temp1
rjmp loop
Wie lange gewartet werden muss, hängt im wesentlichen von der mechanischen Qualität und dem Zustand des Tasters ab. Neue und qualitativ hochwertige Taster prellen wenig, ältere Taster prellen mehr. Grundsätzlich prellen aber alle mechanischen Taster irgendwann. Man sollte nicht dem Trugschluss verfallen, daß ein Taster nur weil er heute nicht erkennbar prellt, dieses auch in einem halben Jahr nicht tut.
Kombinierte Entprellung und Flankenerkennung
Von Herrn Peter Dannegger stammt eine clevere Routine, die mit wenig Aufwand an einem Port gleichzeitig bis zu 8 Tasten erkennen und zuverlässig entprellen kann. Dazu wird ein Timer benutzt, der mittels Overflow-Interrupt einen Basistakt erzeugt. Die Zeitdauer von einem Interrupt zum nächsten ist dabei ziemlich unkritisch. Sie sollte sich im Bereich von 5 bis 50 Millisekunden bewegen.
In jedem Overflow Interrupt wird der jetzt am Port anliegende Tastenzustand mit dem Zustand im letzten Timer Interrupt verglichen. Nur dann wenn an einem Pin eine Änderung festgestellt werden kann (Flankenerkennung) wird dieser Tastendruck zunächst registriert. Ein clever aufgebauter Zähler zählt danach die Anzahl der Timer Overflows mit, die die Taste nach Erkennung der Flanke im gedrückten Zustand verharrte. Wurde die Taste nach Erkennung der Flanke 4 mal hintereinander als gedrückt identifiziert, so wird der Tastendruck weitergemeldet. Die 4 mal sind relativ willkürlich und so gewählt, dass man einen Zähler leicht aufbauen kann. Wird die Interrupt Routine also alle 5 Millisekunden aufgerufen, dann muss die Taste bei 4 Stichproben hintereinander durchgehend gedrückt worden sein. Prellt die Taste in dieser Zeit, dann wird der Zähler einfach auf 0 zurückgesetzt und die Wartezeit beginnt erneut zu laufen. Spätestens 20 Millisekunden nach dem letzten Tastenpreller vermeldet daher diese Routine einen Tastendruck, der dann ausgewertet werden kann.
Einfache Tastenentprellung und Abfrage
.include "m8def.inc"
.def iwr0 = r1
.def iwr1 = r2
.def key_old = r3
.def key_state = r4
.def key_press = r5
.def temp1 = r17
.equ key_pin = PIND
.equ key_port = PORTD
.equ key_ddr = DDRD
.def leds = r16
.equ led_port = PORTB
.equ led_ddr = DDRB
.org 0x0000
rjmp init
.org OVF0addr
rjmp timer_overflow0
timer_overflow0: ; Timer Overflow Interrupt
push r0 ; temporäre Register sichern
in r0, SREG
push r0
push iwr0
push iwr1
get8key: ;/old state iwr1 iwr0
mov iwr0, key_old ;00110011 10101010 00110011
in key_old, key_pin ;11110000
eor iwr0, key_old ; 11000011
com key_old ;00001111
mov iwr1, key_state ; 10101010
or key_state, iwr0 ; 11101011
and iwr0, key_old ; 00000011
eor key_state, iwr0 ; 11101000
and iwr1, iwr0 ; 00000010
or key_press, iwr1 ; gedrückte Taste merken
;
;
pop iwr1 ; Register wiederherstellen
pop iwr0
pop r0
out SREG, r0
pop r0
reti
init:
ldi temp1, HIGH(RAMEND)
out SPH, temp1
ldi temp1, LOW(RAMEND) ; Stackpointer initialisieren
out SPL, temp1
ldi temp1, 0xFF
out led_ddr, temp1
ldi temp1, 0xFF ; Tasten sind auf Eingang
out key_port, temp1 ; Pullup Widerstände ein
ldi temp1, 1<<CS02 | 1<<CS00 ; Timer mit Vorteiler 1024
out TCCR0, temp1
ldi temp1, 1<<TOIE0 ; Timer Overflow Interrupt einrichten
out TIMSK, temp1
clr key_old ; die Register für die Tastenauswertung im
clr key_state ; Timer Interrupt initialisieren
clr key_press
sei ; und los gehts: Timer frei
ldi leds, 0xFF
out led_port, leds
main:
cli ;
mov temp1, key_press ; Einen ev. Tastendruck merken und ...
clr key_press ; Tastendruck zurücksetzen
sei
cpi temp1, 0 ; Tastendruck auswerten. Wenn eine von 8 Tasten
breq main ; gedrückt worden wäre, wäre ein entsprechendes
; Bit in key_press gesetzt gewesen
eor leds, temp1 ; Die zur Taste gehörende Led umschalten
out led_port, leds
rjmp main
Tastenentprellung, Abfrage und Autorepeat
Gerade bei Zahlenreihen ist oft eine Autorepeat Funktion eine nützliche Einrichtung: Drückt der Benutzer eine Taste wird eine Funktion ausgelöst. Drückt er eine Taste und hält sie gedrückt, so setzt nach kurzer Zeit der Autorepeat ein. Das System verhält sich so, als ob die Taste in schneller Folge immer wieder gedrückt und wieder losgelassen würde.
Leider muss hier für die Wartezeit ein Register im oberen Bereich benutzt werden. Der ldi Befehl macht dies notwendig. Alternativ könnte man die Wartezeiten beim Init in eines der unteren Register laden und von dort das Repeat Timer Register key_rep jeweils nachladen.
Alternativ wurde in diesem Code auch die Rolle des Registers key_state umgedreht. Ein gesetztes 1 Bit bedeutet hier, dass die zugehörige Taste zur Zeit gedrückt ist.
Insgesamt ist dieser Code eine direkte Umsetzung des von Herrn Dannegger vorgestellten C-Codes. Durch die Möglichkeit eines Autorepeats bei gedrückter Taste erhöhen sich die Möglichkeiten im Aufbau von Benutzereingaben enorm. Das bischen Mehraufwand im Vergleich zum vorher vorgestellten Code, rechtfertigt dies auf jeden Fall.
.include "m8def.inc"
.def iwr0 = r1
.def iwr1 = r2
.def key_state = r4
.def key_press = r5
.def key_rep_press = r6
.def key_rep = r16
.def temp1 = r17
.equ KEY_PIN = PIND
.equ KEY_PORT = PORTD
.equ KEY_DDR = DDRD
.equ KEY_REPEAT_START = 50
.equ KEY_REPEAT_NEXT = 15
.def leds = r20
.equ led_port = PORTB
.equ led_ddr = DDRB
.equ XTAL = 4000000
rjmp init
.org OVF0addr
rjmp timer_overflow0
timer_overflow0: ; Timer Overflow Interrupt
push r0 ; temporäre Register sichern
in r0, SREG
push r0
push r16
; TCNT0 so vorladen, dass der nächste Overflow nach 10 ms auftritt.
ldi r16, -( XTAL / 1024 * 10 / 1000)
; ^ ^ ^^^^^^^^^
; | | = 10 ms
; | Vorteiler
; Quarz-Takt
;
out TCNT0, r16
pop r16
get8key:
in r0, KEY_PIN ; Tasten einlesen
com r0 ; gedrückte Taste werden zu 1
eor r0, key_state ; nur Änderunden berücksichtigen
and iwr0, r0 ; in iwr0 und iwr1 zählen
com iwr0
and iwr1, r0
eor iwr1, iwr0
and r0, iwr0
and r0, iwr1
eor key_state, r0 ;
and r0, key_state
or key_press, r0 ; gedrückte Taste merken
tst key_state ; irgendeine Taste gedrückt ?
breq get8key_rep ; Nein, Zeitdauer zurücksetzen
dec key_rep
brne get8key_finish; ; Zeit abgelaufen?
mov key_rep_press, key_state
ldi key_rep, KEY_REPEAT_NEXT
rjmp get8key_finish
get8key_rep:
ldi key_rep, KEY_REPEAT_START
get8key_finish:
pop r0 ; Register wiederherstellen
out SREG, r0
pop r0
reti
;
;
init:
ldi temp1, HIGH(RAMEND)
out SPH, temp1
ldi temp1, LOW(RAMEND) ; Stackpointer initialisieren
out SPL, temp1
ldi temp1, 0xFF
out led_ddr, temp1
ldi temp1, 0xFF ; Tasten sind auf Eingang
out KEY_PORT, temp1 ; Pullup Widerstände ein
ldi temp1, 1<<CS00 | 1<<CS02
out TCCR0, temp1
ldi temp1, 1<<TOIE0 ; Timer mit Vorteiler 1024
out TIMSK, temp1
clr key_state
clr key_press
clr key_rep_press
clr key_rep
ldi leds, 0xFF
out led_port, leds
main:
; einen einzelnen Tastendruck auswerten
cli
mov temp1, key_press
clr key_press
sei
cpi temp1, 0x01 ; Nur dann wenn Taste 0 gedrückt wurde
breq toggle
; Tasten Autorepeat auswerten
cli
mov temp1, key_rep_press
clr key_rep_press
sei
; Nur dann wenn Taste 0 gehalten wurde
cpi temp1, 0x01
breq toggle
rjmp main ; Hauptschleife abgeschlossen
toggle:
eor leds, temp1 ; Die zur Taste gehörende Led umschalten
out led_port, leds
rjmp main
Fallbeispiel
Das folgende Programm hat durchaus praktischen Wert. Es zeigt auf dem LCD den ASCII Code dezimal und in hexadezimal an, sowie das zugehörige LCD-Zeichen. An den PORTD werden an den Pins 0 und 1 jeweils 1 Taster angeschlossen. Mit dem einen Taster kann der ASCII Code erhöht werden, mit dem anderen Taster wird der ASCII Code erniedrigt. Auf beiden Tastern liegt jeweils ein Autorepeat, sodass jeder beliebige Code einfach angesteuert werden kann. Insbesondere die ASCII Codes größer als 128 sind interessant :-)
.include "m8def.inc"
.def iwr0 = r1
.def iwr1 = r2
.def key_state = r4
.def key_press = r5
.def key_rep_press = r6
.def key_rep = r16
.def temp1 = r17
.equ KEY_PIN = PIND
.equ KEY_PORT = PORTD
.equ KEY_DDR = DDRD
.equ KEY_REPEAT_START = 40
.equ KEY_REPEAT_NEXT = 15
.def code = r20
.equ XTAL = 4000000
rjmp init
.org OVF0addr
rjmp timer_overflow0
timer_overflow0: ; Timer Overflow Interrupt
push r0 ; temporäre Register sichern
in r0, SREG
push r0
push r16
ldi r16, -( XTAL / 1024 * 10 / 1000 + 1 )
out TCNT0, r16
pop r16
get8key:
in r0, KEY_PIN ; Tasten einlesen
com r0 ; gedrückte Taste werden zu 1
eor r0, key_state ; nur Änderunden berücksichtigen
and iwr0, r0 ; in iwr0 und iwr1 zählen
com iwr0
and iwr1, r0
eor iwr1, iwr0
and r0, iwr0
and r0, iwr1
eor key_state, r0 ;
and r0, key_state
or key_press, r0 ; gedrückte Taste merken
tst key_state ; irgendeine Taste gedrückt ?
breq get8key_rep ; Nein, Zeitdauer zurücksetzen
dec key_rep
brne get8key_finish; ; Zeit abgelaufen?
mov key_rep_press, key_state
ldi key_rep, KEY_REPEAT_NEXT
rjmp get8key_finish
get8key_rep:
ldi key_rep, KEY_REPEAT_START
get8key_finish:
pop r0 ; Register wiederherstellen
out SREG, r0
pop r0
reti
;
;
init:
ldi temp1, HIGH(RAMEND)
out SPH, temp1
ldi temp1, LOW(RAMEND) ; Stackpointer initialisieren
out SPL, temp1
ldi temp1, 0xFF ; Tasten sind auf Eingang
out KEY_PORT, temp1 ; Pullup Widerstände ein
rcall lcd_init
rcall lcd_clear
ldi temp1, 1<<CS00 | 1<<CS02
out TCCR0, temp1
ldi temp1, 1<<TOIE0 ; Timer mit Vorteiler 1024
out TIMSK, temp1
clr key_state
clr key_press
clr key_rep_press
clr key_rep
ldi code, 0x30
rjmp update
main:
cli ; normaler Tastendruck
mov temp1, key_press
clr key_press
sei
cpi temp1, 0x01 ; Increment
breq increment
cpi temp1, 0x02 ; Decrement
breq decrement
cli ; gedrückt und halten -> repeat
mov temp1, key_rep_press
clr key_rep_press
sei
cpi temp1, 0x01 ; Increment
breq increment
cpi temp1, 0x02 ; Decrement
breq decrement
rjmp main
increment:
inc code
rjmp update
decrement:
dec code
update:
rcall lcd_home
mov temp1, code
rcall lcd_number
ldi temp1, ' '
rcall lcd_data
mov temp1, code
rcall lcd_number_hex
ldi temp1, ' '
rcall lcd_data
mov temp1, code
rcall lcd_data
rjmp main
.include "lcd-routines.asm"
Weblinks
- 10 Keys on One Port Pin? - Eine ganz andere Art der Tastenerkennung über einen ADC. Damit lässt sich auch eine von vielen Tasten an nur einem ADC-Eingangspin abfragen.
- Input Matrix Scanning von Open Music Labs. Tutorials und Kostenbetrachtung für 12 Verfahren, um eine Eingabematrix (1-128 Schalter an 1-20 Pins) abzufragen.