Interrupts in Megamax-C: Interrupti te salutant

In modernen Computern wie dem Atari ST spielen durch Interrupt gesteuerte Funktionen eine wichtige Rolle, da sie die Leistungsfähigkeit des Systems beträchtlich erhöhen können. Der weitverbreitete Megamax-C-Compiler gestattet durch die Verwendung von Inline-Assemblercode eine bequeme Einbindung von Interruptroutinen in C-Programme. Probleme treten leider dann auf, wenn man in dieser Interruptroutine auf globale Variablen dieses C-Programms zugreifen möchte. Doch die Lösung ist einfach und ermöglicht es sogar, die Interruptfunktion komplett in C zu schreiben.

Im Atari ST gibt es zahlreiche Quellen von Interrupts. Serielle und parallele Schnittstelle, verschiedene Timer, die Tastatur oder auch der SHIFTER (der Video-Controller) unterbrechen in regelmäßigen oder unregelmäßigen Abständen den Prozessor, um anzuzeigen, daß sie in irgendeiner Form bedient werden wollen. Ein Drucker an der Parallelschnittstelle kann so z. B. mitteilen, daß er wieder bereit ist, neue Zeichen anzunehmen. Wegen dieser Signale müssen Programme nicht ständig die Peripherie abfragen, um solche Ereignisse nicht zu verpassen. Das spart natürlich Zeit.

Aus Platzgründen ist es leider nicht möglich, an dieser Stelle alle Grundlagen der Interruptprogrammierung zu wiederholen. Hier geht es lediglich um die Realisierung von interruptgesteuerten Programmen in Megamax-C. Die zahlreichen Beispiele können vielleicht aber auch Programmierern in anderen Sprachen Anregungen vermitteln.

Bei Auftreten eines Interrupts unterbricht der Prozessor seine momentane Arbeit, legt den Inhalt des Statusregisters auf dem Stack ab und springt in die Interrupt-Service-Routine, also das Unterprogramm, das zur Bearbeitung dieses Interrupts vorgesehen ist. Dieses Unterprogramm muß dafür sorgen, daß bei Beendigung der Routine die Prozessorregister den gleichen Inhalt haben wie beim Eintritt. Sonst könnte es passieren, daß nach dem Rücksprung das Hauptprogramm nicht mehr korrekt fortgesetzt werden kann (z. B. wenn ein wichtiges Zwischenergebnis in einem Register überschrieben wurde). Der Rücksprung selbst muß durch den Assemblerbefehl ’rte’ erfolgen, der dafür sorgt, daß auch das Statusregister wieder mit dem Wert geladen wird, der anfangs auf dem Stack abgelegt wurde. Eine dritte Voraussetzung von Interruptroutinen ist, daß sie keine Annahmen über den Inhalt von Prozessorregistern machen dürfen. Diesem Punkt kommt beim Megamax-Compiler eine besondere Bedeutung zu, die wir gleich sehen werden.

Betrachten wir zuerst die Formulierung einer Interruptroutine in Assembler. Register, die von dieser Funktion benötigt werden, rettet man am besten gleich zu Anfang auf den Stack. Mit dem Befehl ’movem.l'(Reg.liste), - (A7)’ kann man gleich mehrere - oder auch alle - Register auf einmal sichern (A7 ist bekanntlich der Stackpointer im MC68000). Der umgekehrte Befehl ’movem.L (A7) + , (Reg.liste)’ stellt am Ende der Funktion den alten Inhalt der Register wieder her und ein ’rte’ bewirkt den Rücksprung (vgl. Abb. 1):

ir_funktion() 
{	asm
	{
		movem.l D0-D7/A0-A5,-(A7) ;sichert alle Register auf dem Stack 
		...						;hier steht die eigentliche Funktion
		movem.l (A7)+,D0-D7/A0-A5 ;holt die alten Registerinhalte 
		unlk A6
		rte						;beendet die Funktion
	}
}

Abb. 1

Auffällig ist die merkwürdige Behandlung des Registers A6. Dieses Register wird vom Compiler verwaltet. Schaut man sich die Übersetzung von ir_funktion() mit dem Disassembler an, so zeigt sich etwa folgende Sequenz: (vgl. Abb 2):

ir_funktion:	link	A6,#0	;Schafft Platz für lokale Variablen
				movem.l D0-D7/A0-A5,-(A7)
				...
				movem.l (A7)+,D0-D7/A0-A5
				unlk	A6		;holt alten Wert von A6
				rte				;vom Stack
				unlk	A6		;hierhin kommt man nie
				rts				;wegen ’rte'

Abb.2

Die Verwendung von A6 ist völlig transparent, d. h. das Register hat nach der Funktion den gleichen Wert wie vorher. Daher können in Interruptroutinen lokale Variablen problemlos benutzt werden (näheres steht im Megamax-Handbuch, S. 2-3/24, (vgl. Abb. 3):

ir_funktion()
{	int lokale_var; 
	asm
	{	movem.l D0-D7/A0-A5,-(A7) ;sichert alle Register auf dem Stack 
		move.w #1,lokale_var(A6) ;gibt lokale_var den Wert 1
		... 					;hier steht die restliche Funktion 
		movem.l (A7)+,D0-D7/A0-A5 ;holt die alten Registerinhalte 
		unlk A6
		rte						;beendet die Funktion.
	}
}

Abb.3

Etwas anders ist die Situation, wenn auf globale Variablen zugegriffen werden soll, damit etwa Resultate der Interruptroutine von C-Funktionen weiterverarbeitet werden können. Ein Beispiel biete das Listing ’MSTIMER.C’, auf das wir noch zu sprechen kommen.

Globale und Stringvariablen werden in Megamax-C ähnlich wie lokale Variablen angesprochen. Nur zeigt hier das Adressregister A4 auf den Speicherbereich, in dem diese Variablen abgelegt sind. A4 hat während der gesamten Programmdauer den gleichen Wert - anders als Register A6 für lokale Variablen, das seinen Wert erst bei Eintritt in eine Funktion erhält. A4 wird beim Programmstart initialisiert. Und hier liegt das Problem für Interruptroutinen: Man kann ja nicht Vorhersagen, wann ein Interrupt auftritt und wann demnach die zugehörige Routine aufgerufen wird. Das kann auch während der Abarbeitung eines Programmstücks geschehen, welches das Register A4 temporär mit einem anderen Wert belegt hat (z. B. eine andere Interruptroutine, ein externes Programm oder eine Betriebssystemfunktion). Kurzum, wir können nicht sicher sein, daß A4 in unserer Interruptroutine tatsächlich auf die globalen Variablen zeigt und brauchen daher eine andere Möglichkeit, Daten zwischen C und solchen Routinen auszutauschen.

Eine der Möglichkeiten soll im Folgenden kochrezeptärtig dargestellt werden. Zur Verdeutlichung dient das Modul TIMER.C, das eine Stopuhr mit einer Auflösung von 1 ms realisiert. In den Zeilen 23..43 können Sie das Vorgehen mitverfolgen:

  1. Im Assemblercode wird Speicher zur Aufnahme der Daten reserviert, hier für ein Langwort (= Adresse) = > dc.l 0L. Mehrere solcher Definitionen, auch für Wort- oder Bytegrößen, sind natürlich möglich.
  2. Diese Datenbereiche werden mit Labein versehen = > _msadr:.
  3. Die Label müssen global deklariert werden = > static void _msadr() oder extern void _msadr(). Wichtig ist die Deklaration als Funktion, da nur diese von Megamax akzeptiert wird. Der Typ der Funktion ist dagegen beliebig. Sie können statt void also auch long oder etwas anderes schreiben.
  4. Irgendwo sollte eine korrespondierende C-Variable definiert sein. Bei uns ist das Zeile 25: static long millisec;. Static dient nur dazu, diese Variable vor anderen Modulen zu verbergen und kann auch entfallen.
  5. Funktionsnamen sind Adressen von Funktionen, also Zeiger. Die Adressen von Variablen sind ebenfalls Zeiger. Zeiger kann man einander zuweisen!
  6. Mittels cast-Operator werden die Typen der beiden Zeiger angeglichen (Megamax ist da recht pingelig) = > * (long * *)_msadr = &millisec. Man kann auch schreiben: *(long *)_msadr = (long) &millisec.

Das alles hört sich komplizierter an, als es ist. Vergleichen Sie das Listing.

In diesem Beispiel haben wir nur eine Variable der Interruptroutine behandelt. Natürlich könnte man mit dieser Methode auch mehrere Variablen übergeben, aber das wird umständlich. Genausogut kann man auch den Wert des Registers A4 eintragen, über das ja alle globalen Variablen angesprochen werden können. Diesen Wert erfährt man mit folgender kleinen Funktion (Abb. 4):

long get_a4()
{	asm
	{ move.l A4,D0 } ;in D0 wird der Funktionswert an das 
}	;aufrufende	Programm zurückgegeben

Abb.4

Endlich haben wir alles beisammen, um Interruptroutinen gänzlich in C zu formulieren. Gänzlich? Nun ja, ein bißchen Assembler bleibt schon übrig. Sie erinnern sich, daß wir bei Funktionseintritt die Inhalte aller benötigten Register retten müssen, um sie am Ende wieder zurückschreiben zu können (das unterbrochene Programm darf ja nicht merken, was in der Zwischenzeit geschehen ist), und daß wir die Funktion ausdrücklich mit ’rte’ verlassen müssen, weil das ’rts’, das Megamax einsetzt, kein ordnungsgemäßer Abschluß einer Interruptroutine ist. Kurzum, wir brauchen das gleiche Gerüst, das wir oben im Beispiel der ir_funktion() gezeigt haben. Man faßt es am besten in zwei Makros, die dann immer zu Beginn und am Ende einer Interruptfunktion aufgerufen werden (wer mag, schreibt sich diese in eine #include-Datei und kann die Details der Implementierung danach beruhigt wieder vergessen). Die Makros finden Sie in Abb. 5:

#define IR_ENTRY asm {	movem.l D0-D7/A0-A5,-(A7) 
						movea.l _a4,A4\ 
						bra.s _ok\
					_a4: dc.l	0L\
					_ok: }

#define RTE	asm	{	movem.l (A7)+,D0-D7/A0-A5N
					unlk	A6\
					rte }

Abb.5

Bitte beachten Sie jeweils den Backslash ("") am Zeilenende, der dem den ’movem.l’-Anweisungen auf die tatsächlich benötigten Register zu verkürzen (A4 gehört auch dazu!). Auf jeden Fall sollten Sie nicht vergessen, den Inhalt von A4 nach dem beschriebenen Vorgehen an der Stelle _a4: einzutragen.

Mit diesen Hilfen könnten wir die Zeilen 23..43 im Beispielprogramm nun auch so schreiben, wie es Abb. 6 zeigt.


23: static long millisec; 
	static long ms_save; 
	static void _a4();
	#define IR_ENTRY ...	/* wie oben beschrieben */
	#define RTE				/* auch wie oben */
	static long get_a4()
	{ ... }	/* ebenfalls schon bekannt */

	static void _timeavec()
	{	char *mpfisra = (char *)=0xfffa0f;

		IR_ENTRY
		millisec++;
		*(mpfisra) &= 0xdf;	/*	= bclr #5,MFPISRA */
		RTE
	}
	void ti_on()
	{	ti_stop();
		ti_clr();
		*(long *)_a4 = get_a4();	/* wert von A4 in _timavec() eintragen für Zugriff auf millisec */
		xbtimer(TIMER_A,TIMACTRL;TICADAT,_timavec);
	}

Abb. 6

Der Zuwachs an Übersichtlichkeit schaltet mögliche Fehlerquellen aus, und von ihnen gibt es ohnehin genug.

Abschließend noch einige kurze Worte zu dem Beispielprogramm MSTIMER.C:

Kernstück ist eine Routine, die von Interrupts des (im ST sonst nicht benutzten) Timers A aufgerufen wird. Dieser wird so programmiert, daß er etwa jede Millisekunde eine Unterbrechung auslöst (genau läßt sich das leider nicht erreichen). Die Funktion wird also sehr oft abgearbeitet und sollte daher möglichst schnell sein, um die Systemleistung nicht zu arg herabzusetzen.

Compiler mitteilt, daß die Makrodefinition in der nächsten Zeile fortgesetzt wird. Register A6 wird - wie erwähnt - beim Eintritt in die Funktion vom Compiler verwaltet, beim Verlassen müssen wir selbst den alten Wert restaurieren. Um die Makros universell zu halten, werden einfach alle Register auf dem Stack zwischengelagert. Nur bei sehr zeitkritischen Interruptroutinen lohnt es sich, die Registerliste in Deshalb wird als Adresse der Interruptroutine auch nicht die der C-Funk-tion angegeben (die darum dummy() heißt), sondern das Label _timavec: innerhalb der Funktion. Man spart damit die Reservierung von Speicher für lokale Variablen, die hier ohnehin nicht benötigt werden (vgl. Megamax-Handbuch S. 2-3). Somit muß auch am Ende der Funktion nicht erst der Inhalt von A6 restauriert werden. Auch das Einträgen der Adresse des ms-Zählers in den Programmcode ist nicht lehrbuchreif, aber schnell. Vorteil: Wenn diese Interruptroutine mitläuft, reduziert sich die Systemgeschwindigkeit trotz des häufigen Aufrufs nur um knapp ein Prozent.

Die weiteren Funktionen sind sehr einfach. Ti_on() schreibt die Adresse des Zählers in die Funktion, initialisiert den Timer A auf eine Frequenz von ca. 1 kHz, trägt das Label_timavec: als Adresse der Interruptroutine ein und startet den Timer dann - alles mit dem Aufruf der Betriebssystemfunktion Xbtimer(). Ti_cget() bzw. ti_mscget() geben die Zeit seit dem (Re-) Start des Timer wieder (das c steht für cumuliert), ti_get() bzw. ti_msget() ergeben die Zeit seit der letzten Abfrage, damit können also Zwischenzeiten gemessen werden. Die Funktionen mit ms im Namen verrechnen die ermittelten Zeiten noch mit einem Korrekturfaktor, so daß die Zeitbasis tatsächlich einer Millisekunde entspricht. Mit ti_restart() wird der ms-Zähler auf Null gesetzt, und ti_off() sollte immer aufgerufen werden, wenn der Timer nicht immer benötigt wird, unbedingt aber am Programmende. Diese Funktion schaltet nämlich die Interrupts des Timers A wieder ab. Stellen Sie sich einmal vor, was passiert, wenn nach Programmende so ein Interrupt auftritt, und die zugehörige Interruptroutine steht nicht mehr im Speicher: Natürlich gibt das eine „Bombenstimmung“. In der #include-Datei MSTIMER.H werden die Funktionen als extern deklariert. Das ist nötig, da sie nicht vom Typ ’int’ sind.

Viel Spaß und Erfolg beim Experimentieren also, und wenn’s mal schief geht: Abstürze gehören einfach dazu!

H. Huptach

/*************************************************
*                  MSTIMER.H                     *
*       Headerfile zue Millisekunden-Timer       *
*          Megamax C, V1.1 auf Atari ST          *
**************************************************
*    H. Huptasch, Deisterstr. 15, 3250 Hameln    * 
*************************************************/

extern long ti_get(); 
extern long ti_cget(); 
extern long ti_msget(); 
extern long ti_mscget(); 
extern long ti_conv();
/*************************************************
*                  MSTIMER.C                     *
*        Timer mit 1 Millisekunde Auflösung      *
*          Megamax C, V1.1 auf Atari ST          *
**************************************************
*    H. Huptasch, Deisterstr. 15, 3250 Hameln    * 
*************************************************/

#include <osbind.h>
#define void int

#define MFPISRA 0xfffa0f
			/* MPF Interrupt-In-Service Reg. A */ 
#define TIMER_A 0
#define TIMADAT 49 /* Zähler für Timer A */
#define TIMACTRL 4 /* Timer A, Vorteiler 50 */
#define MFP_TIMERA 13
				/* MFP interrupt level, timer A */

#define ti_stop() Xbtimer(TIMER_A,0,0,-1L)
#define ti_clr() (millisec = ms_save = 0L)

static long millisec; 
static lcng ms_save; 
static void _timavec();
static void _msadr(); 			/* Trick 17! */

static void dummy()
{	asm
	{	_timavec:	dc.w 0x52b9 ;addq.l #1,$xxxxxx
		_msadr:		dc.l 0L		;Adr. ms-Zähler
					bclr #5,MFPISRA ;IRQ löschen 
					rte
	}
}

void ti_on() /* Initialisierung und Start */
{	ti_stop(); /* Vorsichtshalber... */
	ti_clr();
	*(long **)_msadr = Smillisec; /* Trick 17! */ 
	/* Adr. des Zählers in IR-Routine eintragen */ 
	Xbtimer(TIMER_A,TIMACTRL, TIMADAT, _timavec);
}

void ti_off() /* Stoppen und Interrupt disablen*/ 
{	ti_stop();
	Jdisint (MFP_TIMERA);
}

void ti_restart() /* Zähler auf Null setzen */
{	ti_clr(); }

long ti_get() /* Zeit seit letzter Abfrage */
{	register long t;

	t = millisec - ms_save; 
	ms_save = millisec; 
	return t;
}

long ti_cget() /* Zeit seit letztem (Re-)Start */ 
{	return millisec; }

#define ADJUST (49.0/49.152) /* Korrekturfaktor */

long ti_msget() /* ti_get() in Millisekunden */ 
{ return (lcng) ((ti_get() *ADJUST)); }

long ti_mscget() /* ti_cget() in Millisekunden */ 
{ return (long) ((ti_cget() * ADJUST)); }

long ti_conv(t) /* Timerwerte in ms umrechnen */ 
	long t;
{ return (long) ((t * ADJUST)); }


Aus: ST-Computer 10 / 1987, Seite 42

Links

Copyright-Bestimmungen: siehe Über diese Seite