![]() |
Raspberry-Pi-Projekte: Interrupt-Verarbeitung mit PythonProf. Jürgen Plate |
Was ist ein Interrupt? In diesem Fall ist es eine Unterbrechung des normalen Programmablaufs durch ein äußeres Ereignis, beispielsweise der Pegelwechsel an einem GPIO-Port, ausgelöst durch eine Taste. Das normale Programm wird unterbrochen und eine sogenannte Interrupt-Serviceroutine (manchmal auch "Callback" genannt) ausgeführt. Danach läuft das Programm ganz normal weitr als sein nichts geschehen. Es ist auso eine Methode, auf ein Ereignis zu warten, ohne ständig überprüfen zu müssen, ob es schon eingetreten ist.
Viele einfache Programme lesen in einer Endlosschleife den Wert eines Eingabesignals aus und reagieren, sobald sich dessen Wert verändert. Bei einer einzelnen Signalquelle mag das gerade noch angehen. Jedoch lastet eine solche Schleife ("Busy Waitig") die CPU stark aus und bremst damit alle anderen Vorgänge auf dem Raspberry. Eine Verbesserung wäre ein Wartezyklus in der Schleife, der mit der Funktion time.sleep() zwischen den Abfragen realisiert werden kann. Das schafft zwar "Luft" für andere Aktionen, bedeutet aber auch, dass immer auch eine gewisse Zeit vergeht, bevor das Programm eine Veränderung des Eingabewerts mitbekommt. Eie derartige Programmierung läuft unter dem Oberbegriff "Polling".
Mit Interrupts gibt es eine wesentlich effektivere Methode, um direkt auf Signalveränderungen zu reagieren. Solche Interrupts sind vor allem dann ideal, wenn es darum geht, Veränderungen an verschiedenen Pins des GPIO zu registrieren. Beim Polling kann unter Umständen schon mal ein Ereignis im wahrsten Sinn des Wortes "verschlafen" werden.
Viele Programm erledigen keine Aufräumarbeiten wenn sie mit Strg-C unterbrochen werden. Da die Pin- und Interrupt-Einstellungen auch nach dem Programmende erhalten bleiben, kann es vorkommen, dass die benutzten Pins bei einer anderen Anwendung anschließend nicht funktioniert. Man sollte also beim Unterbrechen des Programms die Funktion GPIO.cleanup() auszuführen. Der Keyboard-Interrupt läßt sich nutzen, um das Programm ordnungsgemäß zu beenden, wie folgendes Beispiel zeigt:
import RPi.GPIO as GPIO import time GPIO.setmode(GPIO.BCM) GPIO.setup(18,GPIO.OUT) try: while True: GPIO.output(18,1) time.sleep(0.5) GPIO.output(18,0) time.sleep(0.5) except KeyboardInterrupt: GPIO.cleanup() print "Bye"
... channel = 18 # GPIO-Pin ... GPIO.add_event_detect(channel, GPIO.RISING) # add rising edge detection on a channel do_something() if GPIO.event_detected(channel): print('Button pressed') ...Das ist aber noch keine richtige Interrupt-Verarbeitung, es fehlt noch die Callback-Funktion. Diese wird durch eine Flanke am entsprechenden GPIO-Pin ausgelöst. Für die Angabe der Flanke gibt es drei Möglichkeiten:
Die vollständige Syntax der Methode lautet:
GPIO.add_event_detect(channel, GPIO.BOTH, callback=<Name der Callback-Funktion>)Die Callback-Funktion wird wie eine ganz normale Funktion definiert, z. B.:
def my_callback(channel): print('This is a edge event callback function!') ...Zu beachten ist, dass die Callback-Funktion mit den anderen Programmteilen nur über globale Variablen kommunizieren kann. Diese Variablen müssen im Hauptprogramm definiert und in der Callback-Funktion als global deklariert werden.
Die Definition einer Callback-Funktion kann auch mit zwei getrennten Methoden, der oben angeführten GPIO.add_event_detect() und GPIO.add_event_callback() erreicht werden. Angewendet wird dies meist nur dann, wenn man mehr als eine Callback-Funktion hat:
GPIO.add_event_detect(channel, GPIO.RISING) GPIO.add_event_callback(channel, antwort_1) GPIO.add_event_callback(channel, antwort_2)Die beiden Funktionen werden nacheinander ausgeführt.
Gelegentlich kann man feststellen, dass die Callback-Funktion mehr als einmal für jedes Ereignis aufgerufen wird. Dies kann die Folge eines "prellenden" Tasters sein. Es gibt zwei Möglichkeiten, mit Schalterprellen umzugehen:
# Pulldown-Widerstand (gegen Masse) GPIO.setup(XX, GPIO.IN, pull_up_down = GPIO.PUD_DOWN) oder # Pullup-Widerstand (gegen + 3,3 V) GPIO.setup(XX, GPIO.IN, pull_up_down = GPIO.PUD_UP)Die internen Widerstände sind aber recht hochohmig (40 bis 50 kΩ). Bei Störungen im Umfeld kann deren Wert zu hoch sein, um diese auszufiltern. Lässt man den Widerstand weg oder ist sein Wert zu hoch, genügt es, einen Draht am entsprechenden Pin anzuschließen und dessen anderes Ende frei in der Luft hängen zu lassen. Dann kann es durchaus sein, dass der Eingang bereits Signale empfängt (Elektrosmog ist inzwischen überall). Mit einem externen Widerstand kann man auf 10 kΩ bis 1 kΩ heruntergehen und so die Störsicherheit verbessern.
Um das Entprellen per Software zu erledigen, fügen Sie den bouncetime-Parameter dem Aufruf von GPIO.add_event_detect() hinzu, wobei die Zeit in Millisekunden angegeben wird. Die Methode hat demnach zwei obligatorische und zwei optionale Parameter. Zum Beispiel lautet der Aufruf für 300 ms Entprellzeit:
GPIO.add_event_detect(channel, GPIO.BOTH, callback=antwort, bouncetime=300)
Da auch nach einem Programmende die GPIO-Funktionen noch aktiv sein können, sollte man im Programm immer aufräumen. Manchmal will man aber nur eine Interrupt-Definiton löschen, nicht aber alles andere. In solchen Situationen kann dann die Methode GPIO.remove_event_detect() verwendet werden.
GPIO.remove_event_detect(channel)
Das erste Beispiel zählt Tastendrücke (der Taster mit Pullup-Widerstand schliesst gegen GND-Pegel) auf Interrupt-Basis. Als Eingang dient GPIO 18 mit aktiviertem Pullup-Widerstand. Die globale Variable "Counter" wird in der Callback-Funktion verändert, um die Tastendrücke zu zählen. Der Zusatz bouncetime = 250 legt die Totzeit für das Tastenprellen fest. Dies ist nur wichtig, wenn Taster als Interruptquelle dienen. Um zu zeigen, dass das Programm auch läuft, wird ein Zähler namens "Tic" im Sekundentakt hochgezählt. Die Tastendrücke werden per Interrupt registriert.
#!/usr/bin/python import RPi.GPIO as GPIO import time # Zaehler-Variable, global Counter = 0 Tic = 0 # Pinreferenz waehlen GPIO.setmode(GPIO.BCM) # GPIO 18 (Pin 12) als Input definieren und Pullup-Widerstand aktivieren GPIO.setup(18, GPIO.IN, pull_up_down = GPIO.PUD_UP) # Callback-Funktion def Interrupt(channel): global Counter # Counter um eins erhoehen und ausgeben Counter = Counter + 1 print "Counter " + str(Counter) # Interrupt-Event hinzufuegen, steigende Flanke GPIO.add_event_detect(18, GPIO.RISING, callback = Interrupt, bouncetime = 250) # Endlosschleife, bis Strg-C gedrueckt wird try: while True: # nix Sinnvolles tun Tic = Tic + 1 print "Tic %d" % Tic time.sleep(1) except KeyboardInterrupt: GPIO.cleanup() print "\nBye"Ein Programmlauf siht dann z. B. folgendermaßen aus:
Tic 1 Tic 2 Tic 3 Tic 4 Tic 5 Tic 6 Counter 1 Tic 7 Counter 2 Tic 8 Counter 3 Counter 4 Counter 5 Tic 9 Counter 6 Tic 10 ^C Bye
Es lassen sich durchaus mehrere Signalquellen mit Interrupts verbinden. Für jede Signalquelle muss dann eine Callback-Funktion definiert und der Interrupt aktiviert werden. Das folgende Beispiel hat zwei Taster an den GPIO-Ports 17 (Pin 11) und 18 (Pin 12). Die Taster haben wieder den Pullup-Widerstand aktiviert und schliessen gegen GND-Pegel.
#!/usr/bin/python import RPi.GPIO as GPIO import time # GPIO-Ports Counter_17 = 0 Counter_18 = 0 # Zaehlvariable Tic = 0 # GPIO initialisieren GPIO.setmode(GPIO.BCM) GPIO.setup(17, GPIO.IN) # Pin 11 GPIO.setup(18, GPIO.IN) # Pin 12 # internen Pullup-Widerstand aktivieren. GPIO.setup(17, GPIO.IN, pull_up_down = GPIO.PUD_UP) GPIO.setup(18, GPIO.IN, pull_up_down = GPIO.PUD_UP) # Callback fuer GPIO 17 def isr17(channel): global Counter_17 Counter_17 = Counter_17 + 1 print "Counter_17: %d" % Counter_17 # Callback fuer GPIO 18 def isr18(channel): global Counter_18 Counter_18 = Counter_18 + 1 print "Counter_18: %d" % Counter_18 # Interrupts aktivieren GPIO.add_event_detect(17, GPIO.FALLING, callback = isr17, bouncetime = 200) GPIO.add_event_detect(18, GPIO.FALLING, callback = isr18, bouncetime = 200) # Endlosschleife wie oben try: while True: # nix Sinnvolles tun Tic = Tic + 1 print "Tic %d" % Tic time.sleep(1) except KeyboardInterrupt: GPIO.cleanup() print "\nBye"Wenn man das Programm startet kann man beide Interrupts verfolgen. Es funktioniert auch, wenn man beide GPIOs gemeinsam an einen Taster anschließt. Es geht also auf keinen Fall etwas verloren. Unten ein typischer Programmlauf:
Tic 1 Tic 2 Tic 3 Tic 4 Tic 5 Tic 6 Counter_17:1 Tic 7 Counter_17:2 Tic 8 Tic 9 Counter_18:1 Tic 10 Counter_18:2 Counter_18:3 Counter_18:4 Counter_18:5 Tic 11 Tic 12 Tic 13 ^C Bye
Die dritte Möglichkeit bei der Interrupt-Verarbeitung ermöglicht es, auf beide Flanken zu reagieren. Mit dem Befehl GPIO.add_event_detect(17, GPIO.BOTH, callback=measure) wird die Funktion measure() als Interrupt-Serviceroutine für steigende und fallende Flanke (GPIO.BOTH) eingetragen. Innerhalb der Funktion measure() wird dann der Port abgefragt. Hat er den Wert "1", war eine steigende Flanke Auslöser und die globale Variable start speichert die aktuelle Zeit. Im anderen Fall war die fallende Flanke der Auslöser und es wird die aktuelle Zeit in stopp gespeichert. Danach wird die Zeitdifferenz berechnet und ausgegeben. Der Taster hat wieder den Pullup-Widerstand aktiviert und schließt gegen GND-Pegel. Alles andere ist wie gehabt.
#!/usr/bin/python import RPi.GPIO as GPIO import time import datetime # Variablen initialisieren Tic = 0 # Zaehler stopp = 0 # Zeitpunkt steigende Flanke start = 0 # Zeitpunkt fallende Flanke delta = 0 # Zeitdifferenz zwischen start und stopp # GPIO initialisieren GPIO.setmode(GPIO.BCM) GPIO.setup(17, GPIO.IN) # Pin 11 # internen Pullup-Widerstand aktivieren. GPIO.setup(17, GPIO.IN, pull_up_down = GPIO.PUD_UP) # Callback-Funktion fuer beide Flanken def measure(channel): global start global stopp global delta if GPIO.input(17) == 0: # fallende Flanke, Startzeit speichern start = time.time() else: # steigende Flanke, Endezeit speichern stopp = time.time() delta = stopp - start # Zeitdifferenz berechnen print("delta = %1.2f" % delta) # Interrupt fuer beide Flanken aktivieren GPIO.add_event_detect(17, GPIO.BOTH, callback=measure, bouncetime=200) try: while True: # nix Sinnvolles tun Tic = Tic + 1 print "Tic %d" % Tic time.sleep(1) # reset GPIO settings if user pressed Ctrl+C except KeyboardInterrupt: GPIO.cleanup() print("\nBye!")Nach dem Start ermittelt das Programm, wie lange der Taster gedrückt wurde, wobei Zeiten kleiner 200 ms (bouncetime) nicht vorkommen können. Will man genauere Werte, muss die Entprellung der Taste hardwaremäßig erfolgen.
Tic 1 Tic 2 Tic 3 delta = 0.03 Tic 4 delta = 1.52 Tic 5 Tic 6 Tic 7 Tic 8 Tic 9 delta = 0.37 Tic 10 Tic 11 delta = 2.53 delta = 0.26 Tic 12 Tic 13 Tic 14 Tic 15 ^C Bye!
Es gibt manchmal den Fall, dass ein am GPIO angeschlossenes Device nicht reagiert oder dass bei einer seriellen Verbindung die Gegenseite plötzlich nicht mehr reagiert. Das bearbeitende Programm bleibt dann "hängen". Das wäre nicht weiter schlimm, wenn der Prozess möglicherweise nicht ständig die Hardware abfragen und so Rechenzeit aufnehmen würde. Wird er dann noch per crontab in regelmäßigen Zeitabständen gestartet, kommt hinzu, dass ja jedesmal ein neuer, ebenfalls hängender Prozess hinzukommt. Langsam aber sicher würde der RasPi immer träger arbeiten und irgendwann geht dann gar nichts mehr.
Also muss dafür gesorgt werden, dass so ein Programm nach einer gewissen Zeit abgbrochen wird. Dies kann relativ einfach nach folgendem Schema erreicht werden. Im Python-Programm wird ein Timer gestartet und nach fünf Minuten das Programm zwangsweise abgebrochen. Dazu wird einerseits die entsprechende Bibliothek mittels import signal eingebunden und andererseits bei den Funktionen ein Signalhandler hinzugefügt, der das Programm beendet:
# Signalhandler fuer den Timeout def to_handler(signum, frame): print "Timeout!" exit(1)Am Anfang des Hauptprogramms wird der Signalhandler mit dem Timer verknüpft und der Timeout beispielsweise auf 10 s eingestellt:
# signal function handler registrieren signal.signal(signal.SIGALRM, to_handler) # timeout definieren signal.alarm(10)Das war es dann auch schon. Ist das Programm nach weniger als 10 Sekunden fertig, endet es ganz normal. Bleibt es "hängen", wird es nach fünf Minuten zwangsweise beendet.
Man kann das System noch dahingehnd erweitern, dass der Signalhandler das Programm nicht einfach beendet, sondern eine Exception wirft, damit sich noch Aufräumarbeiten erledigen lassen. Dazu wird der kritische Teil in try - except eingebunden. Das folgende Beispiel zeigt diese Erweitereung. Damit sich während der Wartezeit etwas tut, wird im Sekundenrhythmus "tick" ausgegeben:
import signal import time # Signalhandler fuer den timeout def handler(signum, frame): print "Timeout!" raise Exception("end of time") # signal function handler registrieren signal.signal(signal.SIGALRM, handler) # timeout definieren signal.alarm(10) # Hauptprogramm try: while 1: # eigentlich eine Endlosschleife print "tick" time.sleep(1) except Exception, exc: print exc