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