Pfad: Home => AVR-Überblick => Programmiertechniken => Programmablauf

Programmiertechnik für Anfänger in AVR Assemblersprache

Steuerung des Programmablaufes in AVR Assembler

Hier werden alle Vorgänge erläutert, die mit dem Programmablauf zu tun haben und diesen beeinflussen, also die Vorgänge beim Starten des Prozessors, Sprünge und Verzweigungen, Unterbrechungen, etc.

Was passiert beim Reset?

Beim Anlegen der Betriebsspannung, also beim Start des Prozessors, wird über die Hardware des Prozessors ein sogenannter Reset ausgelöst. Dabei wird der Zähler für die Programmschritte auf Null gesetzt. An dieser Stelle des Programmes wird die Verarbeitung also immer begonnen. Ab hier muss der Programm-Code stehen, der ausgeführt werden soll. Aber nicht nur beim Start wird der Zähler auf diese Adresse zurückgesetzt, sondern auch bei
  1. externem Rücksetzen am Eingangs-Pin Reset durch die Hardware,
  2. Ablauf der Wachhund-Zeit (Watchdog-Reset), einer internen Überwachungsuhr,
  3. direkten Sprüngen an die Adresse Null (Sprünge siehe unten).
Die dritte Möglichkeit ist aber gar kein richtiger Reset, denn das beim Reset automatisch ablaufende Rücksetzen von Register- und Port-Werten auf den jeweils definierten Standardwert (Default) wird hierbei nicht durchgeführt. Vergessen wir also besser die 3.Möglichkeit, sie ist unzuverlässig.

Die zweite Möglichkeit, nämlich der eingebaute Wachhund, muss erst explizit von der Leine gelassen werden, durch Setzen seiner entsprechenden Port-Bits. Wenn er dann nicht gelegentlich mit Hilfe der Instruktion

    WDR

zurückgepfiffen wird, dann geht er davon aus, dass Herrchen AVR eingeschlafen ist und weckt ihn mit einem brutalen Reset (Kaltstart).

Ab dem Reset wird also der an Adresse 0x000000 stehende Code wortweise in die Ausführungsmimik des Prozessors geladen und ausgeführt. Während der Ausführung wird die Adresse um 1 erhöht und schon mal die nächste Instruktion aus dem Programmspeicher geholt (Fetch during Execution). Wenn die erste Instruktion keine Verzweigung des Programmes auslöst, kann die zweite Instruktion also direkt nach der ersten ausgeführt werden. Jeder Taktzyklus am Takteingang des Prozessors entspricht daher einer ausgef&uum;hrten Instruktion (wenn nichts dazwischenkommt).

Die erste Instruktion des ausführbaren Programmes muss immer bei Adresse 0x000000 stehen. Um dem Assembler mitzuteilen, dass er nach irgendwelchen Vorwörtern wie Definitionen oder Konstanten nun bitte mit der ersten Zeile Programmcode beginnen möge, gibt es folgende Direktiven:

.CSEG
.ORG 0000


Die erste Direktive teilt dem Assembler mit, dass ab jetzt in das Code-Segment zu assemblieren ist. Ein anderes Segment wäre z.B. das EEPROM, das ebenfalls im Assembler-Quelltext auf bestimmte Werte eingestellt werden könnte. Die entsprechende Assembler-Direktive hierfür würde dann natürlich lauten:

.ESEG

Die zweite Assembler-Direktive oben stellt den Programmzähler beim Assemblieren auf die Adresse 0000 ein. ORG ist die Abkürzung für Origin, also Ursprung. Wir könnten auch bei 0100 mit dem Programm beginnen, aber das wäre ziemlich sinnlos (siehe oben). Da die beiden Angaben CSEG und ORG trivial sind, können sie auch weggelassen werden und der Assembler macht das dann automatisch so. Wer aber den Beginn des Codes nach zwei Seiten Konstantendefinitionen eindeutig markieren will, kann das mit den beiden Direktiven tun.

Auf das ominöse erste Wort Programmcode folgen fast ebenso wichtige Spezialbefehle, die Interrupt-Vektoren. Dies sind festgelegte Stellen mit bestimmten Adressen. Diese Adressen werden bei Auslösung eines Interrupts durch die Hardware angesprungen, wobei der normale Programmablauf unterbrochen wird. Diese Vektoren sind spezifisch für jeden Prozessortyp (siehe unten bei der Erläuterung der Interrupts). Die Instruktionen, mit denen auf eine Unterbrechung reagiert werden soll, müssen an dieser Stelle stehen. Werden also Interrupts verwendet, dann kann die erste Instruktion nur eine Sprunginstruktion sein, damit diese ominösen Vektoren übersprungen werden. Der typische Programmablauf nach dem Reset sieht also so aus:

.CSEG
.ORG 0000
    RJMP Start

[...] hier kommen dann die Interrupt-Vektoren
[...] und danach nach Belieben noch alles mögliche andere Zeug
Start: ; Das hier ist der Programmbeginn
[...] Hier geht das Hauptprogramm dann erst richtig los.

Die Instruktion RJMP bewirkt einen Sprung an die Stelle im Programm mit der Kennzeichnung Start:, auch ein Label genannt. Die Stelle Start: folgt weiter unten im Programm und ist dem Assembler hiermit entsprechend mitgeteilt: Labels beginnen immer in Spalte 1 einer Zeile und enden mit einem Doppelpunkt. Labels, die diese beiden Bedingungen nicht einhalten, werden vom Assembler nicht ernstgenommen. Fehlende Labels aber lassen den Assembler sinnierend innehalten und mit einer "Undefined label"-Fehlermeldung die weitere Zusammenarbeit einstellen.

Zum Seitenanfang

Linearer Programmablauf und Verzweigungen

Nachdem die ersten Schritte im Programm gemacht sind, noch etwas triviales: Programme laufen linear ab. Das heißt: Instruktion für Instruktion wird nacheinander aus dem Programmspeicher geholt und abgearbeitet. Dabei zählt der Programmzähler immer eins hoch. Ausnahmen von dieser Regel werden nur vom Programmierer durch gewollte Verzweigungen oder mittels Interrupt herbeigeführt. Da sind Prozessoren ganz stur immer geradeaus, es sei denn, sie werden gezwungen.

Und zwingen geht am einfachsten folgendermaßen. Oft kommt es vor, dass in Abhängigkeit von bestimmten Bedingungen gesprungen werden soll. Dann benötigen wir bedingte Verzweigungen. Nehmen wir an, wir wollen einen 32-Bit-Zähler mit den Registern R1, R2, R3 und R4 realisieren. Dann muss das niederwertigste Byte in R1 um Eins erhöht werden. Wenn es dabei überläuft, muss auch R2 um Eins erhöht werden usw. bis R4.

Die Erhöhung um Eins wird mit der Instruktion INC erledigt. 32-Bit-Zaehler Wenn dabei ein Überlauf auftritt, also 255 zu 0 wird, dann ist das im Anschluss an die Durchführung am gesetzten Z-Bit im Statusregister zu bemerken. Das eigentliche Übertragsbit C des Statusregisters wird bei der INC-Instruktion nicht verändert, weil es woanders dringender gebraucht wird und das Z-Bit völlig ausreicht. Wenn der Übertrag nicht auftritt, also das Z-Bit nicht gesetzt ist, können wir die Erhöherei beenden.

Wenn aber das Z-Bit gesetzt ist, darf die Erhöherei beim nächsten Register weitergehen. Wir müssen also springen, wenn das Z-Bit nicht gesetzt ist. Der entsprechende Sprungbefehl heißt aber nicht BRNZ (BRanch on Not Zero), sondern BRNE (BRanch if Not Equal). Na ja, Geschmackssache. Das ganze Räderwerk des 32-Bit langen Zählers sieht damit so aus:

    INC R1
    BRNE Weiter
    INC R2
    BRNE Weiter
    INC R3
    BRNE Weiter
    INC R4
Weiter:


Das war es schon. Eine einfache Sache.Das Gegenteil von BRNE ist übrigens BREQ oder BRanch EQual.

Welche der Statusbits durch welche Instruktionen und Bedingungen gesetzt oder rückgesetzt werden (auch Prozessorflags genannt), geht aus den einzelnen Instruktionsbeschreibungen in der Befehlsliste hervor. Entsprechend kann mit den bedingten Sprunginstruktionen

    BRCC/BRCS ; Carry-Flag 0 oder gesetzt
    BRSH ; Gleich oder größer
    BRLO ; Kleiner
    BRMI ; Minus
    BRPL ; Plus
    BRGE ; Größer oder gleich (mit Vorzeichen)
    BRLT ; Kleiner (mit Vorzeichen)
    BRHC/BRHS ; Halbübertrag 0 oder 1
    BRTC/BRTS ; T-Bit 0 oder 1
    BRVC/BRVS ; Zweierkomplement-Übertrag 0 oder 1
    BRIE/BRID ; Interrupt an- oder abgeschaltet

auf die verschiedenen Bedingungen reagiert werden. Gesprungen wird immer dann, wenn die entsprechende Bedingung erfüllt ist. Keine Angst, die meisten dieser Instruktionen braucht man sehr selten. Nur Zero und Carry sind für den Anfang wichtig.

Zum Seitenanfang

Zeitzusammenhänge beim Programmablauf

Wie oben schon erwähnt entspricht die Zeitdauer zur Bearbeitung einer Instruktion in der Regel exakt einem Prozessortakt. Läuft der Prozessor mit 4 MHz Takt, dann dauert die Bearbeitung einer Instruktion 1/4 µs oder 250 ns, bei 10 MHz Takt nur 100 ns. Die Dauer ist quarzgenau vorhersagbar und Anwendungen, die ein genaues Timing erfordern, sind durch entsprechend exakte Gestaltung des Programmablaufes erreichbar. Es gibt aber eine Reihe von Instruktionen, z.B. die Sprungbefehle oder die Lese- und Schreibbefehle für das SRAM, die zwei oder mehr Taktzyklen erfordern. Hier hilft nur die Instruktionstabelle weiter.

Um genaue Zeitbeziehungen herzustellen, muss man manchmal den Programmablauf um eine bestimmte Anzahl Taktzyklen verzögern. Dazu gibt es die sinnloseste Instruktion des Prozessors, den Tu-nix-Befehl:

    NOP

Diese Instruktion heißt "No Operation" und tut nichts außer Prozessorzeit zu verschwenden. Bei 4 MHz Takt braucht es genau vier solcher NOPs, um eine exakte Verzögerung um 1 µs zu erreichen. Zu viel anderem ist diese Instruktion nicht zu gebrauchen. Für einen Rechteckgenerator von 1 kHz müssen aber nicht 1000 solcher NOPs kopiert werden, das geht auch anders! Dazu braucht es die Sprunginstruktionen. Mit ihrer Hilfe können Schleifen programmiert werden, die für eine festgelegte Anzahl von Läufen den Programmablauf aufhalten und verzögern. Das können 8-Bit-Register sein, die mit der DEC-Instruktion heruntergezählt werden, wie z.B. bei

    CLR R1
Zaehl:
    DEC R1
    BRNE Zaehl


Es können aber auch 16-Bit-Zähler sein, wie z.B. bei

    LDI ZH,HIGH(65535)
    LDI ZL,LOW(65535)
Zaehl:
    SBIW ZL,1
    BRNE Zaehl


Mit mehr Registern lassen sich mehr Verzögerungen erreichen. Jeder dieser Verzögerer kann auf den jeweiligen Bedarf eingestellt werden und funktioniert dann quarzgenau, auch ohne Hardware-Timer.

Zum Seitenanfang

Makros im Programmablauf

Es kommt vor, dass in einem Programm identische oder ähnliche Code-Sequenzen an mehreren Stellen benötigt werden. Will oder kann man den Programmteil nicht mittels Unterprogrammen bewältigen, dann kommt für diese Aufgabe auch ein Makro in Frage. Makros sind Codesequenzen, die nur einmal entworfen werden und durch Aufruf des Makronamens in den Programmablauf eingefügt werden. Nehmen wir an, es soll die folgende Codesequenz (Verzögerung um 1 µs bei 4 MHz Takt) an mehreren Programmstellen benötigt werden. Dann erfolgt irgendwo im Quellcode die Definition des folgenden Makros:

.MACRO Delay1
    NOP
    NOP
    NOP
    NOP
.ENDMACRO


Diese Definition des Makros erzeugt keinen Code, es werden also keine vier NOPs in den Code eingefügt. Erst der Aufruf des Makros

[...] irgendwo im Code
    Delay1
[...] weiter mit Code

bewirkt, dass an dieser Stelle vier NOPs eingebaut werden. Der zweimalige Aufruf des Makros baut acht NOPs in den Code ein.

Handelt es sich um größere Codesequenzen, hat man etwas Zeit im Programm oder ist man knapp mit Programmspeicher, sollte man auf den Code-extensiven Einsatz von Makros verzichten und lieber Unterprogramme verwenden.

Zum Seitenanfang

Unterprogramme

Im Gegensatz zum Makro geht ein Unterprogramm sparsamer mit dem Programmspeicher um. Irgendwo im Code steht die Sequenz ein einziges Mal herum und kann von verschiedenen Programmteilen her aufgerufen werden. Damit die Verarbeitung des Programmes wieder an der aufrufenden Stelle weitergeht, folgt am Ende des Unterprogrammes ein Return. Das sieht dann für 10 Takte Verzögerung so aus:

Delay10:
    NOP
    NOP
    NOP
    RET


Unterprogramme beginnen immer mit einem Label, hier Delay10:, weil sie sonst nicht angesprungen werden könnten. Es folgen drei Nix-Tun-Instruktionen und die abschließende Return-Instruktion. Schlaumeier könnten jetzt einwenden, das seien ja bloß 7 Takte (RET braucht 4) und keine 10. Gemach, es kömmt noch der Aufruf dazu und der schlägt mit 3 Takten zu Buche:

[...] irgendwo im Programm:
    RCALL Delay10
[...] weiter im Programm

RCALL ist eine Verzweigung zu einer relativen Adresse, nämlich zum Unterprogramm. Mit RET wird wieder der lineare Programmablauf abgebrochen und zu der Instruktion nach dem RCALL verzweigt. Es sei noch dringend daran erinnert, dass die Verwendung von Unterprogrammen das vorherige Setzen des Stackpointers oder Stapelzeigers voraussetzt (siehe Stapel), weil die Rücksprungadresse beim RCALL auf dem Stapel abgelegt wird.

Wer das obige Beispiel ohne Stapel programmieren möchte, verwendet den absoluten Sprungbefehl:

[...] irgendwo im Programm
    RJMP Delay10
Zurueck:

[...] weiter im Programm

Jetzt muss das "Unterprogramm" allerdings anstelle des RET natürlich ebenfalls eine RJMP-Instruktion bekommen und zum Label Zurueck: verzweigen. Da der Programmcode dann aber nicht mehr von anderen Stellen im Programmcode aufgerufen werden kann (die Rückkehr funktioniert jetzt nicht mehr), ist die Springerei ziemlich sinnlos. Auf jeden Fall haben wir jetzt das relative Springen mit RJMP gelernt.

RCALL und RJMP sind auf jeden Fall unbedingte Verzweigungen. Ob sie ausgeführt werden hängt nicht von irgendwelchen Bedingungen ab. Zum bedingten Ausführen eines Unterprogrammes muss das Unterprogramm mit einem bedingten Verzweigungsbefehl kombiniert werden, der unter bestimmten Bedingungen das Unterprogramm ausführt oder den Aufruf eben überspringt. Für solche bedingten Verweigungen zu einem Unterprogramm eignen sich die beiden folgenden Instruktionen ideal. Soll in Abhängigkeit vom Zustand eines Bits in einem Register zu einem Unterprgramm oder einem anderen Programmteil verzweigt werden, dann geht das so:

    SBRC R1,7 ; Überspringe die folgende Instruktion wenn Bit 7 im Register R1 Null ist
    RCALL UpLabel ; Rufe Unterprogramm auf

Hier wird der RCALL zum Unterprogramm UpLabel: nur ausgeführt, wenn das Bit 7 im Register R1 eine Eins ist, weil die Instruktion bei einer Null (Clear) übersprungen wird. Will man auf die umgekehrte Bedingung hin ausführen, so kommt statt SBRC die analoge Instruktion SBRS zum Einsatz. Die auf SBRC/SBRS folgende Instruktion kann sowohl eine Ein-Wort- als auch eine Zwei-Wort-Instruktion sein, der Prozessor weiß über die Länge der Instruktion korrekt Bescheid und überspringt dann eben um die richtige Anzahl Instruktionen. Zum Überspringen von mehr als einer Instruktion kann dieser bedingte Sprung natürlich nicht benutzt werden.

Soll ein Überspringen nur dann erfolgen, wenn zwei Registerinhalte gleich sind, dann bietet sich die etwas exotische Instruktion

    CPSE R1,R2 ; Vergleiche R1 und R2, springe wenn gleich
    RCALL UpIrgendwas ; Rufe irgendwas

Der wird selten gebraucht und ist ein Mauerblümchen.

Man kann aber auch in Abhängigkeit von bestimmten Port-Bits die nächsten Instruktion überspringen. Die entsprechenden Instruktionen heißen SBIC und SBIS, also etwa so:

    SBIC PINB,0 ; Überspringe wenn Bit 0 Null ist
    RJMP EinZiel ; Springe zum Label EinZiel

Hier wird die RJMP-Instruktion nur übersprungen, wenn das Bit 0 im Eingabeport PINB gerade Null ist. Also wird das Programm an dieser Stelle nur dann zum Programmteil EinZiel: verzweigen, wenn das Portbit 0 Eins ist. Das ist etwas verwirrend, da die Kombination von SBIC und RJMP etwas umgekehrt wirkt als sprachlich naheliegend ist. Das umgekehrte Sprungverhalten kann anstelle von SBIC mit SBIS erreicht werden. Leider kommen als Ports bei den beiden Instruktionen nur die unteren 32 infrage, für die oberen 32 Ports können diese Instruktionen nicht verwendet werden.

Nun noch die Exotenanwendung für den absoluten Spezialisten. Nehmen wir mal an, sie hätten vier Eingänge am AVR, an denen ein Bitklavier angeschlossen sei (vier kleine Schalterchen). Je nachdem, welches der vier Tasten eingestellt sei, soll der AVR 16 verschiedene Tätigkeiten vollführen. Nun könnten Sie selbstverständlich den Porteingang lesen und mit jede Menge Branch-Anweisungen schon herausfinden, welches der 16 Programmteile denn heute gewünscht wird. Sie können aber auch eine Tabelle mit je einem Sprungbefehl auf die sechzehn Routinen machen, etwa so:

MeinTab:
    RJMP Routine1
    RJMP Routine2

    [...]
    RJMP Routine16

Jetzt laden Sie den Anfang der Tabelle in das Z-Register mit

    LDI ZH,HIGH(MeinTab)
    LDI ZL,LOW(MeinTab)


und addieren den heutigen Pegelstand am Portklavier in R16 zu dieser Adresse.

    IN 16,PINB ; Lese Portklavier von Port B
    ANDI R16,0x0F ; Loesche die oberen Bits
    ADD ZL,R16 ; Adiere Portklavierstand zu Tabellenadresse
    BRCC NixUeberlauf ; kein Ueberlauf
    INC ZH ; Ueberlauf in MSB
NixUeberlauf:

Jetzt können Sie nach Herzenslust und voller Wucht in die Tabelle springen, entweder nur mal als Unterprogramm mit

    ICALL

oder auf Nimmerwiederkehr mit

    IJMP

Der Prozessor lädt daraufhin den Inhalt seines Z-Registerpaares in den Programmzähler und macht dort weiter. Manche finden das eleganter als unendlich verschachtelte bedingte Sprünge. Mag sein.

Zum Seitenanfang

Interrupts im Programmablauf

Sehr oft kommt es vor, dass in Abhängigkeit von irgendwelchen Änderungen in der Hardware oder zu bestimmten Gelegenheiten auf dieses Ereignis reagiert wird. Ein Beispiel ist die Pegeländerung an einem Eingang. Man kann das lösen, indem das Programm im Kreis herum läuft und immer den Eingang befragt, ob er sich nun geändert hat. Die Methode heisst Polling, weil es wie bei die Bienchen darum geht, jede Blüte immer mal wieder zu besuchen und deren neue Pollen einzusammeln. Gibt es ausser diesem Eingang noch anderes wichtiges für den Prozessor zu tun, dann kann das dauernde zwischendurch Anfliegen der Blüte ganz schön nerven und sogar versagen: Kurze Pulse werden nicht erkannt, wenn Bienchen nicht gerade vorbei kam und nachsah, der Pegel aber schon wieder weg ist. Für diesen Fall hat man den Interrupt erfunden.

Der Interrupt ist eine von der Hardware ausgelöste Unterbrechung des Programmablaufes. Dazu kriegt das Gerät zunächst mitgeteilt, dass es unterbrechen darf. Dazu ist bei dem entsprechenden Gerät ein oder mehr Bits in bestimmten Ports zu setzen. Dem Prozessor ist durch ein gesetztes Interrupt Enable Bit in seinem Status-Register mitzuteilen, dass er unterbrochen werden darf. Irgendwann nach den Anfangsfeierlichkeiten des Programmes muss dazu das I-Flag im Statusregister gesetzt werden, sonst macht der Prozessor beim Unterbrechen nicht mit und der Interrupt verhungert.

    SEI ; Setze Int-Enable

Wenn jetzt die Bedingung eintritt, also z.B. ein Pegel von Null auf Eins wechselt, dann legt der Prozessor seine aktuelle Programmadresse erst mal auf den Stapel ab (Obacht! Der muss vorher eingerichtet sein! Wie das geht siehe Stapel). Er muss ja das unterbrochene Programm exakt an der Stelle wieder aufnehmen, an dem es unterbrochen wurde, dafür der Stapel. Nun springt der Prozessor an eine vordefinierte Stelle des Programmes und setzt die Verarbeitung erst mal dort fort. Die Stelle nennt man Interrupt Vektor. Sie ist prozessorspezifisch festgelegt. Da es viele Geräte gibt, die auf unterschiedliche Ereignisse reagieren können sollen, gibt es auch viele solcher Vektoren. So hat jede Unterbrechung ihre bestimmte Stelle im Programm, die angesprungen wird. Die entsprechenden Programmstellen einiger älterer Prozessoren sind in der folgenden Tabelle aufgelistet. (Der erste Vektor ist aber gar kein Int-Vektor, weil er keine Rücksprung-Adresse auf dem Stapel ablegt!)

NameInt Vector Adresseausgelöst durch ...
231323238515
RESET000000000000Hardware Reset, Power-On Reset, Watchdog Reset
INT0000100010001Pegelwechsel am externen INT0-Pin
INT10002-0002Pegelwechsel am externen INT1-Pin
TIMER1 CAPT0003-0003Fangschaltung am Timer 1
TIMER1 COMPA--0004Timer1 = Compare A
TIMER1 COMPB--0005Timer1 = Compare B
TIMER1 COMP10004--Timer1 = Compare 1
TIMER1 OVF0005-0006Timer1 Überlauf
TIMER0 OVF000600020007Timer0 Überlauf
SPI STC--0008Serielle Übertragung komplett
UART RX0007-0009UART Zeichen im Empfangspuffer
UART UDRE0008-000AUART Senderegister leer
UART TX0009-000BUART Alles gesendet
ANA_COMP--000CAnalog Comparator


Man erkennt, dass die Fähigkeit, Interrupts auszulösen, sehr unterschiedlich ausgeprägt ist. Sie entspricht der Ausstattung der Chips mit Hardware. Die Adressen folgen aufeinander, sind aber für den Typ spezifisch durchnumeriert. Die Reihenfolge hat einen weiteren Sinn: Je höher in der Liste, desto wichtiger. Wenn also zwei Geräte gleichzeitig die Unterbrechung auslösen, gewinnt die jeweils obere in der Liste. Die niedrigerwertige kommt erst dran, wenn die höherwertige fertig bearbeitet ist. Damit das funktioniert, schaltet der erfolgreiche Interrupt das Interrupt-Status-Bit aus. Dann kann der niederwertige erst mal verhungern. Erst wenn das Interrupt-Status-Bit wieder angeschaltet wird, kann die nächste anstehende Unterbrechung ihren Lauf nehmen.

Für das Setzen des Statusbits kann die Unterbrechungsroutine, ein Unterprogramm, zwei Wege einschlagen. Es kann am Ende mit der Instruktion

    RETI

abschließen. Diese Instruktion stellt den ursprünglichen Befehlszähler wieder her, der vor der Unterbrechung gerade bearbeitet werden sollte und setzt das Interrupt-Status-Bit auf Eins. Der zweite Weg ist das Setzen des Status-Bits per Instruktion

    SEI ; Setze Interrupt Enabled
    RET ; Kehre zurück

und das Abschließen der Unterbrechungsroutine mit einem normalen RET. Aber Obacht, das ist nicht identisch im Verhalten! Im zweiten Fall tritt die noch immer anstehende Unterbrechung schon ein, bevor die anschließende Return-Instruktion bearbeitet wird, weil das Status-Bit schon um eine Instruktion früher wieder Ints zulässt. Das führt zu einem Zustand, der das überragende Stapelkonzept so richtig hervorhebt: ein verschachtelter Interrupt. Die Unterbrechung während der Unterbrechung packt wieder die Rücksprung-Adresse auf den Stapel (jetzt liegen dort zwei Adressen herum), bearbeitet die Unterbrechungsroutine für den zweiten Interrupt und kehrt an die oberste Adresse auf dem Stapel zurück. Die zeigt auf die noch verbliebenen RET-Instruktion, die jetzt erst bearbeitet wird. Diese nimmt nun auch die noch verbliebene Rücksprung-Adresse vom Stapel und kehrt zur Originaladresse zurück, die zur Zeit der Unterbrechung zur Bearbeitung gerade anstand. Durch die LIFO-Stapelei ist das Verschachteln solcher Aufrufe über mehrere Ebenen kein Problem, solange der Stapelzeiger noch richtiges SRAM zum Ablegen vorfindet. Folgen allerdings zu viele Interrupts, bevor der vorherige richtig beendet wurde, dann kann der Stapel auf dem SRAM überlaufen. Aber das muss man schon mit Absicht programmieren, weil es doch sehr selten vorkommt.

Klar ist auch, dass an der Adresse des Int-Vektors nur für eine Ein-Wort-Instruktion Platz ist. Das ist in der Regel eine RJMP-Instruktion an die Adresse des Interrupt Unterprogrammes. Es kann auch einfach eine RETI-Instruktion sein, wenn dieser Vektor gar nicht benötigt wird. Bei sehr großen ATmega ist der Speicherraum mit der Ein-Wort-Instruktion RJMP nicht mehr ganz erreichbar. Bei diesen haben die Vektoren deshalb zwei Worte. Diese MÜSSEN mit JMP (Zwei-Wort-Instruktion) bzw. mit RETI, gefolgt von einem NOP, konstruiert werden, damit die Adressen stimmen.

Wegen der Notwendigkeit, die Interrupts möglichst rasch wieder freizugeben, damit der nächste anstehende Int nicht völlig aushungert, sollten Interrupt-Service-Routinen nicht allzu lang sein. Lange Prozeduren sollten vermieden und durch ein anderes Bearbeitungsschema ersetzt werden (Bearbeitung der zeitkritischen Teile innerhalb des Interrupts, Setzen einer Flagge, weitere Bearbeitung au├čerhalb der Serviceroutine).

Eine ganz, ganz wichtige Regel sollte in jedem Fall eingehalten werden: Die erste Instruktion einer Interrupt-Service-Routine ist die Rettung des Statusregisters in ein Register. Die letzte Instruktion der Routine ist die Wiederherstellung desselben in den Originalzustand vor der Unterbrechung. Jede Instruktion in der Unterbrechungsroutine, die irgendeines der Flags (außer dem I-Bit) verändert, würde unter Umständen verheerende Nebenfolgen auslösen, weil im unterbrochenen Programmablauf plötzlich irgendeins der verwendeten Flags anders wird als an dieser Stelle im Programmablauf vorgesehen, wenn die Interrupt-Service Routine zwischendurch zugeschlagen hat. Das ist auch der Grund, warum alle verwendeten Register in Unterbrechungsroutinen entweder gesichert und am Ende der Routine wiederhergestellt werden müssen oder aber speziell nur für die Int-Routinen reserviert sein müssen. Sonst wäre nach einer irgendwann auftretenden Unterbrechung für nichts mehr zu garantieren. Es ist nichts mehr so wie es war vor der Unterbrechung.

Weil das alles so wichtig ist, hier eine ausführliche beispielhafte Unterbrechungsroutine.

.CSEG ; Hier beginnt das Code-Segment
.ORG 0000 ; Die Adresse auf Null
    RJMP Start ; Der Reset-Vektor an die Adresse 0000
    RJMP IService ; 0001: erster Interrupt-Vektor, INT0 service routine
[...] hier eventuell weitere Int-Vektoren

Start: ; Hier beginnt das Hauptprogramm
[...] hier kann alles mögliche stehen

IService: ; Hier beginnt die Interrupt-Service-Routine
    IN R15,SREG ; Statusregister Zustand einlesen, R15 exklusiv
[...] Hier macht die Int-Service-Routine irgendwas
    OUT SREG,R15 ; und Statusregister wiederherstellen
    RETI ; und zurückkehren

Das klingt alles ziemlich umständlich, ist aber wirklich lebenswichtig für korrekt arbeitende Programme. Es vereinfacht sich entsprechend nur dann, wenn man Register satt zur Verfügung hat und wegen exklusiver Nutzung durch die Service- Routine auf das Sichern verzichten kann.

Das war für den Anfang alles wirklich wichtige über Interrupts. Es gäbe noch eine Reihe von Tips, aber das würde für den Anfang etwas zu verwirrend.

Zum Seitenanfang

©2002-2009 by http://www.avr-asm-tutorial.net