← 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