Funktionsweise

Interrupt-Typen

Schalter, Sensoren und andere externe Komponenten können mit einer MCU über Leiterbahnen bzw. Kabel verbunden sein. Sie sind üblicherweise an I/O-Pins der MCU angeschlossen. Wenn ein Abstandssensor einen Interrupt auslöst, ändert er die Spannung an einem I/O-Pin. Wenn der Pegel eines I/O-Pins von High auf Low oder von Low auf High geändert wird, kann dies einen Interrupt auslösen. Diese Art von Interrupt wird als „Attached Interrupt“ oder Pin-Interrupt bezeichnet, weil der Sensor kein Teil der MCU ist, sondern über einen I/O-Pin mit der MCU verbunden (attached) ist.

Die interne Peripherie einer MCU wie Timer oder Kommunikationseinheiten können „Peripheral Interrupts“ auslösen. Kommt z. B. eine Nachricht vom PC über den Com-Port rein, löst das einen Interrupt aus. Weil die Kommunikationsschnittstelle Teil des ICs der MCU ist, liegt hier ein interner (Peripheral) Interrupt vor. Mit Timern können Sie in regelmäßigen Zeitabständen oder nach einer spezifischen Zeit Interrupts auslösen. Die Hardware eines Timers befindet sich im IC der MCU, deshalb ist auch dies ein Peripheral Interrupt.

Ein Abstandssensor kann also mit einem peripheren Timer-Interrupt regelmäßig ausgelesen werden. Ein Kollisionssensor kann im Notfall einen Attached Interrupt an einem I/O-Pin auslösen.

Interrupt-Quellen

In einem embedded System gibt es viele Ereignisse, die das Hauptprogramm unterbrechen können. Jedes Ereignis wird einer Interrupt-Quelle in der MCU zugeordnet. Eine Quelle ist z. B. ein I/O-Pin, ein Timer oder der Com-Port. Eine MCU hat i. A. mehrere I/O-Pins, die bei externen Ereignissen einen Attached Interrupt auslösen können. Es gibt oft mehrere Timer und verschiedene potentielle Kommunikationskanäle. Folgende Ereignisse lösen beim Arduino Uno Interrupts aus:

– Pin-Changes (Änderungen Low <-> High)

– Timer-Interrupts

– Watchdog-Timer

– Kommunikation (SPI, I2C, USART)

– ADC Konversion abgeschlossen

– Speicher bereit zur Nutzung (EEPROM oder Flash)

In der Vorlesung werden nur oberen beiden Interrupts behandelt. Für die übrigen gelten die gleichen grundlegenden Überlegungen.

Aktivierung

Beim Arduino sind Interrupts per Default inaktiv. Interrupts müssen im Quellcode enabled oder aktiviert werden, bevor sie wirken. Ist ein Interrupt disabled oder deaktiviert, reagiert der Prozessor nicht auf das zugehörige Ereignis. Das Aktivieren oder Deaktivieren einzelner Interrupts wird auch als „Maskieren“ bezeichnet. Interrupts werden über Register maskiert.

Es gibt ein globales Flag zum Aktivieren oder Deaktivieren aller Interrupts. Zusätzlich gibt es für jeden Interrupt ein individuelles Flag, über das er aktiviert wird. Ein Interrupt ist erst aktiv, wenn sein individuelles Aktivierungs-Flag und das globale Interrupt-Enable-Flag gesetzt sind.

Alle Interrupts werden global mit dem Kommando sei() aktiviert. Mit dem Kommando cli() werden sie global deaktiviert. Ein Timer-Interrupt von Timer 1 wird z. B. im Register TIMSK1 aktiviert. Für solche speziellen Informationen müssen wir das Datenblatt der MCU lesen, die Register unterscheiden sich von MCU zu MCU in der Bezeichnung. Mehr dazu in der Vorlesung.

Einige Interrupts können nicht deaktiviert werden. Sie sind immer aktiv. Diese werden als NMI (Non Maskable Interrupts) bezeichnet. Sie sind für sicherheitskritische Funktionen gedacht. Wenn wir z. B. die Kollisionserkennung als sicherheitskritisch erachten, dann sollten wir den Abstandssensor an einen I/O-Pin mit NMI anschließen. Egal was im Code gerade passiert, dieser Interrupt wird immer ausgeführt.

Interrupt Service Routine

Die Funktionen, die bei einem Ereignis abgearbeitet werden sollen, stehen im Quelltext in „Interrupt Service Routinen“ (ISR). Unterbricht ein Interrupt das Hauptprogramm, wird eine ISR ausgeführt. Jede Interrupt-Wuelle ist eine ISR zugeordnet, die der Programmierer mit Inhalt füllen kann.

In der ISR beim Eier kochen würde z. B. stehen, dass Sie aufstehen, die Eier aus dem Topf nehmen und sich anschließend wieder hinsetzen. Die IRS des Abstandssensors zur Kollisionserkennung stoppt z. B. den Antrieb des Roboters. In der ISR des Timers wird eine Abstandsmessung für die Fahrtrichtungsoptimierung ausgeführt.

Interrupt Request

Wenn ein Interrupt aktiv ist und das zugehörige Ereignis eintritt, wird in einem Register ein Interrupt-Request-Flag oder Interrupt-Flag gesetzt. Üblicherweise ändert sich dann ein der Interrupt-Nummer zugeordnetes Bit in einem Register von 0 auf 1 oder von 1 auf 0. Der Prozessor springt anschließend in die zugehörige ISR.

Nachdem der Interrupt abgearbeitet worden ist, springt der Prozessor wieder in das Hauptprogramm zurück. In vielen Prozessoren muss der Interrupt-Request im Quellcode explizit gelöscht werden. Ansonsten springt der Prozessor direkt wieder in die zugehörige ISR. Das ist ein klassischer Programmierfehler mit Interrupts. Wenn ein Interrupt-Flag manuell gelöscht werden muss, dann sollten Sie dies in der ISR tun. Welche Interrupts dies betrifft steht im Datenblatt einer MCU.

Ablauf eines Interrupts

Wenn ein Interrupt-Request vorliegt, sichert die MCU zuerst die Werte wichtigen Prozessorregister. Die MCU muss sicherstellen, dass nach dem Interrupt das Hauptprogramm fehlerfrei weiterlaufen kann. Manche Prozessoren weichen für die Bearbeitung eines Interrupts auch auf andere Register aus. Dann existieren die zentralen Prozessorregister mehrfach parallel. Die MCU friert Ihren Stand vor Bearbeitung des Interrupts ein.

Anschließend führt die MCU die ISR aus. Ist sie damit fertig, stellt sie die vorherigen Werte der Prozessorregister wieder her. Sie setzt die Bearbeitung des Quellcodes an der vorherigen Stelle fort. Hier nochmal der Ablauf in Kurzfassung:

1. Speichern des aktuellen Zustands: Arbeitsregister und Quellcode-Zeile des Programms werden gespeichert

2. Sprung im Quellcode zur ISR

3. Abarbeiten der ISR

4. Wiederherstellen des Zustands vor dem Interrupt

5. Rücksprung zum Programm-Code

6. Weiter Abarbeiten des Programmcodes

Priorisierung

Es können mehrere Ereignisse gleichzeitig auftreten. Oder es kann ein neues Ereignis auftreten, während gerade die ISR eines anderen alten Interrupts abgearbeitet wird. Die Prozessoren gehen mit diesem Problem unterschiedlich um.

Bei vielen MCUs können sich Interrupts gegenseitig unterbrechen. Dafür ist es notwendig zu priorisieren, welcher Interrupt andere Interrupts unterbrechen darf und welcher Interrupt warten muss, bis eine andere ISR fertig ausgeführt worden ist. Interrupts höherer Priorität unterbrechen die ISRs von Interrupts niedrigerer Priorität. Die Priorität jedes Interrupts wird in Registern festgelegt.

Speichern und Wiederherstellen von Prozessorregistern werden kompliziert, wenn Interrupts sich gegenseitig unterbrechen. Dann müssen das Speichern und Wiederherstellen verschachtelt mehrfach funktionieren. Darauf gehe ich hier nicht weiter ein.

Die Kollisionserkennung ist die wichtigste Funktion des Roboters. Sie sollte deshalb die höchste Priorität bekommen. Wenn der Abstand zu einem Hindernis ein paar Mikrosekunden später gemessen wird als geplant, dann ist das nicht schlimm. Deshalb sollte der zugehörige Timer-Interrupt eine niedrigere Priorität bekommen. Damit wird sichergestellt, dass das Abschalten des Antriebs im Notfall niemals von einer Abstandsmessung zur Fahrtrichtungsoptimierung unterbrochen wird.

Bei einem ATMEGA328P im Aruino Uno wird immer nur ein Interrupt ausgeführt. Interrupts können nicht unterbrochen werden. Tritt ein zweiter Interrupt auf, während ein erster Interrupt noch läuft, wird der erste Interrupt nicht unterbrochen. Dieses Verfahren ist unflexibel, aber relativ sicher. Eine Priorisierung von Interrupts ist deshalb beim ATMEGA328 nicht notwendig.

Der Prozessor im ATMEGA328 schaltet die Interrupts während der Bearbeitung einer ISR automatisch aus. Man kann Interrupts auf einem ATMEGA328 in einer ISR auch wieder aktivieren. Dann brauchen Sie auch wieder eine Priorisierung. Das ist aber gerade für Anfänger keine gute Idee. Lassen Sie besser die Finger davon.

Da wir mit einem ATMEGA328 arbeiten, müssen wir damit leben, dass die Kollisionserkennung im Zweifel so lange warten muss, bis der Interrupts der Abstandsmessung fertig ist. Aus diesem Grund optimieren wir den Interrupt zur Abstandsmessung auf geringe Laufzeit.

Beachten Sie beim ATMEGA328, dass in einer ISR keine anderen Interrupts ausgeführt werden. Deshalb funktionieren alle Arduino-Funktionen nicht in ISRs, die selbst Interrupts nutzen. Dazu zählem die COM-Port Kommunikation wie serial.write() oder serial.read() und Warte-Befehle wie z. B. delay().

Latenz

Die Zeit zwischen dem Auftreten eines Ereignisses und der Reaktion der MCU wird als Latenz bezeichnet. Latenz entsteht u. A. durch das Sichern des aktuellen Zustands. Komplexe Rechenoperationen werden oft noch beendet, bevor ein Interrupt ausgeführt wird. Eine gute MCU weist eine geringe Latenz auf. Die Latenz kann i. A. dem Datenblatt entnommen werden. Sie beiträgt beim Arduino Uno wenige Microsekunden. Das ist für unsere Kollisionserkennung ausreichend.

Variablen in einer ISR

Einschub für Fortgeschrittene: Stellen Sie sich vor, der Abstand zum nächsten Hindernis wird in der Variable „Abstand“ gespeichert. Ihr Hauptprogramm liest regelmäßig die Variable „Abstand“, um daraus die optimale Fahrtrichtung zu ermitteln.

Das Hauptprogramm hat den Abstandswert aus der Variablen „Abstand“ gerade in ein Prozessorregister geladen. Bevor der Prozessor mit dem Abstandswert die Fahrtrichtung ausrechnen kann, tritt ein Timer-Interrupt auf. Der Abstandswert im Prozessorregister wird vor Ausführung der Timer-ISR gesichert.

In der Timer-ISR wird ein neuer Abstandswert ermittelt und in die Variable „Abstand“ geschrieben. Nach dem Abarbeiten der ISR werden die Prozessorregister wiederhergestellt. Im Prozessorregister liegt jetzt der alte Abstandswert, in der Variable „Abstand“ liegt der neue Wert. Der Prozessor rechnen die Fahrtrichtung also mit dem falschen alten Abstandswert aus.

Was können wir tun? Wenn Variablen als „volatile“ deklariert werden, dann liest der Prozessor sie unmittelbar vor jeder Nutzung neu aus dem Speicher aus. Also aktualisiert der Prozessor den Wert seines Prozessorregisters unmittelbar vor jeder Nutzung aus dem Speicher bzw. der Variable. Deklarieren wir also die Variable „Abstand“ als volatile, weil sie im Interrupt und im Hauptprogramm genutzt wird. Der Prozessor in unserem Beispiel berechnet dann die Fahrtrichtung mit dem neuen Abstandswert, der im Timer-Interrupt ermittelt worden ist.

Interrupt-Regeln

Folgende Regeln sollten bei der Programmierung von ISRs befolgt werden:

1. Wenig Code, keep is short

2. Nutzen Sie (beim ATMEGA328) keine Funktionen, die selbst Interrupts benötigen

3. Deklarieren Sie Variablen, die auch in ISRs genutzt werden, als „volatile“



Weiterführende Informationen:

ATMEGA328P Datenblatt

MCU-Hersteller Renesas

Guter Post zu Interrupts

arduino.cc Attached Interrupts

Timer-Interrupts