Taster entprellen (debounce) mit dem ESP32 und Micropython

Taster entprellen klingt ja eigentlich ziemlich trivial, ist aber unter realen Bedingungen deutlich vertrackter als man denkt und hat mich etliche Stunden gekostet.

Was das Prellen von Tastern und Schaltern ist und wie es dazu kommt erklärt die Wikipedia besser als ich, als da kurz nachlesen.

Entprellen mit Interrupt und Callbacks – toll unter Laborbedingungen

Im Internet findet man sehr schnell “Referenzlösungen” wie zum Beispiel https://gist.github.com/jedie/8564e62b0b8349ff9051d7c5a1312ed7 , https://kaspars.net/blog/micropython-button-debounce oder die anderen TopTen Treffer bei Google, wenn man nach “micropython esp32 button debounce” oder “micropython esp32 taster entprellen” sucht. Alle basieren auf der Idee einen Interrupt zu verwenden, um herauszufinden wann der Taster gedrückt wurde und rufen dann eine Callback Funktion auf. In etwa so:

from machine import Pin, Timer

def on_pressed(timer):
    print('pressed')

def debounce(pin):
    # Start or replace a timer for 200ms, and trigger on_pressed.
    timer.init(mode=Timer.ONE_SHOT, period=200, callback=on_pressed)

# Register a new hardware timer.
timer = Timer(0)

# Setup the button input pin with a pull-up resistor.
button = Pin(0, Pin.IN, Pin.PULL_UP)

# Register an interrupt on rising button input.
button.irq(debounce, Pin.IRQ_RISING)

Diese Lösungen funktionieren auch in einem normalen Büro Umfeld. Bei mir haben die Buttons aber verrückt gespielt als ich die Schaltung in der Hütte installiert hatte in der ich das Licht mittels LED Streifen schalten wollte und ein Elektromotor losgelaufen ist. Im Büro hatte ich ein paar mal den gleichen Effekt als ich mit dem Bürostuhl auf dem Büro-Teppich herumgerollt bin. Nachvollziehbar wurde es als ich ein Piezo Feuerzeug in 30cm Entfernung zur Schaltung gezündet habe. Der durch den Zünder des Piezo Feuerzeugs ausgelöste elektromagnetische Impuls (EMP) und die als Antennen fungierenden Leitungen lösen den Interrupt aus.
Natürlich kann man die Schaltung vor einem EMP durch Einkapselung der Aktoren in einen Faradayschen Käfig und entsprechende Schutzschaltungen (Galvanische Trennung, Überspannungsableiter) auf allen elektrischen Zuleitungen schützen. Jede dieser Lösungen wäre aber mit viel Aufwand verbunden. Also muss eine andere und simple Lösung her.

Was haben all diese Störungen auf der Leitung gemeinsam?

Sie sind so heftig, dass sie den Interrupt auslösen, aber sehr kurz (im Mikrosekunden (µs) Bereich).
Also wie umgehen wir diese Störungen?
Als erstes lassen wir die Lösung mit dem Interrupt fallen. Egal ob wir machine.Pin.IRQ_RISING oder machine.Pin.IRQ_FALLING verwenden. Der IRQ wird durch den EMP ausgelöst. Der IRQ taugt also nicht zur verlässlichen Erkennung.

Lösung: Wir fragen den Taster mit einem Hardware Timer ab.

Moment, wenn wir einen Timer mit 10ms laufen lassen, kostet uns das nicht jede Menge Rechenleistung?
Schließlich fragen wir den Timer in 99,x% der Fälle umsonst ab.
Kurzer Check: Das Entwicklungsboard das ich verwende ist mit dem ESP-WROOM-32 Modul ausgestattet, das wiederum den Tensilica Xtensa® Dual-Core 32-bit LX6 Mikroprozessor enthält. Dieser Prozessor ähnelt dem ESP8266, hat jedoch zwei CPU-Kerne (die einzeln gesteuert werden können), arbeitet mit einer einstellbaren Taktfrequenz von 80 bis 240 MHz und erreicht eine Leistung von bis zu 600 DMIPS (Dhrystone Million Instructions Per Second) laut Datenblatt. Also sind die paar Befehle im Timer, auch wenn Micropython kein Meilenstein an Geschwindigkeit ist, herzlich egal.

Meine Lösung als eigenständige funktionierende Klasse

Getestet im realen “Außeneinsatz” neben einem Elektromotor, der jede Menge Störungen verursacht.
Die Klasse kann eigenständig aufgerufen werden und kann auch auf GitHub heruntergeladen werden. https://github.com/hagen-gloetter/LED-Strips-ESP32 : bzw. https://github.com/hagen-gloetter/LED-Strips-ESP32/blob/main/code/class_debounce.py

import time
from machine import Pin
from machine import Timer
import sys
import machine
led = machine.Pin(2, machine.Pin.OUT)

print('Debounce Class loaded')


class debounced_Button():
    """ Class to debounce a hardware button
    """

    def __init__(self, pin=10):
        self.pin = Pin(pin, mode=Pin.IN, pull=Pin.PULL_UP)
        self.status = "OFF"
        self.cnt = 0
        self.debounceTime = 5  # x cycles have to be done
        self.oldstatus = self.status

    def get_status(self):
        # Button has to be pressed 5 cycles
        if self.pin.value() == 0:  # PULL_UP -> 0 = pressed
            self.cnt += 1
        if self.pin.value() == 1:  # PULL_UP -> 1 = not pressed
            self.cnt = 0
        if self.cnt == self.debounceTime:  # button toggle
            self.status = self.togglebutton()
            self.oldstatus = self.status
        if self.cnt >= self.debounceTime*100:
            print("button longpress")
            self.cnt = self.debounceTime*2  # prevent buffer overflow
        return self.status

    def get_oldstatus(self):
        return self.oldstatus

    def togglebutton(self):
        if self.status == "OFF":
            self.status = "ON"
        else:
            self.status = "OFF"
        return self.status


global Button1
global Button2


def ButtonDebounceTimer(timer0):
    global Button1
    global Button2
    b3 = Button1.get_oldstatus()
    b4 = Button2.get_oldstatus()
    b1 = Button1.get_status()
    b2 = Button2.get_status()
    if b3 != b1 or b4 != b2:
        print(f"b1={b1}, b2={b2}")


def main():
    global Button1
    global Button2
    Button1 = debounced_Button(13)
    Button2 = debounced_Button(10)
    viaTimer = False
    if viaTimer == True:
        print("Start Timer")
        timer2 = Timer(2)
        timer2.init(period=10, mode=Timer.PERIODIC,
                    callback=ButtonDebounceTimer)
    if viaTimer == False:
        i = 0
        while i == 0:
            b3 = Button1.get_oldstatus()
            b4 = Button2.get_oldstatus()
            b1 = Button1.get_status()
            b2 = Button2.get_status()
            time.sleep_ms(10)
            if b3 != b1 or b4 != b2:
                print(f"b1={b1}, b2={b2}")


if __name__ == '__main__':
    sys.exit(main())

print('Button init done')

0 Kommentare

Hinterlasse einen Kommentar

An der Diskussion beteiligen?
Hinterlasse uns deinen Kommentar!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert