Entprellung

Wer mechanische Taster oder Schalter in seiner Schaltung einsetzten möchte, wird früher oder später auf Probleme stoßen, wenn sich nicht in geeigneter Form um die Tatsache gekümmert wird, dass Taster / Schalter prellen (englisch to bounce). Damit bezeichnet man das mechanische Verhalten der Schaltkontakte, wenn diese betätigt werden.

Beispielfoto eines Kurzhubtasters

Die nachfolgende schematische Darstellung das zeitliche Verhalten beim Betätigen eines einfachen mechanischen Tasters  verdeutlichen und somit helfen das Problem beziehungsweise das Zustandekommen des Signalverlaufes zu verstehen.



Es beginnt damit, dass der Taster in Ruhezustand geöffnet und somit nicht leitend ist. Der Signalverlauf zeigt einen Low-Pegel im Ruhezustand.  Dann beginnt die Betätigung des Tasters und der Kontakt beginnt sich zu schließen, bis er zum erstmal eine elektrische Verbindung erzeugt. Zu diesem Zeitpunkt springt der Signalpegel das erste mal von Low nach High. Jetzt beginnt das Prellen. Der mechanische Kontakt bleibt (noch) nicht in dieser Position. Zunächst springt er noch ein paar mal hin und her – er prellt. Dadurch wechselt nun der Signalpegel mehrmals hin und her, bevor er letztendlich stabil schließt und somit den Pegel auf High hält. Jetzt beginnt das “Loslassen” des Tasters. Der mechanische Kontakt verliert das erste mal die Verbindung und das Signal wechselt von High nach Low. Erneut prellt der Taster, bevor er vollends den Kontakt verliert und sich in der End- bzw. Ausgangsposition wiederfindet. Also nochmal zusammenfassend den Signalverlauf in der folgenden Grafik:



Das Problem daran wird deutlich, wenn man diesen Signalverlauf nun gedanklich auf einen Mikrocontroller überträgt. Stellt euch einfach mal vor, dass ihr mit einem einfachen Taster eine LED im Wechsel Ein- und Ausschalten möchtet. Der Taster soll also den Zustand der LED wechseln / toggeln. In der Regel werden Mikrocontroller mit einer Taktfrequenz im Megahertz (MHz) betrieben. Sprich er kann mehrere Millionen Instruktionen pro Sekunde ausführen. Also der Mikrocontroller überwacht also den Zustand an einem seiner Eingänge, an dem wir z.B. neben einem Pull-down Widerstand zusätzlich unseren Taster angeschlossen haben, der im gedrückten Zustand den Pegel am Eingang des µC auf High zieht.

Jetzt betätigen wir den Taster, da wir die LED einschalten möchten. Der Mikrocontroller sieht den Signalverlauf (siehe oben) und macht was? Genau: Er schaltet zunächst die LED ein, da er sieht, dass der Signalpegel von Low nach High wechselt. Unser Mikrocontroller ist im Vergleich zur Signalgeschwindigkeit relativ schnell, somit sieht er auch alle nachfolgenden Pegelwechsel, die durch das mechanische Prellen entstehen. Die Folge: Die LED wird mehrfach Ein- und Ausgeschaltet. Im günstigsten Fall ist der letzte Zustand der LED dann tatsächlich der, den wir mit unserem Tastendruck erreichen wollten, doch das wäre purer Zufall.

Wir haben nun also das Problem von prellenden Tastern / Schaltern erkannt und müssen uns nun überlegen, wie wir diesem Problem begegnen können, damit wir einen einfachen Tastendruck auch wirklich korrekt als lediglich einzelnen und nicht als mehrfachen Tastendruck erkennen. Für dieses Problem gibt es zwei Antworten: 1) Entprellung via Hardware (zusätzliche Beschaltung) oder 2) und das ist die zeitgemäße Lösung: Wir beseitigen das Problem durch einen geeigneten Software-Algorithmus.

Entprellung via Software

Um das Verständnis auf die eigentliche Entprellroutine zu konzentrieren, wird beim Beispielcode auf die Verwendung von Timern verzichtet. Es folgt jedoch eine Anmerkung im Anschluss zur idealen Nutzung des Algorithmus mittels Timer-Interrupt. Betrachten wir also zunächst einmal den nachfolgenden Code-Ausschnitt:

void main (void)
{
   uint8_t lastKeyDown = 0;
 
   // some necessary pic init stuff here..
 
   while(1)
   {
      __delay_ms(10);
 
      switch( keyPressed(&lastKeyDown) )
      {
         // push button pressed
         case 1:
         {
            // stuff to do after key 1 was pressed
            break;
         }
         
         default: break;
      }
   }
}

Wir sehen im Wesentlichen eine Endlosschleife, die nichts weiter tut als alle 10 Millisekunden die Funktion keyPressed() aufzurufen und ihr noch den Pointer auf eine 8-Bit Variable zu übergeben, siehe &lastKeyDown. Schauen wir uns als nächste also an, was innerhalb der Funktion keyPressed() passiert:

uint8_t keyPressed (uint8_t *pLastKeyDown)
{
   uint8_t actKeyDown;
 
   if(PUSHBUTTON)
   {
      actKeyDown = 1;
   }
   else
   {
      actKeyDown = 0;
   }
 
   if(actKeyDown != *pLastKeyDown)
   {
      *pLastKeyDown = actKeyDown;
      return actKeyDown;
   }
 
   return 0;
}

Die Funktion definiert zunächst eine lokale 8-Bit Variable ( actKeyDown ) und prüft im Anschluss den Signalpegel an dem Eingang, an dem wir unseren Taster angeschlossen haben ( hier mit einem Makro dargestellt, siehe PUSHBUTTON ). Das Makro könnte beispielsweise mit #define PUSHBUTTON PORTBbits.RB0 definiert worden sein. An dieser Stelle (Zeile 5) ist darauf zu achten, dass auf ein High abgefragt wird, da unser Taster im gedrückten Zustand den Pegel am Eingang auf High zieht.  Wenn ihr Schalter/Taster habt, die stattdessen gegen Low schalten, sprich Low-Aktiv sind, dann würde die Zeile 5 entsprechend auf if(!PUSHBUTTON) geändert werden.

Wenn der angeschlossene Taster nun also gedrückt wurde, würde in unserem Beispiel die Bedingung der if-Abfrage aus Zeile 5 wahr sein und wir würden der Variable actKeyDown den Wert 1 zuweisen. Hierbei ist die 1 der Code für diese gedrückte Taste. Mit weiteren Codes können weitere Tasten verwaltet werden, wobei je eine Taste ein eigenes Bit innerhalb der actKeyDown Variable benötigt aber dazu später mehr. Kurzum: Die Funktion hat erkannt, dass die Taste gedrückt wurde und hat dies mit dem Setzen des dieser Taste zugeordneten Bits in actKeyDown gespeichert. Nun kommen wir zu Zeile 14 und vergleichen den Wert von actKeyDown mit dem von pLastKeyDown. Und nur dann, wenn sich diese unterscheiden, wird die Taste als gedrückt identifiziert: Als erstes nehmen wir den Bitcode der erkannten Taste und speichern ihn als neuen letzten Wert in pLastKeyDown. Danach nutzen wir den Bitcode als Rückgabewert für die aufrufende Funktion.


Die Funktion merkt sich also jeweils die zuletzt gedrückte Taste (auch “keine Taste” wird als Taste betrachtet) und vergleicht diesen Wert mit der aktuell gedrückten Taste. Wenn sich die Werte unterscheiden wurde eine Taste gedrückt. Spielen wir es doch einmal durch (PB = PUSHBUTTON):

Zeit [ms] |  Pegel (PB) |  (1) actKeyDown  | (2) *pLastKeyDown  | (3) return |  Bemerkung
----------+-------------+------------------+--------------------+------------+------------------------
       0  |  low        |               0  |            0       |          0 | Taste nicht gedrückt
      10  |  low        |               0  |            0       |          0 | ..
      20  |  high       |          0 -> 1  |            0 -> 1  |          1 | Taste wurde gedrückt
      30  |  high       |               1  |            1       |          0 | Taste wird gehalten
      40  |  high       |               1  |            1       |          0 | ..
      50  |  low        |          1 -> 0  |            1 -> 0  |          0 | Taste wurde losgelassen

In dem gezeigten Ablaufbeispiel sieht man zu Beginn, dass die Taste nicht gedrückt wird und der logische Zustand des Eingangs ist somit low ist. Die Arbeitsvariablen der Entprellfunktion sind beide “0”. Auch nach 10 ms ist nichts passiert, der Taster wurde nicht angerührt. Zum Zeitpunkt 20 ms liegt ein high Pegel am Eingang an, der Taster wurde nun gedrückt. Die Funktion erkennt dies und setzt die lokale Variable actKeyDown auf “1”. Hinweis: Die “1” bedeutet hier nicht etwa high, sondern dient als Codierung für den gedrückten Taster PUSHBUTTON. An dieser Stelle kann die Funktion beliebig erweitert werden um weitere Taster zu entprellen, die dann zum Beispiel die Werte 2, 4 .. bekommen würden, sofern sie gedrückt werden (Hinweis: Je Taster ein Bit verwenden). Aber zurück zum Beispiel: Die Funktion hat den high Pegel über die if-Abfrage am Eingang erkannt und die lokale Variable actKeyDown mit dem Tastencode 1 belegt.

Die zweite if-Abfrage prüft nun, ob sich die bei diesem Durchgang erkannte Taste von der zuletzt erkannten (mit Hilfe des Wertes auf den der Pointer pLastKeyDown zeigt) unterscheidet. Und genau das ist nun der Fall: Zuletzt hat die Funktion keinen Tastendruck erkannt  und die Variable auf die der Pointer zeigt hat somit den Wert Tastencode 0. Die aktuell erkannte Taste hat den Tastencode 1, sprich die Werte sind unterschiedlich. Die if Anweisung wird also aktiv. Innerhalb der if Anweisung speichern wir nun die aktuell erkannte Taste (Tastencode 1) als zuletzt erkannte Taste, damit wir diese bei späteren Funktionsaufrufen nicht wieder als neuen Tastendruck identifizieren und gibt zusätzlich den Tastencode der erkannten Taste zurück (siehe rote 1 im Beispiel oben).

Bei 30 ms wird die Funktion zum Tastenentprellen nun wieder aufgerufen. Die Taste wird nun in der aktiven Position gehalten, so dass der Pegel am Eingang immer noch high ist. Da sich die Funktion jedoch diese Taste bereits beim letzten Mal als zuletzt gedrückte Taste gemerkt hat (Pointer  pLastKeyDown), wird diese Taste nicht erneut als Event erkannt. Die Funktion gibt den Code 0 zurück (kein neues Tastenevent). Das selbe passiert beim Aufruf der Funktion bei 40 ms. Es gibt keine Änderung zum vorherigen Status und somit erneut kein neues Event als Rückgabewert.

Zum Zeitpunkt 50 ms gibt es nun noch einmal eine Änderung, denn der Anwender hat die Taste nun wieder losgelassen, so dass der Pegel am Eingang nun nicht mehr high (wie zuvor), sondern wieder low (Ruhezustand) ist. Beim Aufruf der Funktion bei 50 ms ist der zuletzt gemerkte Tastencode noch die 1 und wir detektieren nun keine aktive Taste mehr, so dass actKeyDown den Wert 0 bekommt. Jetzt unterscheiden sich diese beiden Werte wieder, was erneut die zweite if-Abfrage aktiviert. Diese übernimmt nun die 0 als zuletzt detektierten Tastencode auf und gibt diesen auch zurück. Aufgrund der zeitlichen Begrenzung des Prellens von Tastern (siehe Zeit TB in der zweiten grafischen Darstellung oben), kann mit dieser Methode das Problem beseitigt werden und es können auf elegante Art und Weise beliebig viele (nahezu beliebig viele) Taster in einer Funktion verwaltet werden.

Abschließender Hinweis: Zur Vereinfachung wurde in diesem Beispiel eine Endlosschleife mit einer 10 ms Warteschleife verwendet um die Entprell-Routine aufzurufen. Für dieses einfache Beispiel ist das so auch okay. Wenn man jedoch ein richtiges Projekt entwickelt wird der Mikrocontroller in aller Regel mehr tun als nur eine Taste zu entprellen. Aufgrund der 10 ms Warteschleife würde der Controller ständig blockiert werden und könnte keine weiteren Aufgaben erledigen. Wesentlich effizienter und auch eleganter ist es stattdessen einen Timer zur Hilfe zu nehmen, der bspw. alle 10 ms einen Interrupt auslöst. Innerhalb des Interrupts könnte ein Flag gesetzt werden, was in der Main-Loop wiederum dazu führt, dass die Entprell-Routine ausgeführt wird. So ist der Controller in der Zwischenzeit frei um anderweitige Aufgaben zu erledigen 🙂

Leave a Comment