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