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.
Der 68881 hat neun Interfaceregister, von denen für einfachere Aufgaben aber nur fünf wichtig sind:
(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).
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 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).
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.
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 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.
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.
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:
Im folgenden wird das Protokoll für diese vier Gruppen kurz beschrieben:
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.
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.
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).
In das Conditionregister wird der Conditioncode geschrieben. Anschließend wird der resultierende Wahrheitswert ($800 oder $801) aus dem Responseregister gelesen.
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.
- 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.
- 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
- 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
- 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 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.
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
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.
Bit 0-9: wie oben
Bit 10-12 (Destination-Format): wie oben
Bit 15,14,13: 0,1,1
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
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
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.
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