Interrupt
Bei bestimmten Ereignissen in Prozessoren wird ein Interrupt (Unterbrechungsanforderung) registriert.
Bei Mikrocontrollern werden Interrupts z. B. ausgelöst wenn:
- sich der an einem bestimmten Eingangs-Pin anliegende Pegel ändert
- eine vorher festgelegte Zeitspanne abgelaufen ist (Timer)
- eine serielle Übertragung abgeschlossen ist
- eine Messung des Analog-Digital-Wandlers abgeschlossen ist
Die Registrierung eines Interrupts setzt ein passend zum Ereignis benanntes Interruptflag in Form eines Bits in einem speziellen Statusregister. Bei der Behandlung des Interrupts wird das Anwendungsprogramm unterbrochen, das auslösende Interruptflag gelöscht und ein Unterprogramm, die sogenannte Interrupt Service Routine (ISR), aufgerufen. Wenn dieses beendet ist, läuft das Anwendungsprogramm ganz normal weiter.
Wichtige Eigenschaften von ISRs
ISRs reagieren auf ein bestimmtes Ereignis, welches relativ oft oder selten passiert. Prinzipiell sollte man ISRs möglichst kurzhalten und schnell beenden.
Im Mittel muss die Interruptroutine kürzer sein als die Periodendauer des Ereignisses, andernfalls 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 daher in ISRs nichts zu suchen. Auch typische C-Funktionen wie printf(), scanf(), längere Ausgaben auf ein LCD etc. sollte man nicht in ISRs vornehmen.
Stattdessen kommt bei Interruptbetrieb sinnvollerweise eine andere Programmiertechnik zum Einsatz, nämlich die Übergabe von Parametern bzw. Steuersignalen an das Hauptprogramm. Hierbei ist wichtig, dass die Steuervariable ("Flag"), welche gemeinsam im Interrupt-Programmteil und im Nicht-Interrupt-Programmteil verwendet wird, mit dem C Schlüsselwort volatile deklariert wird. Dadurch wird sichergestellt, dass jeder Zugriff auf die Variable im Code auch in die entsprechenden Maschinenbefehle umgesetzt wird und nicht wegoptimiert wird, weil sich die Variable in einem der beiden unabhängigen Programmteile scheinbar nicht ändert. Außerdem müssen sowohl der Lese- als auch Schreibzugriff auf Steuervariablen ununterbrechbar (atomar) sein.
Interruptsteuerung
Interrupts müssen wie alle anderen Module und Funktionen eines Mikrocontrollers gesteuert werden. Dazu wird auf praktisch allen Mikrocontrollern ein zweistufiges System verwendet.
- Globale Interruptsteuerung über ein CPU-Statusbit: Beim AVR ist das das I-Bit (Interrupt) im Statusregister (SREG). Dieses Bit wirkt wie ein Hauptschalter und kann global die Ausführung aller Interrupts ein - und ausschalten. Das heisst aber nicht, dass während der Zeit der inaktiven Interrupts diese verloren gehen. Vielmehr wird das jeweilige Interruptbit gesetzt, und wenn die Interrupts wieder freigegeben werden wird der Interrupt ausgeführt. Verloren gehen Interrupts erst dann, wenn die Sperrzeit zu groß ist und währenddessen mehr als ein Interrupt vom selben Typ eintrifft. Siehe Beispiel 1 und Beispiel 2.
- Lokale Interruptsteuerung für jede einzelne Interruptquelle über Maskenbits in mehreren Interruptmaskenregistern. Hier kann jede einzelne Interruptquelle individuell ein- und ausgeschaltet werden.
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
- die Interrupts global freigeschaltet sind
- das individuelle Maskenbit gesetzt ist
- der Interrupt eintritt
Verschachtelte Interrupts
Einige 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 PIC18, 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.
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.
- Simulation, dabei muss in einer verzweigten ISR der längste Pfad simuliert werden. Dazu müssen alle beteiligten Variablen auf den ensprechenden Wert gesetzt werden.
- Messung mit dem Oszilloskop, dabei wird zum Beginn der ISR ein Pin auf HIGH gesetzt und am Ende auf LOW. Damit kann man in Echtzeit die Dauer der ISR messen. Die zusätzlichen Taktzyklen zum Aufruf und verlassen der ISR sind konstant und im wesentlichen bekannt. Mit einem modernen Digitaloszilloskop und dem "Infinite Persistence Mode" kann man eine Worst-Case-Messung vornehmen
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.
Zeitverhalten eines Timerinterrupts
Ein 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.
Zeitverhalten des UART Empfangsinterrupts
Ein 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.
Zusammenfassung
Interruptserviceroutinen:
- sollten so kurz wie möglich gehalten werden
- können im Einzelfall nahezu doppelt so lange dauern wie die kürzeste Periodendauer des Ereignisses, ohne dass Interrupts verloren gehen (z. B. Timerinterrupt).
- dürfen im Mittel maximal solange dauern wie die kürzeste Periodendauer des Ereignisses
- dürfen maximal solange dauern, wie die kürzeste Periodendauer des Ereignisses, wenn man auf Nummer sicher gehen will, dass keine Interrupts verschluckt werden
- Die Interruptzeit versteht sich immer abzüglich einer kleinen Reserve für das Anspringen und Verlassen des Interrupt minus Panikreserve
Interruptfeste Programmierung
Atomarer Datenzugriff
Von einem atomaren Zugriff (engl. atomic access) spricht man, wenn der Zugriff innerhalb einer nicht unterbrechbaren Instruktionsfolge 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:
port |= 0x03;
übersetzt sich auf AVR-Prozessoren in
IN r16,port
ORI r16,0x03
OUT port,r16
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 (einige Ports bzw. I/O Register können mit den atomaren Befehlen sbi, cbi zum Setzen und Löschen einzelner Bits erreicht werden, andere nicht).
Ein ähnliches Problem entsteht bei Variablen, deren Größe die Wortbreite der Maschine übersteigt, unter Umständen auch Bitfeld-Zugriffe. 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. Ein Beispiel.
/* var wird in einer ISR inkrementiert */
volatile int var;
int get_var (void)
{
/* Folgende Zuweisung besteht aus mehr als einem Maschinenbefehl */
return var;
}
Das Lesen von var ist in C nur ein Befehl, in Assembler werden aber mindestens 2 Befehle benötigt, da pro Maschinen-Befehl nur 8 Bit bewegt werden können. Die Zuweisung kann also unterbrochen werden. Das kann dazu führen, daß ein Teil alter Bits und ein Teil neuer Bits zugewiesen wird:
- var sei 0x00FF
- Das Lowbyte von var wird für die return-Anweisung gelesen: 0xFF
- Ein Interrupt inkrementiert var um 1 auf 0x0100
- Das Highbyte von var wird in für die return-Anweisung gelesen: 0x01
- Die Funktion gibt 0x01FF zurück
Dies ist auch ein Grund, weshalb für Programmierung auf derart maschinennaher Ebene Kenntnisse in Prozessorarchitektur und Assembler-Programmierung sehr hilfreich sind.
Abhilfe: Wenn man sich nicht wirklich ganz sicher ist, müssen um kritische Aktivitäten herum jedesmal die Interrupts abgeschaltet werden.
Beispiel (AVR-GCC):
// Manuelle Methode
cli(); // Interrupts abschalten
port |= 0x03;
sei(); // Interrupts wieder einschalten
// Per Macros aus dem avr gcc, schöner lesbar und bequemer
// siehe Doku der avr-libc, Abschnitt <util/atomic.h>
ATOMIC_BLOCK(ATOMIC_FORCEON) {
port |= 0x03;
}
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):
{
// ...
{
uint8_t sreg_local; // Lokale Sicherungskopie von SREG
sreg_local = SREG;
cli();
// hierhin kommt der Code mit atomarem Zugriff
SREG = sreg_local;
}
// wieder per Macro
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
// hierhin kommt der Code mit atomarem Zugriff
}
// ...
}
Je nach Prozessor kann man das Problem manchmal auch ohne Abschalten von Interrupts durch geeignete Programmierung lösen. So führt
port = (port & ~0x0F) | lcd_data;
immer zum beschriebenen Problem,
port &= ~0x0F;
port |= lcd_data;
jedoch nicht, wenn die beiden Zeilen zu jeweils einem Assembler-Befehl übersetzt werden, wie z.B. auf dem MSP430, nicht jedoch auf dem AVR. Was dann aber abhängig von den Optimierungs-Einstellungen des Compilers werden kann. Eine interruptfeste Variante für AVR-Prozessoren der neuesten Generation, wie beispielsweise Tiny2313 und Mega88 (alle ab 2004):
PINx = (PORTx & 0x0F) ^ lcd_data;
Wenn man diese Tricks nutzt, sollte man sie auch sehr gut und eindeutig mit einem Kommentar dokumentieren! Im Zweifelsfall sollte man lieber den soliden, eindeutigen Weg wählen und die Interrupts kurz sperren. Diese Methode ist auch gefahrlos auf andere Controller portierbar.
Reentrante Funktionen
Eine 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.
Volatile Variablen
Variablen, auf die sowohl innerhalb wie auch außerhalb einer Interruptserviceroutine zugegriffen wird (schreibend oder lesend), müssen (ähnlich wie Hardwareregister) mit dem Schlüsselwort volatile (flüchtig) versehen werden, damit der C-Compiler berücksichtigen kann, dass diese Variablen jederzeit (durch das Auftreten des Interrupts) gelesen oder geschrieben werden können. Ansonsten würde der C-Compiler das regelmäßige Abfragen oder Beschreiben dieser Variablen ggf. wegoptimieren, da er nicht damit rechnet, dass auf die Variable auch "ohne sein Zutun" zugegriffen wird.
Eine ausführlichere Erklärung zu "volatile" ist hier zu finden: FAQ: Was hat es mit volatile auf sich
Interrupts und Low Power Modes (Sleep)
Wenn der Microcontroller in einen Low Power Mode versetzt wird, wird er durch einen Interrupt wieder aufgeweckt, z.B. in einem festen Zeitraster per Timer-Interrupt oder vom ADC nach Beendigung einer AD-Wandlung. Dabei muß sichergestellt werden, daß der Interrupt erst nach dem In-Low-Power-Gehen (z.B. per Befehl sleep()) kommen kann, da sonst der µC nicht bzw. nicht rechtzeitig geweckt wird. Dazu muß eine Möglichkeit bestehen, daß der Interrupt gesperrt wird und erst mit dem Sleep-Befehl freigegeben wird. Dies scheint auf den ersten Blick unmöglich: Man kann nicht gleichzeitig zwei Befehle (sei() und sleep()) ausführen. Es gibt spezielle Mechanismen für diesen Fall. Beim C51 und beim AVR ist es z.B. so, daß sei() erst einen Befehl später "wirksam" wird. Dadurch wird die Kombination
sei();
sleep();
ununterbrechbar (natürlich sofern zuvor die Interrupts gesperrt wurden). Andere Microcontroller bieten andere Mechanismen, z.B. sperrt der Assembler-Befehl
DISI
beim PIC24 die Interrupts für eine bestimmten Anzahl von CPU-Taktzyklen. Die CPU kann dann eine vorgegebene Anzahl an folgenden Befehlen unter Interruptsperre ausführen und der Interrupt wird automatisch wieder freigegeben.
(AVR-) Beispiele für die praktische Programmierung
Die 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.
Steuersignale zwischen ISR und Hauptprogramm
In 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.
/*
*******************************************************************************
*
* Timer Interrupt Demo
*
* ATmega32 mit internem 1-MHz-Oszillator
*
* LOW Fuse Byte = 0xE1
*
* An PD5 muss eine LED mit 1-kOhm-Vorwiderstand angeschlossen werden
*
*******************************************************************************
*/
#define F_CPU 1000000
#include <avr/io.h>
#include <avr/interrupt.h>
// globale Variablen
volatile uint8_t flag;
int main() {
// IO konfigurieren
DDRA = 0xFF;
DDRB = 0xFF;
DDRC = 0xFF;
DDRD = 0xFF;
// Timer2 konfigurieren
TCCR2 = (1<<CS22) | (1<<CS21); // Vorteiler 256 -> ~65ms Überlaufperiode
TIMSK |= (1<<TOIE2); // Timer Overflow Interrupt freischalten
// Interrupts freigeben
sei();
// Endlose Hauptschleife
while(1) {
if (flag == 1) { // Neuer Timerzyklus ?
flag = 0;
// hier steht jetzt in Normalfall ein grosser Programmblock ;-)
PORTD ^= (1 << PD5); // LED toggeln
}
}
}
// Timer2 overflow Interrupt
// hier wird der Hauptschleife ein neuer Timerinterrupt signalisiert
ISR( TIMER2_OVF_vect ) {
flag = 1;
}
UART mit Interrupts
Der 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.
- RXC (Receive Complete): Ein Zeichen wurde empfangen.
- UDRE (UART Data Register Empty): Der Zwischenpuffer des Senders ist leer und kann ein neues Zeichen aufnehmen. Dieser Zwischenpuffer ist wichtig, um lückenlos auch bei hohen Baudraten senden zu können.
- TXC (Transmit Complete): Das aktuelle Zeichen wurde vollständig inclusive Stopbit gesendet und es liegt kein neues Datenbyte im Sendepuffer. Dieser Interrupt ist extrem nützlich für eine Halbduplexkommunikation, z. B. auf einem RS485-Bus. Hier kann man nach dem vollständigen Senden aller Bytes den Bustranceiver (z. B. MAX485) von Senden auf Empfangen umschalten, um den Bus freizugeben.
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 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, parallel 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.
/*
*******************************************************************************
*
* UART Interrupt Demo
*
* ATmega32 mit 3,6864 MHz Quarz an XTAL1/XTAL2
*
* LOW Fuse Byte = 0xFF
*
* An PD5 muss eine LED mit 1-kOhm-Vorwiderstand angeschlossen werden
* An PD0/PD1 ist ein MAX232 angeschlosssen, um Daten vom PC zu empfangen/senden
*
*******************************************************************************
*/
// Systemtakt in Hz, das L am Ende ist wichtig, NICHT UL verwenden!
#define F_CPU 3686400L
// "Morsedauer" für ein Bit in Millisekunden
#define BITZEIT 100
#include <string.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
// Baudrate, das L am Ende ist wichtig, NICHT UL verwenden!
#define BAUD 9600L
// Berechnungen
// clever runden
#define UBRR_VAL ((F_CPU+BAUD*8)/(BAUD*16)-1)
// Reale Baudrate
#define BAUD_REAL (F_CPU/(16*(UBRR_VAL+1)))
// Fehler in Promille
#define BAUD_ERROR ((BAUD_REAL*1000)/BAUD-1000)
#if ((BAUD_ERROR>10) || (BAUD_ERROR<-10))
#error Systematischer Fehler der Baudrate grösser 1% und damit zu hoch!
#endif
// globale Variablen für den UART
// Puffergrösse in Bytes, RX und TX sind gleich gross
#define uart_buffer_size 32
volatile uint8_t uart_rx_flag=0; // Flag, String komplett empfangen
volatile uint8_t uart_tx_flag=1; // Flag, String komplett gesendet
char uart_rx_buffer[uart_buffer_size]; // Empfangspuffer
char uart_tx_buffer[uart_buffer_size]; // Sendepuffer
// lange, variable Wartezeit, Einheit in Millisekunden
void long_delay(uint16_t ms) {
for (; ms>0; ms--) _delay_ms(1);
}
// einen String senden
// vor Aufruf der Funktion muss man prüfen, ob uart_t_flag==1 ist
// nur dann kann ein neuer String gesendet werden
void put_string(char *daten) {
if (uart_tx_flag==1) {
// String daten ind en Sendepuffer kopieren
strcpy(uart_tx_buffer, daten);
// Flag für 'Senden ist komplett' löschen,
uart_tx_flag = 0;
// UDRE Interrupt einschalten, los gehts
UCSRB |= (1<<UDRIE);
}
}
// einen empfangenen String kopieren
// vor Aufruf der Funktion muss man prüfen, ob uart_rx_flag==1 ist
// anderenfalls ist der RX Buffer noch ungültig
void get_string(char *daten) {
if (uart_rx_flag==1) {
// String kopieren
strcpy(daten, uart_rx_buffer);
// Flag löschen
uart_rx_flag = 0;
}
}
// Ein Byte im RS232 Format auf eine LED ausgeben
void morse(uint8_t data) {
uint8_t i;
// Startbit, immer 0
PORTD &= ~(1 << PD5); // LED aus
long_delay(BITZEIT);
for(i=0; i<8; i++) {
if (data & 0x01) // Prüfe Bit #0
PORTD |= (1 << PD5); // LED an
else
PORTD &= ~(1 << PD5); // LED aus
long_delay(BITZEIT);
data >>= 1; // nächstes Bit auf Bit #0 schieben
}
// Stopbit, immer 1
PORTD |= (1 << PD5); // LED an
long_delay(BITZEIT);
}
// Hauptprogramm
int main (void) {
char stringbuffer[64]; // Allgemeiner Puffer für Strings
uint8_t buffer_full=0; // noch ein Flag, aber nur in der Hauptschleife
char * charpointer; // Hilfszeiger
// IO konfigurieren
DDRA = 0xFF;
DDRB = 0xFF;
DDRC = 0xFF;
DDRD = 0xFF;
// UART konfigurieren
UBRRH = UBRR_VAL >> 8;
UBRRL = UBRR_VAL & 0xFF;
UCSRB = (1<<RXCIE) | (1<<RXEN) | (1<<TXEN);
// Stringbuffer initialisieren
stringbuffer[0] = '\n';
stringbuffer[1] = '\r';
// Interrupts freigeben
sei();
// Endlose Hauptschleife
while(1) {
// "Sinnvolle" CPU Tätigkeit
PORTD &= ~(1<<PD5);
long_delay(300);
PORTD |= (1<<PD5);
long_delay(300);
// Wurde ein kompletter String empfangen
// und der Buffer ist leer?
if (uart_rx_flag==1 && buffer_full==0) {
// ja, dann String lesen,
// die ersten zwei Zeichen
// aber nicht überschreiben
get_string(stringbuffer+2);
buffer_full=1;
}
// Ist letzte Stringsendung abgeschlossen
// und ein neuer String verfügbar?
if (uart_tx_flag==1 && buffer_full==1) {
// Newline + Carrige return anfügen
strcat(stringbuffer, "\n\r");
put_string(stringbuffer); // zurücksenden
buffer_full=0; // Buffer ist wieder verfügbar
// Alle Zeichen per LED morsen
charpointer = stringbuffer;
while(*charpointer) morse(*charpointer++);
}
}
}
// UART RX complete interrupt
// hier werden Daten vom PC empfangen und in einem String zwischengespeichert
// Wird ein Stringterminator empfangen, wird ein Flag gesetzt, welches dem
// Hauptprogramm den kompletten Empfang signalisiert
ISR(USART_RXC_vect) {
static uint8_t uart_rx_cnt; // Zähler für empfangene Zeichen
char data;
// Daten auslesen, dadurch wird das Interruptflag gelöscht
data = UDR;
// Ist Puffer frei für neue Daten?
if (!uart_rx_flag) {
// ja, ist Ende des Strings (RETURN) erreicht?
if (data=='\r') {
// ja, dann String terminieren
uart_rx_buffer[uart_rx_cnt]=0;
// Flag für 'Empfangspuffer voll' setzen
uart_rx_flag=1;
// Zähler zurücksetzen
uart_rx_cnt=0;
}
else if (uart_rx_cnt<(uart_buffer_size-1)) {
// Daten in Puffer speichern
// aber durch if() Pufferüberlauf vermeiden
uart_rx_buffer[uart_rx_cnt]=data;
uart_rx_cnt++; // Zähler erhöhen
}
}
}
// UART TX data register empty interrupt
// hier werden neue Daten in das UART-Senderegister geladen
ISR(USART_UDRE_vect) {
// Zeiger auf Sendepuffer
static char* uart_tx_p = uart_tx_buffer;
char data;
// zu sendendes Zeichen lesen,
// Zeiger auf Sendepuffer erhöhen
data = *uart_tx_p++;
// Ende des nullterminierten Strings erreicht?
if (data==0 ) {
UCSRB &= ~(1<<UDRIE); // ja, dann UDRE Interrupt ausschalten
uart_tx_p = uart_tx_buffer; // Pointer zurücksetzen
uart_tx_flag = 1; // Flag setzen, Übertragung beeendet
}
else UDR = data; // nein, Daten senden
}
Atomarer Zugriff auf eine 16-Bit Variable
// Atmega8 @ 4 MHz
// Siehe http://www.mikrocontroller.net/topic/206455
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/atomic.h>
// Anm. Für das Programm würde wohl eine 8-Bit Variable genügen.
// Mit 16-Bit kann der Sinn eines atomaren Zugriffs besser
// demonstriert werden
volatile int sekunde;
void setup (void)
{
TCCR0 |= ( 1<<CS02 )|( 1<<CS00 ); // counter0,Prescaler auf 1024
TIMSK |= ( 1<<TOIE0 ); // enable counter0 overflow interrupt
TCNT0 = 0x00; // Counter0 auf Null setzen
sei(); // Interrupts global aktivieren
}
ISR(TIMER0_OVF_vect)
{
sekunde++;
}
int main(void)
{
DDRB = (1<<PB0); // Pin PB0 Ausgang
setup();
while (1) {
int sekunde_kopie;
ATOMIC_BLOCK(ATOMIC_FORCEON)
{
sekunde_kopie = sekunde; // 16-Bit Zuweisung ist nicht atomar
// deshalb ATOMIC_BLOCK
}
if ( sekunde_kopie >= 25 ) {
ATOMIC_BLOCK(ATOMIC_FORCEON)
{
sekunde = 0; // 16-Bit Zuweisung ist nicht atomar
// deshalb ATOMIC_BLOCK
}
PORTB ^= (1<<PB0); // Toggle PB0
}
}
}
Siehe auch
- ADC
- Timer
- FAQ#Timer
- AVR-Tutorial - Interrupts in Assembler
- AVR-GCC-Tutorial - Interrupts in C
- Forumsbeitrag: Minimale Pulsbreite zum Auslösen eines Pin Change Interrupts beim AVR
- Forumsbeitrag: Schwerer Bug in AVR-GCC 4.1.1
- Forumsbeitrag: Code in den Atomic Block verschleppt