AVR-Tutorial: Tasten

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

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.

Flankensuche.png

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.

Wahrheitstabelle XOR
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.

Signal eines prellenden Tasters

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.