← ST-Computer 04 / 1989

Apfelmännchen mit 68881-Speed

Grundlagen

Abbildung 1: Realteil: -0.7463827 bis -0.7463821, Imaginärteil: -0.0985054 bis -0.0985060 Rechentiefe: 350

Zur Programmierung des 68881-Arithmetikcoprozessors gibt es ein Standardwerk - das Motorola User Manual. Das Buch geht zwar sehr ausführlich auf alle Details bis hin zu Interrupts aller Arten ein, die Informationen werden aber recht spärlich, wenn als CPU ein anderer Prozessor als der 68020 verwendet wird. Dieser Artikel soll nun zeigen, daß die Assemblerprogrammierung des Coprozessors auch mit einem 68000-Prozessor keine Hexerei ist. Das Ergebnis: ein extrem schnelles Programm zur Errechnung von Apfelmännchengrafiken.

Der 68881 (auch Floating Point Unit - kurz FPU genannt) wurde als Erweiterung zum 68020-Prozessor entwickelt, die Interfaces der beiden Prozessoren sind derart aufeinander abgestimmt, daß der 68020-Programmierer den Eindruck hat, einen einzigen Prozessor zu programmieren. Der Befehlscode, den der Assembler erzeugt, wird vom 68020 Prozessor analysiert, alle notwendigen Aktionen (vor allem das Lesen und Schreiben diverser Interfaceregister des 68881) zur Ausführung des Befehles am 68881 werden automatisch durchgeführt. Und um eben diese Interfaceregister muß sich der geplagte 68000-Programmierer selber kümmern.

Die Interfaceregister

Der 68881 hat neun Interfaceregister, von denen für einfachere Aufgaben aber nur fünf wichtig sind:

  • das Controlregister, mit dem der 68881 betriebsbereit gemacht wird
  • das Commandregister, mit dem der Code des gewünschten Befehles übermittelt wird
  • das Conditionregister, mit dem der Code einer Abfragebedingung übermittelt wird
  • das Operandregister, mit dem Daten zwischen CPU und Coprozessor aus getauscht werden können
  • das Responseregister, mit dem der 68881

(1) das Ergebnis einer Abfragebedingung mitteilt
(2) von der CPU diverse Dienstleistungen anfordert (z.B. die Übertragung von numerischen Daten)
(3) der CPU den aktuellen Betriebszustand mitteilt (z.B. daß der Coprozessor noch mit einer Berechnung beschäftigt ist).

Auf die Aufgaben dieser Register wird im folgenden genau eingegangen. Die Register werden grundsätzlich wie Speicherplätze angesprochen, die Adresse ist vom Hersteller der Coprozessorerweiterung abhängig. Sofern der Prozessor über die von ATARI reservierten Adressen angesprochen wird, kann mit folgendem Befehl das Response-Register in das Register DO des 68000 gelesen werden:

MOVE.W $FFFA40.D0

Tabelle 1 enthält die Länge der fünf wichtigsten Interface-Register, den Adreßoffset von der Coprozessoradresse (die bei älteren 68881-Erweiterungen, insbesondere von Lischka Datentechnik, auch einen anderen Wert als SFFFA40 haben kann) und die erlaubten Zugriffsarten (Read/Write).

Die Daten-Register und das Zahlenformat

Neben den Control- und Statusregistern (ähnliche Funktion wie beim 68800) und einem Instruction-Adreßregister gibt es 8 Datenregister: in ihnen werden Fließkommazahlen in einem internen 80 Bit-Format gespeichert. Dieses Format wird vom Coprozessor für praktisch alle Berechnungen verwendet. Zur Kommunikation mit dem 68881 können die Zahlen in verschiedenen anderen Formaten übertragen werden - dabei erfolgt eine automatische Konvertierung in das gewünschte Format. Es existieren folgende sieben Formate: Integer Byte, -Word, -Longword, Real Single Precision. -Double Precision, -Extended Precision (das ist das interne Format) und Packed Decimal Real (die Zahl wird als Zeichenkette übertragen).

In diesem Artikel wird zur Datenübertragung nur das Real Double Precision-Format verwendet. Die Fließkommazahl wird dabei in 8 Bytes codiert: Bit 0 bis 51 werden zur Darstellung der Mantisse, Bit 52 bis 62 zur Darstellung des Exponenten, Bit 63 für das Vorzeichen verwendet. Der Wert der Zahl kann nach folgender Formel bestimmt werden:

(-1)^vorzeichen2^(exponent-1023)(1+mantisse/2^52)

D.h. der Exponent wird als Integerzahl interpretiert, die Mantisse dagegen als Bruch: die eigentliche Mantisse ergibt sich aus einer Division durch 2^52 und der Addition mit 1. Die Zahl 0 kann dargestellt werden, indem alle 64 Bits auf 0 gesetzt werden. Dazu gleich zwei Beispiele (die hexadezimalen Zahlen sind dabei zur besseren Lesbarkeit mit _ in Vierergruppen unterteilt):

5.390625 wird durch $4015_9000_0000_0000 dargestellt.

Bit 63=0 (positives Vorzeichen), der Exponent ergibt sich als 1025 ($401) minus 1023, die Mantisse als $5_9000_0000_0000 dividiert durch 2A52 plus 1, also:

5.390625 = 2^2 * 1.3475625

Ähnlich wird -0.33333333 als $BFD5_5555_5555_5555 dargestellt. Das Vorzeichenbit ist 1, der Exponent lautet 1021 ($3FD) minus 1023, die Mantisse $5_5555_5555_5555 / 2^52 + 1.

-0.333333 = (-1) * 2^(-2) * 1.3333333333

Das hier beschriebene Zahlenformat ist genormt (IEEE), sollte von allen C-Compilern als Format zur Übertragung von Fließkommazahlen verwendet werden und kann in manchem anderen Programmiersprachen durch eigene Funktionen generiert werden (z.B. Funktion DOUBLE{} in GFA BASIC 3.0). Die Genauigkeit dieses Formates beträgt etwa 15 1/2 Dezimalstellen, der Zahlenbereich liegt zwischen 10^307 und 10^-307. Da der Coprozessor intern mit einer noch höheren Genauigkeit (19 Stellen) rechnet, wirken sich Rundungsfehler praktisch nicht aus.

Der Befehlssatz

Der Befehlssatz kann inhaltlich folgendermaßen unterteilt werden: Befehle zum Datentransport, zur Ausführung arithmetischer Befehle, zur Abfrage von Bedingungen und zum Task-Wechsel/Trap-Handling. Die letzte Befehlsgruppe wird in diesem Artikel komplett ignoriert, von den anderen Gruppen werden hier einige Beispiele angegeben:

Mit dem Befehl FMOVE können Zahlen von der CPU in ein 68881-Datenregister, von einem 68881 -Datenregister zur CPU, und von einem 68881-Datenregister in ein anderes Datenregister übertragen werden. Mit FADD. FSUB, FMUL, FSIN, FSQRT können arithmetische Funktionen aufgerufen werden. FCMP dient zum Vergleich zweier Zahlen. Mit FBcc können Vergleichsbefehle ausgewertet werden (wie der Branch-Befehl Bcc des 68000).

Befehlscodierung mit 68020

Anhand eines Beispiels soll gezeigt werden, wie einfach die Programmierung des 68881 ist, wenn der Steuerprozessor ein 68020 ist.

Der Befehl

FADD FP0.FP1

zur Addition zweier 68881 -Register wird von einem geeigneten Assembler in die zwei Binärcodes

1111 0010 0000 0000 (= $F200) 0000 0000 1010 0010 (= $A2)

umgewandelt. Wenn der 68020-Prozessor diese Codes verarbeiten muß, schreibt er an das Commandregister den Befehlscode (in diesem Fall $A2) liest anschließend das Responseregister und wertet dieses aus. Genau um diese Dinge muß sich der 68000-Programmierer selber kümmern. Bevor darauf im Detail eingegangen wird, müssen aber einige der Interfaceregister näher beschrieben werden.

Das Controlregister

Ein Schreibzugriff auf dieses Register bewirkt unabhängig vom Wert den Abbruch eines eventuell noch in Arbeit befindlichen Befehles. Außerdem wird für das Interface des Coprozessors ein Reset durchgeführt. Der 68881 ist somit (unabhängig von seiner vorherigen Verwendung) betriebsbereit.

Das Operandregister

Das Register wird verwendet, um Daten zwischen CPU und Coprozessor auszutauschen. Je nach Datenformat muß dieses 32 Bit-Register mehrfach gelesen bzw. beschrieben werden. Zur Übertragung einer Zahl im Double-Precision-Real-Format (64 Bit) sind zwei Zugriffe notwendig, wobei der höherwertige Anteil jeweils zuerst behandelt wird.

Das Responseregister

Um einen bestimmten Befehl auszuführen, muß ein Protokoll zwischen CPU und 68881 eingehalten werden. Teil dieses Protokolls (siehe unten) sind die Lesezugriffe auf dieses Register. Durch Auswertung des Registers erhält das Programm Informationen über den Zustand des 68881.

Folgende Codes sind vorgesehen:

$800, $801 Antwort auf Conditionbefehl (entspricht False, True)
$802 Idle, Coprozessor ist arbeitsbereit
$900, $4900 Busy, Coprozessor arbeitet, von der CPU werden keine weiteren Zugriffe erwartet
$1C31-$1D36,$5C0B,$5C30 Exception (z.B. Overflow; Division durch 0; Protocol-Violation: das Kommunikationsprotokoll wurde nicht eingehalten); in das Controlregister muß ein (beliebiger) Wert geschrieben werden, bevor ein weiterer Befehl aus geführt werden kann.
$810C, $A10C mehrere Register blockweise übertragen; darauf wird hier nicht genauer eingegangen.
$8900, $C900 Busy, Coprozessor arbeitet, benötigt aber von CPU noch Informationen bzw. kann mit der Abarbeitung des nächsten Befehls noch nicht beginnen; das Responseregister muß nochmals gelesen werden.
$9501-$9704, $D501-$D60C 68881 wartet auf Operanden, dieser muß über das Operandregister übertragen werden; anschließend muß das Responseregister erneut gelesen werden.
$B101-$B304 68881 möchte Operanden an die CPU übermitteln: dazu muß das Operandregister (eventuell mehrfach) gelesen werden; anschließend muß das Responseregister erneut gelesen werden.

Wenn nun die Bedienung des Coprozessors durch ein handgestricktes Assemblerprogramm erfolgt, kann auf die zeitaufwendige Auswertung des Responseregisters gewöhnlich verzichtet werden, weil (solange kein Fehler auftritt) die Reaktion des 68881 ohnedies vorhersehbar ist.

Eines muß dabei allerdings beachtet werden: solange der Coprozessor noch am vorigen Befehl arbeitet, kann er nicht mit dem nächsten Befehl beginnen. Wird nun das Responseregister (das in einem solchen Fall $8900 oder $C900 lautet) nicht berücksichtigt, kommt es zu einer sogenannten Protocol Violation. Aber auch das kann ohne Abfragen vermieden werden, wenn nach zeitaufwendigen Befehlen einige NOPs im Assemblerprogramm integriert werden, um die CPU zu bremsen.

Eine Ausnahme bilden die Wahrheitswerte nach einem Conditionbefehl - nach einem solchen Befehl muß das Responseregister natürlich explizit ausgewertet werden, um das Ergebnis der Abfrage zu erfahren (False oder True?).

Achtung: Es kann zwar gewöhnlich auf die Auswertung des Responseregisters verzichtet werden, das Register muß aber dennoch gelesen werden (auch der Befehl TST führt einen Lesezugriff aus), damit das Kommunikationsprotokoll (siehe unten) eingehalten wird.

Das Kommunikationsprotokoll

Aus den obigen Ausführungen geht hervor. daß die Kommunikation zwischen 68881 und CPU nach strengen Regeln verläuft. Diese Regeln sind in sogenannten Protokollen für verschiedene Typen von Befehlen festgehalten. Wenn man von diversen Spezialfunktionen zum Trap-Handling, Task-Wechsel... absieht, ergeben sich vier wichtige Befehlsgruppen:

  1. Befehle ohne Operandentransport
  2. Befehle, bei denen der Quelloperant an den 68881 übermittelt werden muß
  3. Befehl, bei denen der Zieloperand (das Ergebnis) vom 68881 zur CPU übermittelt werden muß
  4. Befehle zur Bedingungsabfrage

Im folgenden wird das Protokoll für diese vier Gruppen kurz beschrieben:

  1. In das Commandregister wird der Befehlscode geschrieben, anschließend wird das Responseregister gelesen. Falls der Coprozessor noch mit dem vorigen Befehl beschäftigt ist ($8900, $C900), muß das Responseregister nochmals gelesen werden.

  2. In das Commandregister wird der Befehlscode geschrieben, anschließend wird das Responseregister gelesen. Falls der Coprozessor noch mit dem vorigen Befehl beschäftigt ist ($8900, $C900), muß das Responseregister nochmals gelesen werden. Es ergibt sich nun die Aufforderung zum Operandentransport an den 68881 ($9501-$9704, $D501-$D60C), die Daten werden (je nach Format in mehreren Langwörtern, höherwertiger Anteil zuerst) in das Operandregister geschrieben. Anschließend muß das Responseregister erneut gelesen werden - es wird $802, $900 oder $4900 ergeben, wenn der Datentransport korrekt verlaufen ist.

  3. Anfang wie oben, allerdings ergibt sich hier eine Aufforderung zum Operandentransport an die CPU ($B101-$B304). Der Operand wird aus dem Operandregister gelesen, anschließend muß das Responseregister nochmals gelesen werden (ergibt Idle, $802).

  4. In das Conditionregister wird der Conditioncode geschrieben. Anschließend wird der resultierende Wahrheitswert ($800 oder $801) aus dem Responseregister gelesen.

Abbildung 2: Realteil: 0.3890 bis 0.3907, Imaginärteil: 0.30548 bis 0.3059, Rechentiefe: Punkte mit 120<Ergebnis<179 wurden schwarz ausgegeben

Beispiele zur Befehlscodierung mit 68000

Es folgen nun Beispiele zu diesen vier Gruppen. Das Zustandekommen des Commandcodes wird unten im Detail beschrieben. Auf die Auswertung des Responseregisters wird bei Befehlen der Gruppen 1 bis 3 verzichtet.

  1. FADD FP0,FP1

Das Fließkommaregister FP0 wird zu FP1 addiert.

MOVE.W #$A2,COMMAND_ADR TST.W RESPONSEADR

Der Code $A2 ergibt sich aus dem Befehlscode für FADD ($22), dem Code für das Quellregister 0*$400 und dem Code für das Zielregister 1 *$80.

  1. FADD.D (A0)+,FP1

Es soll also die bei (A0) beginnende Fließkommazahl im Double Precision-Format (daher die Kennung .D) zum 68881-Register FP1 addiert werden.

MOVE.W #$55A2,COMMAND_ADR TST.W RESPONSE_ADR MOVE.L (A0)+,OPERAND_ADR MOVE.L (A0)+,OPERADN_ADR TST.W RESPONSEADR
  1. FMOVE.D FP3,(A0)+

Es soll das Fließkommaregister im Double Precision Format (‘.D’) in den Hauptspeicher beginnend bei (A0) übertragen werden.

MOVE.W #$7580,COMMAND_ADR TST.W RESPONSEADR MOVE.L OPERAND_ADR,(A0)+ MOVE.L OPERAND_ADR,(A0)+ TST.W RESPONSE_ADR
  1. FBGT label

Wenn die nach einem FCMP-Befehl gesetzten Flags signalisieren, daß der Zieloperand größer als der Quelloperand war, soll ein Sprung zum angegebenen Label erfolgen.

MOVE.W #$12,CONDITION_ADR CMP.W #$801,RESPONSE_ADR BEQ LABEL

Der Commandcode

Der Commandcode ist jener Wert, der am Beginn des Protokolls für Befehle der Gruppen 1-3 in das Commandregister geschrieben wird. Er enthält den Erkennungscode des Befehles sowie weitere Informationen über die zu verarbeitenden Operanden (Register oder Zahlen aus dem CPU-Arbeitsspeicher). Es handelt sich dabei um den selben Wert, den ein 68020/68881 Assembler als zweites Wort (Command-Word) bei der Übersetzung eines FPU-Befehles generiert.

Command-Word für Befehle der Gruppe 1

Bit 0-6 (Extension-Field):
Befehlscode (z.B. $0,$20,$22,$23,$28,$38 für FMOVE, FDIV, FADD, FMUL, FSUB, FCMP)
Bit 7-9 (Destination-Register):
Nummer des Zielregisters
Bit 10-12 (Source-Register):
Nummer des Quellregisters
Bit 15,14,13: 0,0,0

Command-Word für Befehle der Gruppe 2

Bit 0-9: wie oben
Bit 10-12 (Source Format):
Formatcode(0,1,2,3,4,5.6 für Long Word Integer, Single Real,Extended Real. Packed Real, Word Integer, Double Real, Byte Integer)
Bit 15,14,13: 0,1,0

Das Sourceformat bestimmt, in welchem Zahlenformat ein Operand bei der Befehlsausführung an die FPU übertragen wird.

Command-Word für Befehle der Gruppe 3

Bit 0-9: wie oben
Bit 10-12 (Destination-Format): wie oben
Bit 15,14,13: 0,1,1

Der Conditioncode

Auch hier handelt es sich um das zweite Wort des im 68881 User Manual bei der Befehlsbeschreibung angegebenen Codes für 68020-Prozessoren (jetzt als ‘Condition-Word* bezeichnet). Es wird bei Befehlen der Gruppe 4 in das Conditionregister geschrieben.

Der Aufbau ist hier sehr einfach: Es handelt sich um einen 6 Bit-Code, der alle vorgesehenen Abfragen beinhaltet. Die wichtigsten Codes sind hier angegeben: $1,$E,$12,$13,$14,$15 für EQ, NE, GT, GE, LT, LE (Bedeutung wie bei 68000-Bedingungen).

Offset Bezeichnung Länge Zugriff
$00 Response 16 Bit R
$02 Control 16 Bit W
$0A Command 16 Bit W
$0E Condition 16 Bit W
$10 Operand 32 Bit R/W

Tabelle 1: Länge der fünf wichtigsten Interface-Register

Apfelmännchengrafiken

Der Algorithmus zur Berechnung der Apfelmännchengrafiken ist inzwischen so oft in Zeitschriftenartikeln und Büchern beschrieben worden, daß hier darauf verzichtet werden kann. In Listing 1 ist der Algorithmus als einfaches BASIC-Programm realisiert. Die Implementierung in Assembler verwendet die exakt gleichen Formeln und Variablennamen, weswegen das kleine Programm nützlich ist, um sich im (leider etwas längeren) Assemblerprogramm zu orientieren.

Um das Assemblerprogramm (Listing 2) möglichst kurz und zugleich flexibel für verschiedene Anwendungen zu halten, ist es nicht als eigenständiges Programm konzipiert. Vielmehr ist es als Unterprogramm in einer höheren Programmiersprache zu verwenden: Es berechnet jeweils eine Zeile der Apfelmännchengrafik, das aufrufende Programm muß sich um die Parameterübergabe und um die Ausgabe am Bildschirm kümmern. Das Assemblerprogramm ist somit vom Ausgabesystem (Bildschirm in allen Modi, Drucker...) unabhängig.

Das Programm beginnt mit einer Menge Konstanten Vereinbarungen: Diese stellen Teile des Commandcodes dar; durch ihre Kombination (Addition) können alle für das Programm notwendigen 68881-Befehle relativ einfach und übersichtlich zusammengesetzt werden. Für eigene Programme empfiehlt es sich, ähnliche Vereinbarungen zu treffen: Wenn arithmetische Befehle möglichst nur mit 68881-Registern ausgeführt werden, der Datentransport zwischen CPU und 68881 nur mit FMOVE durchgeführt wird, ist die Codegenerierung relativ einfach. Soll beispielsweise der Befehl

FADD FP1 ,FP2

durchgeführt werden, muß hierzu ein Code (bestehend aus den Teilcodes für den Befehl ADD, für das Quellregister FP1 und das Zielregister FP2) ins Commandregister geschrieben werden:

MOVE.W #(ADD+FQ1+FZ2),COMMAND_ADR

Beim FMOVE-Befehl erfolgt eine Unterteilung in drei Verwendungsmöglichkeiten dieses Befehls: 68881-Register untereinander austauschen, 68881-Register vom CPU-Speicher lesen oder 68881 -Register an die CPU (bzw. deren Speicher) übertragen (wird im Programm nicht benötigt). Auch beim ADD-Befehl sind zwei Varianten vorgesehen.

Nach der Auflistung der Verwendung der einzelnen Register für Prozessor und Coprozessor beginnt das Programm mit einem Sprung über den Parameterblock. Dieser 44 Byte lange Block dient zur Kommunikation zwischen dem aufrufenden Programm und der Assemblerroutine. In den folgenden Zeilen wird der Parameterblock ausgelesen, die meisten Werte werden in Registern gespeichert, um eine möglichst hohe Verarbeitungsgeschwindigkeit zu erreichen.

Die Register A0-A4 enthalten die Adressen der einzelnen Interfaceregister; die Basisadresse des 68881 muß vom aufrufenden Programm angegeben werden - das Programm ist also unabhängig vom Ort der FPU. Das Ergebnis der Berechnungen wird in einem Integerfeld gespeichert - dessen Adressierung erfolgt über das Register A6. Das Register D3 enthält den Wert $801 (logisch True) zur Auswertung des Responseregisters nach Abfragen.

Anschließend werden die Register des 68881 mit Werten belegt. Diese müssen im Parameterblock im oben beschriebenen Double-Precision-Format vorliegen. Als Grenzwert wird die Zahl 4 bezeichnet, die als Vergleichswert bei der Abbruchbedingung in der innersten Schleife des Algorithmus’ notwendig ist (siehe Listing 1 bei EXIT IF).

Im folgenden werden die einzelnen Programmzeilen von Listing 1 in Assembler codiert. Dabei sind die eingefügten NOPs notwendig, weil der Coprozessor bei aufwendigeren Rechenoperationen (vor allem bei der Multiplikation) langsamer ist als die steuernde CPU. Das Timing des Programmes ist auf eine 8 MHz-CPU und einen 16 MHz-Coprozessor ausgelegt.

Schwierigkeiten gibt es nur, wenn entweder der Coprozessor langsamer oder aber die CPU schneller getaktet ist - in diesem Fall müssen zusätzliche NOPs eingefügt werden.

Der Zähler für die Rechentiefe läuft im Programm (im Gegensatz zu Listing 1) vom Maximalwert bis Null abwärts. Da im Ergebnisfeld aber die Anzahl der notwendigen Schleifendurchläufe gespeichert werden soll, wird (nach dem Label ABBRUCH) dieser Wert durch eine Subtraktion berechnet.

Der Vorgang wiederholt sich für alle Punkte der zu berechnenden Zeile (LOOP2). Abschließend wird das Responseregister nochmals ausgelesen und als letzter Wert im Ergebnisfeld gespeichert. Dieser Wert ist für die Grafik nicht von Bedeutung, er kann lediglich zu Testzwecken verwendet werden: Der Wert sollte $802 betragen, wenn die Kommunikation zwischen CPU und 68881 fehlerfrei funktioniert hat. $ 1DOD bedeutet, daß eine ‘Protocol Violation’ aufgetreten ist - dies würde auf Timing-Probleme deuten.

  Zeit Genauigkeit
Assembler mit 68881 62 15.5 Stellen
Assembler ohne 68881 204 8 Stellen
GFA 3.0 1437 13 Stellen
GFA 68881 1702 (492) 15.5 Stellen
OMIKRON 3.0 SGL 1744 9.5 Stellen
GFA 2.0 1985 (680) 11 Stellen
OMIKRON 3.0 DBL 2547 19 Stellen

Tabelle 2

Das steuernde BASIC-Programm

Listing 3 zeigt die Einlagerung des Programmes in ein GFA BASIC 3.0-Programm. Wird der Befehl PSET durch den langsameren PLOT-Befehl ersetzt, läuft das Programm auch mit Version 2.0. Grundsätzlich kann aber jede höhere Programmiersprache die relativ einfachen Aufgaben der Kommunikation mit dem Assemblerprogramm und der Ausgabe am Bildschirm übernehmen.

Im BASIC-Programm wird zuerst die Auflösung des Bildes und der gewünschte Ausschnitt bestimmt. Ein Feld für die Rechenergebnisse der Assemblerroutine wird dimensioniert, in ein weiteres Feld wird das Assemblerprogramm aus den DATA-Zeilen gelesen.

Anschließend werden die Parameter für das Assemblerprogramm in den dafür vorgesehenen Speicherblock geschrieben. Die Adresse des ersten Elementes des Ergebnisfeldes wird um Zwei vergrößert übergeben, weil das Assemblerprogramm die Resultate als Word-Werte speichert.

Probleme machen dabei die Fließkommaparameter, die im Double-Precision-Format übergeben werden müssen. Besitzer von GFA-BASIC 3.0 können mit

DOUBLE{adresse%}=wert

einfach an eine beliebige Adresse eine Zahl in diesem Format schreiben. Wer diese Möglichkeiten nicht hat, kann stattdessen das Unterprogramm DOUBLE verwenden, das etwas langsamer ist. Das Unterprogramm zerlegt die übergebene Zahl zuerst in Exponent und Mantisse. Daraus werden zwei Integerwerte berechnet, die dann an die angegebene Adresse geschrieben werden. Hierbei muß darauf geachtet werden, daß der Zahlenbereich für Integerzahlen nicht überschritten wird - dieser beträgt -2147483648 bis +2147483647 und nicht 0 bis 4294967295. Werte über 2147483647 müssen daher (durch Subtraktion von 2^31) in negative Werte umgewandelt werden.

Nun zur FOR-NEXT Schleife, mit der das Assemblerprogramm für jede Grafikzeile einmal aufgerufen wird: Der Imaginärstartwert wird für jede Zeile berechnet und durch DOUBLE an das Assemblerprogramm übermittelt. Das Assemblerprogramm muß im Supervisormodus aufgerufen werden (CALL ist also nicht ausreichend), damit der Coprozessor ohne ‘Bomben’-Errors angesprochen werden kann - dies läßt sich sehr einfach mit der XBIOS-Funktion 38 erreichen.

Die Auswertung des Ergebnisfeldes und die Ausgabe am Bildschirm ist leicht verständlich - mit der EVEN-Funktion wird bei allen geradzahligen Resultaten ein Punkt gezeichnet. Natürlich bestehen unzählige andere Möglichkeiten zur Ausgabe - insbesondere in den Farbmodi.

Geschwindigkeit

Die Berechnung und Ausgabe des Basisapfelmännchens (Parameter wie im Programmlisting) dauert etwa 3.5 Minuten, wobei fast die Hälfte dieser Zeit für die Ausgabe benötigt wird. Werden Ausschnitte vergrößert, nimmt die Rechenzeit im Vergleich zur Ausgabezeit natürlich stark zu, so daß sich die Ausgabezeit kaum noch störend bemerkbar macht.

Noch eine Anmerkung zur Geschwindigkeit: Der 68881 besitzt einen Befehl, mit dem die Multiplikation mit halber Rechengenauigkeit, aber etwas höherer Geschwindigkeit durchgeführt werden kann. Auf den Einsatz dieses Befehls wurde verzichtet, weil der Geschwindigkeitsgewinn klein, der Verlust an Genauigkeit dagegen sehr groß wäre. Und erst die hohe Rechengenauigkeit ermöglicht starke Vergrößerungen von Ausschnitten des Apfelmännchens (Abbildung 1 kann mit einer Rechengenauigkeit von 8 Stellen nicht mehr berechnet werden).

Die Geschwindigkeit des Assemblerprogrammes wäre übrigens auch nicht viel höher, wenn statt des 68000 ein 68020 verwendet würde. Es kann dann zwar die Kommunikation mit dem 68881 etwas schneller (und vor allem viel einfacher) durchgeführt werden, die Rechengeschwindigkeit des 68881, der das Tempo hauptsächlich bestimmt, ändert sich aber nicht.

In Tabelle 2 sind einige Benchmarkresultate zu finden (Zeiten in Sekunden inklusive der Ausgabe am Bildschirm). Grundlage war eine Grafik mit 160*100 Punkten Auflösung, die Grenzen des Gebietes lauten -0.7476 bis -0.7452 für den Realteil, 0.0977 bis 0.0993 für den Imaginärteil, die Rechentiefe betrug 100. Zu beachten sind die unterschiedlichen Rechengenauigkeiten (angegeben in Dezimalstellen). Bei GFA-BASIC 2.0 und der 68881-Version stammen die Zeitangaben in Klammem aus compilierten Programmen. Bei OMIKRON.BASIC stand nur ein Interpreter zur Verfügung, die Messung wurde sowohl mit einfacher als auch mit doppelter Genauigkeit durchgeführt. Das verwendete Assemblerprogramm ohne 68881-Unterstützung stammt aus [2].

Quellen:

[1] MC68881 Floating Point Coprocesor User's Manual, Motorola 1985

[2] Kofler, Michael: Atari ST Grafikbuch, SYBEX 1987

' Listing 1 ' Algorithmus zur Berechnung von Apfelmännchen-Grafiken ' ' Michael Kofler, Oktober 88 ' READ realmin,imagmin,realdelta,imagdelta DATA -2, -1.25, 0.005, 0.00625 READ xreso%,yreso%,rechentiefe% DATA 640, 400, 20 ' imagconst=imagmin FOR y%=0 TO yreso% realconst=realmin FOR x%=0 TO xreso% realzahl=realconst imagzahl=imagconst FOR zaehler%=0 TO rechentiefe% realquad=realzahl*realzahl imagquad=imagzahl*imagzahl EXIT IF realquad+imagquad>4 imagzahl=realzahl*imagzahl*2+imagconst realzahl=realquad-imagquad+realconst NEXT zaehler% IF ODD(zaehler%) PLOT x%,y% ENDIF ADD realconst,realdelta NEXT x% ADD imagconst,imagdelta NEXT y%

Listing 1

; LISTING 2 ; 68000-Asseroblerprogramm für ein Apfelmännchen ; mit 68881-ünterstützung ; Das Programm wird v.einem einf.BASIC-Programm ; gesteuert und muP im Supervisor-Modus aufgerufen ; werden (XBIOS(38,..) verwenden), damit die ; Adressierung des Coprozessors ohne Bomben-Errors ; klappt. Es berechnet Farbwerte einer Zeile einer ; Apfelmännchen-Grafik u.speichert diese in einem ; Speicherblock (Feld), der (das) vom BASIC- ; Programm ausgewertet wird. ; Michael Kofler, Oktober 88 ; Code für GST Assembler SECTION text ;Teil-Command-Code für einige ; Befehle ADD EQU $22 ;FADD FPi,FPj ADDMEM EQU $5422 ;FADD memory,FPj SUB EQU $28 ;FSUB FPi,FPj MUL EQU $23 ;FMUL FPi,FPj CMP EQU $38 ;FCMP FPi,FPj MOVEFF EQU $0 ;FMOVE FPn,FPm MOVEMF EQU $5400 ;FMOVE.D memory,FPm MOVEFM EQU $7400 ;FMOVE.D FPn,memory GT EQU $12 ;Condition-Code für BGT ; (greater then) FQ0 EQU $0 ;Codes für Quellregister FQ1 EQU $400 ;nur für Register- FQ2 EQU $800 ;Register-Befehle) FQ3 EQU $C00 FQ4 EQU $1000 FQ5 EQU $1400 FQ6 EQU $1800 FQ7 EQU $1C00 FZ0 EQU $0 ;Codes für Zielregister (nur FZ1 EQU $80 ; für Register-Register und FZ2 EQU $100 ; Memory->Register Befehle) FZ3 EQU $180 FZ4 EQU $200 FZ5 EQU $280 FZ6 EQU $300 FZ7 EQU $380 ;Verwendung der Register: ; FP0,FP1 realconst,imagconst ; FP2,FP3 real,imag ; FP4,FP5 realquad,imagquad ; FP6 Zwischenspeicher für kompliziertere ; Berechnungen ; FP7 Grenzwert (gewöhnlich 4) ; D0 Rechentiefe ; D1 Zähler für Rechentiefe ; D3 Vgl.wert für Response-Register $801 .. True ; (Ergebnis eines Condition-Befehls) ; D4 Zähler für die X-Koordinate ; A6 Adresse für Ergebnis-Speicherfeld ; Adressen der 68881-Kommunikations-Register ; A0 Response: Reaktion des 68881 ; A1 Control: für RESET ; A2 Command: Übergabe d.Command-Codes ; A3 Condition: Übergabe des Condition-Codes ; A4 Operand: Übergabe von Zahlenwerten BRA.S WEITER ;Prg.start hinter den ;Parametern TIEFE DC.W 0 ;Rechentiefe XRESO DC.W 0 ;Zahl der Punkte horiz. FELDADR DC.L 0 ;Adresse des Ergebnis-Feldes REALSTAH DC.L 0 ;Realstart-Wert (High-Lw.) REALSTAL DC.L 0 ; (Low-Lw.) REALDELH DC.L 0 ;Realdelta (High-Longword) REALDELL DC.L 0 ; (Low-Longword) IMAGSTAH DC.L 0 ;Imagstart-Wert (High-Lw.) IMAGSTAL DC.L 0 ; (Low-Lw.) GRENZEH DC.L 0 ;Grenzwert (High-Longword) GRENZEL DC.L 0 ; (Low-Longword) FPU_ADR DC.L 0 ; IO-Adresse des Coprozessors WEITER ;68000 Register initialisieren MOVE.L FPU_ADR(PC),A0 ;Adresse des Response-Registers LEA 2(A0),A1 ; des Control-Reg. LEA $A(A0),A2 ; des Command-Reg. LEA $E(A0),A3 ; des Condition-Reg. LEA $10(A0),A4 ; des Operand-Reg. MOVE.W #0,(A1) ;Teil-Reset des 68881 MOVE.L FELDADR(PC),A6 MOVE.W TIEFE(PC),D0 ;Rechentiefe in DO MOVE.W #$801,D3 ;'True' MOVE.W XRESO(PC),D4 ;Zähler für Zahl der Punkte SUBQ.W #1,D4 ;minus 1 für DBF ;Parameter in 68881 Register lesen MOVE.W #(MOVEMF+FZO),(A2) ;MOVE memory,FP0 TST.W (A0) MOVE.L REALSTAH(PC),(A4) MOVE.L REALSTAL(PC),(A4) TST.W (A0) MOVE.W #(MOVEMF+FZ1),(A2) ;MOVE memory,FP1 TST.W (A0) MOVE.L IMAGSTAH(PC),(A4) MOVE.L IMAGSTAL(PC),(A4) TST.W (A0) MOVE.W #(MOVEMF+FZ7),(A2) ;MOVE memory,FP7 TST.W (A0) MOVE.L GRENZEH(PC),(A4) MOVE.L GRENZEL(PC),(A4) TST.W (A0) LOOP2 ;real=realconst ;imag-imagconst ;zaehler-rechentiefe MOVE.W #(MOVEFF+FQ0+FZ2),(A2) ;MOVE FP0,FP2 TST.W (A0) MOVE.W #(MOVEFF+FQ1+FZ3),(A2) ;MOVE FP1,FP3 TST.W (A0) MOVE.W D0,D1 LOOP1 ;realquad=real*real MOVE.W #(MOVEFF+FQ2+FZ4),(A2) ;MOVE FP2,FP4 TST.W (A0) MOVE.W #(MUL+FQ4+FZ4),(A2) ;MUL FP4,FP4 TST.W (A0) NOP NOP NOP ;imagquad=imag*imag MOVE.W #(MOVEFF+FQ3+FZ5),(A2) ;MOVE FP3,FP5 TST.W (A0) MOVE.W #(MUL+FQ5+FZ5),(A2) ;MUL FP5,FP5 TST.W (A0) NOP NOP NOP ;FP6=realquad+imagquad MOVE.W #(MOVEFF+FQ4+FZ6),(A2) ;MOVE FP4,FP6 TST.W (A0) MOVE.W #(ADD+FQ5+FZ6),(A2) ;ADD FP5,FP6 TST.W (A0) NOP ;realquad+imagquad>grenzwert ? MOVE.W #(CMP+FQ7+FZ6),(A2) ;CMP FP7,FP6 TST.W (A0) MOVE.W #GT,(A3) ;BGT ABBRUCH CMP.W (A0),D3 ;True? BEQ.S ABBRUCH ;ja - Schleife abbrechen ;imag=real*imag*2+imagconst MOVE.W #(MUL+FQ2+FZ3),(A2) ;MUL FP2,FP3 TST.W (A0) NOP NOP NOP MOVE.W #(ADD+FQ3+FZ3),(A2) ;ADD FP3,FP3 TST.W (A0) NOP MOVE.W #(ADD+FQ1+FZ3),(A2) ;ADD FP1,FP3 TST.W (A0) NOP ;neu NOP ;real-realquad-imagquad+realconst MOVE.W #(MOVEFF+FQ4+FZ2),(A2) ;MOVE FP4,FP2 TST.W (A0) MOVE.W #(SUB+FQ5+FZ2),(A2) ;SUB FP5,FP2 TST.W (A0) NOP NOP MOVE.W #(ADD+FQ0+FZ2),(A2) ;ADD FP0,FP2 TST.W (A0) ;zaehler=zaehler+1 ;IF zaehler=rechentiefe -> Abbruch SUBQ.W #1,D1 BNE LOOP1 ABBRUCH ;Ergebnis für diesen Punkt errechnen und Speichern MOVE.W D0,D7 ;Rechentiefe nach D7 SUB.W D1,D7 ;davon Zählerwert abziehen MOVE.W D7,(A6) ;Ergebnis speichern ADDA.W #4,A6 ;Zeiger für Feld erhöhen ;realconst=realconst+realdelta MOVE.W #(ADDMEM+FZO),(A2) ;ADD memory,FP0 TST.W (A0) MOVE.L REALDELH(PC),(A4) MOVE.L REALDELL(PC),(A4) TST.W (A0) ;Zähler für horiz. Punkte minus 1, falls <>0 nochmals DBF D4,LOOP2 ENDE ; abschl.Response-Register in letztes Element des ; Ergebnis-Feldes schreiben/ damit ist Kontrolle ; möglich, ob keine Prot. Viol, aufgetreten ist MOVE.W (A0),(A6) RTS END

Listing 2

' LISTING 3 ' Ass.prg für Apf.männchengrafike mit 68881 ' Unterstützung aufrufen ' ' Michael Kofler, Oktober 88 ' DEFFLT "a-z" !Var. ohne Kennung: Float xreso%=640 !Auflösung der Grafik yreso%~400 realmin=-2 !Parameter des zu realmax=+1.2 !berechnenden Bildes imagmin=-1.25 imagmax=+1.25 realdel=(realmax-realmin)/xreso% imagdel=(imagmax-imagmin)/yreso% rechentiefe%=20 !maximale Rechentiefe DIM ergebnis%(xreso%) !Feld für Ergebnis ' DIM ass_prg%(75) !Speicher für Ass.prg start%=V:ass_prg%(0) !Startadr. des Ass.prg FOR i%=0 TO 300 STEP 2 !Ass.prg. aus DATA lesen READ data DPOKE start%+i%,data NEXT i% ' start%=V:ass_prg%(0) !Startadr. des Ass.prg DPOKE start%+2,rechentiefe% DPOKE start%+4,xreso% LPOKE start%+6,V:ergebnis%(0)+2 !Adr. d.Ergebnisfeldes @double(start%+10,realmin) !Realteil-Startwert @double(start%+18,realdel) !Realteil-Delta @double(start%+34,4) !Vergleichskonstanten LPOKE start%+42,&HFFFA40 !FPU-Adresse ' imag=imagmin FOR y%=0 TO yreso%-1 @double(start%+26,imag) !Imaginärteil-Startwert ~XBIOS(38,L:start%) !Ass.prg aufrufen FOR x%=0 TO xreso%-1 !Zeile am Bildschirm IF EVEN(ergebnis%(x%)) !ausgeben PSET x%,y%,1 ENDIF NEXT x% imag=imag+imagdel !Imaginärteil für jede NEXT y% !Zeile erhöhen ' DATA 24620, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0 DATA 0,0,0, 0,0,0, 0,8314 DATA -6,17384,2,17896,10,18408,14,18920 DATA 16,12988,0,11386,-66,12346,-74,13884 DATA 2049,14394,-80,21316,13500,21504,19024,10426 DATA -86,10426,-86,19024,13500,21632,19024,10426 DATA -86,10426,-86,19024,13500,22400,19024,10426 DATA -94,10426,-94,19024,13500,256,19024,13500 DATA 1408,19024,12800,13500,2560,19024,13500,4643 DATA 19024,20081,20081,20081,13500,3712,19024,13500 DATA 5795,19024,20081,20081,20081,13500,4864,19024 DATA 13500,5922,19024,20081,13500,7992,19024,14012 DATA 18,-18864,26422,13500,2467,19024,20081,20081 DATA 20081,13500,3490,19024,20081,13500,1442,19024 DATA 15895,13500,4352,19024,13500,5416,19024,20081 DATA 20081,13500,290,19024,21313,26250,15872,-25023 DATA 15495,-8964,4,13500,21538,19024,10426,-268 DATA 10426,-268,19024,20940,-160,15504,20085 ' PROCEDURE double(adr%,wert) ' Fließkomma-Zahl ins IEEE-Double Format wandeln ' in GFA 3.0 gehts einfacher: DOUBLE{adr%}=wert IF wert=0 LPOKE adr%,0 LPOKE adr%+4,0 ELSE expo=INT(LOG(ABS(wert))/LOG(2)) !Exponent mantisse=ABS(wert)/2^expo-1 !Mantisse mantissehigh=mantisse*2^20 !höherwertiger mantisselow=FRAC(mantissehigh) !niederw. Teil IF wert<0 !Vorzeichen berücksichtigen LPOKE adr%,(expo+1023)*2^20+mantissehigh 2^31 ELSE LPOKE adr%,(expo+1023)*2^20+mantissehigh ENDIF IF mantisselow<0.5 !Zahlenüberschreitung vermeiden LPOKE adr%+4,mantisselow*2^32 ELSE LPOKE adr%+4,(mantisselow-0.5)*2^32-2^31 ENDIF ENDIF RETURN

Listing 3

Michael Kofler