← ST-Computer 10 / 1987

Interrupts in Megamax-C: Interrupti te salutant

Listings

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)); }