EVERY/AFTER: Begrenztes Multitasking unter GfA-Basic

Haben Sie nicht auch schon einmal im GfA-Basic Befehle vermißt, mit denen man Prozeduren periodisch unabhängig vom Hauptprogramm ausführen lassen kann und somit etwas ähnliches wie Multitasking (wenigstens in gewissen Grenzen) realisieren kann? Mit den Routinen in EVERY. LST wird nun dieser Wunsch erfüllt. Sie haben die Möglichkeit, 8 unabhängige GfA-Basic-Prozeduren zu definieren, die nach einer bestimmten Zeit einmal, oder (was wohl öfter gebraucht wird) immer wieder ausgeführt werden. Außerdem können Sie noch die Alternate-Help-Tastenkombination abfragen und bei Drücken dieser Tasten in eine entsprechende Prozedur springen.

Das Prinzip, das in diesen Routinen angewandt wird, ist relativ einfach. Es nutzt die Funktion des GfA-Basics aus, die wohl jeder schon häufig benutzt hat: Die Break-Abfrage (Alternate-Shift-Control) zum Abbrechen eines Programms. Dies geschieht nun prinzipiell auf folgende Weise: Eine Assembler-Routine wird in die Vertikal-Blank-Interrupt-Tabelle eingetragen und prüft dann bei jedem Bildrücklauf ob z. B. eine bestimmte Zeit abgelaufen ist oder die Break-Tasten gedrückt wurden. Wenn dies der Fall ist, wird an eine bestimmte Adresse der entsprechende Wert für „Break“ geschrieben. Das GfA-Basic hat nun seinerseits eine Routine (die ebenfalls mit dem Vertical-Blank-Interrupt oder bei compilierten Programmen auf eine andere Weise ausgeführt wird), die diese Speicherzelle prüft. Falls Break gedrückt wurde oder in unserem Fall gesetzt wurde, macht das GfA-Basic das, was der Programmierer eingestellt hat.

Normalerweise wird das Programm dann mit einer Dialogbox abgebrochen. Es gibt aber noch zwei andere Möglichkeiten. Entweder das Programm wird trotzdem fortgesetzt oder, und diese Möglichkeit ist für uns interessant, es wird eine Prozedur angesprungen, diese ausgeführt und danach das Programm normal weiter fortgesetzt. Dafür wird der Befehl On Break Gosub... benutzt. In unserem Fall wird in eine spezielle Prozedur verzweigt. Diese prüft spezielle Flags in einem Parameterfeld, entscheidet ob ein „Interrupt“ aufgetreten ist und führt die entsprechende Routine (Every.ProcX) aus. Danach wird das Programm dort fortgesetzt wo es unterbrochen wurde.

Das ist das eigentliche Grundprinzip, das benutzt wurde. Wenn man allerdings genauso verfahren würde, würde es zu einigen Problemen mit der Tastatureingabe oder im Zusammenhang mit GEM (Accessories) kommen (siehe unten). Um dies zu vermeiden, wird nicht direkt die Tastaturstatus-Systemvariable geändert, sondern der „Break“-Status an eine andere Adresse geschrieben und die Abfrage im GfA-Basic entsprechend geändert. Die Abfrageroutine im GfA-Basic beginnt folgendermaßen:

move.b $0e1b,d0 
andi.b #$0e,d0 /
andi.w #$000e,d0 (Compiler, wieso?)
cmpi.b #$0e,d0
beq...
...

Diese Routine wird nun so verändert, daß die eigene „Break“-Variable abgefragt wird:

move.b $00AAAAAA,d0 AAAAAA = Adresse der „Break“-Variablen nop cmpi.b #$0e,d0 ...

Der andi.b-Befehl wird glücklicherweise nicht mehr benötigt, da sowieso nur noch die Werte 0 oder 14 auftreten können. Die Adresse dieser Routine wird vom Interpreter in die Vertikal-Blank-Interrupt-T abeile eingetragen. Dort kann man sie auslesen. Bei com-pilierten Programmen liegt die Sache allerdings etwas anders. Dort muß man die Adresse über das Adressregister a6 (a6-70) bestimmen. Durch dieses „Patchen“ besteht natürlich theoretisch eine Versionsabhängigkeit, da sich die Abfrageroutine ja ändern oder ihre Adresse vor allem beim Compiler verschoben werden könnte. Diese Gefahr ist allerdings sehr klein, da die Abfrageroutine eigentlich nur bei totaler Änderung des Gf A-Basics betroffen sein könnte. Getestet wurden diese Routinen mit der Version 2.02 des Interpreters bzw. Compilers.

Hier folgt nun eine Auflistung und kurze Beschreibung der implementierten Prozeduren:

Init.every
Diese Prozedur muß einmal aufgeruf-gen werden, um die Routinen zu initialisieren. Dabei wird die Assembler-Routine aus Data-Zeilen eingelesen, einige Variablen gesetzt und andere Dinge initialisiert.

Every(Nummer,Zeit)
Dies ist die wichtigste Routine des ganzen Paketes. Einer der 8 Timer-Interrupts wird eingeschaltet und einer entsprechenden Prozedur zugeordnet. Dazu übergeben Sie die Nummer des Interrupts (von 1 bis 8) und die Zeit in Sekunden, die zwischen zwei Prozedur-Aufrufen vergehen soll. Diese Zeit muß größer sein als die Bildschirmfrequenz, also 0,014-0,02s, da sowohl die eigene als auch die Routine des GfA-Basics nicht öfter ausgeführt wird und kleinere Zeiten deshalb nicht realisiert werden können. Nach oben ist diese Zeit unbegrenzt oder genauer gesagt doch begrenzt, nämlich auf knapp zwei Jahre!

Leider wird diese Zeit nicht hundertprozentig eingehalten (den Grund dafür weiß ich nicht). Meistens ist die effektive Zeit etwas zu kurz. Die Abweichung war bei mir aber immer kleiner als 4 Prozent. Sie können dies ja eventuell ausgleichen indem Sie nach einigem Experimentieren etwas größere Zeitwerte einsetzen. Außerdem ist die genaue Zeit ja höchstwahrscheinlich sowieso nicht das Wichtigste.

Achtung: Die Parameter Nummer muß bei allen Routinen auf jeden Fall im Bereich 1 bis 8 liegen. Dies wird nicht überprüft, falsche Angaben könnten aber zu fatalen Fehlern führen.

After(Nummer,Zeit)
Dieser Befehl entspricht im allgemeinen der Routine Every. Der einzige Unterschied ist der, daß die Routine nur einmal angesprungen wird und danach der Interrupt wieder abgeschaltet wird.

Wenn nun ein entsprechender Interrupt auftritt, verzweigt die „Verwaltungs-Prozedur“ in eine Prozedur mit dem Namen Every.ProcX, wobei X der Nummer des Interrupts (1-8) entspricht. Diese Prozedur kann nun vom Programmierer völlig frei belegt werden. Allerdings sollte die Ausführung nicht allzu lange dauern, da sonst das restliche Programm doch schon merklich gebremst würde. Während der Abarbeitung dieser Prozedur werden die übrigen Interrupts unterdrückt, so daß es zu keinen Komplikationen kommt.

Alt_help.on
Die Alternate-Help-Abfrage wird eingeschaltet. Beim Drücken von Alternate-Help wird dann in die Prozedur Alt_help.proc gesprungen anstatt eine Hardcopy auszuführen.

Alt_help.off
Die Alternate-Help-Abfrage wird wieder abgeschaltet.

Every.off(Nr) Der Interrupt mit der Nummer Nr wird abgeschaltet, der interne Zähler wird angehalten. Alle anderen Interrupts bleiben davon unbeeinflußt. Weiter geht’s für diesen Interrupt dann wieder mit

Every.cont(Nr)
Diese Prozedur schaltet den angegebenen Interrupt wieder ein. Da die Zeit in der Zwischenzeit angehalten worden ist, wird nach Every.cont() an derselben Stelle fortgefahren.

Achtung: Wenn Every.cont() aufgerufen wird ohne daß davor Every bzw. After verwendet wurde, kann das zu Fehlern führen.

Every.off.
Im Gegensatz zu Every.off() werden mit dieser Funktion alle definierten Timer-Interrupts angehalten bzw. ausgeschaltet.

Every.cont.
Das entsprechende Gegenstück zu Every.off.: Alle Interrupts werden wieder fortgesetzt.

Disable
Bei Aufruf dieser Routine werden die Interrupts nicht angehalten, sondern es wird nur die Ausführung der Ba-sic-Routine unterdrückt, indem das „Break“-Flag nicht gesetzt wird. Dies ist oftmals sinnvoll, um das Programm nicht zu stören (zum Beispiel bei zeitkritischen Teilen), ist in bestimmten Situationen aber sogar zwangsläufig notwendig (z. B. bei Input, siehe unten).

Enable
Die Basic-Prozeduren werden wieder angesprungen, dabei werden in der Zwischenzeit aufgetretene Interrupts beim nächsten auftretenden Interrupt nachgeholt.

Exit.every
Diese Prozedur muß auf jeden Fall aufgerufen werden, bevor ein Programm beendet wird. Hauptsächlich wird die Assembler-Routine aus der Vertikal-Blank-Interrupt-Tabelle entfernt und der Patch im GfA-Basic wieder rückgängig gemacht. Wenn der Aufruf dieser Routine vergessen werden sollte, kann es nach dem Beenden des Programms zu Abstürzen kommen, da die Assembler-Routine dann ja nicht mehr im Speicher steht.

Während die Interruptroutinen aktiv sind, kann trotzdem noch durch Alter-nate-Shift-Control das Programm abgebrochen werden. Die Steuerung muß allerdings etwas anders erfolgen. Dafür gibt es die Variable Break.stat%. Wenn sie den Wert 0 hat, wird das Programm fortgesetzt, beim Wert 1 abgebrochen und beim Wert 2 verzweigt das Programm in die Prozedur Break .proc (entspricht On Break Gosub Break, proc). Das Programm verhält sich dann beim Drücken der Tastenkombination genauso wie ohne Interruptroutinen. Lediglich der Programm-Abbruch geschieht etwas anders. Die entsprechende Alert-Box wird simuliert und ein Fortsetzen des Programms mit Cont ist nicht mehr möglich. Dies liegt allerdings nur am Compiler. Sie können ohne weiteres in der Every. break.proc ein Stop einsetzen, es funktioniert. Nur der Compiler macht da nicht mit.

Einige wichtige Hinweise sind allerdings noch zu beachten, um die Routinen richtig einsetzen zu können. Der wichtigste betrifft die einzige kleine, aber doch sehr ärgerliche Einschränkung der ganzen Sache. Normalerweise arbeitet das GfA-Basic einen Befehl bei Drücken der „Break“-Kombination erst zu Ende und führt dann die Break-Routine aus. Hier leider mehr hinderliche als nützliche Ausnahmen sind die Befehle Input, Form Input, Line Input und Pause. Sie werden auch mitten im Befehl abgebrochen, danach aber nicht wieder fortgesetzt. Das passiert nun natürlich auch beim Auftreten eines Interrupts. Deshalb muß vor diesen Befehlen Disable und danach Enable eingefügt werden. Wenn man dies beachtet, treten auch mit diesen Befehlen keine Probleme mehr auf.

Programme mit den Every-Routinen können ohne Probleme compiliert werden. Allerdings muß dabei natürlich die „Stoppen“-Option auf „Immer“ gestellt werden bzw. die entsprechenden Optionen im Programm gesetzt werden.

Wenn Sie nun also ein beliebiges Programm mit Interrupt-Routinen verschönern wollen (z. B. eine mitlaufende Uhr, siehe Beispielprogramm), so müssen Sie folgendermaßen vorgehen. Sie mergen die Every-Routinen am Ende dazu, fügen am Anfang des Programms ein Init.every ein und wenn sie es für nötig halten, schreiben Sie zum Beispiel Every(1,1). Dann müssen Sie nur noch folgendes beachten: Natürlich muß auch eine entsprechende Prozedur existieren (hier: Every.proc1), diese sollte nicht zu viel Zeit benötigen. Außerdem müssen Sie die Input und Pause Befehle finden (am Besten mit Find im Editor) und davor bzw. dahinter Disable bzw. Enabel einfügen (siehe oben). Schließlich dürfen Sie nur das Exit.every vor jedem End, Edit oder Quit nicht vergessen und können dann zum Beispiel die Uhr ruhig sich selbst überlassen.

Die Routinen benutzen folgende globale Variablen, die nicht verändert werden dürfen:

Every..adress%, Install..adress%, Gfarout...adress%, Gfarout1%, Gfarout2%, Vbl..fak%, After%, Interrupts% sowie Break.stt% (s. o.).

Nun hoffe ich, daß Sie mit den Routinen etwas anfangen können und sie Ihnen einige Programmiervorhaben erleichtern. So könnte man jetzt zum Beispiel recht leicht eine gespoolte Druckerausgabe realisieren, und, und, und... (Was fällt Ihnen dazu ein?)

' **************************************************
' *  BEGRENZTES MULTITASKING UNTER GfA-BASIC v1.2  *
' *  9/1987 by Lutz Preßler. Sandkrug. 2904 Hatten *
' * Ermöglicht Every und After in GfA-Basic-Prgs   *
' *                 ST-Computer 12/87              *
' **************************************************

Procedure Init.every 
    Local I%,D$,A6%.Rout$,Rout$.Keyad% 
    Every..adress%=Gemdos(&H48,L:216)
    If Every..adress%<=0 
        Error 99 
    Endif
    Restore Every..dat 
    For I%=0 To 53 
        Read D$
        Lpoke Every..adress%*I%*4,Val("&H"+D$)
    Next I%
    Install..adress%=0 
    For I%=0 To Dpeek(&H454)
        If Lpeek(Lpeek(&H456)+4*I%)=0 
            Install.. adress%=Lpeek(&H456)+4*I% 
        Endif
        Exit If Install..adress%>0 
    Next I%
    Lpoke Every..adress%+74,Every..adress%+150 
    Lpoke Every..adress%+20,Every..adress%+146 
    Lpoke Every..adress%+102,Every..adress%+149 
    Lpoke Every..adress%+50,Every..adress%+148 
    Lpoke Every..adress%+146,0 
    If Dpeek(&HFC0002)>=258 
        Keyad%=Lpeek(&HFC0024)  !   Blitter-TOS
    Else
        Keyad%=&HE1B    !   TOS vom 6.2.1986
    Endif
    Dpoke Every..adress%+6,Keyad% 
    Lpoke Every..adress%+14,Every..adress%+214 
    Lpoke Every..adress%+66,Every..adress%+214 
    Lpoke Every..adress%+118,Every..adress%+214
    '
    Option "U0"
    Gfarout..adress%=0 
    For I%=0 To Dpeek(&H454)
        If Lpeek(Lpeek(&H456)+4*I%)>Basepage And Lpeek(Lpeek (&H456))<=Basepage+Lpeek(Basepage*12)
            Gfarout..adress%=Lpeek(&H456)+4*I% 
        Endif
        Exit If Gfarout..adress%>0 
    Next I%
    If Gfarout..adress%=0 ! compiliertes Programm 
        A6%=0
        Rout$=Mki$(&H23CE)*Mkl$(Varptr(A6%))+Mki$(&H4E75) 
        Rout%=Varptr(RoutS)
        Call Rout%
        Gfarout..adress%=A6%-70 
    Else    !   Interpreter
        Gfarout..adress%=Lpeek(Gfarout..adress%)
    Endif
    ' GfA-Abfrage-Routine "patchen“
    Gfarout1%=Lpeek(Gfarout..adress%)
    Gfarout2%=Lpeek(Gfarout..adress%+4)
    Dpoke Gfarout..adress%,&H4E75
    Lpoke Gfarout..adress%+2,Every..adress%+214
    Dpoke Gfarout..adress%+6,&H4E71
    Dpoke Gfarout..adress%,&H1039
    Option "U3"
    '
    If Xbios(4)=2 
        Vbl..fak%=70 
    Else
        If Dpeek(&H448)<>0 
            Vbl..fak%=50 
        Else 
            Vbl..fak%=60 
        Endif
    Endif 
    After%=0 
Return

Procedure Exit.every 
    @Every.off..
    Option "U0"
    Dpoke Gfarout..adress%,&H4E75 
    Lpoke Gfarout..adress%+4.Gfarout2% 
    Lpoke Gfarout..adress%,Gfarout1%
    Option "U3"
    Slpoke Instal1..adress%,0 
    Poke Every..adress%+214,0 
    If Break.stat%=0 
        On Break Cont 
    Else
        If Break.stat%=2 
            On Break Gosub Break.proc 
        Else 
            On Break 
        Endif 
    Endif
    Void Gemdos(&H49,L:Every.adress%)
Return 
Every..dat:
Data 48E72A40,14380E1B,202000E,13C2000B,BBBB3839,EEEEE, 804000E,67000028,4A7804EE 
Data 66000020,31FCFFFF,4EE08F9,6000D,DDD10804,F6600,A0039,E000B,BBBB4286
Data 227C000F,FFFF2411,D046700,265382,22824A82,6600001C, 22A90020,DF9000D,DDDD0804
Data F6600,A0039,E000B,BBBBD3FC,4,6460001,C460008,6600FFC4, 4CDF0254
Data 4E750000,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 
Procedure Every(Nr,S)
    After%-After% And Not 2^(Nr-1)
    @Every.after(Nr,S)
Return
Procedure After(Nr,S)
    After%=After% Or 2^(Nr-1)
    @Every.after(Nr,S)
Return
Procedure Every.after(Nr,S)
    On Break Gosub Every.break.proc
    Dpoke Every..adress%+146,Dpeek(Every..adress%+146) Or 2^(Nr-1)
    Lpoke Every..adress%+150+4*(Nr-1),Vbl..fak%*S 
    Lpoke Every..adress%+182+4*(Nr-1),Vbl..fak%*S 
    Slpoke Install..adress%,Every..adress% 
Return
Procedure Alt_help.on 
    On Break Gosub Every.break.proc
    Dpoke Every..adress%+146,Dpeek(Every..adress%*146) Or 2^14 
    Slpoke Install..adress%,Every..adress% 
Return
Procedure Alt_help.off 
    Dpoke Every..adress%+146,Dpeek(Every..adress%+146) And Not (2^14)
Return
Procedure Disable 
    Dpoke Every..adress%+146,Dpeek(Every..adress%+146) Or 32768 
Return
Procedure Enable 
    Dpoke Every..adress%*+46,Dpeek(Every..adress%+146) And Not (32768)
Return
Procedure Every.off(Nr)
    Dpoke Every..adress%+146,Dpeek(Every..adress%*146) And Not (2^Nr-1))
Return
Procedure Every.cont(Nr)
    Dpoke Every..adress%+146,Dpeek(Every..adress%+146) Or (2^(Nr-1)
Return
Procedure Every.off
    Interrupts%=Dpeek(Every..adress%+146)
    Dpoke Every..adress%+146,0 
Return
Procedure Every.cont
    Dpoke Every..adress%+146,Interrupts% 
Return
Procedure Every.break.proc 
    Local Evnr%,I%,Stop% 
    On Break Cont
    Evnr%=Dpeek(Every..adress%+148)
    Dpoke Every..adress%*148,0 
    @Disable
    Poke Every..adress%*214,0 
    If Evnr%<>0 
        If (Evnr% And 2A14)<>0 
            @Alt.help.proc 
        Endif
        For I%=0 To 7 
            If (Evnr% And 2^I%)<>0 
                If (After% And 2^I%)<>0 
                    @Every.off(I%*1)
                Endif
                On I%+1 Gosub Every.proc1,Every.proc2,Every.proc3, Every.proc4,Every.proc5,Every.proc6,Every.proc7, Every.proc8
            Endif 
        Next I% 
    Else
        If Break.stat%<>0 
            @Every.off
            If Break.stat%<>2 
            ' Stop
            Alert 2,"Programmstop?”.1."Stop|Cont",Stop% 
            If Stop%=1 
                @Exit.every 
                Edit 
            Endif
        Else 
            @Break.proc 
        Endif
        @Every.cont
    Endif
Endif
@Enable
On Break Gosub Every.break.proc 
Return
; Every / After für GfA-Basic: Assembler-Interruptroutine
; LPSoft    v1.2
; 5.10.1987 Lutz Preßler, 2904 Hatten 
; In Vertikal Blank Interrupt einbinden
;
TIMERROUT
    movem.l d2/d4/d6/a1,-(a7)   ; Register retten
    move.b $elb,d2              ; Tastaturstatus
    andl.b #14,d2               ; maskieren
    move.b d2,$bbbbb            ; und kopieren, bbbbb muß von der
;   GfA-Routine durch "Break'-Status-Adresse ersetzt werden
move.w $eeeee,d4    ; Status auslesen
;   eeeee muß durch Status-Parameter-Adresse ersetzt werden
    btst #14,d4         ; Alternate-Help abfragen?
    beq Events          ; ... nein
    tst $4ee            ; Alternate-Help gedrückt?
    bne Events          ; ... nein
    move #-1,$4ee       ; Flag zurücksetzen
    bset #6,$dddd1      ; Alternate-Help-Event kennzeichnen
;   dddd1 muß durch Adresse der "AufgetrNr"-Variablen ersetzt werden
    btst #15,d4         ; Break-Interrupt gesperrt? (disable)
    bne Events          ; ... ja
    ori.b #%1110,$bbbbb ; Alternate/Shift/Control setzen (s.o.)
Events
    clr.l d6            ; Schleifenvariable löschen
    movea.l #$fffff,a1  ; Adresse des Zählerparameterblocks
; auslesen. fffff muß durch diese Adresse ersetzt werden
Schleife
    move.l (a1),d2      ; Akt. Zählerstand auslesen
    btst d6,d4          ; Bit in Statusvar. gesetzt?
    beq weiter          ; ... nein
    subq.l #1,d2        ; Zähler dekrementieren
    move.l d2,(a1)      ; Zähler zurückschreiben
    tst.l d2            ; Zähler gleich Null?
    bne weiter          ; ... nein
    move.l 32(a1),(a1)  ; Zählerstartwert in Zähler kopieren
    bset d6,$ddddd      ; Aufgetretenen Interrupt kennzeichnen
; ddddl muß durch Adresse der "AufgetrNr’-Variablen *1 ersetzt werden
    btst #15,d4         ; Break-Interrupt gesperrt? (disable)
    bne weiter          ; ... ja
    ori.b #%1110,$bbbbb ; Alternate/Shift/Control setzen
weiter
    adda.l #4,a1        ; Zähleradresse erhöhen
    addi #1,d6          ; Bitzähler inkrementieren
    cmpi #8,d6          ; Schon 8 ?
    bne Schleife        ; ... nein —> Schleife ausführen
    movem.l (a7)+,d2/d4/d6/a1   ; Register wiederherstellen
    rts                 ; Routine beenden
; Parameter-Feld
Status ds.w 1   ; Statusvariable:   5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
:                                   D A 0 0 8 0 8 0 I I I I I I I I
;   I:  erlaubte Interrupts A: Alternate-Help abfragen? D: disabled?
AufgetrNr ds.w 1; Aufgetr.Interrups:5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
;                                   0 a 0 0 0 0 0 0 i i i i i i i i
;       i: aufgetretene Interrupts a: Alternate-Help gedrückt
Zaehler ds.l 8  ; Acht Langworte: Aktueller Zählerstand für jeden
; möglichen Interrupt
ZData ds.l 8    ; Acht Langworte: Startwerte für Zähler
BreakStatus ds.b 1  ; neue "Break"-Status-Variable für GfA-Abfrage-Routine


Lutz Preßler



Links

Copyright-Bestimmungen: siehe Über diese Seite
Classic Computer Magazines
[ Join Now | Ring Hub | Random | << Prev | Next >> ]