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.
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.
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.
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.
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.
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