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.
Um unliebsamen Überraschungen vorzubeugen sollten einige Grundregeln bei der Gestaltung der Interruptroutinen beachtet werden.
Die folgenden Ereignisse können einen Interrupt auf dem AVR auslösen, wobei die Reihenfolge der Auflistung auch die Priorität der Interrupts aufzeigt.
Der AT90S2313 verfügt über 2 Register welche mit den Interrupts zusammen hängen.
GIMSK |
General Interrupt Mask Register.
|
GIFR |
General Interrupt Flag Register.
|
MCUCR |
MCU Control Register.
|
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.
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.
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.
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.