Pfad: Home => AVR-Überblick => Programmiertechniken => Fließkommazahlen => Dezimalumwandlung    (This page in English: Flag EN) Logo

Programmiertechnik für Anfänger in AVR Assemblersprache

Fließkommazahlen in Assemblersprache

Umwandlung von Fließkommazahlen in Dezimal in Assemblersprache

Um binär-kodierte Fließkommazahlen in das Dezimalformat umzuwandeln und als ASCII-Zeichenkette ausgeben zu können, brauchen wir einen gehörigen Schatz an binärer und exponentieller Mathetik. Wenn Du in beiden Disziplinen arg schwächelst und zu faul bist, Dir dieses Elend reinzuziehen, dann nimm lieber einen PC-Prozessor mit fertiger Fließkommazahlen-Sub-Maschine oder eine vorgefertigte AVR-Bibliothek in C. Für alle anderen: es ist nicht so arg schwer zu verstehen, wie Fließkommazahlen ticken. Nur für den Prozessor ist es etwas aufwändiger, aber ansonsten besteht es nur aus Malnehmen und Teilen mit Zwei, aus ein wenig Links- und Rechtsschieben sowie aus Addieren ohne und mit übertrag. Also nix Wildes.

Speichern von Zahlen

Das Problem hat der C-Programmierer schon mal gar nicht, das entscheidet sein Compiler schon für ihn, er wird da gar nicht erst damit behelligt.

Wie aus der übergeordneten Seite hier hervorgeht, besteht eine 24-Bit-Binärzahl aus 16 Bits Mantisse und 8 Bits Exponent. Beide Teile haben in ihrem obersten Bit ein Vorzeichen. Die drei Bytes passen prima in normale Register.

Die Auflösung einer solchen Zahl in Dezimalformat umfasst 4 1/2 Ziffern für die Mantisse, den beiden Vorzeichen, einem E und dem zweistelligen Exponenten. Wenn wir noch eine oder zwei weitere Stellen für die Ausgabe vorsehen, sind wir schon bei 12 Zeichen. Wir tun also gut daran, dieses Monster in das SRAM zu stecken. In Registern wäre die Rechnerei schneller, aber dafür bliebe kaum Platz im unteren Rechenwerk für anderes.

Eine 15-Bit-Mantisse im Binärformat korrespondiert mit fünf dezimalen Ziffern (215 = 32.768). Das Format ist daher folgendermaßen aufgebaut:

Format einer 24-Bit-Dezimalzahl als ASCII Wir brauchen das Vorzeichen der Mantisse (wenn positiv: ein Leerzeichen), die normalisierte erste Stelle, dann das Dezimalkomma, vier signifikante und eine insignifikante Stelle, dann das E, das Vorzeichen des Exponenten (+ oder -) und zwei Exponenten-Ziffern. Insgesamt sind das 12 Zeichen Resultat.

Das ist der Platz, der für das Ergebnis reserviert werden sollte. Um diesen Platz zu reservieren, schreiben wir:

.dseg
.org SRAM_START
DecAsc:
  .byte 12 ; Reserviere 12 bytes fuer Resultat

Die Umwandlung braucht das Addieren von Daher sind wir insgesamt bei einer zehnziffrigen Zahl. Wir brauchen davon zwei Puffer: einen für die Berechnung des Mantissenwerts und einen für die Berechnung des Addierwertes für jedes Binär-Bit.

Um das Lesen der Werte im Simulator zu vereinfachen, habe ich noch etwas freien Platz im SRAM hinzugefügt. Der ist aber im Ernstfall unnötig.

.dseg
.org SRAM_START
.equ MantLength = 10
sMant:
  .byte MantLength
sMantEnd:
  .byte 16-MantLength 
sAdder:
  .byte MantLength
sAdderEnd:
  .byte 16-MantLength
sDecAsc:
  .byte 12

Die zwei End:-Labels, die auf das Ende jedes Puffers zeigen, werden gebraucht, um für den Fall, dass wir von hinten nach vorne (z. B. beim Addieren) vorgehen müssen, einen Zeiger auf das Ende der Zahl zu haben.

Eine Grundentscheidung hier ist es, BCD-kodierte Ziffern zu verwenden. Möglich gewesen wäre es auch, gepacktes BCD-Format zu verwenden (verringert die Anzahl Instruktionen beim Addieren, muss aber die H-Flagge für Überläufe auswerten) oder man kann direkt ASCII-kodierte Ziffern verwenden (erspart die abschlie&szli;ende ASCII-Wandlung, braucht aber beim Addieren länger).

Im ersten Schritt des Programms müssen wir den Stapelzeiger setzen, denn es werden zum einfacheren Verständnis Unterroutinen verwendet.

Im zweiten Schritt werden wir das Vorzeichenbit der Mantisse los. Ist es negativ (Bit 7 des MSB's ist Eins), müssen wir die Mantisse von 0000 abziehen, was in diesem Fall ein Invertieren der beiden Mantissenbytes bedeutet. Das macht aus der negativen eine positive Mantisse. Die dezimalen Mantissen Die Dezimalmantisse sieht dann so aus.

Umwandlung der Mantisse in dezimal

Addieren des 14-ten Bits Die Mantissenumwandlung beginnt mit Bit 14 in der binären Mantisse. Weil Bit 14 wegen der Normalisierung immer 1 ist, können wir das Vorgehen vereinfachen und dafür immer 0.50000000 einsetzen. In Assembler formulieren wir das dann so:

;
; Initiiere Dezimalmantisse und Addierer
InitMant:
  ldi ZH,High(sMant) ; Zeiger Z auf Mantissenanfang, MSB
  ldi ZL,Low(sMant) ; dto., LSB
  clr rmp ; Loeschen Bytes
  ldi rCnt,MantLength ; Laenge Mantisse
InitMant1:
  std Z+dAddMant,rmp ; Addierer loeschen
  st Z+,rmp ; Mantisse loeschen
  dec rCnt ; Am Ende?
  brne InitMant1 ; Nein, weiter
  ldi rmp,5 ; Starte mit Bit 15
  sts sMant+2,rmp ; Setze Startwert, Mantissa
  sts sAdder+2,rmp ; und Addierer
  ret

Bitte dabei beachten, dass wir beide Puffer gleichzeitig beschreiben: der Befehl STD befüllt den Addierer im Abstand von dAddMant = sAdd - sMant gleich mit. Die Reihenfolge von STD und ST ist nicht egal, weil ST auch gleich noch den Zeiger um Eins erhöht.

Simulation des Init-Vorgangs Der Init-Vorgang sieht im Simulator avr_sim so aus: beide Zahlen sind auf 0,5 gesetzt.

Teilen des Addierers durch zwei Der nächste Schritt teilt den Addierer durch 2, um die Wertigkeit des Bit 13 zu kriegen. Diese Prozedur beginnt mit dem Teilen der 5 im Addierpuffer und setzt sich über den gesamten Puffer fort. Wenn das Teilen einer Ziffer durch 2 einen Rest ergibt (nach LSR ist das Bit 0 im Carry Eins, ist bei 5 der Fall), dann muss 10 zur nachfolgenden Ziffer addiert werden. Ist das Carry-Bit nach der letzten Ziffer noch gesetzt, muss der Addierer in der letzten Stelle um Eins erhöht werden (Aufrunden des Addierers). Da das einen Überlauf in die vorletzte Ziffer ergeben könnte, muss dies solange in den höheren Ziffern erfolgen, bis kein Überlauf mehr erfolgt.

Weil Bit 13 in der Testmantisse 0x5555 Null ist, entfällt das Addieren der geteilten Zahl zur Dezimalmantisse.

Das Teilen der Zahl geht in Assembler so:

;
; Dividiere Addierer durch zwei
DivideBy2:
  ldi ZH,High(sAdder+1) ; Zeige auf Adderer, MSB
  ldi ZL,Low(sAdder+1)
  clc ; Loesche Carry fuer Ueberlauf
  ldi rCnt,MantLength-2 ; Mantissenlaenge minus Eins in Zaehler
DivideBy2a:
  ld rmp,Z ; Lese Byte aus Addierer
  brcc DivideBy2b ; Carry nicht gesetzt, nicht 10 addieren
  subi rmp,-10 ; Addiere 10
DivideBy2b:
  lsr rmp ; Divididiere durch zwei
  st Z+,rmp ; Speichere Divisionsergebnis
  dec rCnt ; Abwaerts zaehlen
  brne DivideBy2a ; noch nicht fertig
  ld rmp,Z ; Lese letztes Byte des Addierers
  lsr rmp ; Teile durch 2
  st Z,rmp
  brcc Divideby2e
  inc rmp ; Letztes Byte aufrunden
Divideby2c:
  st Z,rmp ; Korriegiere letzte Ziffer
  subi rmp,10 ; Ziffer > 10?
  brcs DivideBy2e ; Nein
DivideBy2d:
  st Z,rmp ; Korrigiere letzte Ziffer
  ld rmp,-Z ; Lese vorletzte Ziffer
  inc rmp ; Erhoehe Ziffer
  st Z,rmp ; und speichere
  subi rmp,10 ; Ziffer >= 10?
  brcc DivideBy2d ; Ja, wiederhole
DivideBy2e:
  ret

Der Teil am Ende ab Divide2c: bzw. eine Zeile davor ist für das Aufrunden der letzten Ziffer verantwortlich. Dabei wird nach dem Abziehen von 10 die letzte Ziffer erneut abgespeichert und mit LD rmp,-Z die vorletzte Ziffer gelesen und ebenfalls um Eins erhöht, wenn das Carry gesetzt war. Dies wird solange wiederholt, bis das Carry beim Abziehen von 10 gesetzt ist.

Dividieren von 0,25 durch 2 und Addieren Dividieren von 0,25 durch 2 und Addieren Das Teilen von 0,25 durch 2 (Zeile 2 der Simulation) ist hier gezeigt. Weil in der Testzahl 0x5555 das 12-te Bit eine Eins ist, wird dieser Addierer anschließend zur Dezimalmantisse hinzuaddiert. Beim Addieren zur Dezimalmantisse wird von hinten begonnen und nach links hin bearbeitet. Jede Ziffer, die 10 erreicht oder überschreitet, wird um zehn vermindert und eine Eins zur nächsten Ziffer hinzugezählt. Wenn das binäre Mantissenbit nicht Eins ist, erfolgt direkt das nächste Teilen durch zwei und der Addierer wird nicht addiert.

Der Quellcode für das Addieren ist hier:

;
; Addiere den Addierer zur Dezimalmantisse
MantAdd:
  ldi ZH,High(sMantEnd) ; Zeiger Z auf Dezimalmantisse Ende, MSB
  ldi ZL,Low(sMantEnd) ; dto., LSB
  ldi rCnt,MantLength-1 ; Mantissenlaenge
  clc ; Beginne mit Carry null
MantAdd1:
  ld rmp,-Z ; Lese letztes Mantissenbyte
  ldd rmp2,Z+dAddMant ; Lese zugehoeriges Addiererbyte
  adc rmp,rmp2 ; Addiere beide mit Carry
  st Z,rmp ; Speichere in SRAM
  subi rmp,10 ; Subtrahiere 10
  brcs MantAdd2 ; Carry gesetzt, kleiner als 10
  st Z,rmp ; Ueberschreibe Ziffer
  sec ; Setze Carry fuer naechste Ziffer
  rjmp MantAdd3 ; Zum Abwaertszaehlen
MantAdd2:
  clc ; Loesche Carry fuer naechstes addieren
MantAdd3:
  dec rCnt ; Abwaerts zaehlen
  brne MantAdd1 ; Noch nicht fertig
  ret



So wird jedes weitere binäre Mantissenbit ebenso behandelt, bis alle verarbeitet sind. Hier das letzte Teilen und Addieren bei einer Binärmantisse von 0x5555.

Das hier sind alle 15 Dezimaladdierer im Überblick. Man sieht, dass der letzte Addierer in der fünften Stelle noch mal 3,1 addiert. Um diese ist die fünfte Stelle ungenau. Hätten wir ein weiteres Bit gehabt (weil das 15.te Bit eigentlich überflüssig ist), wäre diese letzte Stelle mit 1,5 noch etwas genauer geworden.

Umwandeln der Exponenten-Bits

Die Eingangsparemeter beim Wandeln des Exponenten Dies sind nun alle relevanten Parameter, wenn es an das Umwandeln des Exponenten geht. Die drei Komponenten sind:
  1. die dezimale Mantisse, wie sie aus der vorhergehenden Berechnung resultiert,
  2. der dezimale Exponent, der mit Null beginnt und erhöht oder erniedrigt wird, und
  3. der binäre Exponent, der nun auf die Mantisse losgelassen werden muss. Im Beispielfall ist das +4, er kann aber zwischen -128 und +127 liegen.


Erste Normalisierung Im ersten Schritt muss die dezimale Mantisse normalisiert werden, denn ihre erste Ziffer (in der zweistelligen Zahl die zweite Ziffer) ist Null.

Dazu werden alle dezimalen Mantissenziffern um eine Position nach links geschoben. Es beginnt mit der linkesten Ziffer und geht bis zur vorletzten Ziffer. Die letzte Ziffer wird Null gesetzt.

Das bedeutet, die dezimale Mantisse wird mit zehn multipliziert. Entsprechend muss der dezimale Exponent um Eins vermindert werden. Aus der Null wird nun -1 oder 0xFF.

Ist die zweite Ziffer nach einer Normalisierung noch immer nicht Null, muss die Normalisierung weitere Male erfolgen, bis das der Fall ist (tritt in unserem Fall aber nicht auf).

Umgekehrt würde beim Normalisieren vorgegangen, wenn die allererste Ziffer nicht Null wäre (was beim Multiplizieren mit zwei nachfolgend mehrfach passieren wird). Dann müsste die ganze Zahl, von ganz rechts aus beginnend, um eine Position nach rechts geschoben werden. In die linkeste Position kommt dann eine Null. Der dezimale Exponent wird dann um Eins zu erhöhen sein.

Der Quellcode für die Normalisierung:

;
; Normalisieren der Dezimalmantisse
Normalize:
  lds rmp,sMant ; Lese Mantissen-Ueberlauf
  tst rmp ; Nicht Null?
  brne NormalizeRight ; Schiebe nach rechts
Normalize1:
  lds rmp,sMant+1 ; Erste Ziffer lesen
  tst rmp ; Null?
  breq NormalizeLeft ; Ja, links schieben
  ret ; Keine Normalisierung noetig
  ; Schiebe Mantisse eine Position links
NormalizeLeft:
  ldi ZH,High(sMant+1) ; Zeige auf erste Ziffer, MSB
  ldi ZL,Low(sMant+1) ; dto., LSB
  ldi rCnt,MantLength-2 ; Schieberzaehler
NormalizeLeft1:
  ldd rmp,Z+1 ; Lese naechstes Byte
  st Z+,rmp ; Kopiere in aktuelle Position
  dec rCnt ; Abwaerts zaehlen
  brne NormalizeLeft1 ; Weitere Schiebereien
  clr rmp ; Loesche letzte Ziffer
  st Z,rmp ; in der Pufferposition
  dec rDecExp ; Vermindere Dezimalexponent
  rjmp Normalize1 ; Weitere Schiebereinen erforderlich?
  ; Schiebe Zahl nach rechts
NormalizeRight:
  ldi ZH,High(sMantEnd-1) ; Z auf Ende Mantisse, MSB
  ldi ZL,Low(sMantEnd-1) ; dto., LSB
  ldi rCnt,MantLength-1 ; Zaehler
NormalizeRight1:
  ld rmp,-Z ; Lese Ziffer links
  std Z+1,rmp ; Speichere eine Position rechts
  dec rCnt ; Abwaertszaehlen
  brne NormalizeRight1 ; Weitere Ziffern
  clr rmp ; Erste Ziffer loeschen
  st Z,rmp ; in Mantissen-Ueberlauf
  inc rDecExp ; Erhoehe Dezimalexponent
  ret

Simulation Normalisierung zu Beginn Das war nun die erste Normalisierung: die zweite Ziffer ist jetzt nicht mehr Null, die erste Ziffer ist Null geblieben. Am Ende der Zahl ist eine Null hinzugefügt worden.

Multiplikation mit zwei, Binaerexponent minus Eins Das Normalisieren hat am binären Exponenten nichts verändert: er ist weiterhin gleich vier. Das muss sich nun ändern: ist er positiv, muss es abwärts gehen, ist er negativ muss es aufwärts gehen. Zielwert ist in beiden Fällen die Null, dann sind wir fertig.

Damit es abwärts geht, muss die Mantisse mit zwei malgenommen werden. Damit es aufwärts geht, muss sie durch zwei geteilt werden.

Multiplikation der Mantisse mit zwei Hier wird die dezimale Mantisse mit zwei malgenommen. Wie man sieht, erscheint in der linkesten Ziffer ein Überlauf, weil die erste Mantissenziffer bei Verdoppelung größer als 9 wird. Das muss dann anschließend wieder fein weg-normalisiert werden.

Der Binär-Exponent wird jetzt um Eins niedriger.

Der Quellcode der Multiplikation mit zwei:

;
; Multipliziere Zahl mit 2
Multiply2:
  ldi ZH,High(sMantEnd) ; Z auf Ende Mantisse, MSB
  ldi ZL,Low(sMantEnd) ; dto., LSB
  ldi rCnt,MantLength ; Ueber gesamte Laenge
  clc ; Carry = 0 zu Beginn
Multiply2a:
  ld rmp,-Z ; Lese letzte Ziffer
  rol rmp ; Multipliziere mit 2 und addiere Carry
  st Z,rmp ; Ueberschreibe Ziffer
  subi rmp,10 ; Subtrahiere 10
  brcs Multiply2b ; Carry gesetzt, kleiner als 10
  st Z,rmp ; Ueberschreibe letzte Ziffer
  sec ; Setze Carry fuer naechsthoehere Ziffer
  rjmp Multiply2c ; Zum Abwaertszaehlen
Multiply2b:
  clc ; Loesche Carry fuer naechste Ziffer
Multiply2c:
  dec rCnt ; Abwaertszaehlen
  brne Multiply2a ; Weitere Ziffern behandeln
  ret

Die simulierte Multiplikation mit zwei ist oben zu sehen. Die Übertragsziffer ist nun Eins geworden, sie muss durch anschließenden Aufruf von Normalize: nach rechtsgeschoben werden. Dadurch erhöht sich der dezimale Exponent wieder um Eins (war vorher -1, wird jetzt Null.

Dasselbe passiert, wenn der Binärexponent negativ ist, nur muss dann nicht mit zwei multipliziert sondern dividiert werden. Führt die Division zu einer Null in der ersten Ziffer, muss die wieder nach links wegnomalisiert werden.

Das Dividieren geht im Quellcode so:

;
; Dividiere Zahl durch 2
Divide2:
  ldi ZH,High(sMant) ; Zeiger auf Mantisse, MSB
  ldi ZL,Low(sMant) ; dto., LSB
  ldi rCnt,MantLength ; Alle Ziffern behandeln
  clc ; Carry loeschen zu Beginn
Divide2a:
  ld rmp,Z ; Ziffer lesen
  brcc Divide2b ; Wenn carry gesetzt 10 addieren
  subi rmp,-10 ; 10 addieren
Divide2b:
  lsr rmp ; Mit 2 malnehmen
  st Z+,rmp ; und speichern
  dec rCnt ; Abwaertszaehlen
  brne Divide2a ; Weitere Ziffern dividieren
  brcc Divide2d ; Carry ist clear, nicht aufrunden
Divide2c:
  inc rmp ; Aufrunden
  st Z,rmp ; Speichern
  subi rmp,10 ; Ueberlauf?
  brcs Divide2d ; Nein
  ld rmp,-Z ; Lese naechsthoehere Ziffer
  rjmp Divide2c ; Erhoehe weiter
Divide2d:
  ret



Diese Schritte mit Multiplikation/Division wiederholen sich so lange, bis der binäre Exponent Null erreicht.
Drei weitere Verdoppelungen Das gleiche Spiel wiederholt sich nun drei weitere Male bis der binäre Exponent Null geworden ist.

Beim vierten Verdoppeln tritt wieder ein Überlauf ein, der wieder durch Normalisieren bereinigt werden muss. Der dezimale Exponent geht dann von Null auf Eins. Bei noch größerem binären Exponenten tritt so ein &Uuuml;berlauf etwa alle vier Mal auf, so dass wir maximal zirka Exponenten von 128 / 4 = 32 kriegen.

Runden der Dezimalmantisse

Runden der Dezimalmantisse Die letzten drei Dezimalstellen hatten wir zum Schieben und Runden mitgeschleppt. Das wird nun ausgeführt, indem wir zum Ergebnis 0.00000555 hinzuaddieren. Das sollte zu einem vollen Rundungserfolg führen, so dass unsere fünfte Stelle nach dem Komma etwas genauer wird.

Runden geht in Assembler dann so:

;
; Aufrunden der Mantisse
RoundUp:
  ldi ZH,High(sMantEnd) ; Zeiger auf Ende Mantisse, MSB
  ldi ZL,Low(sMantEnd) ; dto., MSB
  ldi rmp2,5 ; Fuenfen addieren
  ldi rCnt,3 ; Drei mal
  clc ; Carry loeschen
RoundUp1:
  ld rmp,-Z ; Letzte Ziffer lesen
  adc rmp,rmp2 ; Fuenf mit Carry addieren 
  st Z,rmp ; Speichern
  subi rmp,10 ; Zehn abziehen
  brcs RoundUp2 ; Nicht groesser als 9
  st Z,rmp ; Ziffer - 10 speichern
  sec ; Carry-Flagge setzen
  rjmp RoundUp3 ; Zum Zaehlen
RoundUp2:
  clc ; Carry-Flagge loeschen
RoundUp3:
  dec rCnt ; Abwaertszaehlen
  brne RoundUp1 ; Weitere Ziffern
  ldi rmp2,0 ; Nullen addieren
  ldi rCnt,MantLength-3 ; Anzahl weitere Ziffern
RoundUp4:
  ld rmp,-Z ; Letzte Ziffer lesen
  adc rmp,rmp2 ; Addieren von Null und Carry
  st Z,rmp ; speichern
  subi rmp,10 ; Zehn abziehen
  brcs RoundUpRet ; Carry gesetzt, Ende
  dec rCnt ; Abwaerts zaehlen
  brne RoundUp4 ; Weiter erhoehen
  rcall Normalize ; Am Ende noch mal normalisieren
RoundUpRet:
  ret

In den ersten drei Durchläufen von hinten wird fünf zu jeder Ziffer hinzuaddiert, bei allen weiteren Durchläufen wird nur der mögliche Überlauf addiert. Treten Überläufe bis hin zur ersten Ziffer auf, muss wieder normalisiert werden.

Simulation Runden In diesem Fall trat bei der Addition von 555 kein Übertrag in die letzte Stelle statt.

Die Dezimalmantisse und der Dezimalexponent sind jetzt komplett für die Umwandlung in die ASCII-Zahl.

Wandlung von BCD in ASCII

Wandlung in eine ASCII-Zeichenkette Alle Ziffern sind BCD-Zahlen. Wir müssen daher 0x30 hinzu addieren (oder -'0' subtrahieren), um ASCII-Ziffern zu erhalten. Natürlich müssen wir der Zeichenkette noch folgendes hinzufügen:
  1. das Vorzeichen der Dezimalmantisse, wenn die Zahl negativ war (wenn nicht: ein Leerzeichen),
  2. den Dezimalpunkt,
  3. wenn der Dezimalexponent Null ist, vier Leerzeichen. Wenn nicht: E und das Vorzeichen des Exponenten und den Exponenten als zweiziffrige Zahl (zehn abziehen bis zum Unterlauf und Anzahl minus Eins als Zehner schreiben, Rest sind dann die Einer).
Der Quellcode hierfür:

;
; In Ascii-Zeichenkette umwandeln
ToAsc:
  ldi ZH,High(sDecAsc) ; Zeiger auf ASCII Zeichenkette, MSB
  ldi ZL,Low(sDecAsc) ; dto., LSB
  ldi XH,High(cMantissa) ; Mantissenvorzeichen lesen
  ldi rmp,' ' ; Positiv
  lsl XH ; Vorzeichen in Carry
  brcc ToAsc1 ; Nicht negativ
  ldi rmp,'-' ; Vorzeichen negativ
ToAsc1:
  st Z+,rmp ; Vorzeichen in Zeichenkette
  ldi XH,High(sMant+1) ; X als Zeiger auf Dezimalmantisse, MSB
  ldi XL,Low(sMant+1) ; dto., LSB
  ld rmp,X+ ; Lese erste Mantissenziffer
  subi rmp,-'0' ; in ASCII 
  st Z+,rmp ; und schreibe in Zeichenkette
  ldi rmp,',' ; Dezimalkomma
  st Z+,rmp ; in Zeichenkette
  ldi rCnt,MantLength-5
ToAsc2:
  ld rmp,X+ ; Lese Ziffer
  subi rmp,-'0' ; in ASCII
  st Z+,rmp ; in Zeichenkette
  dec rCnt ; Abwaerts zaehlen
  brne ToAsc2 ; Weitere Ziffern kopieren
  tst rDecExp ; Dezimalexponent = 0?
  breq ToAscExpBlk ; Vier Leerzeichen ausgeben
  ldi rmp,'E' ; Exponentenzeichen
  st Z+,rmp ; in Zeichenkette
  ldi rmp,'+' ; Vorzeichen Exponent
  sbrs rDecExp,7 ; Bei negativ ueberspringen
  rjmp ToAsc3 ; Exponent positiv
  neg rDecExp ; 0 minus Exponent
  ldi rmp,'-' ; Minuszeichen
ToAsc3:
  st Z+,rmp ; Vorzeichen ausgeben
  ldi rmp,'0'-1 ; Exponentenzehner
ToAsc4:
  inc rmp ; Naechster Zehner
  subi rDecExp,10 ; 10 abziehen
  brcc ToAsc4
  st Z+,rmp ; Zehner ausgeben
  ldi rmp,'0'+10 ; Einer
  add rmp,rDecExp ; in ASCII
  st Z,rmp ; und in Zeichenkette
  ret
ToAscExpBlk:
  ldi rmp,' ' ; Leerzeichen
  ldi rCnt,4 ; Vier Stueck
ToAscExpBlk1:
  st Z+,rmp ; in Zeichenkette
  dec rCnt ; Abwaerts zaehlen
  brne ToAscExpBlk1 ; Weitere Zeichen
  ret

Umwandlung in ASCII-Zeichenkette Das sieht nun ganz so aus, als könnte und wollte es auf eine LCD geschrieben werden.

Ausführungszeiten

Wenn Dein Programm es eilig hat und den Prozessor eh schon ziemlich auslastet, dann ist die Umwandlung von Fließkommazahlen in Dezimal genau der Sargnagel, den Du gebrauchen kannst, um ihm den Rest zu geben.

MantisseExponentDauer
0x40000x00448 µs
0x01668 µs
0x02816 µs
0x102,15 ms
0x7F23,2 ms
0xFFFF0x00449 µs
0x55552,88 ms
0x7FFF3,87 ms


Die Fälle mit negativen Mantissen und Exponenten brauchen nur unwesentlich mehr Zeit (zwei zusätzliche Instruktionen NEG und einmal COM).

Wenn Du den Assembler-Quellcode für eigene Experimente oder zum Erweitern auf 32, 40 oder 64 Bits brauchst (380 Zeilen), dann ist er hier fliesskomma24.asm zugänglich.

Schnellere Methode: eine binäre 40-Bit-Fließkommazahl in dezimal umwandeln

Die oben gezeigte Methode ist nicht sehr effektiv, weil sie eine Menge SRAM verwendet und jede Ziffer in ein eigenes Byte packt. Würden wir die 16-Bit-Mantisse auf eine 32-Bit-Mantisse erweitern, wie sie 40-Bit-Fließkommazahlen verwenden, dann würden wir eine langwierige Umwandlungsroutine kriegen, die 50 ms und länger dauert. Wir brauchen daher eine optimierte Variante, und die macht es dann so:
  1. Zuerst wird die 32-Bit-Mantisse in einen Integer-Wert umgewandelt. Da ein 32-Bit-Wert Zahlen bis 233-1 = 8.589.934.591 = 0x01FFFFFFFF annehmen kann, brauchen wir fünf Bytes für diese dezimale Ganzzahl. Wir packen links noch ein Byte hinzu, für den Fall, dass bei einer Multiplikation mit zehn ein Überlauf auftreten sollte. Diese Zahl heißt im Quellcode rAdd5:4:3:2:1:0. Für jedes der 32 Mantissen-Bits wird dessen dezimale Wertigkeit gebildet, indem ausgehend von 50.000.000.000 oder hexadezimal 0B.A4.3B.74.00 dieses jeweils durch zwei geteilt wird (natürlich unter Aufrunden der jeweils letzten Teilung) diese Wertigkeiten jeweils dann zusammengezählt werden, wenn das jeweilige Mantissenbit eine Eins ist.
  2. Dann wird der binäre Exponent verarbeitet. Ist der positiv, dann wird die Zahl in rAdd5:4:3:2:1:0 solange mit zwei multipliziert wie der Exponent es sagt. Tritt bei dieser Multiplikation ein Überlauf nach rAdd5 auf, dann wird der dezimale Exponent um eins erhöht und die Zahl durch 10 geteilt. Ist der Exponent negativ, wird die Zahl so lange durch zwei geteilt bis der negative Exponent abgearbeitet ist. Tritt beim Zweiteilen der Fall ein, dass das höchste Byte rAdd4 Null wird, dann wird der dezimale Exponent um Eins vermindert und die Zahl mit 10 malgenommen (zweimal Linksschieben, Addieren von sich selbst, einmal Linksschieben).
  3. Jetzt wird die Zahl in rAdd5:4:3:2:1:0 normiert. Ist sie größer als 100.000.000.000 (wenn entweder rAdd5 grßer Null ist oder wenn rAdd4:3:2:1:0 größer oder gleich 100.000.000.000), dann wird die Zahl durch zehn geteilt und der dezimale Exponent um Eins erhöht. Wurde diese Teilung nicht vorgenommen, dann wird überprüft, ob die Zahl kleiner ist als 1.000.000.000. Falls dies der Fall ist, wird die Zahl solange mit 10 malgenommen (unter Verminderung des dezimalen Exponenten), bis das der Fall ist.
  4. Diese Ganzzahl wird nun in dezimale Ziffern umgewandelt, indem zuerst solange 1.000.000.000 abgezogen wird, bis ein Unterlauf auftritt. Das ergibt die erste Ziffer. Auf diese Ziffer folgt das Dezimalkomma (im Quellcode ist das ein Dezimalpunkt). Dann geht es mit 100.000.000 genauso weiter und bis herunter nach 10. Die letzte Ziffer ist der Rest der Zahl. Ist der dezimale Exponent nicht Null, wird er dann mit einem E, dem + oder - und nachfolgend als zweistellige Dezimalzahl angehängt.
Fliesskommawandlung 40-Bit Man beachte, dass das Dividieren durch 10 es erforderlich macht, alle sechs Bytes bzw. 48 Bits einzeln nach links zu schieben, zu prüfen, ob 10 erreicht sind und, falls ja, eine Eins, bzw. eine Null in das Ergebnis hereinzuschieben ist. Diese umfangreiche Schieberei ist zeitaufwändig und verlängert die Ausführungsdauer in Fällen, bei denen der positive binäre Exponent groß ist.

Die Tabelle rechts zeigt, dass für binäre Exponenten von 0x7F lange Zeitdauern zu Buche schlagen. Hier lohnt es sich, über Optimierungen wie die Division durch 100 oder 1000 nachzudenken. Alle anderen Fälle sind unkritisch.

Beim Vergleich der Zeiten mit der obigen Methode ist zu berücksichtigen, dass wir hier die doppelte Anzahl an Mantissenbits mit der doppelten Genauigkeit verarbeiten. Dafür sind die Zeiten recht kurz.

Der Quellcode in Assembler-Format ist hier zugänglich. Ich habe ihn nicht in Deutsch übersetzt, da er kaum mit Kommentaren glänzt und auch so verständlich sein sollte. Er arbeitet daher in englischer Zahlennotation. Für seriöse Anwendungen könnte es sich lohnen, ein weiteres Byte hinten anzuhängen, um ein Mehr an Genauigkeit zu erreichen. Register dafür sind noch satt vorhanden und verfügbar. Auch der erforderliche Umbau, um ein um ein Bit komprimiertes Format von Fließkommazahlen (mit unterdrücktem Bit 31) zu verarbeiten, ist sehr einfach.

Schlussfolgerung

Halte Dich von Fließkomma-Zahlen so fern als möglich. Sie fressen Deine Performance auf, blasen Deinen Code mit unnützem Beiwerk auf und sind, bis auf ganz wenige Ausnahmen, komplett unnötiger Müll. Mache Zeugs, für das Du unbedingt Fließkomma brauchst, auf einem PC, der kann das besser als ein AVR, weil er einen eigenen Fließkomma-Prozessor hat.

Zum Seitenanfang

©2021 by http://www.avr-asm-tutorial.net