Programmieren mit Interrupts

Nachdem wir nun alles Wissenswerte für die serielle Programmerstellung gelernt haben nehmen wir jetzt ein völlig anderes Thema in Angriff, nämlich die Programmierung unter Zuhilfenahme der Interrupts des AVR.

Als erstes wollen wir uns noch einmal den allgemeinen Programmablauf bei der Interrupt-Programmierung zu Gemüte führen.

Man sieht, dass die Interruptroutine quasi parallel zum Hauptprogramm abläuft. Da wir nur eine CPU haben ist es natürlich keine echte Parallelität, sondern das Hauptprogramm wird beim Eintreffen eines Interrupts unterbrochen, die Interruptroutine wird ausgeführt und danach erst wieder zum Hauptprogramm zurückgekehrt.

Anforderungen an die Interrupt-Routine

Um unliebsamen Überraschungen vorzubeugen sollten einige Grundregeln bei der Gestaltung der Interruptroutinen beachtet werden.

Interrupt-Quellen

Die folgenden Ereignisse können einen Interrupt auf dem AVR auslösen, wobei die Reihenfolge der Auflistung auch die Priorität der Interrupts aufzeigt.

Register

Der AT90S2313 verfügt über 2 Register welche mit den Interrupts zusammen hängen.

GIMSK

General Interrupt Mask Register.

Bit 7 6 5 4 3 2 1 0
Name INT1 INT0 - - - - - -
R/W R/W R/W R R R R R R
Initialwert 0 0 0 0 0 0 0 0
INT1 External Interrupt Request 1 Enable
Wenn dieses Bit gesetzt ist wird ein Interrupt ausgelöst wenn am INT1-Pin eine steigende oder fallende (je nach Konfiguration im MCUCR) Flanke erkannt wird.
Das Global Enable Interrupt Flag muss selbstverständlich auch gesetzt sein.
Der Interrupt wird auch ausgelöst, wenn der Pin als Ausgang geschaltet ist. Auf diese Weise bietet sich die Möglichkeit, Software-Interrupts zu realisieren.
INT0 External Interrupt Request 0 Enable
Wenn dieses Bit gesetzt ist wird ein Interrupt ausgelöst wenn am INT0-Pin eine steigende oder fallende (je nach Konfiguration im MCUCR) Flanke erkannt wird.
Das Global Enable Interrupt Flag muss selbstverständlich auch gesetzt sein.
Der Interrupt wird auch ausgelöst, wenn der Pin als Ausgang geschaltet ist. Auf diese Weise bietet sich die Möglichkeit, Software-Interrupts zu realisieren.

GIFR

General Interrupt Flag Register.

Bit 7 6 5 4 3 2 1 0
Name INTF1 INTF0 - - - - - -
R/W R/W R/W R R R R R R
Initialwert 0 0 0 0 0 0 0 0
INTF1 External Interrupt Flag 1
Dieses Bit wird gesetzt, wenn am INT1-Pin eine Interrupt-Kondition, entsprechend der Konfiguration, erkannt wird. Wenn das Global Enable Interrupt Flag gesetzt ist wird die Interruptroutine angesprungen.
Das Flag wird automatisch gelöscht, wenn die Interruptroutine beendet ist.
Alternativ kann das Flag gelöscht werden, indem der Wert 1(!) eingeschrieben wird.
INTF0 External Interrupt Flag 0
Dieses Bit wird gesetzt, wenn am INT0-Pin eine Interrupt-Kondition, entsprechend der Konfiguration, erkannt wird. Wenn das Global Enable Interrupt Flag gesetzt ist wird die Interruptroutine angesprungen.
Das Flag wird automatisch gelöscht, wenn die Interruptroutine beendet ist.
Alternativ kann das Flag gelöscht werden, indem der Wert 1(!) eingeschrieben wird.

 

MCUCR

MCU Control Register.
Das MCU Control Register enthält Kontrollbits für allgemeine MCU-Funktionen.

Bit 7 6 5 4 3 2 1 0
Name - - SE SM ISC11 ISC10 ISC01 ISC00
R/W R R R/W R/W R/W R/W R/W R
Initialwert 0 0 0 0 0 0 0 0
SE Sleep Enable
Dieses Bit muss gesetzt sein, um den Controller mit dem SLEEP-Befehl in den Schlafzustand versetzen zu können.
Um den Schlafmodus nicht irrtümlich einzuschalten wird empfohlen, das Bit erst unmittelbar vor Ausführung des SLEEP-Befehls zu setzen.
SM Sleep Mode
Dieses Bit bestimmt der Schlafmodus.
Ist das Bit gelöscht so wird der Idle-Modus ausgeführt.
Ist das Bit gesetzt so wird der Power-Down-Modus ausgeführt.
ISC11
ISC10
Interrupt Sense Control 1 Bits
Diese beiden Bits bestimmen, ob die steigende oder die fallende Flanke für die Interrupterkennung am INT1-Pin ausgewertet wird.
ISC11 ISC10 Bedeutung
0 0 Low Level an INT1 erzeugt einen Interrupt.
In der Beschreibung heisst es, der Interrupt wird getriggert, solange der Pin auf 0 bleibt, also eigentlich unbrauchbar.
0 1 Reserviert
1 0 Die fallende Flank an INT1 erzeugt einen Interrupt.
1 1 Die steigende Flanke an INT1 erzeugt einen Interrupt.
ISC01
ISC00
Interrupt Sense Control 0 Bits
Diese beiden Bits bestimmen, ob die steigende oder die fallende Flanke für die Interrupterkennung am INT0-Pin ausgewertet wird.
ISC01 ISC00 Bedeutung
0 0 Low Level an INT0 erzeugt einen Interrupt.
In der Beschreibung heisst es, der Interrupt wird getriggert, solange der Pin auf 0 bleibt, also eigentlich unbrauchbar.
0 1 Reserviert
1 0 Die fallende Flank an INT0 erzeugt einen Interrupt.
1 1 Die steigende Flanke an INT0 erzeugt einen Interrupt.

 

Allgemeines über die Interrupt-Abarbeitung

Wenn ein Interrupt eintrifft wird automatisch das Global Interrupt Enable Bit im Status Register SREG gelöscht und alle weiteren  Interrupts unterbunden. Obwohl es möglich ist, zu diesem Zeitpunkt bereits wieder das I-bit zu setzen rate ich dringend davon ab. Dieses wird nämlich automatisch gesetzt, wenn die Interruptroutine beendet wird. Wenn in der Zwischenzeit weitere Interrupts eintreffen werden die zugehörigen Interrupt-Bits gesetzt und die Interrupts bei Beendigung der laufenden Interrupt-Routine in der Reihenfolge ihrer Priorität ausgeführt. Dies kann eigentlich nur dann zu Problemen führen, wenn ein hoch priorisierter Interrupt ständig und in kurzer Folge auftritt. Dieser sperrt dann womöglich alle anderen Interrupts mit niedrigerer Priorität. Dies ist einer der Gründe, weshalb die Interrupt-Routinen sehr kurz gehalten werden sollen.

Das Status-Register

Es gilt auch zu beachten, dass das Status-Register während der Abarbeitung einer Interruptroutine nicht automatisch gesichert wird. Falls notwendig muss dies vom Programmierer selber vorgesehen werden.

Interrupts mit dem AVR GCC Compiler (WinAVR)

Selbstverständlich können alle Interruptspezifischen Registerzugriffe wie gewohnt über I/O-Adressierung vorgenommen werden. Etwas einfacher geht es jedoch, wenn wir die vom Compiler zur Verfügung gestellten Mittel einsetzen.
Damit diese Mittel zur Verfügung stehen müssen wir die Includedatei interrupt.h einbinden mittels

#include <avr/interrupt.h>
oder
#include <avr\interrupt.h>

Und dann kann's losgehen

sei ();
Das Makro sei() schaltet die Interrupts ein. Eigentlich wird nichts anderes gemacht als das Global Interrupt Enable Bit im Status Register gesetzt.

cli ();
Das Makro cli() schaltet die Interrupts aus oder anders gesagt, das Global Interrupt Enable Bit im Status Register wird gelöscht.

timer_enable_int (unsigned char ints);
Schaltet Timerbezogene Interrupts ein bzw. aus.
Wenn als Argument ints der Wert 0 übergeben wird so werden alle Timerinterrupts ausgeschaltet, ansonsten muss in ints angegeben werden, welche Interrupts zu aktivieren sind. Dabei müssen einfach die entsprechend zu setzenden Bits definiert werden.
Beispiel: timer_enable_int (1 << TOIE1));
Achtung: Wenn ein Timerinterrupt eingeschaltet wird während ein anderer Timerinterrupt bereits läuft, dann müssen beide Bits angegeben werden sonst wird der andere Timerinterrupt versehentlich ausgeschaltet.

enable_external_int (unsigned char ints);
Schaltet die externen Interrupts ein bzw. aus.
Wenn als Argument ints der Wert 0 übergeben wird so werden alle externen Interrrups ausgeschaltet, ansonsten muss in ints angegeben werden, welche Interrupts zu aktivieren sind. Dabei müssen einfach die entsprechend zu setzenden Bits definiert werden.
Beispiel: enable_external_int ((1<<INT0) | (1<<INT1));
Schaltet die externen Interrupts 0 und 1 ein.

Nachdem nun die Interrupts aktiviert sind braucht es selbstverständlich noch den auszuführenden Code, der ablaufen soll wenn ein Interrupt eintrifft.
Dazu gibt es zwei Definitionen welche allerdings AVR-GCC spezifisch sind und bei anderen Compilern womöglich anders heissen können.

SIGNAL(signame);

Mit SIGNAL wird eine Funktion für die Bearbeitung eines Interrupts eingeleitet. Als Argument muss dabei die Benennung des entsprechenden Interruptvektoren angegeben werden. Diese sind in den jeweiligen Includedateien IOxxxx.h zu finden. Mögliche Funktionsrümpfe für solche Interruptfunktionen sind zum Beispiel:

SIGNAL (SIG_INTERRUPT0)
{
    // Hier kommt der Code hin
}

SIGNAL (SIG_OVERFLOW1)
{
    // Und hier kommt auch Code hin
}

SIGNAL (SIG_UART_RECV)
{
    // rate mal was hier hin kommt
}

// und so weiter und so fort...

Während der Ausführung des Funktion sind alle weiteren Interrupts automatisch gesperrt. Beim Verlassen der Funktion werden die Interrupts wieder zugelassen.
Sollte während der Abarbeitung der Interruptroutine ein weiterer Interrupt (gleiche oder andere Interruptquelle) auftreten so wird das entsprechende Bit im zugeordneten Interrupt Flag Register gesetzt und die entsprechende Interruptroutine automatisch nach dem Beenden der aktuellen Funktion aufgerufen.
Ein Problem ergibt sich eigentlich nur dann, wenn während der Abarbeitung der aktuellen Interruptroutine mehrere gleichartige Interrupts auftreten. Die entsprechende Interruptroutine wird im Nachhinein zwar aufgerufen jedoch wissen wir nicht, ob nun der entsprechende Interrupt einmal, zweimal oder gar noch öfter aufgetreten ist. Deshalb soll hier noch einmal betont werden, dass Interruptroutinen so schnell wie nur irgend möglich wieder verlassen werden sollten.

INTERRUPT (signame);

Mit INTERRUPT wird genau gleich gearbeitet wie mit SIGNAL. Der Unterschied ist derjenige, dass bei INTERRUPT beim Aufrufen der Funktion das Global Enable Interrupt Bit automatisch wieder gesetzt und somit weitere Interrupts zugelassen werden. Dies kann zu nicht unerheblichen Problemen von im einfachsten Fall einem Stack overflow bis zu sonstigen unerwarteten Effekten führen und sollte wirklich nur dann angewendet werden wenn man sich absolut sicher ist, das ganze auch im Griff zu haben.

Was tut das Hauptprogramm

In einfachsten Fall gar nichts mehr.
Es ist also durchaus denkbar, ein Programm zu schreiben, welches in der main-Funktion lediglich noch die Interrupts aktiviert und dann in eine Endlosschleife folgender Art verzweigt:

for (;;) {}

Normalerweise wird man allerdings in den Interruptroutinen die Interrupts erfassen und im Hauptprogramm dann gemütlich auswerten.

Wir wir im bisherigen Kursverlauf gesehen haben ist es ohnehin mit so schnellen Controller meistens gar nicht unbedingt notwendig mit Interruptfunktionen zu arbeiten.
Es ist allerdings auch zu bemerken, dass mit den Interruptroutinen ein Programm sehr schön strukturiert werden kann, wenn man es richtig macht.