← ST-Computer 04 / 1992

Interruptprogrammierung in MAXON-Pascal

Programmierpraxis

Verschiedene Programme blenden eine Uhr am Bildschirm ein. Diese Uhr wird im Sekundentakt erhöht, obwohl der ST die Uhrzeit mittels der GEMDOS-Funktion Tgettime nur im 2-Sekunden-Takt zur VerfĂŒgung stellt. Dazu benötigt man eine Routine, die pro Sekunde einmal einen ZĂ€hler erhöht und die neue Uhrzeit am Bildschirm ausgibt. Die Unit I_TIMER ermöglicht die einfache Handhabung dieser Uhr. ZunĂ€chst aber einiges zum Thema Interrupts.

Derjenige, der die Bedeutung und Funktion der Interrupts bereits kennt, kann den nachfolgenden Teil getrost ĂŒberspringen. Interrupts im Atari ST sind nichts anderes als Exceptions (ĂŒbersetzt: Ausnahmen). Diese Exceptions veranlassen die CPU, das momentan laufende Programm zu unterbrechen und die Exception-Behandlung einzuleiten . Dabei kann es sich um eine eigene Routine oder eine Routine des Betriebssystems (z.B. Zeichnen von 2 Bomben) handeln.

Die 68000-CPU unterscheidet sieben Interrupt-PrioritĂ€tsebenen (IPL1 - 7). Die PrioritĂ€tsebene 0 ist der Normalzustand (kein Interrupt liegt vor). Im ST sind nur die PrioritĂ€tsebenen 2 (HBL-Interrupt=ZeilenrĂŒcklauf), 4 (VBL-Interrupt = BildrĂŒcklauf) und 6 (MFP68901) benutzt. FĂŒr uns ist nur die PrioritĂ€tsebene 6 interessant, da hier die Interrupt-Anforderungen des MFP68901 (= Multi Function Peripheral) erscheinen. Der MFP-Baustein kann intern 16 mögliche Peripherie-Interrupts verarbeiten. Tabelle 1 zeigt die zur VerfĂŒgung stehenden Interrupts. Dabei hat Ebene 15 die höchste PrioritĂ€t. Die Unit I_TIMER benutzt den Timer-A-Interrupt (Ebene 13).

FĂŒr die spĂ€tere Realisierung ist wichtig, daß die CPU, wenn sie sich bereits in einer Ausnahmebehandlung (Interrupt) befindet, nur durch einen Interrupt der gleichen oder einer höheren PrioritĂ€tsebene unterbrochen werden kann. Das bedeutet, unsere Routine kann nur durch MFP-Interrupts (Ebene 13,14,15) unterbrochen werden. Der MFP-Interrupt befindet sich auf der höchsten Interrupt-PrioritĂ€tsebene des 68000. Der FIBL- und VBL-Interrupt können unsere Routine nicht unterbrechen, da sie eine niedrigere PrioritĂ€t haben. Doch nun zur Realisierung.

Realisierung

Unsere Interrupt-Routine unterbricht ein laufendes Programm. Das bedeutet, nach Abarbeitung der Routine mĂŒssen alle Register beim Verlassen wieder denselben Inhalt wie beim Eintritt in die Routine haben. Im Programm werden die Register D0-D7/A0-A5 mit dem Assembler-Befehl MOVEM.L D0-D7/A0-A5,-(A7) (Move Multiple Registers) auf den Stack gerettet. Beim Verlassen werden die Register mit dem Befehl MOVEM.L (A7)+,D0-D7/A0-A5 wieder restauriert. Das Register A7 adressiert den Stack.

Wie bereits erwĂ€hnt, kann unsere Interrupt-Routine durch Interrupts der gleichen PrioritĂ€tsebene unterbrochen werden. Die Routine könnte also durch sich selbst unterbrochen werden. Deshalb muß der Timer-A-Interrupt gesperrt werden, solange die Timer-Routine lĂ€uft. Das Sperren des Timer A-Interrupts besorgt die XBIOS-Funktion JDISINT. Mit ihr ist es möglich, einzelne Interrupts des MFP-Bausteins zu sperren. Das Freigeben geschieht dann mit der Funktion JENABINT. Die Ausgabe der Zeit erfolgt mit der Funktion OutTextXY aus der GRAPH-Unit. Nun zu den Prozeduren.

Prozeduren

Die Unit besteht aus sechs Prozeduren. FĂŒnf sind dem Anwender zugĂ€nglich.

Die Prozedur DO_TIME ist nur intern verfĂŒgbar. Sie ĂŒbernimmt das Inkrementieren des internen ZĂ€hlers und die Ausgabe der Zeit am Bildschirm. Diese Prozedur wird jedesmal angesprungen, wenn ein Timer-A-Interrupt auftritt. Alles weitere kann dem Listing entnommen werden.

Ebene Interrupt von Bedeutung im ST
15 I/O-Port 7 Monochrom Monitor Detect
14 I/O-Port 6 RI von serieller Schnittstelle
13 Timer A nicht benutzt
12 RCV-Buffer full ser. Schnittst. EmpfÀngerpuffer voll
11 RCV Error ser. Schnittst. Empfangsfehler
10 XMIT Buff, empty ser. Schnittst. Sendepuffer leer
9 XMIT Error ser. Schnittst. Sendefehler
8 Timer B Disable-Enable-Signal-ZĂ€hler
7 I/O-Port 5 FDC-Ready/ACSI-Bus Steuerung
6 I/O-PORT 4 IRQ von Tastatur MIDI-ACIAs
5 Timer C 200-Hz-Systemtakt
4 Timer D ser. Schnittst Baud-Raten-Generator
3 I/O-Port 3 GPU-Done-Signal vom Blitter
2 I/O-Port 2 CTS von ser Schnittstelle
1 I/O-Port 1 DCD von ser. Schnittstelle
0 I/O-Port 0 BUSY von par Schnittstelle

Tabelle 1: Interrupt-Ebenen des MFP6890I

Die dem Anwender zugÀnglichen Prozeduren sind:

init_timer: Die Prozedur initialisiert die Uhr und hĂ€ngt die Prozedur do_time in den Timer A-Interrupt ein. Dazu wird die XBIOS-Funktion Xbtimer verwendet. Die ermittelte Adresse der Prozedur do_time wird der Funktion Xbtimer zusammen mit dem VorteilungsverhĂ€ltnis 1:50 und einem Initialwert 49 ĂŒbergeben. Als Parameter verlangt die Prozedur die Koordinaten fĂŒr die Bildschirmausgabe. Sind die Koordinaten ungĂŒltig, wird die Initialisierung abgebrochen. Bei erfolgreicher Initialisierung wird das globale Flag is_init auf TRUE gesetzt. Ab sofort wird jede Sekunde einmal die Uhr am Bildschirm mit der neuen Zeit ausgegeben.

exit_timer: Die Prozedur hÀngt die Uhr aus dem Timer A-Interrupt aus und sperrt den Timer-A-Interrupt. Das globale Flag is_init wird auf FALSE gesetzt.

restore_timer: Die aktuelle Uhrzeit wird am Bildschirm ausgegeben. Diese Prozedur ist fĂŒr TOS-Programme gedacht, die den gesamten Bildschirm irgendwann einmal löschen. Die Uhr erscheint dann im ungĂŒnstigsten Fall erst nach einer Sekunde wieder. Mit der Prozedur kann die Uhr sofort nach dem Löschen des Bildschirms wieder eingeblendet werden.

disable_timer: Auch die schönste Uhr kann einmal stören. Deshalb kann die Ausgabe am Bildschirm abgeschaltet werden. Die Uhrzeit wird intern jedoch weiter fortgeschaltet. Das globale Flag disabled wird auf TRUE gesetzt.

enable_timer: Die Prozedur stellt das GegenstĂŒck zu disable_timer dar. Die Prozedur restore_timer wird aufgerufen, um die aktuelle Uhrzeit sofort auszugeben. Das globale Flag disabled wird auf FALSE gesetzt.

Noch ein Wort zur Programmierung. Der grĂ¶ĂŸte Teil der Variablen im Implementierungsteil ist global und wird nicht an die Prozeduren ĂŒbergeben. Der Grund dafĂŒr ist, daß der Zugriff auf globale Variablen schneller ist als auf lokale. Globale Variablen werden im Hauptspeicher, lokale auf dem Stack abgelegt. Die Adressierung direkt in den Hauptspeicher ist jedoch in der Regel schneller als die im Stack. Alles andere kann dem kommentierten Listing 1 entnommen werden.

Anwendung, EinschrÀnkungen

Die vorliegende Unit ist als Beispiel gedacht. Sie kĂŒmmert sich nicht darum, ob bereits ein Programm den Timer-A-Interrupt belegt hat.

Zur Ausgabe am Bildschirm wird die GRAPH-Unit von MAXON-Pacsal verwendet. Sie muß vor dem Aufrufen von init_timer mit InitGraph initialisiert werden. Zuwiderhandlungen werden mit Absturz bestraft.

Am Programmende ist dafĂŒr Sorge zu tragen, daß die Prozedur exit_timer aufgerufen wird. Dies kann z.B. dadurch geschehen, daß die ExitProc-Variable von MAXON-Pascal auf eine eigene Prozedur umgebogen wird, die dann exit_timer und CloseGraph aufruft. Wird exit_timer nicht aufgerufen, fehlt die Interrupt-Routine beim nĂ€chsten Interrupt im Speicher. Das fĂŒhrt unweigerlich zu einem Absturz.

Soll Ihr Programm aus dem Auto-Ordner gestartet werden, können Sie die Uhr in dieser Form nicht verwenden. Es gibt Probleme mit der GRAPH-Unit.

Beispielprogramm

In Listing 2 ist ein Beispielprogramm abgedruckt, das die Uhr verwendet. Es ist speicherresident. Die dazu erforderliche GEMDOS-Funktion PtermRes wurde in Assembler geschrieben.

Das Beispielprogramm initialisiert die GRAPH-Unit, berechnet die ProgrammlĂ€nge und initialisiert die Uhr. Danach werden die von MAXON-Pascal verĂ€nderten Vektoren zurĂŒckgesetzt (SwapVectors) und das Programm wird resident im Speicher gehalten.

Ausblick

Die Unit soll als Beispiel dienen. Der Assembler-Teil beschrÀnkt sich auf das Retten und Restaurieren der Prozessorregister.

Weitere Anwendungen wĂ€ren Routinen, die sich z.B. in die VBL-Queue einhĂ€ngen. Hier ist zu beachten, daß die Routinen kurz und schnell sein sollen.

Eine andere Idee wÀren eigene Traps. Das Betriebssystem des ST benutzt nur die Trap-Vektoren 1,2, 13 und 14. Man könnte eine Routine erstellen und deren Adresse in die Tabelle der Exception-Vektoren eintragen. Mit dem Trap-Befehl kann diese Routine aus einem Assembler-Programm aufgerufen werden.

Literatur:

[1] Atari ST Profibuch, 4. Auflage, Sybex-Verlag 1988, ISBN 3-88745-501-0

[2] Atari ST intern, 2. Auflage, Data Becker 1986, ISBN 3-89011-119-X

(************************************************** * * * Interrupt-Programmierung in MAXON-Pascal * * UNIT I_TIMER zum Handling einer Uhr * * von Thomas Krieger * * (C) 1992 MAXON Computer * * * **************************************************) (*$S-,R-,D-,F+*) unit i_timer; interface (* öffentliche Prozeduren und deren Schnittstellen *) uses dos, bios, graph; (* Prozedur : init_timer Parameter: x, y - Koordinaten fĂŒr OutTextXY Zweck : Einrichten des Timers im Interrupt und Start VerĂ€ndert: Flag is_init *) procedure init_timer(x, y: Integer); (* Prozedur : exit_timer Parameter: keine Zweck : Entfernen des Timers und Interrupt abschalten VerĂ€ndert: Flag is_init *) procedure exit_timer; (* Prozedur : restore_timer Parameter: keine Zweck : Aktuellen Stand des Timers ausgeben, wenn initialisiert VerĂ€ndert: nichts *) procedure restore_timer; (* Prozedur : enable_timer Parameter: keine Zweck : Bildschirmausgabe des Timers erfolgt VerĂ€ndert: Flag disabled *) procedure enable_timer; (* Prozedur : disable_timer Parameter: keine Zweck : Bildschirmausgabe des Timers erfolgt nicht mehr VerĂ€ndert: Flag disabled *) procedure disable_timer; implementation (* Implementierung der Prozeduren. Nach außen verdeckte Konstanten und Variaben *) const MFPISRA = $FFFA0F; (* Interrupt-Service-Register *) TIMER_A = 0; (* Nummer Timer A *) MFP_TIMERA = 13; (* Interrupt Timer A im MFP *) var zaehler, (* Milisekunden *) stunde, (* Initialwerte des Timers *) minute, sekunde, i, (* Laufvariable *) tx, (* Position fĂŒr String-Ausgabe *) ty : Integer; stundestr, (* String fĂŒr Stunde *) minutestr, (* Minute *) sekundestr : String[2]; (* Sekunde *) zeit : String; (* Zeit, format- tiert fĂŒr die Ausgabe *) adresse : Pointer; (* Zeiger auf Timer-Prozedur *) disabled, (* Timer nicht angezeigen *) is_init: Boolean; (* Flag, ob Timer initialisiert *) (* Interrupt-Routine mit Zeitausgabe am Bildschirm benutzt diverse interne globale Variablen. Die String-Ausgabe erfolgt ĂŒber die Graph-Unit-Funktion OutTextXY *) procedure do_time; begin asm movem.l D0-D7/A0-A5,-(A7); (* Register retten *) bclr #5, MFPISRA end; if (zaehler = 999) (* Eine Sekunde ist vorbei *) then begin Jdisint(MFP_TIMERA); (* Timer_A-Interrupt sperren *) sekunde := sekunde +1; (* Sekunden um eins erhöhen *) if (* Minute erhöhen ? *) sekunde > 59 then begin sekunde := 0; minute := minute + 1; end; if (* Stunde erhöhen ? *) minute > 59 then begin minute := 0; stunde := stunde + 1; end; if (* Stunde auf Null zurĂŒcksetzen? *) stunde > 23 then stunde := 0; str(stunde,stundestr); (* Umwandeln *) str(minute,minutestr); (* in Strings *) str(sekunde,sekundestr);(* zur Ausgabe*) if length(stundestr) <2 (* FĂŒhrende Nullen einfĂŒgen, wenn nötig *) then stundestr := '0' + stundestr; if length(minutestr) < 2 then minutestr := '0' + minutestr; if length(sekundestr) < 2 then sekundestr := '0' + sekundestr; zeit := stundestr + ':' + minutestr +':' + sekundestr + #00; (* Zeit-String zusammensetzen *) for i := 1 to length(zeit) do (* Ziffern in Digital- Ziffern umwandeln *) begin case zeit[i] of 'O': zeit[i] := chr(16); '1': zeit[i] := chr(17); '2': zeit[i] := chr(18); '3‘: zeit[i] := chr(19); '4': zeit[i] := chr(20); '5': zeit[i] := chr(21); '6': zeit[i] := chr(22); '7’: zeit[i] := chr(23); '8': zeit[i] := chr(24); '9': zeit[i] := chr(25); end; end; if not disabled (* Ausgabe möglich *) then begin SetTextStyle(DefaultFont,HorizDir,8); OutTextXY(tx, ty, zeit); end; zaehler := 1; (* ZĂ€hler zurĂŒcksetzen *) Jenabint(MFP_TIMERA); (* Timer_A-Interrupt wieder zulassen *) end else (* Sonst, Mili- sekunden-Zahler um eins erhöhen *) begin zaehler := zaehler + 1; end; asm movem.l (A7)+,D0-D7/A0-A5 (* Register zurĂŒckholen und Ende *) unlk A6 rte end; end; (* Initialisieren des Timers; holen der aktuellen Uhrzeit. ZĂ€hler setzen Initialisierungsflag setzen, Adresse der Interrupt-Routine ermitteln; Koordinaten fĂŒr Ausgabe in interne Variablen ĂŒbernehmen *) procedure init_timer(x, y: Integer); var dummy: Integer; begin if not is_init (* Timer noch nicht initialisiert *) then begin if (* Koordinaten korrekt ? *) ((x >= 0) and (x <= GetMaxX) and ((y >= 0) and (y <= GetMaxY))) then begin tx := x; (* Koordinaten ĂŒbernehmen *) ty := y; zaehler := 999; (* ZĂ€hler initialisieren und sofort Zeit ausgeben *) adresse := @do_time; (* Adresse der Interrupt- Routine ermitteln *) is_init := true; (* Timer ist jetzt initialisiert *) GetTime(stunde,minute,sekunde,dummy); (* aktuelle Zeit holen *) XbTimer(NIL,0,0,TIMER_A); (* evtl. vorhandene Timer_A-Interrupt- Routine löschen *) XbTimer(adresse,49,4,TIMER_A); (* Timer_A starten mit Vorteiler 1:50 und Initialwert 49 *) end else (* Koordinaten falsch => nicht initialisieren + Meldung *) begin write('Interrupt-Timer nicht '); writeln('initialisiert !'); delay(3000); (* 3 Sekunden Pause *) end; end; end; (* Timer_A Interrupt-Routine löschen; Timer-Routine beenden *) procedure exit_timer; begin if is_init (* Timer schon initialisiert ? *) then begin XbTimer(NIL,0,0,TIMER_A); (* Interrupt-Routine löschen *) Jdisint(MFP_TIMERA); (* Timer_A-Interrupt sperren *) is_init = false; (* Timer ist nicht mehr da *) end; end; (* Zeit am Bildschirm ausgeben, auch wenn noch keine Sekunde vorbei ist. Dient dazu, wenn der Bildschirm in TOS-Programmen gelöscht wird, den Timer ohne Verzögerung wieder auf den Bildschirm zu bringen *) procedure restore_timer; begin if is_init and not disabled (* Timer initialisiert und eingeschaltet ? *) then begin SetTextStyle(DefaultFont, HorizDir, 8); OutTextXY(tx, ty, zeit); end; end; (* Zeitausgabe wieder einschalten und sofort Zeit ausgeben *) procedure enable_timer; begin if is_init (* Uhr initialisiert ? *) then begin disabled := false; (* Ausgabe einschalten *) restore_timer; (* Zeit am Bildschirm ausgeben *) end; end; (* Zeitausgabe ausschalten *) procedure disable_timer; begin if is_init (* Uhr initialisiert ? *) then disabled := true; (* Ausgabe abschalten *) end; begin is_init := false; (* Initialisierungen *) disabled:= false; end.

Listing 1

(************************************************** * * * Interrupt-Programmierung in MAXON-Pascal * * Beispielprogramm * * von Thomas Krieger * * (C) 1992 MAXON Computer * * * **************************************************) (*$S-,R-,D-,F+*) program timer; uses Dos, Bios, i_timer, Graph; var laenge: LongInt; driver, mode: Integer; procedure Ptermres(keep: LongInt, ret: Integer); begin asm move.w ret,-(sp) move.l keep,-(sp) move.w #$31,-(sp) trap #1 addq.l #8,sp end; end; begin driver = detect; InitGraph(driver, mode, ''); laenge := 256 + BasePage^.data.size + BasePage^.text.size + BasePage^.bss.size; Write('Interrupt-Timer (C) 1991, 1992 ... '); WriteLn('Thomas Krieger installiert'); imt_timer(566,2); SwapVectors; PtermRes(laenge, 0); end.

Listing 2

Thomas Krieger