Apfelmännchen mit 68881-Speed

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:

(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
Aus: ST-Computer 04 / 1989, Seite 150

Links

Copyright-Bestimmungen: siehe Über diese Seite