AVR-Tutorial: Interrupts

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Definition

Bei bestimmten Ereignissen in Prozessoren wird ein sogenannter Interrupt ausgelöst. Interrupts machen es möglich, beim Eintreten eines Ereignisses sofort informiert zu werden, ohne permanent irgendeinen Status abzufragen, was teure Rechenzeit kosten würde. Dabei wird das Programm unterbrochen und ein Unterprogramm aufgerufen. Wenn dieses beendet ist, läuft das Hauptprogramm ganz normal weiter.

Mögliche Auslöser

Bei Mikrocontrollern werden Interrupts z. B. ausgelöst, wenn:

  • sich der an einem bestimmten Eingangs-Pin anliegende Wert von high auf low ändert (oder umgekehrt)
  • eine vorher festgelegte Zeitspanne abgelaufen ist (Timer)
  • eine serielle Übertragung abgeschlossen ist (UART)

Der ATmega8 besitzt 18 verschiedene Interruptquellen. Standardmäßig sind diese alle deaktiviert und müssen über verschiedene IO-Register einzeln eingeschaltet werden.

INT0, INT1 und die zugehörigen Register

Wir wollen uns hier ersteinmal die beiden Interrupts INT0 und INT1 anschauen. INT0 wird ausgelöst, wenn sich der an PD2 anliegende Wert ändert, INT1 reagiert auf Änderungen an PD3.

Als erstes müssen wir die beiden Interrupts konfigurieren. Im Register MCUCR wird eingestellt, ob die Interrupts bei einer steigenden Flanke (low nach high) oder bei einer fallenden Flanke (high nach low) ausgelöst werden. Dafür gibt es in diesem Register die Bits ISC00, ISC01 (betreffen INT0) sowie ISC10 und ISC11 (betreffen INT1).

Hier eine Übersicht über die möglichen Einstellungen und was sie bewirken:

ISC11 bzw. ISC01 ISC10 bzw. ISC00 Beschreibung
0 0 Low-Level am Pin löst den Interrupt aus
0 1 Jede Änderung am Pin löst den Interrupt aus
1 0 Eine fallende Flanke löst den Interrupt aus
1 1 Eine steigende Flanke löst den Interrupt aus

Danach müssen diese beiden Interrupts aktiviert werden, indem die Bits INT0 und INT1 im Register GICR auf 1 gesetzt werden.

Die Register MCUCR und GICR gehören zwar zu den IO-Registern, können aber nicht wie andere mit den Befehlen cbi und sbi verwendet werden. Diese Befehle wirken nur auf die IO-Register bis zur Adresse 0x1F (welches Register sich an welcher IO-Adresse befindet, steht in der Include-Datei, hier „m8def.inc“, und im Datenblatt des Controllers). Somit bleiben zum Zugriff auf diese Register nur die Befehle in und out übrig.

Interrupts generell zulassen

Schließlich muss man noch das Ausführen von Interrupts allgemein aktivieren, was man durch einfaches Aufrufen des Assemblerbefehls sei bewerkstelligt.

Die Interruptvektoren

Woher weiß der Controller jetzt, welche Routine aufgerufen werden muss, wenn ein Interrupt ausgelöst wird?

Wenn ein Interrupt auftritt, dann springt die Programmausführung an eine bestimmte Stelle im Programmspeicher. Diese Stellen sind festgelegt und können nicht geändert werden:

Nr. Adresse Interruptname Beschreibung
1 0x000 RESET Reset bzw. Einschalten der Stromversorgung
2 0x001 INT0 Externer Interrupt 0
3 0x002 INT1 Externer Interrupt 1
4 0x003 TIMER2_COMP Timer/Counter2 Compare Match
5 0x004 TIMER2_OVF Timer/Counter2 Overflow
6 0x005 TIMER1_CAPT Timer/Counter1 Capture Event
7 0x006 TIMER1_COMPA Timer/Counter1 Compare Match A
8 0x007 TIMER1_COMPB Timer/Counter1 Compare Match B
9 0x008 TIMER1_OVF Timer/Counter1 Overflow
10 0x009 TIMER0_OVF Timer/Counter0 Overflow
11 0x00A SPI_STC SPI-Übertragung abgeschlossen
12 0x00B USART_RX USART-Empfang abgeschlossen
13 0x00C USART_UDRE USART-Datenregister leer
14 0x00D USART_TX USART-Sendung abgeschlossen
15 0x00E ADC AD-Wandlung abgeschlossen
16 0x00F EE_RDY EEPROM bereit
17 0x010 ANA_COMP Analogkomparator
18 0x011 TWI Two-Wire Interface
19 0x012 SPM_RDY Store Program Memory Ready

So, wir wissen jetzt, dass der Controller zu Adresse 0x001 springt, wenn INT0 auftritt. Aber dort ist ja nur Platz für einen Befehl, denn die nächste Adresse ist doch für INT1 reserviert. Wie geht das? Ganz einfach: Dort kommt ein Sprungbefehl rein, z. B. rjmp interrupt0. Irgendwo anders im Programm muss in diesem Fall eine Stelle mit interrupt0: gekennzeichnet sein, zu der dann gesprungen wird. Diese durch den Interrupt aufgerufene Routine nennt man Interrupthandler (engl. Interrupt Service Routine, ISR).

Beenden eines Interrupthandlers

Und wie wird die Interruptroutine wieder beendet? Durch den Befehl reti. Wird dieser aufgerufen, dann wird das Programm ganz normal dort fortgesetzt, wo es durch den Interrupt unterbrochen wurde. Es ist dabei wichtig, daß hier der Befehl reti und nicht ein normaler ret benutzt wird. Wird ein Interrupthandler betreten, so sperrt der Mikrocontroller automatisch alle weiteren Interrupts. Im Unterschied zu ret hebt ein reti diese Sperre wieder auf.

Aufbau der Interruptvektortabelle

Jetzt müssen wir dem Assembler nur noch klarmachen, dass er unser rjmp interrupt0 an die richtige Stelle im Programmspeicher schreibt, nämlich an den Interruptvektor für INT0. Dazu gibt es eine Assemblerdirektive. Durch .org 0x001 sagt man dem Assembler, dass er die darauffolgenden Befehle ab Adresse 0x001 im Programmspeicher platzieren soll. Diese Stelle wird von INT0 angesprungen.

Damit man nicht alle Interruptvektoren immer nachschlagen muss, sind in der Definitionsdatei m8def.inc einfach zu merkende Namen für die Adressen definiert. Statt 0x001 kann man z. B. einfach INT0addr schreiben. Das hat außerdem den Vorteil, dass man bei Portierung des Programms auf einen anderen AVR-Mikrocontroller nur die passende Definitionsdatei einbinden muss, und sich über evtl. geänderte Adressen für die Interruptvektoren keine Gedanken zu machen braucht.

Nun gibt es nur noch ein Problem: Beim Reset (bzw. wenn die Spannung eingeschaltet wird) wird das Programm immer ab der Adresse 0x000 gestartet. Deswegen muss an diese Stelle ein Sprungbefehl zum Hauptprogramm erfolgen, z. B. rjmp RESET, um an die mit RESET: markierte Stelle zu springen.

Wenn man mehrere Interrupts verwenden möchte, kann man auch, anstatt jeden Interruptvektor einzeln mit .org an die richtige Stelle zu rücken, die gesamte Sprungtabelle ausschreiben:

.include "m8def.inc"

.org 0x000                    ; kommt ganz an den Anfang des Speichers
         rjmp RESET           ; Interruptvektoren überspringen
                              ; und zum Hauptprogramm
         rjmp EXT_INT0        ; IRQ0 Handler
         rjmp EXT_INT1        ; IRQ1 Handler
         rjmp TIM2_COMP
         rjmp TIM2_OVF
         rjmp TIM1_CAPT       ; Timer1 Capture Handler
         rjmp TIM1_COMPA      ; Timer1 CompareA Handler
         rjmp TIM1_COMPB      ; Timer1 CompareB Handler
         rjmp TIM1_OVF        ; Timer1 Overflow Handler
         rjmp TIM0_OVF        ; Timer0 Overflow Handler
         rjmp SPI_STC         ; SPI Transfer Complete Handler
         rjmp USART_RXC       ; USART RX Complete Handler
         rjmp USART_DRE       ; UDR Empty Handler
         rjmp USART_TXC       ; USART TX Complete Handler
         rjmp ADC             ; ADC Conversion Complete Interrupthandler
         rjmp EE_RDY          ; EEPROM Ready Handler
         rjmp ANA_COMP        ; Analog Comparator Handler
         rjmp TWSI            ; Two-wire Serial Interface Handler
         rjmp SPM_RDY         ; Store Program Memory Ready Handler

RESET:                        ; hier beginnt das Hauptprogramm

Hier ist es unbedingt nötig, bei unbenutzten Interruptvektoren statt des Sprungbefehls den Befehl reti (bzw. reti nop, wenn jmp 4 Byte lang ist) reinzuschreiben. Wenn man einen Vektor einfach weglässt, stehen die nachfolgenden Sprungbefehle sonst alle an der falschen Adresse im Speicher.

Wer auf Nummer sicher gehen möchte kann aber auch alle Vektoren einzeln mit .org adressieren:

.include "m8def.inc"

.org 0x000
       rjmp RESET
.org INT0addr                 ; External Interrupt0 Vector Address
       reti                   
.org INT1addr                 ; External Interrupt1 Vector Address
       reti                   
.org OC2addr                  ; Output Compare2 Interrupt Vector Address
       reti                   
.org OVF2addr                 ; Overflow2 Interrupt Vector Address
       reti                   
.org ICP1addr                 ; Input Capture1 Interrupt Vector Address
       reti                   
.org OC1Aaddr                 ; Output Compare1A Interrupt Vector Address
       reti                   
.org OC1Baddr                 ; Output Compare1B Interrupt Vector Address
       reti                   
.org OVF1addr                 ; Overflow1 Interrupt Vector Address
       reti                   
.org OVF0addr                 ; Overflow0 Interrupt Vector Address
       reti                   
.org SPIaddr                  ; SPI Interrupt Vector Address
       reti                   
.org URXCaddr                 ; USART Receive Complete Interrupt Vector Address
       reti                   
.org UDREaddr                 ; USART Data Register Empty Interrupt Vector Address
       reti                   
.org UTXCaddr                 ; USART Transmit Complete Interrupt Vector Address
       reti                   
.org ADCCaddr                 ; ADC Interrupt Vector Address
       reti                   
.org ERDYaddr                 ; EEPROM Interrupt Vector Address
       reti                   
.org ACIaddr                  ; Analog Comparator Interrupt Vector Address
       reti                   
.org TWIaddr                  ; Irq. vector address for Two-Wire Interface
       reti                   
.org SPMRaddr                  ; SPM complete Interrupt Vector Address
       reti                   

.org INT_VECTORS_SIZE
RESET:                        ; hier beginnt das Hauptprogramm

Statt die unbenutzten Interruptvektoren mit reti zu füllen könnte man sie hier auch einfach weglassen, da die .org-Direktive dafür sorgt, dass jeder Vektor in jedem Fall am richtigen Ort im Speicher landet.

Beispiel

So könnte ein Minimal-Assemblerprogramm aussehen, das die Interrupts INT0 und INT1 verwendet. An die Interruptpins können z. B. Taster nach bewährter Manier angeschlossen werden. Die Interrupts werden auf fallende Flanke konfiguriert, da ja die Taster so angeschlossen sind, dass sie im Ruhezustand eine 1 liefern und bei einem Tastendruck nach 0 wechseln.

Download extinttest.asm

.include "m8def.inc"

.def temp = r16

.org 0x000
         rjmp main            ; Reset Handler
.org INT0addr
         rjmp int0_handler    ; IRQ0 Handler
.org INT1addr
         rjmp int1_handler    ; IRQ1 Handler


main:                         ; hier beginnt das Hauptprogramm

         ldi temp, LOW(RAMEND)
         out SPL, temp
         ldi temp, HIGH(RAMEND)
         out SPH, temp

         ldi temp, 0x00
         out DDRD, temp

         ldi temp, 0xFF
         out DDRB, temp

         ldi temp, (1<<ISC01) | (1<<ISC11) ; INT0 und INT1 auf fallende Flanke konfigurieren
         out MCUCR, temp

         ldi temp, (1<<INT0) | (1<<INT1) ; INT0 und INT1 aktivieren
         out GICR, temp

         sei                   ; Interrupts allgemein aktivieren

loop:    rjmp loop             ; eine leere Endlosschleife

int0_handler:
         sbi PORTB, 0
         reti

int1_handler:
         cbi PORTB, 0
         reti

Für dieses Programm braucht man nichts weiter als eine LED an PB0 und je einen Taster an PD2 (INT0) und PD3 (INT1). Wie diese angeschlossen werden, steht in Teil 2 des Tutorials.

Die Funktion ist auch nicht schwer zu verstehen: Drückt man eine Taste, wird der dazugehörige Interrupt aufgerufen und die LED an- oder abgeschaltet. Das ist zwar nicht sonderlich spektakulär, aber das Prinzip sollte deutlich werden.

Meistens macht es keinen Sinn, Taster direkt an einen Interrupteingang anzuschließen. Das kann bisweilen sogar sehr schlecht sein, siehe Entprellung. Häufiger werden Interrupts in Zusammenhang mit dem UART verwendet, um z. B. auf ein empfangenes Zeichen zu reagieren. Wie das funktioniert, wird im Kapitel über den UART beschrieben.

Besonderheiten des Interrupthandlers

Der Interrupthandler kann ja mehr oder weniger zu jedem beliebigen Zeitpunkt unabhängig vom restlichen Programm aufgerufen werden. Dabei soll das restliche Programm auf keinen Fall durch den Interrupthandler negativ beeinflusst werden, das heißt, das Hauptprogramm soll nach dem Beenden des Handlers weiterlaufen als wäre nichts passiert. Insbesondere muss deshalb darauf geachtet werden, dass im Interrupthandler Register, die vom Programmierer nicht ausschließlich nur für den Interrupthandler reserviert wurden, auf dem Stack gesichert und zum Schluss wieder hergestellt werden müssen.

Ein Register, das gerne übersehen wird, ist das Statusregister (SREG). In ihm merkt sich der Prozessor bestimmte Zustände von Berechnungen, z. B. ob ein arithmetischer Überlauf stattgefunden hat, ob das letzte Rechenergebnis 0 war, etc. Sobald ein Interrupthandler etwas komplizierter wird als im obigen Beispiel, tut man gut daran, das Statusregister auf jeden Fall zu sichern. Ansonsten kann das Hinzufügen von weiterem Code zum Interrupthandler schnell zum Boomerang werden: Die dann möglicherweise notwendige Sicherung des SREG wird vergessen. Überhaupt empfiehlt es sich, in diesen Dingen bei der Programmierung eines Interrupthandlers eher vorausschauend, übervorsichtig und konservativ zu programmieren. Wird dies getan, so vergeudet man höchstens ein bisschen Rechenzeit. Im anderen Fall handelt man sich allerdings einen Super-GAU ein: Man steht dann vor einem Programm, das sporadisch nicht funktioniert und keiner weiß warum. Solche Fehler sind nur sehr schwer und oft nur mit einem Quäntchen Glück zu finden.

Im Beispiel wäre zwar das Sichern und Wiederherstellen der Register „temp“ und SREG nicht wirklich notwendig, aber hier soll die grundsätzliche Vorgehensweise gezeigt werden:

.include "m8def.inc"

.def temp = r16

.org 0x000
         rjmp main            ; Reset Handler
.org INT0addr
         rjmp int0_handler    ; IRQ0 Handler
.org INT1addr
         rjmp int1_handler    ; IRQ1 Handler


main:                         ; hier beginnt das Hauptprogramm

         ldi temp, LOW(RAMEND)
         out SPL, temp
         ldi temp, HIGH(RAMEND)
         out SPH, temp

         ldi temp, 0x00
         out DDRD, temp

         ldi temp, 0xFF
         out DDRB, temp

         ldi temp, (1<<ISC01) | (1<<ISC11) ; INT0 und INT1 auf fallende Flanke konfigurieren
         out MCUCR, temp

         ldi temp, (1<<INT0) | (1<<INT1) ; INT0 und INT1 aktivieren
         out GICR, temp

         sei                   ; Interrupts allgemein aktivieren

loop:    rjmp loop             ; eine leere Endlosschleife

int0_handler:
         push temp             ; Das SREG in temp sichern. Vorher
         in   temp, SREG       ; muss natürlich temp gesichert werden

         sbi PORTB, 0

         out SREG, temp        ; Die Register SREG und temp wieder
         pop temp              ; herstellen
         reti

int1_handler:
         push temp             ; Das SREG in temp sichern. Vorher
         in   temp, SREG       ; muss natürlich temp gesichert werden

         cbi PORTB, 0

         out SREG, temp        ; Die Register SREG und temp wieder
         pop temp              ; herstellen
         reti

Siehe auch

  • Artikel Interrupt: Interrupts in C
  • Forumsbeitrag: Flexible Verwaltung von Interruptroutinen ohne zentrale Interrupttabelle