Interrupt-Handler schreiben¶
Auf geeigneter Hardware bietet MicroPython die Möglichkeit, Interrupt-Handler in Python zu schreiben. Interrupt-Handler - auch bekannt als Interrupt-Service-Routinen (ISRs) - werden als Callback-Funktionen definiert. Sie werden als Reaktion auf ein Ereignis ausgeführt, etwa einen Timer-Auslöser oder eine Spannungsänderung an einem Pin. Solche Ereignisse können zu jedem beliebigen Zeitpunkt während der Ausführung des Programmcodes auftreten. Das hat erhebliche Konsequenzen, von denen einige spezifisch für die Sprache MicroPython sind. Andere betreffen alle Systeme, die in der Lage sind, auf Echtzeitereignisse zu reagieren. Dieses Dokument behandelt zunächst die sprachspezifischen Themen und gibt anschließend eine kurze Einführung in die Echtzeitprogrammierung für alle, die damit noch nicht vertraut sind.
Diese Einführung verwendet vage Begriffe wie „langsam“ oder „so schnell wie möglich“. Das ist beabsichtigt, da Geschwindigkeiten anwendungsabhängig sind. Akzeptable Laufzeiten für eine ISR hängen von der Rate ab, mit der Interrupts auftreten, von der Beschaffenheit des Hauptprogramms und vom Vorhandensein anderer gleichzeitiger Ereignisse.
Tipps und empfohlene Vorgehensweisen¶
Dies fasst die nachfolgend ausführlich beschriebenen Punkte zusammen und listet die wichtigsten Empfehlungen für Interrupt-Handler-Code auf.
Halten Sie den Code so kurz und einfach wie möglich.
Vermeiden Sie Speicherallokationen: kein Anhängen an Listen oder Einfügen in Dictionaries, keine Gleitkommaarithmetik.
Erwägen Sie die Verwendung von
micropython.schedule, um die oben genannte Einschränkung zu umgehen.Wenn eine ISR mehrere Bytes zurückgibt, verwenden Sie ein vorab allokiertes
bytearray. Wenn mehrere Ganzzahlen zwischen einer ISR und dem Hauptprogramm geteilt werden sollen, erwägen Sie ein Array (array.array).Wenn Daten zwischen dem Hauptprogramm und einer ISR geteilt werden, erwägen Sie, Interrupts vor dem Zugriff auf die Daten im Hauptprogramm zu deaktivieren und unmittelbar danach wieder zu aktivieren (siehe Kritische Abschnitte).
Allokieren Sie einen Notfall-Ausnahmepuffer (siehe unten).
MicroPython-spezifische Themen¶
Der Notfall-Ausnahmepuffer¶
Wenn in einer ISR ein Fehler auftritt, kann MicroPython keinen Fehlerbericht erzeugen, es sei denn, es wurde zu diesem Zweck ein spezieller Puffer angelegt. Das Debugging wird vereinfacht, wenn der folgende Code in jedem Programm enthalten ist, das Interrupts verwendet.
import micropython
micropython.alloc_emergency_exception_buf(100)
Der Notfall-Ausnahmepuffer kann nur einen einzigen Ausnahme-Stacktrace aufnehmen. Das bedeutet: Wird während der Behandlung einer Ausnahme bei gesperrtem Heap eine zweite Ausnahme ausgelöst, ersetzt der Stacktrace dieser zweiten Ausnahme den ursprünglichen - selbst wenn die zweite Ausnahme sauber behandelt wird. Das kann zu verwirrenden Ausnahmemeldungen führen, wenn der Puffer später ausgegeben wird.
Einfachheit¶
Aus verschiedenen Gründen ist es wichtig, ISR-Code so kurz und einfach wie möglich zu halten. Er sollte nur das tun, was unmittelbar nach dem auslösenden Ereignis erledigt werden muss: Operationen, die aufgeschoben werden können, sollten an die Hauptprogrammschleife delegiert werden. Typischerweise behandelt eine ISR das Hardwaregerät, das den Interrupt ausgelöst hat, und macht es bereit für den nächsten Interrupt. Sie kommuniziert mit der Hauptschleife, indem sie gemeinsam genutzte Daten aktualisiert, um anzuzeigen, dass der Interrupt aufgetreten ist, und kehrt dann zurück. Eine ISR sollte die Kontrolle so schnell wie möglich an die Hauptschleife zurückgeben. Dies ist kein spezifisches MicroPython-Thema und wird daher ausführlicher unten behandelt.
Kommunikation zwischen einer ISR und dem Hauptprogramm¶
Normalerweise muss eine ISR mit dem Hauptprogramm kommunizieren. Die einfachste Möglichkeit dazu ist über ein oder mehrere gemeinsam genutzte Datenobjekte, die entweder als global deklariert oder über eine Klasse geteilt werden (siehe unten). Es gibt dabei verschiedene Einschränkungen und Gefahren, die weiter unten ausführlicher behandelt werden. Ganzzahlen, bytes- und bytearray-Objekte werden zu diesem Zweck häufig verwendet, ebenso wie Arrays (aus dem Modul array), die verschiedene Datentypen speichern können.
Die Verwendung von Objektmethoden als Callbacks¶
MicroPython unterstützt diese leistungsfähige Technik, die es einer ISR ermöglicht, Instanzvariablen mit dem zugrunde liegenden Code zu teilen. Sie ermöglicht es außerdem einer Klasse, die einen Gerätetreiber implementiert, mehrere Geräteinstanzen zu unterstützen. Das folgende Beispiel bringt zwei LEDs mit unterschiedlichen Raten zum Blinken.
import machine
import micropython
micropython.alloc_emergency_exception_buf(100)
class Foo(object):
def __init__(self, freq, led):
self.led = led
self.timer = machine.Timer(-1, freq=freq, callback=self.cb, hard=True)
def cb(self, tim):
self.led.toggle()
red = Foo(1, machine.LED("LED_RED"))
green = Foo(0.8, machine.LED("LED_GREEN"))
In diesem Beispiel steuert die red-Instanz die rote LED über einen virtuellen 1-Hz-Timer: Jedes Mal, wenn der Timer auslöst, wird red.cb() aufgerufen und die rote LED umgeschaltet. Die green-Instanz arbeitet analog mit einem 0,8-Hz-Timer, der die grüne LED umschaltet. Die Verwendung von Instanzmethoden bietet zwei Vorteile. Erstens ermöglicht eine einzige Klasse, dass Code zwischen mehreren Hardwareinstanzen geteilt wird. Zweitens ist das erste Argument der Callback-Funktion als gebundene Methode self. Dadurch kann der Callback auf Instanzdaten zugreifen und Zustand zwischen aufeinanderfolgenden Aufrufen speichern. Hätte die obige Klasse beispielsweise eine im Konstruktor auf null gesetzte Variable self.count, könnte cb() den Zähler erhöhen. Die Instanzen red und green würden dann unabhängige Zählungen darüber führen, wie oft jede LED ihren Zustand geändert hat.
Erzeugung von Python-Objekten¶
ISRs können keine Instanzen von Python-Objekten erzeugen. Das liegt daran, dass MicroPython Speicher für das Objekt aus einem Vorrat freier Speicherblöcke allokieren muss, der heap genannt wird. Dies ist in einem Interrupt-Handler nicht erlaubt, da die Heap-Allokation nicht wiedereintrittsfähig ist. Mit anderen Worten: Der Interrupt könnte auftreten, während das Hauptprogramm mitten in einer Allokation steckt - um die Integrität des Heaps zu wahren, unterbindet der Interpreter Speicherallokationen in ISR-Code.
Eine Folge davon ist, dass ISRs keine Gleitkommaarithmetik verwenden können; das liegt daran, dass Floats Python-Objekte sind. Ebenso kann eine ISR kein Element an eine Liste anhängen. In der Praxis kann es schwierig sein, genau zu bestimmen, welche Code-Konstrukte versuchen, eine Speicherallokation durchzuführen und eine Fehlermeldung hervorzurufen: ein weiterer Grund, ISR-Code kurz und einfach zu halten.
Eine Möglichkeit, dieses Problem zu vermeiden, besteht darin, dass die ISR vorab allokierte Puffer verwendet. Beispielsweise erzeugt ein Klassenkonstruktor eine bytearray-Instanz und ein boolesches Flag. Die ISR-Methode schreibt Daten an Positionen im Puffer und setzt das Flag. Die Speicherallokation erfolgt im Hauptprogrammcode bei der Instanziierung des Objekts und nicht in der ISR.
Die E/A-Methoden der MicroPython-Bibliothek bieten in der Regel die Möglichkeit, einen vorab allokierten Puffer zu verwenden. Beispielsweise liest machine.I2C.readfrom_into() in einen vom Aufrufer bereitgestellten, veränderbaren Puffer: Dies ermöglicht die Verwendung in einer ISR.
Eine Möglichkeit, ein Objekt ohne Verwendung einer Klasse oder von Globals zu erzeugen, ist die folgende:
def set_volume(t, buf=bytearray(3)):
buf[0] = 0xa5
buf[1] = t >> 4
buf[2] = 0x5a
return buf
Der Compiler instanziiert das standardmäßige buf-Argument, wenn die Funktion zum ersten Mal geladen wird (üblicherweise beim Import des Moduls, in dem sie enthalten ist).
Eine Objekterzeugung findet statt, wenn eine Referenz auf eine gebundene Methode erstellt wird. Das bedeutet, dass eine ISR keine gebundene Methode an eine Funktion übergeben kann. Eine Lösung besteht darin, im Klassenkonstruktor eine Referenz auf die gebundene Methode zu erstellen und diese Referenz in der ISR zu übergeben. Zum Beispiel:
class Foo():
def __init__(self):
self.bar_ref = self.bar # Allocation occurs here
self.x = 0.1
self.tim = machine.Timer(-1, freq=2, callback=self.cb, hard=True)
def bar(self, _):
self.x *= 1.2
print(self.x)
def cb(self, t):
# Passing self.bar would cause allocation.
micropython.schedule(self.bar_ref, 0)
Andere Techniken bestehen darin, die Methode im Konstruktor zu definieren und zu instanziieren oder Foo.bar() mit dem Argument self zu übergeben.
Verwendung von Python-Objekten¶
Eine weitere Einschränkung bei Objekten ergibt sich aus der Funktionsweise von Python. Wenn eine import-Anweisung ausgeführt wird, wird der Python-Code zu bytecode kompiliert, wobei eine Codezeile typischerweise mehreren Bytecodes entspricht. Beim Ausführen des Codes liest der Interpreter jeden Bytecode und führt ihn als eine Folge von Maschinencode-Befehlen aus. Da ein Interrupt jederzeit zwischen Maschinencode-Befehlen auftreten kann, ist die ursprüngliche Python-Codezeile möglicherweise nur teilweise ausgeführt. Folglich kann einem Python-Objekt wie einem Set, einer Liste oder einem Dictionary, das in der Hauptschleife verändert wird, im Moment des Interrupts die innere Konsistenz fehlen.
Ein typisches Ergebnis ist das folgende. In seltenen Fällen läuft die ISR genau in dem Moment, in dem das Objekt nur teilweise aktualisiert ist. Wenn die ISR versucht, das Objekt zu lesen, kommt es zu einem Absturz. Da solche Probleme typischerweise nur selten und zu zufälligen Zeitpunkten auftreten, sind sie schwer zu diagnostizieren. Es gibt Möglichkeiten, dieses Problem zu umgehen, die unten unter Kritische Abschnitte beschrieben werden.
Es ist wichtig, sich darüber im Klaren zu sein, was als Veränderung eines Objekts gilt. Das Ändern des Inhalts eines Arrays oder Bytearrays ist sicher. Das liegt daran, dass Bytes oder Wörter als ein einzelner, nicht unterbrechbarer Maschinencode-Befehl geschrieben werden: In der Terminologie der Echtzeitprogrammierung ist der Schreibvorgang atomar. Dasselbe gilt für das Aktualisieren eines Dictionary-Eintrags, da Einträge Maschinenwörter sind, nämlich Ganzzahlen oder Zeiger auf Objekte. Ein benutzerdefiniertes Objekt könnte ein Array oder Bytearray instanziieren. Es ist zulässig, dass sowohl die Hauptschleife als auch die ISR den Inhalt dieser ändern.
Die Gefahr entsteht, wenn die Struktur eines Objekts verändert wird, insbesondere bei Dictionaries. Das Hinzufügen oder Löschen von Schlüsseln kann ein Rehashing auslösen. Wenn eine harte ISR läuft, während ein Rehashing im Gange ist, und versucht, auf einen Eintrag zuzugreifen, kann es zu einem Absturz kommen. Intern sind Globals als Dictionary implementiert. Folglich sollte das Hauptprogramm alle erforderlichen Globals erzeugen, bevor ein Prozess gestartet wird, der harte Interrupts erzeugt. Anwendungscode sollte außerdem das Löschen von Globals vermeiden.
MicroPython unterstützt Ganzzahlen mit beliebiger Genauigkeit. Werte zwischen 230 -1 und -230 werden in einem einzigen Maschinenwort gespeichert. Größere Werte werden als Python-Objekte gespeichert. Folglich können Änderungen an langen Ganzzahlen nicht als atomar betrachtet werden. Die Verwendung langer Ganzzahlen in ISRs ist unsicher, da beim Ändern des Variablenwerts eine Speicherallokation versucht werden könnte.
Überwindung der Float-Einschränkung¶
Im Allgemeinen ist es am besten, die Verwendung von Floats in ISR-Code zu vermeiden: Hardwaregeräte verarbeiten normalerweise Ganzzahlen, und die Umwandlung in Floats erfolgt normalerweise in der Hauptschleife. Es gibt jedoch einige DSP-Algorithmen, die Gleitkommaarithmetik erfordern. Auf Plattformen mit Hardware-Gleitkommaeinheit (wie den STM32-basierten OpenMV Cams) kann der Inline-Assembler für ARM Thumb verwendet werden, um diese Einschränkung zu umgehen. Das liegt daran, dass der Prozessor Float-Werte in einem Maschinenwort speichert; Werte können zwischen der ISR und dem Hauptprogrammcode über ein Array von Floats geteilt werden.
Verwendung von micropython.schedule¶
Diese Funktion ermöglicht es einer ISR, einen Callback zur Ausführung „sehr bald“ einzuplanen. Der Callback wird zur Ausführung in eine Warteschlange gestellt, die zu einem Zeitpunkt erfolgt, an dem der Heap nicht gesperrt ist. Daher kann er Python-Objekte erzeugen und Floats verwenden. Außerdem ist garantiert, dass der Callback zu einem Zeitpunkt läuft, an dem das Hauptprogramm jede Aktualisierung von Python-Objekten abgeschlossen hat, sodass der Callback nicht auf nur teilweise aktualisierte Objekte trifft.
Typischerweise wird dies verwendet, um Sensor-Hardware zu behandeln. Die ISR erfasst Daten von der Hardware und versetzt sie in die Lage, einen weiteren Interrupt auszulösen. Anschließend plant sie einen Callback ein, um die Daten zu verarbeiten.
Eingeplante Callbacks sollten den unten dargelegten Prinzipien des Interrupt-Handler-Designs entsprechen. Dies soll Probleme vermeiden, die durch E/A-Aktivität und die Veränderung gemeinsam genutzter Daten entstehen können und in jedem Code auftreten, der die Hauptprogrammschleife unterbricht.
Die Ausführungszeit muss im Verhältnis zur Häufigkeit, mit der Interrupts auftreten können, betrachtet werden. Tritt ein Interrupt auf, während der vorherige Callback noch ausgeführt wird, wird eine weitere Instanz des Callbacks zur Ausführung in die Warteschlange gestellt; diese läuft, nachdem die aktuelle Instanz abgeschlossen ist. Eine anhaltend hohe Interrupt-Wiederholungsrate birgt daher das Risiko eines unbegrenzten Wachstums der Warteschlange und schließlich eines Fehlers mit einem RuntimeError.
Wenn der an schedule() zu übergebende Callback eine gebundene Methode ist, beachten Sie den Hinweis unter „Erzeugung von Python-Objekten“.
Ausnahmen¶
Wenn eine ISR eine Ausnahme auslöst, wird diese nicht an die Hauptschleife weitergereicht. Der Interrupt wird deaktiviert, sofern die Ausnahme nicht vom ISR-Code behandelt wird.
Anbindung an asyncio¶
Wenn eine ISR läuft, kann sie den asyncio-Scheduler unterbrechen. Führt die ISR eine asyncio-Operation aus, kann der Betrieb des Schedulers gestört werden. Dies gilt unabhängig davon, ob der Interrupt hart oder weich ist, und auch dann, wenn die ISR die Ausführung über micropython.schedule an eine andere Funktion übergeben hat. Insbesondere ist das Erstellen oder Abbrechen von Tasks in einem ISR-Kontext ungültig. Der sichere Weg, mit asyncio zu interagieren, besteht darin, eine Coroutine zu implementieren, deren Synchronisation über asyncio.ThreadSafeFlag erfolgt. Das folgende Fragment veranschaulicht die Erstellung eines Tasks als Reaktion auf einen Interrupt:
tsf = asyncio.ThreadSafeFlag()
def isr(_): # Interrupt handler
tsf.set()
async def foo():
while True:
await tsf.wait()
asyncio.create_task(bar())
In diesem Beispiel gibt es eine variable Latenz zwischen der Ausführung der ISR und der Ausführung von foo(). Dies ist dem kooperativen Scheduling inhärent. Die maximale Latenz ist anwendungs- und plattformabhängig, lässt sich aber typischerweise in mehreren zehn ms messen.
Allgemeine Themen¶
Dies ist lediglich eine kurze Einführung in das Thema Echtzeitprogrammierung. Einsteiger sollten beachten, dass Konstruktionsfehler in Echtzeitprogrammen zu Fehlern führen können, die besonders schwer zu diagnostizieren sind. Das liegt daran, dass sie nur selten und in im Wesentlichen zufälligen Abständen auftreten können. Es ist entscheidend, das anfängliche Design richtig zu machen und Probleme zu antizipieren, bevor sie auftreten. Sowohl Interrupt-Handler als auch das Hauptprogramm müssen unter Berücksichtigung der folgenden Themen entworfen werden.
Design von Interrupt-Handlern¶
Wie oben erwähnt, sollten ISRs so einfach wie möglich gestaltet sein. Sie sollten stets innerhalb einer kurzen, vorhersehbaren Zeitspanne zurückkehren. Das ist wichtig, weil die Hauptschleife nicht läuft, während die ISR läuft: Unweigerlich erfährt die Hauptschleife an zufälligen Stellen im Code Pausen in ihrer Ausführung. Solche Pausen können eine Quelle schwer zu diagnostizierender Fehler sein, insbesondere wenn ihre Dauer lang oder variabel ist. Um die Auswirkungen der ISR-Laufzeit zu verstehen, ist ein grundlegendes Verständnis von Interrupt-Prioritäten erforderlich.
Interrupts sind nach einem Prioritätsschema organisiert. ISR-Code kann selbst durch einen Interrupt höherer Priorität unterbrochen werden. Das hat Auswirkungen, wenn die beiden Interrupts Daten teilen (siehe Kritische Abschnitte unten). Tritt ein solcher Interrupt auf, fügt er eine Verzögerung in den ISR-Code ein. Tritt ein Interrupt niedrigerer Priorität auf, während die ISR läuft, wird er verzögert, bis die ISR abgeschlossen ist: Ist die Verzögerung zu lang, kann der Interrupt niedrigerer Priorität fehlschlagen. Ein weiteres Problem bei langsamen ISRs ist der Fall, dass während ihrer Ausführung ein zweiter Interrupt desselben Typs auftritt. Der zweite Interrupt wird nach Beendigung des ersten behandelt. Wenn jedoch die Rate der eingehenden Interrupts die Kapazität der ISR, sie zu bedienen, dauerhaft übersteigt, wird das Ergebnis kein erfreuliches sein.
Folglich sollten Schleifenkonstrukte vermieden oder minimiert werden. E/A zu anderen Geräten als dem auslösenden Gerät sollte normalerweise vermieden werden: E/A wie Festplattenzugriffe, print-Anweisungen und UART-Zugriffe sind relativ langsam, und ihre Dauer kann variieren. Ein weiteres Problem hierbei ist, dass Dateisystemfunktionen nicht wiedereintrittsfähig sind: Dateisystem-E/A in einer ISR und im Hauptprogramm zu verwenden, wäre gefährlich. Entscheidend ist, dass ISR-Code nicht auf ein Ereignis warten sollte. E/A ist akzeptabel, wenn garantiert werden kann, dass der Code in einer vorhersehbaren Zeitspanne zurückkehrt, beispielsweise das Umschalten eines Pins oder einer LED. Der Zugriff auf das auslösende Gerät über I2C oder SPI kann notwendig sein, doch sollte die für solche Zugriffe benötigte Zeit berechnet oder gemessen und ihre Auswirkung auf die Anwendung bewertet werden.
Üblicherweise besteht die Notwendigkeit, Daten zwischen der ISR und der Hauptschleife zu teilen. Dies kann entweder über globale Variablen oder über Klassen- bzw. Instanzvariablen geschehen. Variablen sind typischerweise Ganzzahl- oder boolesche Typen oder Ganzzahl- bzw. Byte-Arrays (ein vorab allokiertes Ganzzahl-Array bietet schnelleren Zugriff als eine Liste). Wenn mehrere Werte von der ISR verändert werden, muss der Fall berücksichtigt werden, dass der Interrupt zu einem Zeitpunkt auftritt, an dem das Hauptprogramm auf einige, aber nicht alle Werte zugegriffen hat. Dies kann zu Inkonsistenzen führen.
Betrachten Sie das folgende Design. Eine ISR speichert eingehende Daten in einem Bytearray und addiert dann die Anzahl der empfangenen Bytes zu einer Ganzzahl, welche die Gesamtzahl der zur Verarbeitung bereiten Bytes darstellt. Das Hauptprogramm liest die Anzahl der Bytes, verarbeitet die Bytes und setzt anschließend die Anzahl der bereiten Bytes zurück. Dies funktioniert, bis ein Interrupt unmittelbar nachdem das Hauptprogramm die Anzahl der Bytes gelesen hat auftritt. Die ISR legt die hinzugekommenen Daten in den Puffer und aktualisiert die empfangene Anzahl, aber das Hauptprogramm hat die Anzahl bereits gelesen und verarbeitet daher die ursprünglich empfangenen Daten. Die neu eingetroffenen Bytes gehen verloren.
Es gibt verschiedene Möglichkeiten, diese Gefahr zu vermeiden, die einfachste besteht in der Verwendung eines Ringpuffers. Ist es nicht möglich, eine Struktur mit inhärenter Thread-Sicherheit zu verwenden, werden weiter unten andere Wege beschrieben.
Wiedereintrittsfähigkeit¶
Eine potenzielle Gefahr kann auftreten, wenn eine Funktion oder Methode zwischen dem Hauptprogramm und einer oder mehreren ISRs oder zwischen mehreren ISRs geteilt wird. Das Problem dabei ist, dass die Funktion selbst unterbrochen und eine weitere Instanz dieser Funktion ausgeführt werden kann. Damit dies möglich ist, muss die Funktion so entworfen sein, dass sie wiedereintrittsfähig ist. Wie das geschieht, ist ein fortgeschrittenes Thema, das den Rahmen dieses Tutorials sprengt.
Kritische Abschnitte¶
Ein Beispiel für einen kritischen Codeabschnitt ist einer, der auf mehr als eine Variable zugreift, die von einer ISR beeinflusst werden kann. Tritt der Interrupt zufällig zwischen den Zugriffen auf die einzelnen Variablen auf, sind deren Werte inkonsistent. Dies ist ein Beispiel für eine als Race Condition bekannte Gefahr: Die ISR und die Hauptprogrammschleife wetteifern darum, die Variablen zu ändern. Um Inkonsistenz zu vermeiden, muss ein Mittel eingesetzt werden, das sicherstellt, dass die ISR die Werte für die Dauer des kritischen Abschnitts nicht ändert. Eine Möglichkeit, dies zu erreichen, besteht darin, vor dem Beginn des Abschnitts machine.disable_irq() und am Ende machine.enable_irq() auszuführen. Hier ist ein Beispiel für diesen Ansatz:
import machine
import micropython
import array
import random
import time
micropython.alloc_emergency_exception_buf(100)
class BoundsException(Exception):
pass
ARRAYSIZE = const(20)
index = 0
data = array.array('i', [0] * ARRAYSIZE)
def callback1(t):
global data, index
for x in range(5):
data[index] = random.getrandbits(30) # simulate input
index += 1
if index >= ARRAYSIZE:
raise BoundsException('Array bounds exceeded')
tim = machine.Timer(-1, freq=100, callback=callback1, hard=True)
for loop in range(1000):
if index > 0:
irq_state = machine.disable_irq() # Start of critical section
for x in range(index):
print(data[x])
index = 0
machine.enable_irq(irq_state) # End of critical section
print('loop {}'.format(loop))
time.sleep_ms(1)
tim.deinit()
Ein kritischer Abschnitt kann aus einer einzigen Codezeile und einer einzigen Variable bestehen. Betrachten Sie das folgende Codefragment.
count = 0
def cb(): # An interrupt callback
count += 1
def main():
# Code to set up the interrupt callback omitted
while True:
count += 1
Dieses Beispiel veranschaulicht eine subtile Fehlerquelle. Die Zeile count += 1 in der Hauptschleife birgt eine spezifische Race-Condition-Gefahr, die als Read-Modify-Write bekannt ist. Dies ist eine klassische Fehlerursache in Echtzeitsystemen. In der Hauptschleife liest MicroPython den Wert von count, addiert 1 dazu und schreibt ihn zurück. In seltenen Fällen tritt der Interrupt nach dem Lesen und vor dem Schreiben auf. Der Interrupt verändert count, doch seine Änderung wird von der Hauptschleife überschrieben, wenn die ISR zurückkehrt. In einem realen System könnte dies zu seltenen, unvorhersehbaren Fehlern führen.
Wie oben erwähnt, ist Vorsicht geboten, wenn eine Instanz eines eingebauten Python-Typs im Hauptcode verändert wird und auf diese Instanz in einer ISR zugegriffen wird. Der Code, der die Veränderung durchführt, sollte als kritischer Abschnitt betrachtet werden, um sicherzustellen, dass sich die Instanz in einem gültigen Zustand befindet, wenn die ISR läuft.
Besondere Vorsicht ist geboten, wenn ein Datensatz zwischen verschiedenen ISRs geteilt wird. Die Gefahr besteht hier darin, dass der Interrupt höherer Priorität auftreten kann, wenn der niedriger priorisierte die gemeinsam genutzten Daten nur teilweise aktualisiert hat. Der Umgang mit dieser Situation ist ein fortgeschrittenes Thema, das den Rahmen dieser Einführung sprengt, abgesehen von dem Hinweis, dass die unten beschriebenen Mutex-Objekte manchmal verwendet werden können.
Das Deaktivieren von Interrupts für die Dauer eines kritischen Abschnitts ist die übliche und einfachste Vorgehensweise, aber es deaktiviert alle Interrupts und nicht nur denjenigen, der potenziell Probleme verursachen kann. Es ist im Allgemeinen unerwünscht, einen Interrupt lange zu deaktivieren. Im Fall von Timer-Interrupts führt es zu Schwankungen beim Zeitpunkt, an dem ein Callback auftritt. Im Fall von Geräte-Interrupts kann es dazu führen, dass das Gerät zu spät bedient wird, mit möglichem Datenverlust oder Überlauffehlern in der Gerätehardware. Wie ISRs sollte auch ein kritischer Abschnitt im Hauptcode eine kurze, vorhersehbare Dauer haben.
Ein Ansatz zum Umgang mit kritischen Abschnitten, der die Zeit, für die Interrupts deaktiviert sind, drastisch reduziert, besteht darin, ein Objekt namens Mutex zu verwenden (der Name leitet sich vom Begriff des gegenseitigen Ausschlusses, engl. mutual exclusion, ab). Das Hauptprogramm sperrt den Mutex vor der Ausführung des kritischen Abschnitts und entsperrt ihn am Ende. Die ISR prüft, ob der Mutex gesperrt ist. Ist er es, meidet sie den kritischen Abschnitt und kehrt zurück. Die Designherausforderung besteht darin zu definieren, was die ISR tun soll, falls der Zugriff auf die kritischen Variablen verweigert wird. Ein einfaches Beispiel für einen Mutex findet sich hier. Beachten Sie, dass der Mutex-Code Interrupts zwar deaktiviert, aber nur für die Dauer von acht Maschinenbefehlen: Der Vorteil dieses Ansatzes besteht darin, dass andere Interrupts praktisch unbeeinträchtigt bleiben.
Interrupts und die REPL¶
Interrupt-Handler, wie etwa die mit Timern verbundenen, können auch nach dem Beenden eines Programms weiterlaufen. Dies kann zu unerwarteten Ergebnissen führen, wenn man erwartet hätte, dass das den Callback auslösende Objekt den Gültigkeitsbereich verlassen hätte. Zum Beispiel auf einer OpenMV Cam:
def bar():
foo = machine.Timer(-1, freq=4, callback=lambda t: print('.', end=''), hard=True)
bar()
Dies läuft so lange weiter, bis der Timer explizit deaktiviert oder das Board mit Ctrl-D zurückgesetzt wird.