Entprellen von Tasten mit dem Candidate-Pattern
-----------------------------------------------
Ein einfacher und robuster Stabilitätsfilter für
Mikrocontroller-Eingaben.
Die folgende Idee habe ich für ein ATMega-Projekt (DIY Stream-Deck)
entwickelt. Ich möchte die Idee mit diesem Artikel teilen.
Es geht hier mehr um eine Idee als um Code für eine spezielle Platform.
Einleitung
----------
Um mechanische Tasten und Schalter software-seitig zu entprellen,
habe ich folgende Idee entwickelt und nenne sie Candidate-Pattern.
Das Candidate-Pattern ist ein Stabilitätsfilter: Ein neu beobachteter
Zustand wird zunächst als möglicher Kandidat betrachtet und erst dann
übernommen, wenn er über mehrere Messzyklen hinweg unverändert bleibt.
Der Algorithmus ist bewusst plattformunabhängig:
- funktioniert auf praktisch jedem Mikrocontroller oder Mikroprozessor
- ist unabhängig davon, wie die Tasten eingelesen werden
- funktioniert mit GPIO, Tastaturmatrix, IO-Expander oder anderen
Eingabemechanismen
Der Debounce-Algorithmus arbeitet ausschließlich auf dem bereits
gelesenen
Rohzustand der Tasten.
Warum Taster entprellt werden müssen
------------------------------------
Beispiel eines prellenden Signals:
1 | Rohsignal (Bounce)
|
2 |
|
3 | HIGH ────────┐ ┌─┐ ┌──┐ ┌─┐
|
4 | │ │ │ │ │ │ │
|
5 | LOW ─────────┴───┘ └─┘ └─┘ └────────
|
6 |
|
7 | Zeit →
|
Der Mikrocontroller würde mehrere Flanken erkennen,
obwohl der Benutzer nur einmal gedrückt hat.
Nach der Entprellung entsteht ein stabiler Zustand:
1 | Debounced Signal
|
2 |
|
3 | HIGH ────────────────┐
|
4 | │
|
5 | LOW ────────────────┴──────────────
|
6 |
|
7 | Zeit →
|
Grundidee des Candidate-Patterns
--------------------------------
Der Algorithmus arbeitet mit drei Zuständen:
1 | Variable Bedeutung
|
2 | --------------------------------------------
|
3 | rawMask aktueller Rohzustand der Tasten
|
4 | candidateMask möglicher neuer Zustand
|
5 | stableMask zuletzt bestätigter stabiler Zustand
|
Zusätzlich existiert ein Zähler:
Ein neuer Zustand wird nicht sofort akzeptiert.
Er wird zunächst als Kandidat gespeichert.
Die gemessenen Eingangswerte (z.b. Tastenmatrix-Scan) sind der
Candidate
Erst wenn dieser Kandidat lange genug unverändert bleibt, wird er zum
neuen stabilen Zustand.
Ändert sich der gemessene Zustand, gilt dieser als neuer Kandidat und
der Zähler wird zurückgesetzt (reset).
Periodischer Aufruf des Algorithmus
-----------------------------------
Der Algorithmus kann einen Timer verwenden, muss aber nicht.
Er kann beispielsweise durch einen Hardware-Timer regelmäßig aufgerufen
werden, funktioniert aber ebenso gut innerhalb einer zyklischen
Hauptschleife.
Der interne candidateTimer zählt lediglich, wie viele Update-Zyklen
der Kandidat unverändert geblieben ist.
Entscheidend ist nicht der Timer selbst, sondern ein *regelmäßiger
Aufruf des Debounce-Algorithmus*.
Variante 1 – Timer-Interrupt
1 | Timer Interrupt (1 ms)
|
2 | -> scanKeys()
|
3 | -> debounceUpdate()
|
Variante 2 – Hauptschleife
1 | while (1)
|
2 | {
|
3 | scanKeys();
|
4 | debounceUpdate();
|
5 | }
|
Solange der Algorithmus in einem annähernd konstanten
zeitlichen Raster ausgeführt wird, arbeitet er zuverlässig.
Ablauf des Algorithmus
----------------------
Bei jedem Update passiert Folgendes:
1. Rohzustand der Tasten lesen (rawMask)
2. Prüfen, ob sich der Kandidat geändert hat
3. Wenn ja → neuen Kandidaten setzen und Zähler zurücksetzen
4. Wenn nein → Zähler erhöhen
5. Wenn stabil → neuen stabilen Zustand übernehmen und Event setzen
Mehrere Tasten mit Bitmasken
----------------------------
1 | Bit 0 -> Taste 0
|
2 | Bit 1 -> Taste 1
|
3 | Bit 2 -> Taste 2
|
Beispiele (gemessener raw/candidate wert):
1 | 00000000 keine Taste gedrückt
|
2 | 00000001 Taste 0 gedrückt
|
3 | 00000101 Taste 0 und Taste 2 gedrückt
|
Press- und Release-Events
-------------------------
1 | pressedMask = newState & ~oldState
|
2 | releasedMask = oldState & ~newState
|
Damit erhält man
- neu gedrückte Tasten
- losgelassene Tasten
Event-Semantik
--------------
Ein Event ist ein einmaliger Impuls.
Typische Events:
- PRESSED
- RELEASED
Zusatzinformation im Event
--------------------------
1 | typedef struct
|
2 | {
|
3 | uint32_t pressedMask;
|
4 | uint32_t releasedMask;
|
5 | uint16_t stabilityCount;
|
6 | } ButtonEvent;
|
Beispielimplementierung (Arduino)
---------------------------------
[cpp]
void MatrixKeys::Loop()
{
uint8_t scannedIndex = ScanRow(0);
if (!scannedIndex) scannedIndex = ScanRow(1);
if (!scannedIndex) scannedIndex = ScanRow(2);
if (scannedIndex == m_candidateIndex)
{
if (millis() - m_startMillis > BUTTON_DEBOUNCE_TIME)
{
if (m_currentKeyIndex != m_candidateIndex)
{
m_currentKeyIndex = m_candidateIndex;
m_eventFlag = 1;
}
}
}
else
{
m_candidateIndex = scannedIndex;
m_startMillis = millis();
if (m_candidateIndex == 0 && m_currentKeyIndex != 0)
{
m_currentKeyIndex = 0;
m_eventFlag = 1;
}
}
}
[/cpp]
Beispiel-Pseudocode
-------------------
1 | void debounceUpdate(uint32_t rawMask)
|
2 | {
|
3 | if (rawMask != candidateMask)
|
4 | {
|
5 | candidateMask = rawMask;
|
6 | candidateTimer = 0;
|
7 | }
|
8 | else
|
9 | {
|
10 | candidateTimer++;
|
11 |
|
12 | if (candidateTimer >= DEBOUNCE_COUNT)
|
13 | {
|
14 | if (candidateMask != stableMask)
|
15 | {
|
16 | pressedMask = candidateMask & ~stableMask;
|
17 | releasedMask = stableMask & ~candidateMask;
|
18 |
|
19 | stableMask = candidateMask;
|
20 | }
|
21 | }
|
22 | }
|
23 | }
|
Typische Parameter
------------------
1 | Aufrufintervall: 1–5 ms
|
2 | DEBOUNCE_COUNT: 5–20
|
Zusammenfassung
---------------
Das Candidate-Pattern ist eine einfache und robuste Methode zur
Entprellung mechanischer Taster.
Voraussetzung ist, dass das Eingangssignal zu einem stabilen Zustand
gelangen kann. Bei mechanischen Tastern und Schaltern ist dies in der
Regel der Fall, wenn sie einmal die entsprechende Endlage erreicht
haben, während sie beim herunterdrücken/loslassen oft den "Prell-Effekt"
haben. Eine fein-Justierung und Glättung kann durch Aufrufhäufigkeit und
der minimal erforderlichen Stabilitäts-Zeit eingestellt werden.
**Vergleich mit anderen Debounce-Algorithmen**
Es existieren verschiedene Methoden, um das Prellen mechanischer
Kontakte zu
unterdrücken. Die folgenden Ansätze sind in Mikrocontroller-Projekten
besonders
verbreitet.
RC-Filter (Hardware)
- Prinzip: Analogfilter glättet das Signal
- Vorteile: sehr einfach, keine CPU-Last
- Nachteile: zusätzliche Bauteile, feste Zeitkonstante
Timer-Debounce
- Prinzip: nach einer Flanke wird eine feste Sperrzeit gestartet
- Vorteile: leicht zu implementieren
- Nachteile: reagiert träge
Integrator / Counter
- Prinzip: Zähler integriert den Eingang über mehrere Zyklen
- Vorteile: robust gegen Störungen
- Nachteile: etwas komplexer
*(Neu)*: Candidate-Pattern
- Prinzip: neuer Zustand wird erst nach stabiler Dauer akzeptiert
- Vorteile: flexibel, effizient
- Nachteile: benötigt zyklischen Aufruf
**Einordnung des Candidate-Patterns**
Das Candidate-Pattern lässt sich als Stabilitätsprüfung eines
beobachteten
Zustands verstehen.
Im Gegensatz zu einem klassischen Timer-Debounce wird keine feste
Sperrzeit
gestartet. Stattdessen wird kontinuierlich geprüft, ob ein neu
beobachteter
Zustand lange genug stabil bleibt.
Daraus ergeben sich einige praktische Vorteile:
- keine individuellen Timer pro Taste notwendig
- funktioniert mit vielen Eingängen gleichzeitig
- unterstützt mehrere gleichzeitig gedrückte Tasten
- gut geeignet für zyklische Scan-Systeme (z.B. Tastaturmatrizen)
Der Algorithmus eignet sich besonders für Systeme, in denen Eingänge
ohnehin
regelmäßig gescannt werden, beispielsweise:
- Tastaturmatrizen
- GPIO-Scanning
- Polling-basierte Eingabeverarbeitung
**Wann andere Methoden sinnvoll sein können**
Andere Debounce-Methoden können in bestimmten Situationen besser
geeignet sein:
- RC-Filter bei sehr einfachen Schaltungen ohne Softwarelogik
- Timer-Debounce bei einzelnen Tastern mit Interrupts
- Integrator-Methoden bei stark verrauschten Signalen
In vielen Mikrocontroller-Anwendungen mit zyklischer Eingabeerfassung
stellt
das Candidate-Pattern jedoch eine sehr einfache und flexible Lösung dar.
Vorteile:
- plattformunabhängig
- geringer Rechenaufwand
- funktioniert mit beliebig vielen Tasten
- keine Timer pro Taste notwendig
- saubere Event-Erzeugung