|
|
Interrupt
[Bearbeiten] EinleitungBei bestimmten Ereignissen in Prozessoren wird ein Interrupt ausgelöst. Dabei wird das Programm unterbrochen und ein Unterprogramm, die sogenannte Interrupt Service Routine (ISR), aufgerufen. Wenn dieses beendet ist läuft das Hauptprogramm ganz normal weiter. Bei Mikrocontrollern werden Interrupts z. B. ausgelöst wenn:
[Bearbeiten] Wichtige Eigenschaften von ISRsISRs reagieren auf ein bestimmtes Ereignis, welches relativ oft oder selten passiert. Prinzipiell sollte man ISRs möglichst kurz halten und schnell beenden. Im Mittel muss die Interruptroutine kürzer sein als die Periodendauer des Ereignises, anderenfalls wird es passieren, dass Interrupts "verschluckt" werden, d.h. beim UART gehen Daten verloren, beim Timer gehen Zählzyklen verloren, beim AD-Wandler gehen Daten verloren etc. Solche verschluckten Interrupts sind bisweilen schwer zu finden, weil es nur sehr wenige in ganz bestimmten Konstellationen sind. Wenn dann eine per Timer realisierte Uhr in der Stunde um 1s falsch geht, merkt man das oft nicht. Langwierige Berechnungen, Auswertungen, Ausgaben oder gar Warteschleifen haben in ISRs nichts zu suchen. Typische C-Funktionen wie printf(), scanf(), längere Ausgaben auf ein LCD etc. sollte man nicht in ISRs vornehmen. Hier kommt sinnvollerweise eine andere Programmiertechnik zu Einsatz, nämlich die Übergabe von Parametern bzw. Steuersignalen an das Hauptprogramm. [Bearbeiten] InterruptsteuerungInterrupts müssen wie alle anderen Module und Funktionen eines Mikrocontrollers gesteuert werden. Dazu wird auf praktisch allen Mikrocontrollern ein zweistufiges System verwendet.
Dieses System hat eine Reihe von Vorteilen. So können sehr schnell und einfach alle Interrupts kurzzeitig gesperrt werden, wenn beispielsweise atomare Operationen durchgeführt werden sollen, oder besonders zeitkritische Abläufe ausgeführt werden. Danach können alle konfigurierten Interrupts einfach wieder freigeschaltet werden, ohne dass die CPU viele verschiedene Interruptmaskenbits verwalten müsste. Eine ISR wird demnach nur dann ausgeführt, wenn
[Bearbeiten] Verschachtelte InterruptsEinige Mikrocontroller, wie z. B. der AVR kennen nur zwei CPU-Zustände. Normale Programmausführung und Interruptausführung, gesteuert durch das I-Bit der CPU. Die normale Programmausführung kann jederzeit durch Interrupts unterbrochen werden. Die Interruptausführung kann nicht durch neue Interrupts unterbrochen werden. Die ISR wird erst zu Ende bearbeitet, zurück in die normale Programmausführung gesprungen und erst dann werden neue, wartende (engl. pending) Interrupts bearbeitet. Etwas komplexere Mikrocontroller oder große Prozessoren bieten verschiedene Interruptlevel (Stufen) an . Dabei gilt meist je niedriger die Zahl des Levels, um so höher die Priorität. Ein Interrupt mit höherer Priorität kann einen Interrupt mit niedriger Priorität unterbrechen. Ein Interrupt mit gleicher Priorität wie der gerade bearbeitete Interrupt kann das im allgemeinen nicht. Das nennt man verschachtelte Interrupts (engl. nested interrupts). Klassische Vertreter hierfür sind 8051, PowerPC, X86 und Motorola 68000. Auf dem AVR kann man verschachtelte Interrupts sowohl in Assembler als auch in C nachbilden, allerdings mit einigen Einschränkungen und Tücken. Das ist jedoch Leuten vorbehalten, die schon viel Erfahrung auf diesem Gebiet haben. Zu 99,9% braucht man sie nicht. [Bearbeiten] Zeitverhalten eines TimerinterruptsEin Timerinterrupt wird im allgemeinen dazu genutzt, in konstanten, periodischen Abständen bestimmte Funktionen aufzurufen. Es ist möglich, dass während eines Timerinterrupts derselbe Interrupt wieder aktiv wird, weil die Routine sehr verzweigt ist und dieses Mal sehr lange dauert. Wenn zum Beispiel der Timerinterrupt mit einer Periodendauer von 100ms aufgerufen wird, er aber unter bestimmten Umständen 180ms benötigt, dann wird nach 100ms nach Eintritt in die ISR der Interrupt wieder aktiv, das Timer Interrupt Flag wird gesetzt. Da aber gerade ein Interrupt bearbeitet wird, wird er nicht sofort angesprungen, weil währenddessen die Interruptfunktion global gesperrt ist (beim AVR ist das I-Bit in der CPU gelöscht). Der Interrupt wird zu Ende bearbeitet, die CPU springt zurück zum Hauptprogramm. Dabei werden die Interrupts wieder global eingeschaltet. Der zwischenzeitlich eingetroffene und zwischengespeicherte Interrupt wird nun sofort ausgeführt, sodass das Hauptprogramm praktisch gar nicht weiter kommt, bestenfalls einen Maschinenbefehl. Nun sind aber nur noch 20ms bis zum nächsten Timerinterrupt übrig. Wenn dieser nun wieder 180 ms benötigt werden in dieser Zeit aber zwei Interrupts ausgelöst, nach 20ms und 120ms. Da diese aber nicht gezählt oder andersweitig einzeln gespeichert werden können, geht ein Interrupt verloren. Das ist ein Programmfehler. [Bearbeiten] Zeitverhalten des UART EmpfangsinterruptsEin UART Interrupt zum Empfang von Daten per RS232 mit 115200 Baud ist ein recht häufiges Ereignis (1 Zeichen = 10 Bits = 86,8μs). Wenn kontinuierlich Daten empfangen werden, wird nach jeweils 86,8μs ein neuer Interrupt ausgelöst. Dabei wird das empfangene Datenbyte vom UART aus dem Empfangsschiebegregister in einem Puffer kopiert. Während das neue Zeichen Bit für Bit empfangen wird, wird es zunächst im Schieberegister des UART gespeichert. Die Daten im Puffer bleiben davon unberührt. Die CPU muss nun schnell das empfangene Datenbyte aus dem Empfangsbuffer auslesen. Die maximale Verzögerung, die sich die CPU erlauben kann von der Aktivierung des Interrupts bis zum tatsächlichen Auslesen des Datenregisters beträgt ziemlich genau die Übertragungszeit von einem Zeichen. Wenn bis dahin nicht das Zeichen von der CPU ausgelesen wurde, wird es vom UART überschrieben und ein Fehler im Statusregister des UART signalisiert (Overrun, Überlauf des Datenpuffers). Die UARTs in heutigen Mikrocontrollern haben mindestens ein Byte Puffer wie hier beschrieben. Die neueren AVRs haben sogar effektiv 3 Byte Puffer im UART, praktisch ein kleines FIFO, womit der Datenempfang besser gepuffert werden kann, wenn die CPU gerade mit anderen sehr wichtigen Dingen beschäftigt ist. D.h. kurzzeitig kann sich die CPU erlauben, die Übertragungszeit von bis zu drei Zeichen zu warten, ehe sie die Daten ausliest. Dann müssen sie aber sehr schnell hintereinander gelesen werden. Im Mittel hat die CPU aber nur die Übertragungszeit eines Zeichens zur Verfügung, um es abzuholen. [Bearbeiten] ZusammenfassungInterruptserviceroutinen:
[Bearbeiten] Interruptfeste Programmierung[Bearbeiten] Atomarer DatenzugriffVon einem atomaren (engl. atomic) Datenzugriff spricht man, wenn der Zugriff innerhalb einer nicht unterbrechbaren Maschinenanweisung abgearbeitet wird. Alle Variablen, Steuerregister und I/O-Ports, die sowohl im Hauptprogramm als auch in Interrupts verwendet werden, sind mit viel Sorgfalt zu behandeln. Beispiel:
übersetzt sich auf AVR-Prozessoren in
Wenn nun zwischen IN und OUT ein Interrupt auftritt, der beispielsweise Bit 7 verändert, dann geht mit dem OUT-Befehl diese Änderung verloren, da der OUT-Befehl den alten Zustand vor dem Interrupt wiederherstellt. Gefährlich ist das insbesondere deshalb, weil der Fall nur selten auftritt und dieses Verhalten sehr schlecht reproduzierbar ist. Bei verschiedenen Prozessor-Architekturen tritt das Problem verschieden häufig auf. So übersetzt sich obiger Code bei MSP430 Prozessoren in einen einzelnen Befehl OR #0x03,port und stellt somit kein Problem dar. Im Zweifel hilft nur ein Blick in den erzeugten Assembler-Code. Bei der Übernahme fremden Codes ist dies zu beachten. Was beim 8051 kein Problem war, kann beim AVR zu einem Problem werden, unter Umständen sogar abhängig vom verwendeten Port sein. Ein ähnliches Problem entsteht bei Variablen, deren Größe die Wortbreite der Maschine übersteigt. Bei 8-Bit-Prozessoren wie AVR oder 8051 also bereits bei normalen "int" Variablen. Diese Variablen werden zwangsläufig byteweise verarbeitet. Wenn genau dazwischen ein Interrupt erfolgt, wird ein falscher Wert gelesen. Wenn beispielsweise eine Interrupt-Routine einen 16-Bit-Zähler verwendet und von 0x00FF auf 0x0100 hochzählt, dann kann das Hauptprogramm auch schon mal versehentlich die Werte 0x01FF oder 0x0000 lesen. Dies ist auch ein Grund, weshalb für Programmierung auf derart "niedriger" Ebene Kenntnisse in Prozessorarchitektur und Assembler-Programmierung sehr hilfreich sind. Abhilfe: Wenn man sich nicht wirklich ganz sicher ist, sollten um kritische Aktivitäten herum jedesmal die Interrupts abgeschaltet werden. Beispiel (AVR-GCC):
Wenn man ein globales Einschalten der Interrupts mit sei() vermeiden will, kann man die folgende Methode benutzen. Hierbei werden die Interrupts nur eingeschaltet, wenn sie vorher bereits eingeschaltet waren (Hinweis aus der FAQ von avr-libc):
Je nach Prozessor kann man das Problem manchmal auch ohne Abschalten von Interrupts durch geeignete Programmierung lösen. So führt
immer zum beschriebenen Problem,
jedoch nicht, wenn die beiden Zeilen zu jeweils einem Assembler-Befehl übersetzt werden. Was dann aber abhängig von den Optimierungs-Einstellungen des Compilers werden kann. Eine Interrupt-feste Variante für AVR-Prozessoren der neuesten Generation, wie beispielsweise Tiny2313 und Mega88 (alle ab 2004):
[Bearbeiten] Reentrante FunktionenEine Funktion ist reentrant (wiedereintrittsfest), wenn sie mehrmals gleichzeitig aktiv sein kann, ohne dass sich diese Aufrufe gegenseitig beeinflussen. Betrifft beispielsweise Funktionen, die sowohl im Hauptprogramm als auch in Interrupts aufgerufen werden. Manche C Compiler erfordern eine besondere Kennzeichnung solcher Funktionen. Wenn möglich sollte man es jedoch vermeiden, eine Funktion aus dem Hauptprogramm und aus einem Interrupt aus aufzurufen. Das ist meist problemlos machbar. [Bearbeiten] Beispiele für die praktische ProgrammierungDie Beispiele sind mit WINAVR 20060421 compiliert und getestet worden. Als Mikrocontroller wird ein AVR vom Typ ATmega32 verwendet. Alle Programme wurden mit Optimierungsstufe -Os compiliert. [Bearbeiten] Steuersignale zwischen ISR und HauptprogrammIn vielen Anwendungen wird ein Timer verwendet, um in regelmäßigen Abständen bestimmte Aktionen auszuführen, wie z. B. Tasten abfragen, ADC-auslesen, ein LCD auffrischen etc. Wenn viele Dinge zu erledigen sind, nebenbei aber noch andere Interrupts verwendet werden, dann ist es notwendig die Funktionsaufrufe aus dem Timerinterrupt in die Hauptschleife zu verlagern. Der Interrupt signalisiert über eine Steuervariable (engl. Flag, Flagge), dass ein neuer Timerzyklus begonnen hat. Dadurch wird der Timerinterrupt sehr kurz und die langwierigen, aber meist nicht zeitkritischen Funktionen werden als normales Programm ausgeführt. Damit kann die CPU auf andere Interrupts schnell reagieren. Wichtig ist auf jeden Fall, dass die Steuervariable, welche in der ISR und in der Hauptschleife verwendet wird, mit volatile deklariert wird. Ausserdem müssen sowohl der Lese- als auch Schreibzugriff auf die Steuersignale atomar sein. Auf dem AVR ist das mit 8-Bit-Variablen direkt möglich, für grössere Variablen müssen die Interrupts kurzzeitig gesperrt werden. Das Beispiel ist sehr einfach gehalten um das Prinzip zu veranschaulichen. Ein Timer mit einer Überlaufperiodendauer von ca. 65ms stößt periodisch eine Funktion zum Togglen einer LED an, welche dadurch mit ca. 7 Hz blinkt.
[Bearbeiten] UART mit InterruptsDer UART ist ein oft benutztes Modul eines Mikrocontrollers. Anfänger nutzen ihn meist im sogenannten Polling Betrieb (engl. to poll, abfragen). D.h. wenn ein Zeichen empfangen werden soll, fragt eine Funktion den UART in einer Schleife ununterbrochen ab, ob Daten empfangen wurden. In dieser Zeit macht die CPU nichts anderes! Und wenn lange kein Zeichen eintrifft tut sie sehr lange nichts, sie ist praktisch blockiert! Senden verläuft ähnlich, nur dass hier die CPU vor dem Senden prüft, ob der UART ein neues Byte aufnehmen kann. D.h. während der UART selbsttätig das Zeichen sendet ist die CPU zum Warten verdammt. All diese Nachteile haben nur einen Vorteil. Die Funktionen und Mechanismen zur UART-Nutzung sind sehr einfach, klein und leicht anwendbar. Will man aber die CPU nicht sinnlos warten lassen, was vor allem bei niedrigeren Baudraten ziemlich lange sein kann, muss man die Interrupts nutzen. Der AVR hat gleich drei davon.
Bei Nutzung der Interrupts kann die CPU andere Dinge bearbeiten und muss nur kurz einen Interrupt ausführen, wenn ein Zeichen empfangen oder gesendet wurde. Die Kommunikation zwischen ISRs und Hauptschleife erfolgt wieder durch Flags und zwei Pufferarrays (uart_rx_buffer und uart_tx_buffer). Es gibt zwei Funktionen, eine zum Senden von Strings, eine zum Empfangen. Das Senden sowie Empfangen kann parallel erfolgen und läuft vollkommen unabhängig vom Hauptprogramm. Die Daten werden in spezielle Puffer kopiert, sodass das Hauptprogramm mit seinen Strings sofort weiterarbeiten kann. Im Beispiel ist die CPU nicht wirklich mit sinnvollen Dingen beschäftigt, zur Demonstration des Prinzips aber ausreichend. Um das das Programm real zu nutzen braucht man ein Terminalprogramm, z. B. Hyperterminal von Windows. Dort muss nur die richtige Baudrate eingestellt werden (9600 8N1, 9600 Baud, 8 Bits, keine Parität, 1 Stopbit, keine Flusskontrolle). Ausserdem muss man im Menu Datei -> Eigenschaften -> Einstellungen -> ASCII Konfiguration den Punkt "Eingegebene Zeichen lokal ausgeben (lokales Echo)" aktivieren. Nun kann man beliebige Texte eintippen. Mit RETURN wird die Eingabe abgeschlossen und der AVR vermittelt den empfangenen String an das Hauptprogramm. Diese sendet ihn einfach zurück, parall dazu wird der String gemorst per LED angezeigt. Sollte es Probleme bei der Inbetriebnahme des UART geben, so findet man hier wichtige Hinweise zur Fehlersuche.
[Bearbeiten] Wie lange dauert meine Interruptroutine?Diese Frage sollte man beantworten können, zumindest sollte eine Worst-Case-Abschätzung gemacht werden. Das geht auf zwei Wegen.
Als Hilfsmittel zur Fehlersuche kann man auch am Ende der ISR prüfen, ob das jeweilige Interrupt-Request-Bit schon wieder gesetzt ist. Wenn ja, dann ist die ISR in den meisten Fällen zu lang. Auch hier kann man einen Ausgang auf HIGH setzen und somit den Fehler anzeigen. [Bearbeiten] Siehe auch |