Entprellung

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

Problembeschreibung

Mechanische stromführende Komponenten wie Schalter und Taster neigen beim Ein- und Ausschalten zum sogenannten Prellen, d. h. sie schalten schnell mehrfach aus und ein, was durch mechanische Vibrationen des Schaltkontaktes verursacht wird, sofern sie nicht mit aufwändigen mechanischen Maßnahmen dagegen geschützt sind. Besonders Drehgeber sind aufgrund der Rasterfunktion und der Bewegung des Bedieners dafür empfindlich. Auch optoelektronische Bauelemente und chemische Kontaktschalter sowie Flüssigkeitsschalter haben das Problem.

Vereinfacht dargestellt, sieht eine von einem prellenden Schalter oder Taster geschaltete Spannung wie folgt aus: Entprellen.png

Es existieren also mehrere kurze Einschaltimpulse, welche bei Tastern als Mehrfachbefehl und bei Drehgebern als falsche Winkelbewegung interpretiert werden können. Bei Schaltern wiederum kommt es in der elektronischen Baugruppe zu mehreren Resets und Einschaltvorgängen, die unnötig Strom ziehen oder im schlechtesten Fall die Schaltung stressen oder beschädigen können. Wichtige Schalter und solche, die hohe Ströme führen sollen, werden dazu mit geeigneten Maßnahmen wie Redundanz, Stufenschaltkonzepten oder bei Gas- und Flüssigkeitsschaltern durch elektrochemische Maßnahmen abgesichert. Bei einfachen Schaltern spart man sich dies jedoch.

Da es bei diesen einfachen, ungeschützten Schaltern keine sichere Möglichkeit gibt, diese Effekte zu vermeiden, muss das prellende Signal sinnvoll ausgewertet werden. Dafür gibt es verschiedene Ansätze, sowohl mit Elektronik als auch mit Software, die im Folgenden vorgestellt werden.

Hardwareentprellung

Prellfreie Schalter

Wie bereits angedeutet, hält die elektromechanische Industrie für Spezialanwendungen verschiedene Sonderkonstruktionen bereit, die saubere Schaltzustände nach außen generieren, indem sie entweder eine mechanische Dämpfung in Form eines selbsthemmenden Federmechanismus oder eine integrierte elektronische Signalverzögerung benutzen. Solche Systeme sind jedoch teuer und werden meist nur im Leistungsbereich eingesetzt, wo nicht einfach nur das Signal ausgewertet werden kann. Zudem sind auch diese nicht 100%ig sicher und fallen alterungsbedingt öfters aus. Wo immer es geht, werden im Kleinsignalbereich daher Maßnahmen getroffen, die Wirkung des Prellens zu unterdrücken.

Wechselschalter

Für die Entprellung von Wechselschaltern (engl. Double Throw Switch) kann ein klassisches RS-Flipflop genutzt werden. Bei dieser Variante werden neben zwei NAND-Gattern nur noch zwei Pull-Up-Widerstände benötigt.

Taster entprellen mit NAND-RS-Flipflop


In der gezeigten Schalterstellung liegt an der Position /S der Pegel 0 an. Damit ist das Flipflop gesetzt und der Ausgang auf dem Pegel 1. Schließt der Schalter zwischen den Kontakten 2 und 3, liegt an der Postion /R der Pegel 0 an. Dies bedeutet, dass der Ausgang des Flipflops auf den Pegel 0 geht. Sobald der Schalter von einem zum anderen Kontakt wechselt, beginnt er in der Regel zu prellen. Während des Prellens wechselt der Schalter zwischen den beiden Zuständen „Schalter berührt Kontakt“ und „Schalter ist frei in der Luft“. Der Ausgang des Flipflops bleibt in dieser Prellzeit aber stabil, da der Schalter während des Prellens nie den gegenüberliegenden Kontakt berührt und das RS-Flipflop seinen Zustand allein halten kann. Die Prellzeit ist stark vom Schaltertyp abhängig und liegt zwischen 0,1 und 10 ms. Die Dimensionierung der Widerstände ist relativ unkritisch. Als Richtwert können hier 100 kΩ verwendet werden.

Wechselschalter ohne Flipflop

Wenn man einmal kein Flipflop zur Hand hat, kann man sich auch mit dieser Schaltung behelfen.

Wechsler entprellen mit Kondensator


Zur Funktionsweise: Beim Umschalten wird der Kondensator immer sofort umgeladen. Während der Kontakt prellt, befindet er sich in der Luft und hat keinerlei Verbindung. Während dieser Zeit übernimmt der Kondensator das Halten des Pegels.

Dimensionierung: Ist der entprellte Taster an ein IC angeschlossen, ist der Input Leakage Current der ausschlaggebende Strom. Falls weitere Ströme fließen sind diese mit zu berücksichtigen. Bei einem Mikrocontroller von Atmel ist 1 µA typisch. Es gilt:

[math]\displaystyle{ \frac{\mathrm d U}{\mathrm d t} = \frac{I}{C} }[/math]

Da ein Prellen maximal ca. 10 ms dauert und die Spannung in dieser Zeit beispielsweise um maximal 0,5 V fallen soll, kommt man auf folgende Kapazität:

[math]\displaystyle{ C = \frac{I \cdot \Delta t}{\Delta U} = \frac{1~\text{µA} \cdot 10~\text{ms}}{\text{0,5}~\text{V}} = 20~\text{nF} }[/math]

Um Stromspitzen zu verringern kann ein Widerstand mit eingefügt werden. Eine Zeitkonstante von 1 µs bis 1 ms scheint sinnvoll. Also 500 Ω bis 500 kΩ sind nutzbar, wobei bei niedrigem Widerstand die Stromspitzen höher sind, und bei 500 kΩ der Pinstrom störend wird.

Einfacher Taster

Auch wenn das RS-Flipflop sehr effektiv ist, wird diese Variante der Entprellung nur selten angewendet. Der Grund dafür ist, dass in Schaltungen häufiger einfache Taster eingesetzt werden. Diese sind oft kleiner und preisgünstiger. Um solche Taster (engl. push button / momentary switch) zu entprellen, kann ein einfacher RC-Tiefpass eingesetzt werden. Hierbei wird ein Kondensator über einen Widerstand je nach Schalterstellung auf- oder entladen. Das RC-Glied bildet einen Tiefpass, sodass die Spannung über dem Kondensator nicht von einem Pegel auf den anderen springen kann.

Taster entprellen mit RC-Entpreller
Entstehender Spannungsverlauf

Wenn der Schalter geöffnet ist, lädt sich der Kondensator langsam über die beiden Widerstände R1 und R2 auf VCC auf. Beim Erreichen der Umschaltschwelle springt der Ausgang auf den Pegel 0. Wird der Schalter geschlossen, entlädt sich der Kondensator langsam über den Widerstand R2. Demnach ändert sich der Ausgang des Inverters auf den Pegel 1. Während der Taster prellt, kann sich die Spannung über dem Kondensator nicht sprunghaft ändern, da das Auf- und Entladen eher langsam über die Widerstände erfolgt. Außerdem sind die Schaltschwellen für die Übergänge lowhigh und highlow stark verschieden (Hysterese, siehe Artikel Schmitt-Trigger). Bei richtiger Dimensionierung der Bauelemente wird somit der Ausgang des Inverters prellfrei. Zu beachten ist, dass der Inverter unbedingt über Schmitt-Trigger-Eingänge verfügen muss, weil schon bei gewöhnlichen digitalen Eingängen immer auch analoge Effekte wie Rauschen wirken, die im Umschaltpunkt zu unsicherem Verhalten führen. Bei Standard-Logikeingängen ist der Bereich von üblicherweise 0,8…2,0 V deren Ausgang nicht definiert. Trotz Filterung käme es daher in diesem Bereich zu einer Weiterleitung des Prellens.

Als Inverterbaustein kann zum Beispiel der 74HC14 oder der CD40106 (pinkompatibel) eingesetzt werden. Alternativ kann auch ein CD4093 Verwendung finden. Bei dem CD4093 handelt es sich um NAND-Gatter mit Schmitt-Trigger-Eingängen. Um aus einem NAND-Gatter einen Inverter zu machen, müssen einfach nur die beiden Eingänge verbunden werden oder ein Eingang fest auf high gelegt werden.

Für eine geeignete Dimensionierung muss man etwas mit den Standardformeln für einen Kondensator jonglieren. Die Spannung über dem Kondensator beim Entladen berechnet sich nach

[math]\displaystyle{ U_C(t) = U_0 \cdot \exp{\frac{-t}{R_2 C_1}} }[/math].

Damit der Ausgang des Inverters stabil ist, muss die Spannung über dem Kondensator und damit die Spannung am Eingang des Inverters über der Spannung bleiben, bei welcher der Inverter umschaltet. Diese Schwellwertspannung ist genau die zeitabhängige Spannung über dem Kondensator.

[math]\displaystyle{ U_C(t)\!\ = U_{th} }[/math]

Durch Umstellen der Formel ergibt sich nun

[math]\displaystyle{ R_2=\frac{-t}{C_1 \cdot \ln\left(\frac{U_{th}}{U_0}\right)} }[/math].

Ein Taster prellt üblicherweise bis zu etwa 10 ms. Zur Sicherheit kann bei der Berechnung des Widerstandes eine Prellzeit von 20 ms angenommen werden. [math]\displaystyle{ U_0 }[/math] ist die Betriebsspannung, also VCC. Die Schwellwertspannung muss aus dem Datenblatt des eingesetzten Schmitt-Triggers abgelesen werden. Beim 74HC14 beträgt der gesuchte Wert 2,0 V. Nimmt man für den Kondensator 1 µF und beträgt die Betriebsspannung 5 V, ergibt sich für den Widerstand ein Wert von etwa 22 kΩ.

Wird der Schalter geöffnet, lädt sich der Kondensator nach folgender Formel auf:

[math]\displaystyle{ U_C(t) = U_0 \cdot \left( 1-\exp{\frac{-t}{(R_1+R_2)\cdot C_1}} \right) }[/math]

Mit [math]\displaystyle{ U_{th}=U_C }[/math] ergibt das Umstellen nach [math]\displaystyle{ (R_1+R_2) }[/math]:

[math]\displaystyle{ R_1+R_2 = \frac{-t}{C_1 \cdot \ln\left(1-\frac{U_{th}}{U_0} \right)} }[/math]

Für die Schwellspannung lässt sich aus dem Datenblatt ein Wert von 2,3 V ablesen. Mit diesem Wert und den Annahmen von oben ergibt sich für [math]\displaystyle{ R_1+R_2 }[/math] ein Wert von 32 kΩ. Somit folgt für [math]\displaystyle{ R_1 }[/math] ein Wert von etwa 10 kΩ.

Anmerkung: Beim 74LS14 von Hitachi z. B. sind die oberen und unteren Schaltschwellwerte unterschiedlich. Es muss darauf geachtet werden, dass [math]\displaystyle{ U_{th} }[/math] beim Entladen die untere Schwelle und [math]\displaystyle{ U_{th} }[/math] beim Laden die obere Schwelle einnimmt.

Softwareentprellung

In den Zeiten der elektronischen Auswertung von Tastern und Schaltern ist das softwaretechnische Entprellen oft billiger als die Benutzung eines teuren Schalters. Daher werden heute z. B. auch Computertastaturen nicht mehr mit prellarmen Tasten oder Entprellkondensatoren ausgestattet.

Bei Verwendung des in den meisten Geräten ohnehin vorhandenen Mikrocontrollers z. B. kann man sich die zusätzliche Hardware sparen, da die Entprellung in Software praktisch genauso gut funktioniert. Dabei ist nur zu beachten, dass zusätzliche Rechenleistung und je nach Umsetzung auch einige Hardwareressourcen (z. B. Timer) benötigt werden. Dafür hat man aber den Vorteil, kurze Pulse, die offensichtlich keine Tastenbetätigung sein können sondern z. B. durch Einstreuungen hervorgerufen werden, einfach ausfiltern zu können.

Flankenerkennung

Bei einem Taster gibt es insgesamt 4 theoretische Zustände:

  1. war nicht gedrückt und ist nicht gedrückt
  2. war nicht gedrückt und ist gedrückt (steigende Flanke)
  3. war gedrückt und ist immer noch gedrückt
  4. war gedrückt und ist nicht mehr gedrückt (fallende Flanke)

Diese einzelnen Zustände lassen sich jetzt bequem abfragen/durchlaufen. Die Entprellung geschieht dabei durch die ganze Laufzeit des Programms. Die Taster werden hierbei als active-low angeschlossen, um die internen Pull-Ups zu nutzen.

Diese Routine gibt für den Zustand „steigende Flanke“ den Wert „1“ zurück, sonst „0“.

#define TASTERPORT PINC
#define TASTERBIT PINC1

char taster(void)
{
    static unsigned char zustand;
    char rw = 0;

    if(zustand == 0 && !(TASTERPORT & (1<<TASTERBIT)))   //Taster wird gedrueckt (steigende Flanke)
    {
        zustand = 1;
        rw = 1;
    }
    else if (zustand == 1 && !(TASTERPORT & (1<<TASTERBIT)))   //Taster wird gehalten
    {
         zustand = 2;
         rw = 0;
    }
    else if (zustand == 2 && (TASTERPORT & (1<<TASTERBIT)))   //Taster wird losgelassen (fallende Flanke)
    {
        zustand = 3;
        rw = 0;
    }
    else if (zustand == 3 && (TASTERPORT & (1<<TASTERBIT)))   //Taster losgelassen
    {
        zustand = 0;
        rw = 0;
    }

    return rw;
}

Eine Erweiterung, damit beliebig lange das Halten einer Taste erkannt wird, kann man ganz einfach so implementieren:

    // Zustand kann entweder zum ersten Mal als gehalten detektiert werden oder aber jedes weitere Mal
    else if (((zustand == 1) || (zustand == 2)) && !(TASTERPORT & (1<<TASTERBIT)))   //Taster wird gehalten
    {
         zustand = 2;
         rw = 0;
    }

Warteschleifen-Verfahren

Soll nun mit einem Mikrocontroller gezählt werden, wie oft ein Kontakt oder ein Relais geschaltet wird, muss das Prellen des Kontaktes exakt berücksichtigt und von einem gewollten Mehrfachschalten abgegrenzt werden, da sonst möglicherweise Fehlimpulse gezählt oder andererseits echte Schaltvorgänge übersprungen werden. Dem muss beim Schreiben des Programms hinsichtlich des Abtastens des Kontaktes unbedingt Rechnung getragen werden.

Beim folgenden einfachen Beispiel für eine Entprellung ist zu beachten, dass der AVR im Falle eines Tastendrucks 200 ms wartet, also brach liegt. Bei zeitkritischen Anwendungen sollte man ein anderes Verfahren nutzen (z. B. Abfrage der Tastenzustände in einer Timer-Interrupt-Service-Routine).

#include <avr/io.h>
#include <inttypes.h>
#ifndef F_CPU
#warning "F_CPU war noch nicht definiert, wird nun mit 3686400 definiert"
#define F_CPU 3686400UL     /* Quarz mit 3.6864 Mhz  */
#endif
#include <util/delay.h>     /* bei alter avr-libc: #include <avr/delay.h> */

/* Einfache Funktion zum Entprellen eines Tasters */
inline uint8_t debounce(volatile uint8_t *port, uint8_t pin)
{
    if ( !(*port & (1 << pin)) )
    {
        /* Pin wurde auf Masse gezogen, 100ms warten   */
        _delay_ms(50);   // Maximalwert des Parameters an _delay_ms
        _delay_ms(50);   // beachten, vgl. Dokumentation der avr-libc
        if ( *port & (1 << pin) )
        {
            /* Anwender Zeit zum Loslassen des Tasters geben */
            _delay_ms(50);
            _delay_ms(50);
            return 1;
        }
    }
    return 0;
}

int main(void)
{
    DDRB &= ~( 1 << PB0 );        /* PIN PB0 auf Eingang Taster)  */
    PORTB |= ( 1 << PB0 );        /* Pullup-Widerstand aktivieren */
    ...
    if (debounce(&PINB, PB0))
    {
        /* Falls Taster an PIN PB0 gedrueckt     */
        /* LED an Port PD7 an- bzw. ausschalten: */
        PORTD = PORTD ^ ( 1 << PD7 );
    }
    ...
}

Die obige Routine hat leider mehrere Nachteile:

  • sie detektiert nur das Loslassen (unergonomisch),
  • sie verzögert die Mainloop immer um 100 ms bei gedrückter Taste,
  • sie verliert Tastendrücke, je mehr die Mainloop zu tun hat.

Eine ähnlich einfach zu benutzende Routine, aber ohne all diese Nachteile findet sich im Forenthread Entprellung für Anfänger.

Der DEBOUNCE-Befehl in dem BASIC-Dialekt BASCOM für AVR ist ebenfalls nach dem Warteschleifen-Verfahren programmiert. Die Wartezeit beträgt standardmäßig 25 ms, kann aber vom Anwender überschrieben werden. Vgl. BASCOM Online-Manual zu DEBOUNCE.

Eine C-Implementierung für eine Tastenabfrage mit Warteschleife ist im Artikel AVR-GCC-Tutorial: IO-Register als Parameter und Variablen angeben.

Der Nachteil dieses Verfahrens ist, dass der Controller durch die Warteschleife blockiert wird. Günstiger ist die Implementierung mit einem Timer-Interrupt.

Warteschleifenvariante mit Maske und Pointer (nach Christian Riggenbach)

Hier eine weitere Funktion, um Taster zu entprellen: Durch den zusätzlichen Code kann eine Entprellzeit von durchschnittlich 1…3 ms (mindestens 8·150 µs = 1,2 ms) erreicht werden. Grundsätzlich prüft die Funktion den Pegel der Pins auf einem bestimmten Port. Wenn die/der Pegel 8-mal konstant war, wird die Schleife verlassen. Diese Funktion kann sehr gut eingesetzt werden, um in einer Endlosschleife Taster abzufragen, da sie, wie erwähnt, eine kurze Wartezeit hat.

void entprellung( volatile uint8_t *port, uint8_t maske ) {
  uint8_t   port_puffer;
  uint8_t   entprellungs_puffer;

  for( entprellungs_puffer=0 ; entprellungs_puffer!=0xff ; ) {
    entprellungs_puffer<<=1;
    port_puffer = *port;
    _delay_us(150);
    if( (*port & maske) == (port_puffer & maske) )
      entprellungs_puffer |= 0x01;
  }
}

Die Funktion wird wie folgt aufgerufen:

  // Bugfix 20100414
  // https://www.nongnu.org/avr-libc/user-manual/FAQ.html#faq_port_pass
  entprellung( &PINB, (1<<PINB2) ); // ggf. Prellen abwarten
  if( PINB & (1<<PINB2) )           // dann stabilen Wert einlesen
  {
    // mach was
  }
  else
  {
    // mach was anderes
  }

Als Maske kann ein beliebiger Wert übergeben werden. Sie verhindert, dass nichtverwendete Taster die Entprellzeit negativ beeinflussen.

Debounce-Makro von Peter Dannegger

Peter Dannegger hat im Forumsbeitrag Entprellen für Anfänger folgendes vereinfachtes Entprellverfahren beschrieben. Das Makro arbeitet in der Originalversion mit active low geschalteten Tastern, kann aber einfach für active high geschaltete Taster angepasst werden (Tasty Reloaded).

/************************************************************************/
/*                                                                      */
/*                      Not so powerful Debouncing Example              */
/*                      No Interrupt needed                             */
/*                                                                      */
/*              Author: Peter Dannegger                                 */
/*                                                                      */
/************************************************************************/
// Target: ATtiny13

#include <avr/io.h>
#define F_CPU 9.6e6
#include <util/delay.h>


#define debounce( port, pin )                                         \
({                                                                    \
  static uint8_t flag = 0;     /* new variable on every macro usage */  \
  uint8_t i = 0;                                                      \
                                                                      \
  if( flag ){                  /* check for key release: */           \
    for(;;){                   /* loop ... */                         \
      if( !(port & 1<<pin) ){  /* ... until key pressed or ... */     \
        i = 0;                 /* 0 = bounce */                       \
        break;                                                        \
      }                                                               \
      _delay_us( 98 );         /* * 256 = 25ms */                     \
      if( --i == 0 ){          /* ... until key >25ms released */     \
        flag = 0;              /* clear press flag */                 \
        i = 0;                 /* 0 = key release debounced */        \
        break;                                                        \
      }                                                               \
    }                                                                 \
  }else{                       /* else check for key press: */        \
    for(;;){                   /* loop ... */                         \
      if( (port & 1<<pin) ){   /* ... until key released or ... */    \
        i = 0;                 /* 0 = bounce */                       \
        break;                                                        \
      }                                                               \
      _delay_us( 98 );         /* * 256 = 25ms */                     \
      if( --i == 0 ){          /* ... until key >25ms pressed */      \
        flag = 1;              /* set press flag */                   \
        i = 1;                 /* 1 = key press debounced */          \
        break;                                                        \
      }                                                               \
    }                                                                 \
  }                                                                   \
  i;                           /* return value of Macro */            \
})

/*
   Testapplication
 */
int main(void)
{
  DDRB  &= ~(1<<PB0);
  PORTB |=   1<<PB0;
  DDRB  |=   1<<PB2;
  DDRB  &= ~(1<<PB1);
  PORTB |=   1<<PB1;
  DDRB  |=   1<<PB3;
  for(;;){
    if( debounce( PINB, PB1 ) )
      PORTB ^= 1<<PB2;
    if( debounce( PINB, PB0 ) )
      PORTB ^= 1<<PB3;
  }
}

Wenn das Makro für die gleiche Taste (Pin) an mehreren Stellen aufgerufen werden soll, muss eine Funktion angelegt werden, damit beide Aufrufe an die gleiche Zustandsvariable flag auswerten (siehe diesen Forumsbeitrag):

// Hilfsfunktion
uint8_t debounce_C1( void )
{
  return debounce(PINC, PC1);
}

// Beispielanwendung
int main(void)
{
  DDRB  |=   1<<PB2;
  DDRB  |=   1<<PB3;
  DDRC  &= ~(1<<PC1);
  PORTC |=   1<<PC1; // Pullup für Taster

  for(;;){
    if( debounce_C1() )  // nicht: debounce(PINC, PC1)
      PORTB ^= 1<<PB2;
    if( debounce_C1() )  // nicht: debounce(PINC, PC1)
      PORTB ^= 1<<PB3;
  }
}

Timer-Verfahren (nach Peter Dannegger)

Grundroutine (AVR-Assembler)

Siehe dazu: Forumsbeitrag Tasten entprellen.

Vorteile:

  • besonders kurzer Code
  • schnell

Außerdem können 8 Tasten (aktiv low) gleichzeitig bearbeitet werden, es dürfen also alle exakt zur selben Zeit gedrückt werden. Andere Routinen können z. B. nur eine Taste verarbeiten, d. h. die zuerst oder zuletzt gedrückte gewinnt, oder es kommt Unsinn heraus.

Die eigentliche Einlese- und Entprellroutine ist nur 8 Instruktionen kurz. Der entprellte Tastenzustand ist im Register key_state. Mit nur 2 weiteren Instruktionen wird dann der Wechsel von Taste offen zu Taste gedrückt erkannt und im Register key_press abgelegt. Im Beispielcode werden dann damit 8 LEDs ein- und ausgeschaltet. Jede Taste entspricht einem Bit in den Registern, d. h. die Verarbeitung erfolgt bitweise mit logischen Operationen. Zum Verständnis empfiehlt es sich daher, die Logikgleichungen mit Gattern für ein Bit = eine Taste aufzumalen. Die Register kann man sich als Flipflops denken, die mit der Entprellzeit als Takt arbeiten. D. h. man kann das auch so z. B. in einem GAL22V10 realisieren.

Als Kommentar sind neben den einzelnen Instruktionen alle 8 möglichen Kombinationen der 3 Signale dargestellt.

Beispielcode für AVR (Assembler):

.nolist
.include "c:\avr\inc\1200def.inc"
.list
.def  save_sreg         = r0
.def  iwr0              = r1
.def  iwr1              = r2

.def  key_old           = r3
.def  key_state         = r4
.def  key_press         = r5

.def  leds              = r16
.def  wr0               = r17

.equ  key_port          = pind
.equ  led_port          = portb

      rjmp   init
.org OVF0addr		;timer interrupt 24ms
      in     save_sreg, SREG
get8key:                               ;/old      state     iwr1      iwr0
      mov    iwr0, key_old             ;00110011  10101010            00110011
      in     key_old, key_port         ;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           ;store key press detect
;
;			insert other timer functions here
;
      out    SREG, save_sreg
      reti
;-------------------------------------------------------------------------
init:
      ldi    wr0, 0xFF
      out    ddrb, wr0
      ldi    wr0, 1<<CS02 | 1<<CS00    ;divide by 1024 * 256
      out    TCCR0, wr0
      ldi    wr0, 1<<TOIE0             ;enable timer interrupt
      out    TIMSK, wr0

      clr    key_old
      clr    key_state
      clr    key_press
      ldi    leds, 0xFF
main: cli
      eor    leds, key_press           ;toggle LEDs
      clr    key_press                 ;clear, if key press action done
      sei
      out    led_port, leds
      rjmp   main
;-------------------------------------------------------------

Komfortroutine (C für AVR)

Siehe dazu: Forumsbeitrag Universelle Tastenabfrage.

Anmerkung: Wenn statt active-low (Ruhezustand high) active-high (Ruhezustand low) verwendet wird, muss eine Zeile geändert werden; siehe gesamter Beitrag im Forum, Stelle 1 im Beitrag (Stelle 2 im Beitrag muss nicht geändert werden, da hier die Polarität gar keinen Einfluß hat).

Anmerkung 2: Zur Initialisierung siehe diesen Forumsbeitrag.

Funktionsprinzip wie oben plus zusätzliche Features:

  • Kann Tasten sparen durch unterschiedliche Aktionen bei kurzem oder langem Drücken
  • Wiederholfunktion, z. B. für die Eingabe von Werten

Das Programm ist für avr-gcc/avr-libc geschrieben, kann aber mit ein paar Anpassungen auch mit anderen Compilern und Mikrocontrollern verwendet werden. Eine Portierung für den AT91SAM7 findet man hier (aus dem Projekt ARM MP3/AAC Player).

/************************************************************************/
/*                                                                      */
/*                      Debouncing 8 Keys                               */
/*                      Sampling 4 Times                                */
/*                      With Repeat Function                            */
/*                                                                      */
/*              Author: Peter Dannegger                                 */
/*                      danni@specs.de                                  */
/*                                                                      */
/************************************************************************/

#include <stdint.h>
#include <avr/io.h>
#include <avr/interrupt.h>

#ifndef F_CPU
#define F_CPU           1000000                   // processor clock frequency
#warning kein F_CPU definiert
#endif

#define KEY_DDR         DDRB
#define KEY_PORT        PORTB
#define KEY_PIN         PINB
#define KEY0            0
#define KEY1            1
#define KEY2            2
#define ALL_KEYS        (1<<KEY0 | 1<<KEY1 | 1<<KEY2)

#define REPEAT_MASK     (1<<KEY1 | 1<<KEY2)       // repeat: key1, key2
#define REPEAT_START    50                        // after 500ms
#define REPEAT_NEXT     20                        // every 200ms

#define LED_DDR         DDRA
#define LED_PORT        PORTA
#define LED0            0
#define LED1            1
#define LED2            2

volatile uint8_t key_state;                                // debounced and inverted key state:
                                                  // bit = 1: key pressed
volatile uint8_t key_press;                                // key press detect

volatile uint8_t key_rpt;                                  // key long press and repeat


ISR( TIMER0_OVF_vect )                            // every 10ms
{
  static uint8_t ct0 = 0xFF, ct1 = 0xFF, rpt;
  uint8_t i;

  TCNT0 = (uint8_t)(int16_t)-(F_CPU / 1024 * 10e-3 + 0.5);  // preload for 10ms

  i = key_state ^ ~KEY_PIN;                       // key changed ?
  ct0 = ~( ct0 & i );                             // reset or count ct0
  ct1 = ct0 ^ (ct1 & i);                          // reset or count ct1
  i &= ct0 & ct1;                                 // count until roll over ?
  key_state ^= i;                                 // then toggle debounced state
  key_press |= key_state & i;                     // 0->1: key press detect

  if( (key_state & REPEAT_MASK) == 0 )            // check repeat function
     rpt = REPEAT_START;                          // start delay
  if( --rpt == 0 ){
    rpt = REPEAT_NEXT;                            // repeat delay
    key_rpt |= key_state & REPEAT_MASK;
  }
}

///////////////////////////////////////////////////////////////////
//
// check if a key has been pressed. Each pressed key is reported
// only once
//
uint8_t get_key_press( uint8_t key_mask )
{
  cli();                                          // read and clear atomic !
  key_mask &= key_press;                          // read key(s)
  key_press ^= key_mask;                          // clear key(s)
  sei();
  return key_mask;
}

///////////////////////////////////////////////////////////////////
//
// check if a key has been pressed long enough such that the
// key repeat functionality kicks in. After a small setup delay
// the key is reported being pressed in subsequent calls
// to this function. This simulates the user repeatedly
// pressing and releasing the key.
//
uint8_t get_key_rpt( uint8_t key_mask )
{
  cli();                                          // read and clear atomic !
  key_mask &= key_rpt;                            // read key(s)
  key_rpt ^= key_mask;                            // clear key(s)
  sei();
  return key_mask;
}

///////////////////////////////////////////////////////////////////
//
// check if a key is pressed right now
//
uint8_t get_key_state( uint8_t key_mask )

{
  key_mask &= key_state;
  return key_mask;
}

///////////////////////////////////////////////////////////////////
//
uint8_t get_key_short( uint8_t key_mask )
{
  cli();                                          // read key state and key press atomic !
  return get_key_press( ~key_state & key_mask );
}

///////////////////////////////////////////////////////////////////
//
uint8_t get_key_long( uint8_t key_mask )
{
  return get_key_press( get_key_rpt( key_mask ));
}

int main( void )
{
  LED_PORT = 0xFF;
  LED_DDR = 0xFF;

  // Configure debouncing routines
  KEY_DDR &= ~ALL_KEYS;                // configure key port for input
  KEY_PORT |= ALL_KEYS;                // and turn on pull up resistors

  TCCR0 = (1<<CS02)|(1<<CS00);         // divide by 1024
  TCNT0 = (uint8_t)(int16_t)-(F_CPU / 1024 * 10e-3 + 0.5);  // preload for 10ms
  TIMSK |= 1<<TOIE0;                   // enable timer interrupt

  sei();

  while(1){
    if( get_key_short( 1<<KEY1 ))
      LED_PORT ^= 1<<LED1;

    if( get_key_long( 1<<KEY1 ))
      LED_PORT ^= 1<<LED2;

    // single press and repeat

    if( get_key_press( 1<<KEY2 ) || get_key_rpt( 1<<KEY2 )){
      uint8_t i = LED_PORT;

      i = (i & 0x07) | ((i << 1) & 0xF0);
      if( i < 0xF0 )
        i |= 0x08;
      LED_PORT = i;
    }
  }
}

Das Single-press-und-repeat-Beispiel geht nicht in jeder Beschaltung; folgendes Beispiel sollte universeller sein (einzelne LED an/aus):

// single press and repeat
if( get_key_press( 1<<KEY2 ) || get_key_rpt( 1<<KEY2 ))
    LED_PORT ^=0x08;

Neuere Variante, die einer Taste folgende Funktionen erlaubt: Universelle Tastenabfrage mit 2 Tastenerkennung

- get_key_press()
- get_key_rpt()
- get_key_press() mit get_key_rpt()
- get_key_short() mit get_key_long()
- get_key_short() mit get_key_long_r() und get_key_rpt_l()

Erweiterung für die Erkennung von zwei gleichzeitig gedrückten Tasten: [1]

- get_key_common()
Funktionsweise

Der Code basiert auf 8 parallelen vertikalen Zählern, die über die Variablen ct0 und ct1 aufgebaut werden,

8 vertikale Zähler in zwei 8-Bit-Variablen

wobei jeweils ein Bit in ct0 mit dem gleichwertigen Bit in ct1 zusammengenommen einen 2-Bit-Zähler bildet. Der Code, der sich um die 8 Zähler kümmert, ist so geschrieben, daß er alle 8 Zähler gemeinsam parallel behandelt.

  i = key_state ^ ~KEY_PIN;                       // key changed ?

i enthält an dieser Stelle für jede Taste, die sich im Vergleich mit dem vorhergehenden entprellten Zustand (key_state) verändert hat, ein 1-Bit.

  ct0 = ~( ct0 & i );                             // reset or count ct0
  ct1 = ct0 ^ (ct1 & i);                          // reset or count ct1

Diese beiden Anweisungen erniedrigen den 2-Bit-Zähler ct0/ct1 für jedes Bit um 1, welches in i gesetzt ist. Liegt an der entsprechenden Stelle in i ein 0-Bit vor (keine Änderung des Zustands), so wird der Zähler ct0/ct1 für dieses Bit auf 1 gesetzt. Der Grundzustand des Zählers ist also ct0 == 1 und ct1 == 1 (Wert 3). Der Zähler zählt daher mit jedem ISR-Aufruf, bei dem die Taste im Vergleich zu key_state als verändert erkannt wurde,

  ct1   ct0
    1    1   // 3
    1    0   // 2
    0    1   // 1
    0    0   // 0
    1    1   // 3
  i &= ct0 & ct1;                                 // count until roll over ?

in i bleibt nur dort ein 1-Bit erhalten, wo sowohl in ct1 als auch in ct0 ein 1-Bit vorgefunden wird, der betreffende Zähler also bis 3 zählen konnte. Durch die zusätzliche Verundung mit i wird der Fall abgefangen, dass ein konstanter Zählerwert von 3 in i ein 1-Bit hinterlässt. Im Endergebnis bedeutet das, dass nur ein Zählerwechsel von 0 auf 3 zu einem 1-Bit an der betreffenden Stelle in i führt, aber auch nur dann, wenn in i an dieser Bitposition ebenfalls ein 1-Bit war (welches wiederum deswegen auf 1 war, weil an diesem Eingabeport eine Veränderung zum letzten bekannten entprellten Zustand festgestellt wurde). Alles zusammengenommen heißt das, dass ein Tastendruck dann erkannt wird, wenn die Taste 4-mal hintereinander in einem anderen Zustand vorgefunden wurde als dem zuletzt bekannten entprellten Tastenzustand.

An dieser Stelle ist i daher ein Vektor von 8 Bits, von denen jedes einzelne der Bits darüber Auskunft gibt, ob die entsprechende Taste mehrmals hintereinander im selben Zustand angetroffen wurde, der nicht mit dem zuletzt bekannten Tastenzustand übereinstimmt. Ist das der Fall, dann wird eine entsprechende Veränderung des Tastenzustands in key_state registriert

  key_state ^= i;                                 // then toggle debounced state

und wenn sich in key_state das entsprechende Bit von 0 auf 1 verändert hat, wird dieses Ereignis als „Taste wurde niedergedrückt“ gewertet.

  key_press |= key_state & i;                     // 0->1: key press detect

Damit ist der Tasteneingang entprellt. Und zwar sowohl beim Drücken einer Taste als auch beim Loslassen (damit Tastenpreller beim Loslassen nicht mit dem Niederdrücken einer Taste verwechselt werden). Der weitere Code beschäftigt sich dann nur noch damit, diesen entprellten Tastenzustand weiter zu verarbeiten.

Der Codeteil sieht durch die Verwendung der vielen bitweisen Operationen relativ komplex aus. Behält man aber im Hinterkopf, dass einige der bitweisen wie ein „paralles If“ gleichzeitig auf allen 8 Bits eingesetzt werden, dann vereinfacht sich vieles. Ein

    key_press |= key_state & i;

ist nichts anderes als ein

    // teste ob Bit 0 sowohl in key_state als auch in i gesetzt ist
    // und setze Bit 0 in key_press, wenn das der Fall ist
    if( ( key_state & ( 1 << 0 ) ) &&
        ( i & ( 1 << 0 ) )
       key_press |= ( 1 << 0 );

    // Bit 1
    if( ( key_state & ( 1 << 1 ) ) &&
        ( i & ( 1 << 1 ) )
       key_press |= ( 1 << 1 );

    // Bit 2
    if( ( key_state & ( 1 << 2 ) ) &&
        ( i & ( 1 << 2 ) )
       key_press |= ( 1 << 2 );

    ...

nur als wesentlich kompaktere Operation ausgeführt und für alle 8 Bits gleichzeitig. Die Kürze und Effizienz dieser paar Codezeilen ergibt sich aus dem Umstand, dass jedes Bit in den Variablen für eine Taste steht und alle 8 (maximal möglichen) Tasten gleichzeitig die Operationen durchlaufen.

Die Funktionsweisen der verschiedenen Modi anhand von Zeitstrahlen erklärt dieser Forumsbeitrag.

„Walkthrough“ der verschiedenen Zustände der einzelnen Variablen anhand eines Tastendrucks auf avrfreaks.net.

Reduziert auf lediglich 1 Taste

Diskussionen im Forum zeigen immer wieder, dass viele eine Abneigung gegen diesen Code haben, weil er ihnen sehr kompliziert vorkommt.

Der Code ist nicht leicht zu analysieren und er zieht alle Register dessen, was möglich ist, um sowohl Laufzeit als auch Speicherverbrauch einzusparen. Oft hört man auch das Argument: Ich benötige ja nur eine Entprellung für 1 Taste, gibt es da nichts Einfacheres?

Hier ist die „Langform“ des Codes, so wie man das für lediglich 1 Taste schreiben würde, wenn man exakt dasselbe Entprellverfahren einsetzen würde. Man sieht: Da ist keine Hexerei dabei: In key_state wird der letzte bekannte entprellte Zustand der Taste gehalten. Der Pin-Eingang wird mit diesem Zustand verglichen und wenn sich die beiden unterscheiden, dann wird ein Zähler heruntergezählt. Produziert dieses Herunterzählen einen Unterlauf des Zählers, dann gilt die Taste als entprellt und wenn dann auch noch die Taste gerade gedrückt ist, dann wird dieses in key_press entsprechend vermerkt.

uint8_t key_state;
uint8_t key_counter;
volatile uint8_t key_press;

ISR( ... Overflow ... )
{
  uint8_t input = KEY_PIN & ( 1 << KEY0 );

  if( input != key_state ) {
    key_counter--;
    if( key_counter == 0xFF ) {
      key_counter = 3;
      key_state = input;
      if( input )
        key_press = TRUE;
    }
  }
  else
    key_counter = 3;

}

uint8_t get_key_press()
{
  uint8_t result;

  cli();
  result = key_press;
  key_press = FALSE;
  sei();

  return result;
}

Der vollständige Entprellcode, wie weiter oben gelistet, besticht jetzt aber darin, dass er compiliert kleiner ist als diese anschaulichere Variante für lediglich 1 Taste. Und das bei gleichzeitig erhöhter Funktionalität. Denn z. B. ein Autorepeat ist in diesem Code noch gar nicht eingebaut. Und spätestens wenn man dann eine zweite Taste entprellen möchte, dann ist auch der SRAM-Speicherverbrauch dieser Langform höher als der des Originals für 8 Tasten. Daraus folgt: Selbst für lediglich 1 Taste ist die Originalroutine die bessere Wahl.

Und wegen der Komplexität mal eine Frage: Sind Sie selbst in der Lage eine entsprechend effiziente sqrt()-Funktion zu schreiben, wie die, die Sie in der Standard-C-Bibliothek vorfinden? Nein? Dann dürften Sie eigentlich Ihrer Argumentation nach die Bibliotheksfunktion sqrt() nicht verwenden, sondern müssten sich stattdessen selbst eine Wurzel-Funktion schreiben.

Selbstsättigendes Filter (nach Jürgen Schuhmacher)

Durch die Nutzung der diskreten Signalanalyse in Software kann die Funktionalität der oben beschriebenen Entprellung mit einem Widerstand, einem Kondensator und einem Schmitt-Trigger nachgebildet werden, indem ein abstraktes IIR-Filter benutzt wird, das eine Kondensatorladekurve emuliert. Mit der Vorschrift Y(t) = k Y(t−1) + Eingangswert wird ein einfaches Filter erzeugt, das dem Eingangswert träge folgt. Bei Überschreiten eines bestimmten Wertes erfolgt mit einer einfachen Abfrage das Schalten des Ausgangssignals.

Für Assembler und VHDL bei FPGAs eignet sich aufgrund der leicht zu implementierenden binären Operationen folgende Darstellung mit einer Auflösung des Filterwertspeichers von nur 8 bit: Wert_Neu = Wert_Alt − Wert_Alt/16 + 16*(Taste = True). Der Filterwert bildet dann den gedämpften Verlauf des Eingangs (flankenverschliffen) ab und kann Prellvorgänge bis nahe an den Grenzbereich zum schnellen Tasten unterdrücken. Der Ausgangswert ist dann z.B. einfach das höchstwertige Bit des Filterwertes, was aber nur bei sehr trägen Filtern zuverlässig funktioniert, weil im Übergangsbereich zwischen dem Wert, bei dem das höchste Bit auf 1 geht , wieder eine Empfindlichkeit entsteht, sollte das Prellen sich genau in diesem Bereich abspielen. Daher ist immer eine H<stereseschaltung nötig.

Entprellung mit IIR-Filter.gif

Dazu muss das Signal des Tasters idealerweise um den Faktor 10…20 schneller abgetastet werden, als die höchste gewünschte Tippgeschwindigkeit vorgibt. Noch schneller abzutasten ist möglich, führt aber zu mehr Bedarf an Bits beim Filter. Die Schmitt-Trigger-Funktion wird auch hier wieder benötigt und kann dadurch gebildet werden, dass eine 1 am Ausgang bei z. B. Überschreiten einer 60%-%-Grenze und eine 0 bei Unterschreitung der 40-%-Grenze ausgegeben wird. Im Zwischenbereich wird der alte Wert gehalten. Die realen Grenzen dieser Hysterese müssen in ähnlicher Weise an die Applikation angepasst werden, da zu enge Grenzen zu empfindlich gegenüber Störungen sind und zu weit gefasste Grenzen zu einem übermäßig trägen Verhalten führen.

Als Alternative für einen solchen Filter erster Ordnung bieten sich auch einfache FIR-Filter an, welche ein verbessertes Zeitverhalten (Ansprech- und Verzögerungszeit, bei optimierter Hysterese) ermöglichen.

Einfaches Mittelwertfilter (nach Lothar Miller)

Für digitale Schaltungen oder PLDs empfiehlt sich ein FIR-Filter mit aneinandergereihten Flipflops. Man schiebt das Eingangssignal in eine Flipflop-Kette und schaltet oberhalb der Mitte um:

Signal-Eingang → FF1 → FF2 → FF3 → FF4 → FF5 → FF6 → FF7 → FF8

Wenn alle FFs = 1 (Summe der FFs=8) dann Signal-Ausgang = 1
Wenn alle FFs = 0 (Summe der FFs=0) dann Signal-Ausgang = 0

Dieses Verfahren kann sehr einfach in Logik abgebildet werden, weil für die Berechnung des Ausgangs nur ein NOR- bzw. ein AND-Gatter nötig ist.

Das reale Signal muss dazu aber genügend langsam abgetastet werden, sodass die Filterperiode die Prelldauer übersteigt, um zu verhindern, dass nicht inmitten einer passiven Phase eines Prellvorgangs 8 mal die 1 gesehen wird. Damit wird die Interpretation vergleichsweise langsam. Ferner ist sie nicht tolerant gegenüber einmaligen Störungen im Signal. Es kann daher sinnvoll sein eine Mehrheitsentscheidung zu treffen und auf >6 und <2 zu prüfen.

Gegenüberstellung der Verfahren

Hardware

  • „entprellte Schalter“: sehr teuer, große Bauform, verschleißbelastet, geringe Haltbarkeit
  • „Umschalter“: benötigt aufwendigeren Schalter, benötigt Elektronik
  • „Umschalter ohne FF“: benötigt aufwendigeren Schalter und kleinen Kondensator
  • „Kondensatorentprellung“: benötigt etwas mehr Platz, kommt mit schlechten Schaltern zurecht

Software

  • Flankenverfahren:
  • Warteschleife: Durch die Warteschleifen entsteht eine nicht zu vernachlässigende Verzögerung im Code. Speziell wenn mehrere Tasten zu überwachen sind, nicht unproblematisch.
  • Timer: Universalfunktionalität, die durch geringen Speicherverbrauch, geringen Rechenzeitverbrauch und gute Funktion besticht. Der „Verbrauch“ eines Timers sieht auf den ersten Blick schlimmer aus, als er ist, denn in den meisten Programmen hat man sowieso einen Basistimer für die Zeitsteuerung des Programms im Einsatz, der für die Tastenentprellung mitbenutzt werden kann.
  • Filter: sehr geringer Platzbedarf in FPGAs, relativ gute Wirkung
  • Filter 2: sehr geringer Platzbedarf, gute Wirkung

Siehe auch

Weblinks