Taster entprellen mit MicroPython und Rasperry Pi Pico

Python auf Einplatinencomputer wie Raspberry Pi, Banana Pi / Python für Micro-Controller
JanUndPython
User
Beiträge: 6
Registriert: Donnerstag 4. August 2022, 09:44

Liebe Mitglieder des Python Forums.
Ich mache derzeit meine ersten Erfahrungen mit Micropython auf einem Rasperry Pi Pico. Leider habe ich bisher keine Python Kenntnisse.
Ich möchte gerne einen Taster auf Pin 5 entprellt abfragen.
Ohne Entprellung habe ich es bereits hinbekommen. Leider ist da Ergebnis nicht so doll.
Wie ist der einfachste Weg ein Zeitverzögerung bei der Pinabfrage hinzubekommen? Also der Taster soll mindesten 20ms gedrückt sein bis ein Rückgabewert im Programm gemeldet wird.

Hier meine „nicht Entprellter“ Pinabfrage:

from machine import Pin
taste = Pin(5,Pin.IN,Pin.PULL_UP)
while True:
if taste.value() == 0:
print("button is pressed")
__deets__
User
Beiträge: 12288
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das so zu machen wie du es vorschlaegst ist schwieriger als notwendig. Denn dann musst du einen Timer starten, und stoppen, wenn der Pin den Pegel wechselt. Es ist viel einfacher, auf IRQs fuer einen Zeitraum *nicht* zu reagieren. Quasi so (pseudo-code):

Code: Alles auswählen

class DebouncedPin:
     def __init__(pin, callback, timeout=20):
        self._pin = machine.Pin(pin, ...) # setup
        self._pin.irq(self._irq)
        self._callback = callback
        self._timeout = timeout
        self._timestamp = time_ms()
    def _irq(self):
         now = time_ms()
         if self._timestamp < now:
              self._timestamp = now + self._timeout
              self._callback() 
Benutzeravatar
DeaD_EyE
User
Beiträge: 809
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Das Problem ist, dass der irq-callback so schnell wie möglich fertig sein muss, da ansonsten die anderen Callbacks einfach nicht ausgeführt werden. Es ist immer besser, mit einem RC-Glied Hardwaremäßig zu entprellen.

Hier ist ein Online-Recher: https://protological.com/debounce-calaculator/

High logic level: 1.8 V
Final voltage: 3.3 V
Bounce time: ? ms

High logic level: 1000 # 1KOhm

Dann einfach die Zeit zum entprellen festlegen und dann den Kondensator berechnen.
Damit sparst du dir ein bisschen Code.
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
__deets__
User
Beiträge: 12288
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das ist zwar prinzipiell richtig, aber im Kontext insbesonderen von micropython nicht wahr. IRQs werden dort nie direkt abgearbeitet, das liesse der Python Interpreter nicht zu - denn dessen interne Strukturen sind nicht in der Lage, zu jedwedem Zeitpunkt unterbrochen zu werden, und anderen Python code laufen zu lassen. Darum werden IRQs einfach nur gescheduled, sprich: der Byte-Code Interpreter nimmt bei naechstbester Gelegenheit den IRQ handler dran. Der eigentliche IRQ ist ein Stueck C/ASM code. Darum ist das unkritisch(er), man sollte natuerlich trotzdem nicht viel im handler machen. Ein Vergleich und ein Zeitstempel sind aber absolut ok.

Das hardware-entprellen immer besser ist, wuerde ich so auch nicht sagen. Man erkauft sich damit verzoegerte Ansprechzeit. Das muss aus den Anforderungen auch moeglich sein. Es ist einer der grossen Vorteile von MCU-basierten Systemen, dass man auf sowas mit Software nach Bedarf reagieren kann. Statt das im Hardware-Design fest zu verbacken. Nicht umsonst gibt es SDR und DSP Programmierung als wichtige Felder.
JanUndPython
User
Beiträge: 6
Registriert: Donnerstag 4. August 2022, 09:44

Hallo zusammen. Danke für eure professionellen Antworten.

Ob nun Hardware oder Softwareentprellung besser ist, kann ich nicht bewerten.
Ich habe versucht deets Vorschlag umzusetzen. Scheitere aber an dem noch nicht vorhanden Wissen über Klassen. Mache mich gerade dazu schlau.
Nach meinem Verständnis speichere ich den Code als "DebouncedPin.py" ab und importiere diese in der "main.py" mit "from testClass import DebouncedPin".
Ist das richtig? Wie kann ich dann in der main.py dann den Taster abfragen?
Sorry, ich bin wirklich blutiger Anfänger.
__deets__
User
Beiträge: 12288
Registriert: Mittwoch 14. Oktober 2015, 14:29

Nein, das ist nicht richtig. Wenn du eine Datei DeboundecPin.py nennst, kannst du nicht "from testClass ..." zum importieren benutzen. Das muss dann auch DeboundecdPin heissen. Es gibt aber auch erstmal gar keinen Grund, das in eine extra Datei zu schreiben. Die paar Zeilen koennen auch erstmal in deinem main.py stehen.

Und fuer deinen augenblicklichen Code reicht es, als Callback dann eben eine Funktion zu uebergeben, die deine print-Ausgabe macht. Danach bleibt in einer Endlos-Schleife, der IRQ sorgt dann schon fuer Verabeitung. Alternativ kann der Callback zB auch eine Variable beeinflussen, auf die du in deiner Hauptschleife reagierst. Was da das beste ist, haengt vom Anwendungszweck ab.
Benutzeravatar
DeaD_EyE
User
Beiträge: 809
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

__deets__ hat geschrieben: Donnerstag 4. August 2022, 11:25 Das ist zwar prinzipiell richtig, aber im Kontext insbesonderen von micropython nicht wahr. IRQs werden dort nie direkt abgearbeitet, das liesse der Python Interpreter nicht zu - denn dessen interne Strukturen sind nicht in der Lage, zu jedwedem Zeitpunkt unterbrochen zu werden, und anderen Python code laufen zu lassen. Darum werden IRQs einfach nur gescheduled, sprich: der Byte-Code Interpreter nimmt bei naechstbester Gelegenheit den IRQ handler dran. Der eigentliche IRQ ist ein Stueck C/ASM code. Darum ist das unkritisch(er), man sollte natuerlich trotzdem nicht viel im handler machen. Ein Vergleich und ein Zeitstempel sind aber absolut ok.
Der C Code ruft den Callback auf und genau das ist auch der Grund, wieso man in der ISR nichts machen kann/darf, was zu lange dauert oder Speicher allokiert. Deswegen soll man micropython.schdule verwenden, damit es nicht mehr im Kontext des ISR ausgeführt wird.

Hier noch mal zum Nachlesen: https://docs.micropython.org/en/latest/ ... #isr-rules
__deets__ hat geschrieben: Donnerstag 4. August 2022, 11:25 Das hardware-entprellen immer besser ist, wuerde ich so auch nicht sagen. Man erkauft sich damit verzoegerte Ansprechzeit. Das muss aus den Anforderungen auch moeglich sein. Es ist einer der grossen Vorteile von MCU-basierten Systemen, dass man auf sowas mit Software nach Bedarf reagieren kann. Statt das im Hardware-Design fest zu verbacken. Nicht umsonst gibt es SDR und DSP Programmierung als wichtige Felder.

Weil das so super funktioniert, fragt jeder 2. nach dem Code und die Klasse, die du gepostet hast, kann gar nicht funktionieren und ignoriert noch das obere (ISR).

Hier mal die korrigierte Fassung, die trotzdem Mist ist:

Code: Alles auswählen

from machine import Pin
from micropython import schedule
from time import ticks_ms


class DebouncedPin:
    """
    pin := Pin nummer
    callback := Aufzurufende Funktion mit einem Argument (Pin instanz)
    timeout_ms := timeout in milisekunden
    """
    def __init__(self, pin, callback, timeout_ms=20):
        self._pin = Pin(pin, mode=Pin.IN, pull=Pin.PULL_DOWN)
        self._pin.irq(handler=self._irq, trigger=Pin.IRQ_RISING)
        self._callback = callback
        self._timeout_ms = timeout_ms
        self._timestamp = ticks_ms()
    def _irq(self, pin):
         now = ticks_ms()
         if self._timestamp < now:
              self._timestamp = now + self._timeout_ms
              # Callback wird außerhalb des Kontextes des ISR aufgerufen
              # und wenn der callback zu oft kommt:
              # RuntimeError: schedule queue full
              try:
                  schedule(self._callback, pin) # <- hier wird die callback-funktion später durch micropython.schedule aufgerufen
              except RuntimeError: # <- Der Fehler kommt, wenn die Queue voll ist
                  # Fehler wird hier einfach ignoriert
                  # und der Callback wird nicht aufgerufen
                  pass


P17 = DebouncedPin(17, print, timeout_ms=1000)
Getestet auf ESP32 ohne SPIRAM.

Der RuntimeError kommt regelmäßig, wenn nicht Hardwaremäßig entprellt wird. Mit einem RP2040 hat man weniger Probleme, da der etwas langsamer als der ESP32 läuft.
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
__deets__
User
Beiträge: 12288
Registriert: Mittwoch 14. Oktober 2015, 14:29

DeaD_EyE hat geschrieben: Donnerstag 4. August 2022, 14:43 Der C Code ruft den Callback auf und genau das ist auch der Grund, wieso man in der ISR nichts machen kann/darf, was zu lange dauert oder Speicher allokiert. Deswegen soll man micropython.schdule verwenden, damit es nicht mehr im Kontext des ISR ausgeführt wird.

Hier noch mal zum Nachlesen: https://docs.micropython.org/en/latest/ ... #isr-rules
Hier nochmal zum nachlesen, was *wirklich* passiert bei deinem ESP32, https://github.com/micropython/micropyt ... pin.c#L250

Genauso wie beim PICO: https://github.com/micropython/micropyt ... pirq.c#L91

Also *immer* gescheduled. Use the source, Luke. Frueher musste man da noch aufpassen, und das irgendwie selbst machen. Ist halt nicht mehr so. Und darum ist es vollkommen legitim, solche zwei-drei Zeilen laufen zu lassen. Das ist kein Freibrief fuer beliebige Mengen an Applikationslogik, aber das habe ich ja auch nirgends behauptet, oder?
DeaD_EyE hat geschrieben: Donnerstag 4. August 2022, 14:43
__deets__ hat geschrieben: Donnerstag 4. August 2022, 11:25 Das hardware-entprellen immer besser ist, wuerde ich so auch nicht sagen. Man erkauft sich damit verzoegerte Ansprechzeit. Das muss aus den Anforderungen auch moeglich sein. Es ist einer der grossen Vorteile von MCU-basierten Systemen, dass man auf sowas mit Software nach Bedarf reagieren kann. Statt das im Hardware-Design fest zu verbacken. Nicht umsonst gibt es SDR und DSP Programmierung als wichtige Felder.
Weil das so super funktioniert, fragt jeder 2. nach dem Code und die Klasse, die du gepostet hast, kann gar nicht funktionieren und ignoriert noch das obere (ISR).
Die gepostete Klasse war klar als pseudo-Code markiert. Und deine Klasse baut jetzt einfach ein zweites schedule ein, verschlimmert das Problem also einfach.

Das man sich wuenschen wuerde, sowas gleich im IRQ hander in C noch *vor* einem eventuellen Aufruf passiert, ist davon unbenommen. Aber wenn eine Software-Implementation im konkreten Fall das gewuenschte Ergebnis erziehlt, dann gibt es da keinen Grund, das *nicht* so zu machen. Aussagen wie "immer" und "nie" sind halt nie richtig, sondern immer ueberzogen....
__deets__
User
Beiträge: 12288
Registriert: Mittwoch 14. Oktober 2015, 14:29

Nachtrag: augenscheinlich kann man beim PICO das "alte" Verhalten eines "harten" IRQ explizit einschalten: https://github.com/micropython/micropyt ... pin.c#L350 - dann gelten die Einschraenkungen. Das der PICO das also immer so machen wuerde ist von mir falsch dargestellt. Man soll halt nie immer sagen... :?

Aber man muss das ja nicht. Wenn das Ergebnis dann eh wieder in ein schedule muendet. Aendert also das Argument nicht, aber ist wahrscheinlich gedacht fuer zeitkritischere Faelle, bei denen nur gezaehlt werden soll.
JanUndPython
User
Beiträge: 6
Registriert: Donnerstag 4. August 2022, 09:44

Hallo zusammen,
da habe ich aber eine heiße Diskussion eröffnet. Leider verstehe ich als Anfänger nur wenig.

Ich habe mit euren Beispielen nun auf dem RaspberryPi Pico den entprellten Taster hinbekommen.
Ich habe den PIN auf 5 geändert und pull=Pin.PULL_UP geändert. Nun reagiert das Ding auf abfallenden Tastendruck wenn ich den Kontakt zwischen GND und PIN 5 über den Taster schließe. Ich bin Happy. Danke für eure Hilfe.
Hier der Code wie er bei mir funktioniert:

Code: Alles auswählen

from machine import Pin
from micropython import schedule
from time import ticks_ms


class DebouncedPin:
    def __init__(self, pin, callback, timeout_ms=20):
        self._pin = Pin(pin,Pin.IN,Pin.PULL_UP)
        self._pin.irq(handler=self._irq, trigger=Pin.IRQ_RISING)
        self._callback = callback
        self._timeout_ms = timeout_ms
        self._timestamp = ticks_ms()
    def _irq(self, pin):
         now = ticks_ms()
         if self._timestamp < now:
              self._timestamp = now + self._timeout_ms
              try:
                  schedule(self._callback, pin) 
              except RuntimeError: 
                  pass

P17 = DebouncedPin(5, print, timeout_ms=1000)

__deets__
User
Beiträge: 12288
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das try except kannst du dir sparen, genauso wie das schedule. Das ist die Essenz der Diskussion. Also einfach self._callback(pin)
JanUndPython
User
Beiträge: 6
Registriert: Donnerstag 4. August 2022, 09:44

Super. Das bekomme ich hin. Wo ich noch Probleme habe, was nichts mit eurer Diskussion zu tun hat, sondern hier mir noch einfach Fachwissen fehlt, ist wie ich in meinem einfachen Script den Tastendruck auswerte.

Ohne das Entprellen der Taste habe ich diesen Code genutzt. Wie rufe ich die Funktion „startSprinkling()“ mit eurem Code nach Tastendruck auf?

Code: Alles auswählen


buttonstart = Pin(5,Pin.IN,Pin.PULL_DOWN)

while True:
     if buttonstart.value() == 0:
         print("Startknopf ist gedückt.")

Ich hoffe ich nerve euch nicht mit diesen banalen Fragen.

Gruß

Jan
Benutzeravatar
Dennis89
User
Beiträge: 524
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,

'callback' ist das was ausgeführt werden soll, wenn der Tastendruck richtig erkannt wurde.

Für

Code: Alles auswählen

def __init__(self, pin, callback, timeout_ms=20):
übergibst du das Argument 'callback' 'print' und zwar hier:

Code: Alles auswählen

P17 = DebouncedPin(5, print, timeout_ms=1000)
Das steht auch in den Kommentaren von DeaDEyE.

Grüße
Dennis

Edit: War vielleicht etwas unverständlich geschrieben, du musst/kannst für 'callback' deine gewünschte Funktion übergeben. In dem Fall 'print' durch deinen Wunsch ersetzen.
“A ship is always safe at the shore, but that is not what it is built for.”
JanUndPython
User
Beiträge: 6
Registriert: Donnerstag 4. August 2022, 09:44

Super. Danke für deine Erklärung. Ich werde es heute Abend oder morgen ausprobieren ob ich es verstanden habe. :lol:
JanUndPython
User
Beiträge: 6
Registriert: Donnerstag 4. August 2022, 09:44

Hallo zusammen,
als so richtig vertehe ich den Code nicht. Habe versucht die Funktion "PressButton" nach Tastendruck mit eurer Beschreibung aufzurufen. Ich weiß nicht wie ich den "callback" Rückgavbe Wert abfrage. In meiner Hilflosigkeit habe ich es nur geschafft in eurem Code eine LED ein und auszuschalten. Natürlich könnte ich diese abfragen. Aber ich würde gerne verstehen, wie ich einen Rückgabewert aus eurem Code weiterverarbeite (z.B. mit dem aufrufen der Funktion "PressButton".
Könnt Ihr mir da über die Straße helfen. :)

Code: Alles auswählen

from machine import Pin
from micropython import schedule
from time import ticks_ms

# Initialisierung von GPIO25 als Ausgang
led_onboard = Pin(25, Pin.OUT, value=0)

class DebouncedPin:
    """
    pin := Pin nummer
    callback := Aufzurufende Funktion mit einem Argument (Pin instanz)
    timeout_ms := timeout in milisekunden
    """
    def __init__(self, pin, callback, timeout_ms=20):
        self._pin = Pin(pin,Pin.IN,Pin.PULL_UP)
        self._pin.irq(handler=self._irq, trigger=Pin.IRQ_RISING)
        self._callback = callback
        self._timeout_ms = timeout_ms
        self._timestamp = ticks_ms()
        
    def _irq(self, pin):
         now = ticks_ms()
         if self._timestamp < now:
              self._timestamp = now + self._timeout_ms
              led_onboard.toggle()              
              schedule(self._callback, pin)
              

def PressButton():
    print ("Taste gedrückt")

P17 = DebouncedPin(5,print, timeout_ms=1000)
__deets__
User
Beiträge: 12288
Registriert: Mittwoch 14. Oktober 2015, 14:29

Statt print PressButton in der letzten Zeile angeben.

Was ist eigentlich dein Ziel hier? Fragen nach ‚startSprinkling‘ klingen nach heimautomation, und für sowas gibt es fertige Systeme, die sich ohne hart zu erwerbende Programmierkenntnisse nutzen lassen. Hast du sowas in Erwägung gezogen?
Benutzeravatar
DeaD_EyE
User
Beiträge: 809
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Die Funktion micropython.schedule ruft self._callback(pin) auf.

Code: Alles auswählen

schedule(self._callback, pin)
Ohne die Funktion ist es einfacher zu verstehen:

Code: Alles auswählen

self._callback(pin)
Die an die Klasse übergebene Funktion (print) muss ein Argument akzeptieren. Im Beispiel ist es die Instanz von machine.Pin.

Der __init__ Methode der Klasse wird der Callback überheben (3. Argument) und dem Attribut self._callback zugewiesen. Im Beispiel habe ich die print Funktion verwendet.

Du kannst dem Callback auch mehr als ein Argument übergeben. Um z.B. den self._timestamp zusätzlich zu übergeben:

Code: Alles auswählen

self._callback(pin, self._timestamp)
Mit Verwendung von micropython.schedule ist das etwas "doof", da die Funktion nur ein Argument unterstützt und nicht beliebig viele.
Falls du mehr, als nur die Pin-Instanz benötgist, müsstest du mehrere Argumente als Tupel übergeben und sie in der aufgerufenen Funktion wieder entpacken.

Code: Alles auswählen

import micropython

# ziel: meine_funktion(1, 2, 3)

def meine_funktion(args):
    eins, zwei, drei = args
    print(eins, zwei, drei)


micropython.schedule(meine_funktion, (1, 2, 3))
Anderes Callback-Beispiel:

Code: Alles auswählen

def rufe_auf(aufzurufende_funktion, wert):
    aufzurufende_funktion(wert)


def verarbeite(wert):
    print(wert)


# nun der Aufruf
rufe_auf(verarbeite, 42)

Angewandt auf den Beispielcode mit nur einem Argument (pin):

Code: Alles auswählen

from machine import Pin
from micropython import schedule
from time import ticks_ms

# Initialisierung von GPIO25 als Ausgang
led_onboard = Pin(25, Pin.OUT, value=0)

class DebouncedPin:
    """
    pin := Pin nummer
    callback := Aufzurufende Funktion mit einem Argument (Pin instanz)
    timeout_ms := timeout in milisekunden
    """
    def __init__(self, pin, callback, timeout_ms=20):
        self._pin = Pin(pin,Pin.IN,Pin.PULL_UP)
        self._pin.irq(handler=self._irq, trigger=Pin.IRQ_RISING)
        self._callback = callback
        self._timeout_ms = timeout_ms
        self._timestamp = ticks_ms()
        
    def _irq(self, pin):
         now = ticks_ms()
         if self._timestamp < now:
              self._timestamp = now + self._timeout_ms
              led_onboard.toggle()              
              schedule(self._callback, pin)

# Funkion umbenannt
# Argument für pin hinzugefügt
def press_button(pin):
    if pin.value() # <- stammt aus der Instanz von DebouncedPin
        print ("Taste gedrückt")
    else:
        print("Taste nicht gedrückt")


# hier press_button hinzugefügt
P17 = DebouncedPin(5, press_button, timeout_ms=1000)
Was ist eigentlich dein Ziel hier? Fragen nach ‚startSprinkling‘ klingen nach heimautomation, und für sowas gibt es fertige Systeme, die sich ohne hart zu erwerbende Programmierkenntnisse nutzen lassen. Hast du sowas in Erwägung gezogen?
Schau mal hier: https://esphome.io/
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
__deets__
User
Beiträge: 12288
Registriert: Mittwoch 14. Oktober 2015, 14:29

DeaD_EyE hat geschrieben: Dienstag 9. August 2022, 17:31 Mit Verwendung von micropython.schedule ist das etwas "doof", da die Funktion nur ein Argument unterstützt und nicht beliebig viele.
Falls du mehr, als nur die Pin-Instanz benötgist, müsstest du mehrere Argumente als Tupel übergeben und sie in der aufgerufenen Funktion wieder entpacken.
Oder einfach auf schedule verzichten, da es ohne explizite und unnötige Angabe von ishard schlicht überflüssig ist.
Benutzeravatar
DeaD_EyE
User
Beiträge: 809
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

__deets__ hat geschrieben: Dienstag 9. August 2022, 18:42
Oder einfach auf schedule verzichten, da es ohne explizite und unnötige Angabe von ishard schlicht überflüssig ist.
[/quote]

Ja, klar. Scheint ja eh nix zu bewirken und es zu verschlimmern.


Selbst Speicherzuweisungen scheinen zu funktionieren (ESP32). Ich muss mir das mit dem RP Pico aber noch mal genauer untersuchen.
Der ESP32, den ich Zuhause zum Testen verwende, ist ein ESP32 mit 4 MiB SPIRAM.
Die RP2040 verwende ich nicht so gerne, da der Speicher sehr limitiert ist.
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
__deets__
User
Beiträge: 12288
Registriert: Mittwoch 14. Oktober 2015, 14:29

Der Pico macht das auch so, außer man fragt das extra an. Laut https://docs.micropython.org/en/latest/ ... ne.Pin.irq, und der oben gepostete Code legt das auch nahe.
Antworten