Path: Home => AVR-Überblick => Interrupt-Programmierung => Interrupts und Ressourcen
Interrupt

Interrupts und Ressourcen

Interrupts haben den Vorteil, dass sie nur dann auftreten und bearbeitet werden müssen, wenn ein bestimmtes Ereignis tatsächlich eintritt. Selten ist vorhersagbar, wann das Ereignis genau eintreten wird. Sogar mehrere unterschiedliche Ereignisse können gleichzeitig oder kurz hintereinander eintreten. Zum Beispiel kann der Timer gerade einen Overflow haben und der AD-Wandler ist gerade mit einer Messumwandlung fertig. Diese Eigenart, dass sie jederzeit auftreten können, macht einige besondere Überlegungen beim Programmieren erforderlich, die in Hochsprachen entweder nicht vorkommen oder vom Compiler eigenständig erledigt werden. In Assembler müssen wir selber denken, und werden dafüfür mit einem schlanken, sauschnellen und zuverlässigen Programm belohnt. Die folgenden Überlegungen nimmt uns Assembler-Programmierern keiner ab.

Klare Ressourcenzuordnung

Das Statusregister als sensible Ressource

Das Zuschlagen eines Interrupts kann jederzeit und überall, nur nicht innerhalb von Interrupt-Service-Routinen erfolgen. Erfolgt innerhalb der Service-Routine eine Beeinflussung von Flaggen des Statusregisters, was nahezu immer der Fall ist, dann kann das im Hauptprogramm-Loop üble Folgen haben. Plötzlich ist das Zero- oder Carry-Flag im Statusregister gesetzt, nur weil zwischendurch der Interrupt zuschlug. Nichts funktioniert mehr so, wie man es erwartet hat. Aber das nur machmal und nicht immer. Ein solches Programm verhält sich eher wie ein Zufallsgenerator als wie ein zuverlässiges Stück Software.

AVR-Prozessoren haben keinen zweiten Satz Statusregister, auf den sie bei einem Interrupt umschalten können. Deshalb muss das Statusregister vor einer Veränderung in der Interrupt-Service-Routine gesichert und vor deren Beendigung wieder in seinen Originalzustand versetzt werden. Dazu gibt es drei Möglichkeiten:
Nr.Sicherung inCodeZeitbedarf
Takte
VorteileNachteile
1Registerin R15,SREG
[...]
out SREG,R15
2 SchnellRegister-
verbrauch
2Stapelpush R0
in R0,SREG
[...]
out SREG,R0
pop R0
6Kein Register-
verbrauch
Lahm
3SRAMsts $0060,R0
in R0,SREG
[...]
out SREG,R0
lds R0,$0060
6
Damit ist die Auswahl und Priorität klar. Alles entscheidend ist, ob man Register satt verfügbar hat oder machen kann.

Oft verwendet man für eine einzelne Flagge gerne das T-Bit im Statusregister. Da gibt es bei allen drei Methoden einen üblen Fallstrick: Jede Veränderung am T-Bit wird wieder überschrieben, wenn am Ende der Routine der Originalstatus des Registers wieder hergestellt wird. Bei Methode 1 muss also Bit 7 des R15 gesetzt werden, damit das T-Bit nach der Beendigung der Routine als gesetzt resultiert. Obacht: Kann einige Stunden Fehlersuche verursachen!

Verwendung von Registern

Angenommen, eine Interrupt-Service-Routine mache nichts anderes als das Herunterzählen eines Zählers. Dann ist klar, dass das Register, in dem der Zähler untergebracht ist (z.B. mit .DEF rCnt = R17 definiert), zu nichts anderem eingesetzt werden kann. Jede andere Verwendung von R17 würde den Zählrhythmus empfindlich stören.

Dabei hülfe es bei einer Verwendung in der Hauptprogramm-Schleife auch nicht, wenn wir den Inhalt des Registers mit PUSH rCnt auf dem Stapel ablegen würden und nach seiner Verwendung den alten Zustand mit POP rCnt wieder herstellen würden. Irgendwann schlägt der Interrupt genau zwischen dem PUSH und dem POP zu, und dann haben wir den Salat. Das passiert vielleicht nur alle paar Minuten mal, ein schwer zu diagnostizierender Fehler.

Wir müssten dann schon vor der Ablage auf dem Stapel alle Interrupts mit CLI abschalten und nach der Verwendung und Wiederherstellung des Registers die Interrupts wieder mit SEI zulassen. Liegen zwischen Ab- und Anschalten der Interrupts viele Hundert Instruktionen, dann stauen sich die zwischenzeitlich aufgelaufenen Interrupts und können verhungern. Wenn zwischendurch der Zähler zwei mal übergelaufen ist, dann geht unsere Uhr danach etwas verkehrt, weil aus den zwei Ereignissen nur ein Interrupt geworden ist.

Innerhalb einer anderen Interrupt-Service-Routine können wir rCnt auf diese Weise (also mit PUSH und POP) verwenden, weil diese nicht durch andere Interrupts unterbrochen werden kann. Allerdings sollte man sich bewusst sein, dass jedes PUSH- und POP-Pärchen vier weitere Taktimpulse verschwendet. Wer also satt Zeit hat, hat auch Register en masse. Wer keine Register übrig hat, kommt um so was vielleicht nicht herum.

Manchmal MUSS auf ein Register oder auf einen Teil eines Registers (z.B. ein Bit) sowohl von innerhalb als auch von außerhalb einer Interrupt-Service-Routine zugegriffen werden, z.B. weil die ISR dem Hauptprogramm-Loop etwas mitzuteilen hat. In diesen Fällen muss man sich ein klares Bild vom Ablauf verschaffen. Es muss klar sein, dass Schreibzugriffe sich nicht gegenseitig stören oder blockieren. Es muss dann auch klar sein, dass dasselbe Register bei zwei nacheinderfolgenden Lesevorgängen bereits von einem weiteren Interrupt verändert worden sein kann. Liegen die zwei Zeitpunkte längere Zeit auseinander, dann ist es je nach dem Timing des Gesamtablaufes fast zwingend, dass irgendwann ein Konflikt auftritt.

An einem Beispiel: sowohl der Timer als auch der ADC haben dem Hauptprogramm etwas mitzuteilen und setzen jeweils ein Bit eines Flaggenregisters rFlag. Also etwa so:
Isr1:
	[...]
	sbr rFlag,1<<bitA ; setze Bearbeitungsflagge A
	[...]
Isr2:
	[...]
	sbr rFlag,1<<bitB ; setze Bearbeitungsflagge B
	[...]
Wenn jetzt in der Hauptprogrammschleife die beiden Bits nacheinander abgefragt und Behandlungsroutinen aufgerufen werden, kann entweder bitA oder bitB oder sowohl bitA als auch bitB gesetzt sein. Auf jeden Fall müssen die gesetzten Flaggen so schnell wie möglich wieder auf Null gesetzt werden, denn der nächste Interrupt kann schon bald wieder zuschlagen. Auf keinen Fall dürfen wir beide Flaggen erst nach einer aufwändigen Bearbeitung löschen, weil wir dann vielleicht die nächste Bearbeitungsanforderung verpassen würden. Es lohnt sich dann, mit der in schnellerer Folge auftretenden Anforderung zu beginnen und dann die etwas gemütlicher werkelnde zweite Anforderung zu bearbeiten. Wenn A häufiger ist als B, z.B. so:
Loop:
	sleep ; schlafen legen
	nop ; Dummy nach Aufwachen
	sbrc rFlag,bitA ; frage erst bitA ab
	rcall BearbA ; bearbeite Anforderung A
	sbrc rFlag,bitB ; frage dann bitB ab
	rcall BearbB ; bearbeite dann Anforderung B
	sbrc rFlag,bitA ; frage zur Sicherheit noch mal bitA ab
	rcall BearbA ; bearbeite weitere Anforderung A, falls nötig
	rjmp Loop ; gehe wieder schlafen
;
BearbA: ; Bearbeite Anforderung A
	cbr rFlag,1<<bitA ; lösche vor der Bearbeitung die gesetzte Flagge
	[...]
	ret
;
BearbB: ; Bearbeite Anforderung B
	cbr rFlag,1<<bitB ; lösche vor der Bearbeitung die gesetzte Flagge
	[...]
	ret
Man beachte, dass in beiden Fällen immer nur ein Bit im Register rFlag zurückgesetzt werden darf, also auf keinen Fall CLR rFlag, weil es könnten ja in seltenen Fällen auch beide gleichzeitig gesetzt worden sein.

Ist eine der Behandlungsroutinen ein längliches Monster, z.B. der Update einer LCD-Anzeige, 16 Schreibvorgänge im EEPROM oder das Leeren eines Pufferspeichers mit 80 Zeichen im SRAM, dann kann das zum Verhungern der jeweils anderen Bearbeitungsroutine führen. In diesen Fällen ist Obacht geboten. Dann muss man eben das EEPROM auch interrupt-gesteuert befüllen (mit dem EE_RDY-Interrupt), auch wenn das einigen Programmieraufwand mehr erfordert. Oder sich eine andere Lösung ausdenken.

Betrachten Sie die genannten Konflikte der Bearbeitung von Signalen als anspruchsvolle Denksportaufgabe, dann haben Sie den richtigen Ansatz.

Die oben beschriebene Zugriffssteuerung über eine Interrupt-Blockage mit CLI/SEI ist auf jeden Fall dann zwingend, wenn ein Doppelregister außerhalb von Interrupt-Service-Routinen auf einen neuen Wert gesetzt werden muss und innerhalb von Service-Routinen verwendet wird. Zwischen dem Setzen des einen Bytes des Doppelregisters und des zweiten könnte ein Interrupt zuschlagen und einen völlig verqueren Wert vorfinden. Solche Fehler sind teuflisch, weil es in der Regel korrekt funktioniert und nur alle paar Stunden ein Mal dieser Fall eintritt. So einen eingebauten Bug findet man garantiert nicht, weil er sich so selten in freier Wildbahn in der Schaltung zeigt.

Daher sollten folgende Regeln gelten:

To the top of that page

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