Assembler ohne Grenzen: Arbeiten mit dem Coprozessor

Programme werden meistens dann in Assembler geschrieben, wenn sie möglichst schnell und kurz sein sollen. Müssen jedoch mathematische Berechnungen mit Fließkommazahlen durchgeführt werden, ist der Programmieraufwand gewaltig. In diesen Fällen sind höhere Programmiersprachen oft der einzige Ausweg. Durch den mathematischen Coprozessor des TT gehört dieses Problem nun der Vergangenheit an.

Das Prozessorgespann 68020/30 + 68881/2, wie z.B. im Atari TT, macht es möglich, auf Assembler-Ebene genauso selbstverständlich mit Fließkommazahlen umzugehen wie in Compilersprachen. Der Umfang an mathematischen Funktionen des Coprozessors entspricht dem von Hochsprachen und geht sogar teilweise darüber hinaus, wobei die Anwendung dieser Funktionen nicht wesentlich schwieriger ist. Wenn man also mathematische Anwendungen in Assembler programmieren möchte, braucht man nicht jedesmal das Rad neu zu erfinden, sondern kann, wie in anderen Programmiersprachen, auf einen leistungsfähigen Befehlssatz zurückgreifen.

Ohne Coprozessor ist die Programmierung einer Sinusfunktion eine recht komplizierte Angelegenheit, da der Sinus durch eine Potenzreihe angenähert werden muß. Wenn eine hohe Genauigkeit gewünscht wird, muß außerdem noch mit unhandlichen, 80 Bit großen Zahlen gearbeitet werden, wodurch das Ganze in eine wilde Bit-Schieberei ausartet. Eine entsprechende Routine würde mehrere Seiten Quelltext beanspruchen. Für Nichtmathematiker: Die Potenzreihe für eine Sinusberechnung mit ca. 3 genauen Nachkommastellen sieht folgendermaßen aus:

sin(x)=x-x^3/3!+x^5/5!-x^7/7!+x^9/9!

Mit Coprozessor sieht das gleiche so aus:

FSIN.X #x,FP0

Diese „Routine“ ist nicht nur ungleich kürzer, sondern auch um den Faktor 50 schneller als die andere, außerdem wird hier mit bis zu 19 Nachkommastellen gerechnet. Man erspart sich also viel Arbeit und hat gleichzeitig den Vorteil einer wesentlich höheren Geschwindigkeit. Es ist durchaus möglich, durch effiziente Programmierung des Coprozessors, mathematische Routinen um das 30-50fache zu beschleunigen. Selbst wenn man eine Hochsprache hat, die den Coprozessor unterstützt, lohnt es sich, diejenigen Programmteile, die Fließkommaarithmetik benutzen, in Assembler zu schreiben. Denn selbst gegenüber den schnellsten Compiler-Sprachen kann man noch leicht eine 50-200-prozentige Geschwindigkeitssteigerung erzielen. Außerdem sind Programmiersprachen, die den Coprozessor unterstützen, zur Zeit sehr teuer, und einige sind zudem recht fehlerhaft.

Wenn man über ein wenig Programmiererfahrung mit dem 68000 verfügt, ist es mit wenig Aufwand und Einarbeitungszeit möglich, auch komplizierte mathematische Probleme mit dem 68881/2 in Assembler zu lösen. Man braucht dafür nicht wesentlich länger als mit einer Hochsprache, aber durch die ungleich schnellere Ausführung der Programme wird man für diese Arbeit auf jeden Fall belohnt.

Ich möchte noch bemerken, daß ich hier nicht mehr auf die Programmierung der Prozessoren 60000-68030 eingehe. Es ist nicht unbedingt nötig, die zusätzlichen Befehle und Adressierungsarten des 68020/ 30 zu kennen, aber es erleichtert viele Dinge ungemein.

Extras!

Durch den 68881/2 wird der Befehlssatz des 68020/30 in drei Bereichen erweitert:

  1. 49 Befehle (siehe Bild 1)
  2. 11 Register : 8 Datenregister FP0-FP7 (80 Bit), 3 Kontrollregister FPCR, FPSR, FPIAR (32 Bit)
  3. 4 Datentypen: Single Precision Real (32 Bit), Double Precision Real (64 Bit), Extended Precision Real (96 Bit), Packed Decimal String Real (96 Bit)

Wichtig ist dabei, daß die physikalische Trennung der beiden Prozessoren für den Programmierer nicht transparent ist, d.h. die Entscheidung, welcher Prozessor einen Befehl ausführen muß, wird von der Hardware getroffen. Dies gilt nicht für den 68000-Prozessor, da er keine Coprozessorschnittstelle besitzt. Bei einem Mega ST/E wird der Coprozessor, falls vorhanden, über Hardware-Adressen angesprochen. Dadurch wird die Programmierung sehr kompliziert und damit unattraktiv. Bei einem Rechner mit 68020/30 und 68881/2 hat man also sozusagen, „einen“ Prozessor mit erweitertem Befehlssatz und zusätzlichen Registern. Der Coprozessor kann insgesamt mit 7 Datentypen arbeiten, welche allerdings nur für die Kommunikation mit der Außenwelt bestimmt sind, da intern immer mit höchster Genauigkeit und einem eigenen Format gearbeitet wird. Das heißt, es müssen alle Operanden in das interne Format gewandelt werden, bevor mit ihnen gerechnet werden kann.

Bild 1: Der Befehlssatz des 68881/2

Darüber braucht man sich jedoch keine Gedanken zu machen, da der Coprozessor sämtliche Formatumwandlungen vollkommen automatisch erledigt. Dadurch ist es möglich, Berechnungen mit beliebigen Zahlenformaten durchzuführen, ohne daß man sich um deren Anpassung kümmern muß. Notwendig ist nur die Angabe des Datentypes des Quelloperanden. Der Zieloperand ist bei arithmetischen Operationen immer ein FPU-Datenregister, wie aus Bild 1 zu ersehen ist.

Der Aufbau der 4 neuen Datentypen ist in Bild 2 veranschaulicht. Bei SINGLE, DOUBLE und EXTENDED ist der innere Aufbau jedoch für die Programmierung nicht von Bedeutung, hier aber der Vollständigkeit halber angegeben. Wichtig ist nur, welche Genauigkeit die Datentypen haben und wieviel Platz sie im Speicher benötigen. Zum Rechnen können alle Datentypen von Byte bis Extended benutzt werden, nur das Packed-Decimal-String Real Format sollte nicht verwendet werden. Es ist, wie der Name schon sagt, einem String sehr ähnlich und daher leicht in einen solchen zu verwandeln, wie das kleine Programm zeigt. Die umgekehrte Richtung geht so ähnlich, allerdings muß man dann eventuell vorhandene Fehler abfangen. Der Befehl FMOVE.P FPm, <ea>{#kj bzw. {Dnj bietet die Möglichkeit zu bestimmen, mit wievielen Stellen eine Zahl ausgegeben werden soll. Man hat dabei die Wahl zwischen der Anzahl der Stellen rechts vom Komma, wobei der Exponent berücksichtigt wird, und der Anzahl der Stellen in der Mantisse. Mit Hilfe dieser Informationen steht der Programmierung beliebiger mathematischer Funktionen nichts mehr im Wege (siehe Bilder 1-3).

Beispiele:

D0.B=SQRT(D0.B)
FSQRT.B     D0,FP0 
FMOVE.B     FP0,D0 
D1.W=SIN(Pi/D0.B)
FMOVECR.X   #0,FP0 
FDIV.B      D0,FP0 
FSIN.X      FP0 
FMOVE.W     FP0,D1

Es soll folgender Graph gezeichnet werden:

20*e^(SIN(x/k1)+COS(x/k2))+k3

Die Konstanten k1, k2, k3 (16 Bit) stehen ab der Stelle, auf die A0 zeigt. Die Variable x läuft von 0 bis 640.

MOVEQ #0,D0
    FMOVE.B #20,FP0 
LOOP:
    FMOVE.W     D0,FP1
    FMOVE.W     D0,FP2 
    FDIV.W      (A0),FP1 
    FDIV.W      2(A0),FP2 
    FSIN.X      FP1 
    FCOS.X      FP2
    FADD X      FP2,FP1 
    FETOX.X     FP1 
    FMUL.X      FP0,FP1 
    FADD.W      4(A0),FP1
    FMOVE.W     FP1,D1 
BSR     PUNKT
ADDQ.W  #1,D0
CMPI.W  #640,D0 
BNE     LOOP

(siehe auch das Rosetten- und das Wellen-Programm auf der Leserservicediskette)

Bedingte Verzweigungen

Bild 2: Außau der neuen Datentypen

Genau wie bei der CPU, gibt es auch bei der FPU Befehle, deren Ausführung von Bedingungen abhängt. Dies sind die Befehle FBcc, FDBcc, FScc und FTRAPcc.

Der Unterschied zu den entsprechenden Befehlen der CPU besteht darin, daß hier das Condition-Code-Byte des FPSR ausgewertet wird. Da die Bedeutung der Bits N, Z und I klar sein dürfte, werde ich nur das NAN-Bit erläutern: Das Ergebnis einer arithmetischen Operation (z.B. FADD) kann in drei Fälle aufgespalten werden. Der 1. Fall (Normalfall) tritt ein, wenn alle Operanden im Definitionsbereich der jeweiligen Funktion liegen. Im 2. Fall kann die Funktion zwar nicht errechnet werden, das Ergebnis ist jedoch mathematisch festgelegt, z.B. O+Unendlich = Unendlich, -1*Unendlich = -Unendlich, LN(0) = Unendlich. Der letzte Fall tritt dann auf, wenn das Ergebnis mathematisch nicht zu bestimmen ist, z.B. -Unendlich+Unendlich, 0/0, LN(-1). Wenn dies passiert, wird das NAN-Bit gesetzt, und gleichzeitig wird dem Zieloperanden ein sogenanntes NAN-Format zugewiesen. Sollte bei einer Operation einer der Operanden gleich NAN sein, so ist das Ergebnis ebenfalls NAN, und das NAN-Bit wird in diesem Fall auch gesetzt. Das I-Bit wird übrigens von den Bedingungscodes nicht berücksichtigt. Wenn es für eigene Zwecke benötigt wird, kann man es durch ein „FMOVE.L FPSR,Dn“ in ein CPU-Datenregister schieben und dann selbst auswerten. Es gibt zwei Gruppen von Bedingungscodes (Bild 4), die sich voneinander darin unterscheiden, daß die einen das BSUN-Exception-Bit setzen können und die anderen nicht (BSUN s.u.). Die sogenannten IEEE Non-Aware-Bedingungscodes sind jedoch nur für denjenigen von Interesse, der tiefer in diese Materie einsteigen will. Nur noch eine Bemerkung hierzu : Durch die Existenz von NANs, also Zahlen, die keine sind, werden die Bedingungscodes etwas komplizierter als nötig. Das Gegenteil von „Less Than“ ist dann nicht mehr „Greater Than or Equal“, sondern „Not Less Than“.

Nun noch ein abschließendes Beispiel hierzu: Die folgenden Zeilen realisieren eine Schleife, die von 0 bis 2*Pi in Schritten der Größe 0.1 zählt.

    FMOVECR.X   #0,FP0 
    FMUL.B      #2,FP0 
    FMOVE.B     #1,FP1
    FDIV.B      #10,FP1
    FMOVE.B     #0,FP2
LOOP:

(beliebiges Programm)

    FADD.X      FP1,FP2 
    FCMP.X      FP0,FP2 
    FBOLE       LOOP

Fehler aufgetreten?

Um festzustellen, ob während einer Rechnung ein Fehler aufgetreten ist, benötigt man das Status- und das Control-Register der FPU (Bild 5), genauer gesagt das Exception-Enable- und das Exception-Status-Byte. Beide Bytes haben den gleichen Aufbau, jedoch eine unterschiedliche Bedeutung. Wenn irgendein Fehler auftritt, wird im Exception Status Byte das entsprechende Bit gesetzt. Das Exception Enable-Byte hingegen stellt eine Maske dar, die angibt, welche Fehler eine Exception-Behandlung auslösen können. Ist z.B. im Exception-Enable-Byte das DZ-Bit gesetzt, wird im Falle einer Division durch Null eine Ausnahmebehandlung (Exception) über Vektor 50 ausgelöst. Dadurch hat man die Möglichkeit, für jeden Fehler eine Routine im Betriebssystem zu installieren, die eine entsprechende Fehlerbehandlung durchführt. Man könnte zum Beispiel eine Dialogbox ausgeben, die es einem ermöglicht, das Programm abzubrechen oder es mit korrigierten Parametern fortzusetzen. Da das TOS diese Vektoren nicht belegt, sollten normalerweise alle Bits im Exception-Enable-Byte auf Null gesetzt sein.

Bild 5: Das Status- und Control-Register der FPU
Bild 3+4: Es gibt zwei Gruppen von Bedingungscodes

Das Exception-Status-Byte stellt jedoch nur eine Momentaufnahme dar, d.h. beim nächsten Befehl werden unter Umständen alle Bits verändert. Dadurch wäre man gezwungen, nach jedem Befehl abzufragen, ob irgendein Fehler aufgetreten ist. Dies ist aber in den meisten Fällen nicht sinnvoll und auch zu umständlich. Das Accrued-Exception-Byte schafft hier Abhilfe, da es sich das Auftreten eines Fehlers beliebig lange merkt. Jedes einmal gesetzte Bit kann nur vom Benutzer durch einen FMOVE-Befehl gelöscht werden. Wenn man also dieses Byte vor einer längeren Berechnung löscht, kann hinterher festgestellt werden, ob ein bestimmter Fehler aufgetreten ist, um dann zu entscheiden, ob das Ergebnis gültig ist oder nicht. Es ist dann allerdings nicht mehr möglich zu rekonstruieren, welcher Befehl den Fehler ausgelöst hat.

Die Bedeutung der verschiedenen Bits im einzelnen:

BSUN: Dieses Bit wird gesetzt, wenn das NAN-Bit gesetzt ist und einer der Befehle FBcc. FDBcc, FScc und FTRAPcc im Zusammenhang mit einer IEEE Non-Aware Bedingung benutzt wird.

SNAN: Durch das sogenannte „Signaling Nan“-Format ist es möglich, eigene Datenformate zu kreieren.

OPerr: Dieser Fehler tritt auf, wenn bei bestimmten arithmetischen Operationen der Quelloperand außerhalb des mathematischen Definitionsbereiches liegt. OPerr wird ebenfalls gesetzt, wenn das NAN-Bit gesetzt wird. Beispiele: ACOS(x) ist nicht definiert für x=+-Unendlich, x>1 und x<-1; SQRT(x) ist nicht definiert für x<0 und x=-Unendlich; OPerr wird auch gesetzt, wenn bei FMOVE.B/W/L der Quelloperand nicht in das Integerformat paßt oder wenn er NAN ist.

OVFL: Ein Overflow tritt dann auf, wenn beim Arbeiten mit single-, double- oder extended-precision-Zahlen, unter Beachtung der Rundungsgenauigkeit, die zuzuweisende Zahl zu groß ist, um im Zieloperanden gespeichert werden zu können.

UNFL: Ähnlich wie OVFL, aber hier ist die Zahl zu klein, um noch dargestellt werden zu können. Bei B/W/L wird die Null zugewiesen ohne UNFL zu setzen.

DZ: Division durch Null. Tritt aber auch in folgenden Fällen ein: FATANH(+-1), FLOGxx(0), FLOGNPl(-1).

INEX1: tritt dann auf, wenn eine dezimale Zahl (,P Format) eingelesen wird, die binär nicht genau dargestellt werden kann, z.B:0.1.

INEX2: Sobald eine Zahl gerundet wird, also Nachkommastellen abgeschnitten werden, wird INEX2 gesetzt. Dies hängt natürlich von der Rundungsgenauigkeit ab.

Geht es noch schneller?

Diese Frage kann positiv beantwortet werden, denn trotz der enormen Geschwindigkeit der FPU ist es möglich, Programme mit einfachen Mitteln wirkungsvoll zu beschleunigen. Ein bekanntes Mittel zur Beschleunigung von Programmen ist die registeroptimierte Programmierung, welche natürlich auch bei der FPU nicht wirkungslos ist, denn es fällt nicht nur ein Lesezyklus weg, sondern auch die Umwandlung der Operanden in das interne 80-Bit-Format.

Die zweite Möglichkeit zur Optimierung liegt in der Tatsache begründet, daß die FPU ein eigenständiger Prozessor ist, denn während die FPU eine arithmetische Berechnung durchführt, kann die CPU ganz normal Weiterarbeiten. Dies ist jedoch nur dann machbar, wenn kein Prozessor auf das Ergebnis einer Berechnung des anderen wartet und kein Register von beiden benutzt wird. Die parallele Ausführung von Befehlen scheitert auch dann, wenn ein zweiter FPU-Befehl gestartet wird, während der erste noch bearbeitet wird. In diesem Fall warten beide Prozessoren auf die Fertigstellung des ersten Befehls. Zur Parallelverarbeitung eignen sich am besten diejenigen FPU-Befehle, welche die meiste Ausführungszeit benötigen. So kann die CPU beispielsweise 10 Multiplikationen durchführen, während die FPU einen Sinus berechnet. Dadurch wird die Sinusberechnung praktisch in Nullzeit erledigt. Diese effektive Art der Optimierung ist aber leider nur selten einsetzbar, da die genannten Einschränkungen bei der Programmierung oft auftreten.

Bild 6: Der FMOVE-Befehl beim 68882

Darüber hinaus ist der 68882, im Gegensatz zum 68881, in der Lage, auch eigene Befehle parallel zu verarbeiten oder deren Ausführung teilweise zu überlappen. Dies sind vor allem arithmetische Operationen im Zusammenhang mit FMOVE-Befehlen, welche keine Formatumwandlungen vornehmen müssen, wie z.B.: FMOVE FPm, FPn oder FMOVE.X <ea>,FPm. Man sollte immer versuchen, schnelle FMOVE-Befehle mit schnellen arithmetischen Befehlen zu kombinieren und umgekehrt. Die parallele Verarbeitung ist nicht möglich, wenn Registerkonflikte auftreten und wenn mit den Datentypen B, W, L, P gearbeitet wird. Folgende Befehle können nicht oder nur ganz minimal parallel ausgeführt werden: FBcc, FDBcc, FScc, FTRAPcc, FMOVEM. Beispiel für eine Optimierung auf dem 68882:

Programmschleifen sollten, wenn möglich, teilweise ‘aufgerollt’ werden.

FMOVE.X (A0)+,FP0 
FMUL.X  FP2,FP0 
FMOVE.X FP0,(A1)+

Die folgende Routine ist ca. 12 % schneller:

FMOVE.X (A0)+,FP0 
FMOVE.X (A0)+,FP1 
FMUL.X  FP2,FP0 
FMUL.X  FP2,FP1 
FMOVE.X FP0,(A1)+
FMOVE.X FP1,(A1)+

Diese Optimierungen bringen natürlich nichts, wenn die programmierten Funktionen nicht bereits auf mathematischem Wege vereinfacht wurden. Man sollte möglichst versuchen, die Anzahl der trigonometrischen Operationen so klein wie möglich zu halten. Zur Vereinfachung von Polynomen eignet sich das sogenannte Homer-Schema ganz besonders.

Beispiel:

2*x^3-4*x^2+x-1 (5 Multiplikationen)
-> -1+(1+(-4+2*x)*x)*x (3 Multiplikationen).

Die Demoprogramme

Das hier abgedruckte Rosette- und das Wellen-Programm auf der Leserservicediskette (siehe auch Titelbild) sollen verdeutlichen, wie einfach die Programmierung des Coprozessors ist. Da ich hier auf die Funktionsweise der Programme nicht eingehen möchte, habe ich die beiden GFA-BASIC-Listings beigefügt, welche die grundsätzliche Arbeitsweise verdeutlichen. Wie man sieht, sind die eigentlichen Hauptprogramme sehr kurz im Gegensatz zu den Eingaberoutinen, welche man allerdings universell einsetzen kann. Wer nicht viel tippen möchte, kann den Eingabeteil inklusive der entsprechenden Unterprogramme weglassen, muß dann allerdings die Parameter direkt eintragen und jedesmal neu assemblieren. Es kann jeder Assembler verwendet werden, der die Befehle des 68030 und des 68882 unterstützt, wie z.B. der MAS 68K. Allerdings sollte man den dazugehörigen Linker TLINK.TTP nicht verwenden, da er ab bestimmten Quellcodegrößen Fehler macht.

Die beiden Demos zeigen, wie groß die Beschleunigung von mathematischen Programmen durch den Coprozessor ist, und daß sich die Beschäftigung mit dessen Programmierung lohnt. Das Wellen-Programm benötigt beispielsweise für eine Animation mit 80 Bildern ca. 10 Minuten. Ein entsprechendes Programm in TURBO C benötigt ohne Coprozessor 3 Stunden, auf einem normalen ST sogar 11 Stunden. Auf einem Atari TT ist es also möglich, selbst aufwendige Animationen in einem Bruchteil der bisher üblichen Zeit zu berechnen.

Zum Schluß!

Diese kleine Einführung in die Programmierung des 68881/2 kann und will kein Ersatz für ein ausführliches Handbuch sein, aber es sollte trotzdem möglich sein, mit Hilfe dieser Informationen eigene Programme zu erstellen. Die Programmierung des Coprozessors ist wie gesagt sehr einfach, da er einem alle Aufgaben, die kompliziert sind oder viel Arbeit machen, einfach abnimmt; es ist jedoch ein umfangreiches Detailwissen vonnöten, um alle Möglichkeiten des 68881/2 nutzen zu können. Ich habe hier versucht, die wichtigsten Infonnationen zusammenzustellen, bin mir aber bewußt, daß diese Auswahl nicht jeden zufriedenstellen wird. Daher empfehle ich jedem, der sich intensiver mit dieser Materie beschäftigen möchte, entsprechende Fachliteratur zu erwerben. Für Fragen und Anregungen hier noch meine Adresse:

Jochen Fischer
Gierlichstr. 2
W-5120 Herzogenrath

Literatur:
Steve Williams
“68030 Assembly Language Reference“
Addison-Wesley Publishing Company Inc.


* Umwandlung des Packed Decimal String Real
* Formates in einen String

    lea         zahl,a0         ;Puffer für Zahl
;   fmove.f     <ea>,fpn
;   fmove.p     fpn,(a0){#k}
    lea         string,a1       ;Puffer fur String
    btst.b      #7,(a0)         ;Zahl negativ ?
    beq         positiv         ;nein
    move.b      #'-',(a1)+      ;ja
positiv:
    move.b      3(a0),d0        ;erste Ziffer
    unpk        d0,d1,#$3030    ;in ASCII wandeln
    move.b      d1,(a1)+        ;in String schreiben
    move.b      #'.',(a1)+      ;Komma setzen
    moveq       #4,d2           ;Offset
    moveq       #7,d3           ;Zähler
m_loop:
    move.b      (a0,d2.w),d0    ;2 Ziffern holen 
    unpk        d0,d1,#$3030    ;in ASCII wandeln
    move.w      d1,(a1)+        ;in String schreiben
    addq.w      #1,d2           ;Offset erhöhen
    dbf         d3,m_loop       ;bis zur letzten Ziffer
    move.b      #'e',(a1)+
    btst.b      #6,(a0)         ;Exponent negativ?
    beq         positiv2        ;nein
    move.b      #’-',(a1)+      ;ja
positiv2:
    bfextu      2(a0){0,4},d0   ;1. Stelle d Exponents 
    lsl.b       #4,d0
    bfextu      (a0)(4,4},d1    ;2 Stelle d Exponents
    or.b        d1,d0           ;1. u. 2. Stelle in D0
    unpk        d0,d1,#$3030    ;in ASCII wandeln
    move.w      d1,(a1)+
    move.b      1(a0).d0
    unpk        d0,d1,#$3030    ;3. und 4. Stelle
    move.w      d1,(a1)+
* ----------------------------------------------
zahl:   dc.l 0,0,0
*
        bss
string: ds.b 20

******************************
*                            *
*  ROSETTE.TOS               *
*                            *
*  by Jochen Fischer         *
*                            *
*  (c) 1991 MAXON Computer   *
******************************

macro Cconws str
    movem.l     d1-d2/a0-a2,-(sp)   ;Ausgabe des
    pea         str                 ;Strings str
    move.w      #9,-(sp)            ;unter Beachtung
    trap        #1                  ;der ESC-Sequenzen
    addq.l      #6,sp
    movem.l     (sp)+,d1-d2/a0-a2
endm
macro Cconrs buf
    movem.l     d1-d2/a0-a2,-(sp)   ;Lesendes
    pea         buf                 ;Strings str
    move.w      #10,-(sp)           ;unter Beachtung
    trap        #1                  ;der ESC-Sequenzen
    addq.l      #6,sp
    movem.l     (sp)+,d1-d2/a0-a2
endm
macro Cconin
    movem.l     d1-d2/a0-a2,-(sp)   ;Warten auf
    move.w      #1,-(sp)            ;einen
    trap        #1                  ;Tastendruck
    addq.l      #2,sp               ;d0=Zeichen
    movem.l     (sp)+,d1-d2/a0-a2
endm
macro rd_int buf,frage,len,pos      ;Read Integer 
    pea         buf(pc)             ;Puffer für Eingabe
    pea         frage(pc)           ;Zeiger auf Frage
    move.w      len,-(sp)           ;Max. Eingabelänge
    move.w      pos,-(sp)           ;(X,Y)-Position
    bsr         lies_int
    adda.l      #12,sp
endm
macro rd_card buf,frage,len,pos     ;Read Cardinal 
    pea         buf(pc)             ;s.o.
    pea         frage(pc)
    move.w      len,-(sp)
    move.w      pos,-(sp)
    bsr         lies_card
    adda.l      #12,sp
endm

* ----------------------------------------------
init        equ $0
maus_off    equ $a
maus_on     equ $9
a_line      equ $3

* ----------------------------------------------
*                   Initialisierung
* ----------------------------------------------
    text
    movem.l     d0-d7/a0-a6,-(sp)   ;CPU. Reg und
    fmovem.l    fpcr/fpsr/fpiar,-(sp) ;alle FPU Reg
    fmovem.x    fp0-fp7,-(sp)       ;sichern
    aline       init                ;line_a init
    lea         a_zeiger,a1         ;Line_a Zeiger
    move.l      a0,(a1)             ;sichern
    move.w      -$c(a0),d0          ;x_max
    lsr.w       #1,d0               ;d0=x_halbe
    move.w      -$4(a0),d1          ;y_max
    lsr.w       #1,d1               ;d1=y_halbe
    lea         x_hlb(pc),a0        ;x und y_halbe
    move.w      d0,(a0)             ;sichern
    move.w      d1,2(a0)
    Cconws      clr_scr(pc)

* ----------------------------------------------
*                   Eingabeteil
* ----------------------------------------------
    rd_card     Antwort,Frage1,#4,#$0202 ;Rad.1 lesen
    cmp.w       d1,d0               ;Radius l<=y_halbe ?
    bge         e1
    move.w      d0,4(a0)            ;ja -> nehme Eingabe
    bra         w1
e1:
    move.w      2(a0),4(a0)         ;nein->nehme y_halbe/2
    lsr.w       4(a0)
w1:
    move.w      2(a0),d2            ;y_halbe
    sub.w       4(a0),d2            ;y_halbe-Radius 1
    rd_card     Antwort,Frage2,#4,#$0402 ;Rad.2 lesen
    cmp.w       d2,d0               ;Rad 2<=y_halbe-Rad.1 ?
    bge e2
    move.w      d0,6(a0)            ;ja -> nehme Eingabe
    bra w2
e2:
    move.w      d2,6(a0)            ;nein->nehme y_halbe-Rad 1 
w2:
    rd_int      Antwort,Frage3,#5,#$0602 ;lese Freq.l
    move.w      d0,8(a0)
    rd_int      Antwort,Frage4,#5,#$0802 ;lese Freq.2 
    move.w      d0,10(a0)
    rd_card     Antwort,Frage5,#3,#$0a02 ;lese Konst1
    move.w      d0,12(a0)
    rd_card     Antwort,Frage6,#3,#$0c02 ;lese Konst2 
    move.w      d0,14(a0)
    rd_card     Antwort,Frage7,#5,#$0e02 ;Genauigkeit 
    move.w      d0,16(a0)
    Cconws      cur_off(pc) ;Cursor aus
    Cconws      clr_scr(pc)

* ----------------------------------------------
*       Hauptprogramm (hier wird gerechnet)
* ----------------------------------------------
    lea         x_pos(pc),a1        ;1.Position berechnen
    move.w      (x_hlb,pc),d0       ;x Position
    add w       (rad1,pc),d0        ;rad.1 addieren
    add.w       (rad2,pc),d0        ;rad.2 addieren
    move.w      d0,(a1)             ;sichern
    move.w      y_hlb(pc),4(a1)     ;y position
    fmove.x     #0,fp0              ;Bei 0 anfangen
    fmove.x     #1,fp1
    fdiv.w      step(pc),fp1        ;fp1=1/step
    move.w      x_hlb(pc),d0        ;d0=x_hlb
    move.w      y_hlb(pc),d1        ;d1=y_hlb
    move.w      rad1(pc),d2         ;d2=rad1
    move.w      rad2(pc),d3         ;d3=rad2
    fmove.w     freq1(pc),fp6       ;fp6=freq1
    fmove.w     freq2(pc),fp7       ;fp7=freq2
rloop:
    fmove.x     fp0,fp2             ;x=w
    fmul.x      fp6,fp2             ;x=x*f1
    fcos.x      fp2                 ;x=cos(x)
    fmul.w      d2,fp2              ;x=x*rad1
    fadd.w      d0,fp2              ;x=x+x_hlb
    fmove.x     fp0,fp3             ;y=w
    fmul.x      fp6,fp3             ;y=y*freq1
    fmul.w      ry1(pc),fp3         ;y=y*ry1
    fsin.x      fp3                 ;y=sin(y)
    fmul.w      d2,fp3              ;y=y*rad1
    fadd.w      d1,fp3              ;y=y+y_hlb
    fmove.x     fp0,fp4             ;x1=w
    fmul.x      fp7,fp4             ;x1=x1*freq2
    fcos.x      fp4                 ;x1=cos(x1)
    fmul.w      d3,fp4              ;x1=x1*rad2
    fmove.x     fp0,fp5             ;y1=w
    fmul.x      fp7,fp5             ;y1=y1*freq2
    fmul.w      ry2(pc),fp5         ;y1=y1*ry2
    fsin.x      fp5                 ;y1=sin(y1)
    fmul.w      d3,fp5              ;y1=y1*rad2
    fadd.x      fp4,fp2             ;x=x+x1
    fadd.x      fp5,fp3             ;y=y+y1
    bsr         line                ;linie zeichnen
    fadd.x      fp1,fp0             ;w=w+step
    fcmp.p      max(pc),fp0         ;Schluß ?
    fble        rloop
    Cconin                          ;auf Taste warten
    fmovem.x    (sp)+,fp0-fp7       ;alle Register zurück 
    fmovem 1    (sp)+,fpcr/fpsr/fpiar 
    movem.l     (sp)+d0-d7/a0-a6 
    clr.w       -(sp)
    trap        #1                  ;zurück zu GEM
* -------------------------------------------------
*               Unterprogramme
* -------------------------------------------------
* Unterprogramm für Linien 
line:
    movem.l     d0-d2/a2-a3,-(sp)
    lea         a_zeiger,a3
    movea.l     (a3),a3
    lea         x_pos,a2
    move.w      #1,$18(a3)          ;farbe =1
    move.w      #-1,$20(a3)
    move.w      #$ffff,$22(a3)      ;linienstil
    clr.w       $1a(a3)             ;die restlichen
    clr.w       $1c(a3)             ;Bitplanes sind
    clr.w       $1e(a3)             ;gleich null
    fmove.w     fp2,$26(a3)         ;x neu
    fmove.w     fp3,$28(a3)         ;y neu
    move.w      (a2),$2a(a3)        ;x alt
    move.w      4(a2),$2c(a3)       ;y alt
    move.w      #0,$24(a3)          ;replace
    fmove.w     fp2,(a2)            ;neue x_pos sichern
    fmove.w     fp3,4(a2)           ;neue y_pos sichern
    aline       a_line              ;linie zeichnen
    movem.l     (sp)+,d0-d2/a2-a3
    rts
* ------------—---------------—-----—
* Unterprogramm fur cardinal Eingabe 
* ------------—---------------—-----—
lies_card:
    movem.l     d1-d3/a0-a2,-(sp)
    movea.l     36(sp),a0           ;Bufferadr in A0
    move.w      #$1b59,(a0)         ;ESC Y in Buffer
    move.w      28(sp),2(a0)        ;Koord. in Buffer
    addi.w      #$2020,2(a0)        ;jeweils 32 addieren
    move.b      #0,4(a0)            ;Stringende
    Cconws      (a0)                ;String ausgeben
    Cconws      clr_line(pc)        ;Zeile löschen
    movea.l     32(sp),a1           ;Textadr in A1
    Cconws      (a1)                ;Text ausgeben
    move.b      31(sp),(a0)         ;Länge in Buffer
    Cconrs      (a0)                ;String einlesen
    move.b      1(a0),d1            ;Anzahl Zeichen in D1
    beq         lcl_exit            ;Anzahl=0 -> exit
    ext.w       d1
    subq.w      #1,d1
    clr.w       d2
    clr.w       d0
lc_loop:
    mulu        #10,d0              ;Zahl mal 10
    move.b      2(a0,d2.w),d3       ;Zeichen in D3
    subi.b      #$30,d3             ;ASCII - 48
    bmi         lcl_exit            ;keine Ziffer -> exit
    cmpi.b      #9,d3               ;Wert>9 ?
    bgt         lcl_exit            ;keine Ziffer -> exit
    ext.w       d3
    add.w       d3,d0               ;d0=d0+Ziffer
    addq.b      #1,d2               ;nächste Ziffer
    dbf         d1,lc_loop          ;bis zur letzten Z.
    bra         lc_ende
lcl_exit:
    moveq.l     #0,d0               ;Fehler aufgetreten
lc_ende:
    movem.l     (sp)+,d1-d3/a0-a2
    rts

* Unterprogramm fur integer Eingabe 
lies_int:
    movem.l     d1-d3/a0-a2,-(sp)
    movea.l     36(sp),a0           ;Bufferadr. in A0
    move.w      #$1b59,(a0)         ;ESC Y in Buffer
    move.w      28(sp),2(a0)        ;Koord. in Buffer
    addi.w      #$2020,2(a0)        ;jeweils 32 addieren
    move.b      #0,4(a0)            ;Stringende
    Cconws      (a0)                ;String ausgeben
    Cconws      clr_line(pc)        ;Zeile löschen
    movea.l     32(sp),a1           ;Textadr in A1
    Cconws      (a1)                ;Text ausgeben
    move.b      31(sp),(a0)         ;Länge in Buffer
    Cconrs      (a0)                ;String eingeben
    move.b      1(a0),d1            ;Zeichen gelesen ?
    beq         lil_exit            ;nein -> exit
    ext.w       d1
    move.b      2(a0),d0            ;erstes Zeichen in D0
    clr.l       d3                  ;Zahl positiv
    cmp.b       #'+',d0             ;erstes Zeichen ?
    beq         li_plus
    cmp.b       #'-',d0             ;'-' erstes Zeichen ?
    beq         li_minus
    bra         li_ziffer           ;1.Zeichen ist Ziffer
li_minus:
    or.l        #$10000,d3          ;Zahl negativ
li_plus:
    moveq       #1,d2               ;1.Zeichen fällt weg
    subq.b      #2,d1               ;ein Zeichen weniger
    bra         li_pl_mi
li_ziffer:
    subq.w      #1,d1
    clr.w       d2
li_pl_mi:
    clr.w       d0
li_loop:
    mulu        #10,d0              ;Zahl mal 10
    move.b      2(a0,d2.w),d3       ;Zeichen in D3
    subi.b      #$30,d3             ;ASCII - 48
    bmi         lil_exit            ;keine Ziffer -> exit
    cmpi.b      #9,d3               ;Wert>9 ?
    bgt         lil_exit            ;keine Ziffer -> exit
    ext.w       d3
    add.w       d3,d0               ;d0=d0+Ziffer
    addq.b      #1,d2               ;nächste Ziffer
    dbf         d1,li_loop          ;bis zur letzten Z.
    swap        d3
    btst        #0,d3               ;Zahl negativ ?
    beq         li_ende             ;nein -> ende
    neg.w       d0                  ;sonst negieren
    bra         li_ende             ;-> ende
lil_exit:
    moveq.l     #0,d0               ;Fehler aufgetreten
li_ende:
    movem.l     (sp)+,d1-d3/a0-a2
    rts
* -------------------------------------------------
*               Daten
* -------------------------------------------------
x_hlb:          dc.w 0
y_hlb:          dc.w 0
* Hier werden die Eingaben eingetragen
rad1:           dc.w 0              ;Radius 1
rad2:           dc.w 0              ;Radius 2
freq1:          dc.w 0              ;Frequenz 1
freq2:          dc.w 0              ;Frequenz 2
ry1:            dc.w 0              ;Konstante 1
ry2:            dc.w 0              ;Konstante 2
step:           dc.w 0              ;Genauigkeit
*
max:            dc.p 6.283185
a_zeiger:       dc.l 0
x_pos:          dc.w 0
y_pos:          dc.w 0
*
Frage1: asciiz 'Radius 1 :'
Frage2: asciiz 'Radius 2 :'
Frage3: asciiz 'Frequenz 1 :'
Frage4: asciiz 'Frequenz 2 :'
Frage5: asciiz 'Konstante 1 (1-3) :'
Frage6: asciiz 'Konstante 2 (1-3) :'
Frage7: asciiz 'Genauigkeit (>200):'
Antwort:    dc.b 0,0,0,0,0,0,0,0, 0, 0
clr_scr:    dc.b 27,'E',0
cur_off:    dc.b 27,'f',0
cur_on:     dc.b 27,'e',0
clr_line:   dc.b 27,'K',0


Aus: ST-Computer 12 / 1991, Seite 100

Links

Copyright-Bestimmungen: siehe Über diese Seite