Interruptprogrammierung in MAXON-Pascal

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
Aus: ST-Computer 04 / 1992, Seite 84

Links

Copyright-Bestimmungen: siehe Über diese Seite