Festkommaarithmetik

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Das Problem

Ein immer wiederkehrendes Problem ist die Anzeige von Messwerten per UART/RS232 oder LCD. Die Messwerte werden praktisch immer von AD-Wandlern oder anderen Sensoren in digitaler Form geliefert. Doch wie wandelt man diese in eine "normale" Anzeige wie z. B. 4,56V um?

Der einfachste Ansatz ist der Einsatz von Gleitkommazahlen. Dabei gilt nahezu immer die Formel

[math]\displaystyle{ \text{Eingangsspannung} = \frac{\text{ADC-Wert} \cdot \text{Referenzspannung}}{ 2^{\text{Bitbreite}_\text{ADC}}} }[/math]

Den so errechneten Wert der Eingangsspannung kann man einfach per C-Funktion

sprintf(mein_string, "%4.2f", Eingangsspannung);

in einen String umwandeln und dann auf ein LCD oder den UART ausgeben.

Soweit so gut. Das Problem ist "nur".

  • Das Rechnen mit Gleitkommazahlen ist auf den meisten Mikrocontrollern mit recht viel Rechenzeit sowie Programmspeicher verbunden, da diese keine Befehle zur direkten Verarbeitung von Gleitkommazahlen haben. Alle Rechnungen werden mit Hilfe von Bibliotheksfunktionen nachgebildet.
  • Die Ausgabe von Gleitkommazahlen per sprintf und Konsorten benötigt ebenfalls sehr viel Programmspeicher, da diese Funktionen sehr mächtig und damit umfangreich sind

Study your libraries

printf ist nicht nur groß sondern auch einigermaßen mächtig. Bevor man sich an eine eigene printf-Implementierung macht, sollte man wissen, wie printf arbeitet und was für Möglichkeiten mit der Formatierungsangabe zur Verfügung stehen.

Ein standardkonformes printf betrachtet '%' als das Formatierungs-Escape. Danach folgen 0 oder mehr Flag-Zeichen mit unterschiedlichen Effekten, gefolgt von genau einem Formatierungszeichen. Die Flags sind optional und so gemacht, dass man diese meistens nicht braucht. So spuckt "%i" bzw. "%d" eine Festkommazahl dezimal aus. (Unterschiedlich wirken "%i" und "%d" nur bei scanf.)

Die wichtigsten Flags sind Feldbreite und (nach '.') Dezimalstellen. Dazu kommen die Zeichen "0+- #*lh": Ja, die Null hat eine Sonderbedeutung. Alles nachzulesen in avr-libc.chm.

Die Formatierungszeichen (am Ende) sind "diouxXpcs%eEfFgGS". Folgt keines dieser Zeichen ist das Verhalten von printf undefiniert, in aller Regel spuckt es das unbekannte Zeichen aus (wie bei '%'). 'S' ist eine avr-gcc-Erweiterung: Es interpretiert den übergebenen Zeiger als Flash-Adresse und liest die Bytes intern mit pgm_read_byte(). Anders als bei Windows, wo "%S" mit Unicode zu tun hat.

Dadurch ist die printf-Funktionalität recht umfangreich, und das erklärt die Größe sowie warum drei printf-Versionen für den Linker per Kommandozeile zur Verfügung stehen.

Die Flucht nach C++ mit dem Stream cout bringt da keine Verbesserung. Obwohl std::ostream genau mit diesem printf-Problem im Hinterkopf ausgedacht wurde, rufen die mir bekannten Implementierungen allesamt hinterrücks printf auf. Für C++-Programmierer erscheint es daher sinnvoll, my::ostream zu implementieren und zu nutzen.

Das Problem von printf ist, dass es sich überhaupt nicht anwenderseitig modifizieren oder erweitern lässt. Weder kann man eigene Flags und Formatierungszeichen implementieren, noch kann man einfach auf einen Ausgabestream umleiten. So lassen sich weder Ganzzahlen mit Dezimalstellen ausgeben, noch kann man ein Komma statt Punkt ausspucken.

Dabei hat die Grundidee, dass sich die variablen Parameter per korrespondierender Position in den Formatstring einsetzen, zwei Vorzüge:

  • Einfache innere Verarbeitung mittels Laufzeiger auf Stack oder va_list-Argument
  • Auslagerung des Formatstrings und Übersetzung in andere Sprachen leicht möglich (Reihenfolge muss bleiben)

Letztlich ist ein selbst programmiertes, auf das Problem maßgeschneidertes printf() nicht soo schwer. Dieses auch noch mit wenig Kodebytes hinzubekommen (d.h. dem avr-gcc zweckdienlich unter die Arme zu greifen) ist das eigentliche Kunststück. Sparen kann man durch:

  • Weglassen nicht benötigter Flags und Formatierungszeichen
  • Reduktion der maximalen Ausgabebreite auf 16 Bit oder gar 8 Bit
  • Weglassen der Vorzeichenverarbeitung (dann nur unsigned-Anzeige)
  • Feste Formatierung (bspw. "%I" entspricht "%5,i": Ausgabe mit Komma und 1 Nachkommastelle)
  • Festverdrahtung des Ausgabestroms bspw. auf lcd_putc()

Die Lösung

Die allerwenigsten Anwendungen benötigen die volle Leistung von Gleitkommazahlen (Dynamikbereich). Wesentlich sinnvoller ist die Anwendung von Festkommazahlen. Diese sind normale Integerzahlen (ganzzahlig ohne Kommastellen), allerdings mit "gedachten" Kommastellen. Wie geht das? Ganz einfach. Anstatt 10,45 kann man auch 1045 schreiben und die beiden letzten Stellen als Nachkommastellen betrachten. 1045 kann man in einer ganz normalen 16 Bit Integervariable speichern. Und auch damit rechnen!

Anstatt nun in Gleitkomma zu schreiben und zu rechnen

[math]\displaystyle{ U = \frac{756 \cdot 5{,}0}{1024} }[/math]

wird geschrieben

[math]\displaystyle{ U = \frac{756 \cdot 5000}{1024} }[/math]

Die Referenzspannung wird in 1/1000 V (mV) ausgedrückt und damit gerechnet. Das Ergebnis ist eine Spannung in mV.

Wichtig ist dabei, daß

  • erst alle Multiplikationen und dann erst die Divisionen durchgeführt werden, um Rundungsfehler zu minimieren
  • in sämtlichen Zwischenergebnissen keine arithmetischen Überläufe auftreten, also genügend grosse Variablen benutzen (16/32/64 Bit Integer)
Achtung!
Ein klassischer Fehler ist die Benutzung zu kleiner Variablentypen für den Multiplikationsfaktor oder andere Variablen. Das geht schief, weil C dann nur mit dem kleinen Zahlentyp rechnet und es zum Überlauf kommt. Erst danach wird das nun falsche Ergebnis in den größeren Zahlentyp konvertiert.
Wisse
C / C++ konvertiert automatisch jeden kleineren Datentyp zu int oder unsigned. Aber nicht höher! Dabei genügt es, einen der beiden Operanden auf den Zieldatentyp zu casten. Mit der (üblichen) Option -Os optimiert der Compiler selbständig die Operanden auf minimale Größe: In der Zeile ergebnis = adc * (long)k setzt der Compiler das Unterprogramm 16 bit × 16 bit = 32 bit ein, und nicht das nächstgrößere 32 bit × 32 bit = 32 bit.
int k, adc;
long kl;
long ergebnis;

ergebnis = adc * k;          // Fehler, Berechung als int
ergebnis = adc * (long)k;    // richtig, Berechung als long durch Cast
ergebnis = adc * kl;         // richtig, Berechung als long, wegen kl

Man sollte mindestens eine Variable auf der rechten Seite genauso groß deklarieren wie das Ergebnis auf der linken Seite oder einen Cast benutzen.

Doch wie wird nun diese Zahl in einen darstellbaren String umgewandelt? Im einfachsten Fall durch Verwendung der C-Funktion itoa (Integer to ASCII). Diese ist auf vielen Mikrocontrollern als C-Bibliothek verfügbar. Wenn dies jedoch nicht so sein sollte muss man sie selber schreiben. Und da wir hier etwas lernen wollen, werden wir das auch tun.

ITOA selbst gemacht

Wie funktioniert nun die Umwandlung einer Zahl in einen String? Ganz einfach. Für jede Stelle der Dezimalzahl muss ein ASCII-Zeichen erzeugt werden. Wenn z. B. die Zahl 28943 in einen String gewandelt werden soll muss am Ende der String die ASCII-Codes 0x32, 0x38, 0x39, 0x34, 0x33 und 0x00 (Stringabschlusszeichen, Stringterminator) enthalten. Wie man bei genauem Hinsehen sieht, besteht der ASCII-Code einer Ziffer zwischen 0..9 immer aus 0x30 + Ziffer. Das ist einfach. Und wie kommt man nun an die einzelnen Ziffern? Dazu wird eine MODULO Operation durchgeführt. Diese liefert den Rest einer ganzzahligen Division.

28943 MOD 10 =       3

Nun gehts an die nächste Stelle. Dazu wird die Zahl einfach durch 10 dividiert

28943 / 10 = 2894

Und nun das Spiel von vorn.

2894 MOD 10 =        4
2894 / 10   = 289
289 MOD 10  =        9
289 / 10    = 28
28 MOD 10   =        8
28 / 10     = 2
2 MOD 10    =        2

Das wars eigentlich schon. Beachtet werden muss nur, dass bei dieser Methode die einzelnen Stellen in umgekehrter Reihenfolge entstehen: Die höchstwertigen Stellen kommen erst zum Schluss.

Hier ist nun unsere erste einfache Funktion, um eine vorzeichenlose 32 Bit Zahl in einen String umzuwandeln. Diese kann maximal 10 Dezimalstellen haben (0..4294967295), also wird ein Speicher für 11 Bytes benötigt (letztes Byte für den Stringterminator). Bei dem Verfahren wird die Zahl rückwärts berechnet. Das muss bei der Ablage im Speicher berücksichtigt werden. Im Sinne der Verständlichkeit wurde bewusst auf Optimierungen und kompakt/kryptische Schreibweisen verzichtet. Der Syntax ist Standard-C und somit auf jedem Compiler nutzbar.

#include <stdint.h>
/*

Funktion zur Umwandlung einer vorzeichenlosen 32 Bit Zahl in einen String

*/

void my_uitoa(uint32_t zahl, char* string) {
  int8_t i;                             // schleifenzähler

  string[10]='\0';                       // String Terminator
  for(i=9; i>=0; i--) {
    string[i]=(zahl % 10) +'0';         // Modulo rechnen, dann den ASCII-Code von '0' addieren
    zahl /= 10;
  }
}

Diese Funktion gibt auch führende Nullen aus. Das ist erstmal OK, denn so wissen wir immer wo unser gedachtes Komma ist. Bei der Ausgabe können die führenden Nullen unterdrückt werden. Wie das geht, wird weiter unten beschrieben.

Benötigt man neben der Ausgabe für vorzeichenlose (unsigned) Werte auch noch eine Funktion für vorzeichenbehaftete Werte, so ist auch dieses keine Hexerei. Dazu verwenden wir ein zusätzliches Byte im String um das Vorzeichen zu speichern. Der String muss nun also mindestens 12 Bytes Speicherplatz zur Verfügung stellen.

#include <stdint.h>
// Funktion zur Umwandlung einer vorzeichenbehafteten 
// 32-Bit Zahl in einen String

void my_itoa(int32_t zahl, char* string) {
  uint8_t i;

  string[11]='\0';                  // String Terminator
  if( zahl < 0 ) {                  // ist die Zahl negativ?
    string[0] = '-';              
    zahl = -zahl;
  }
  else string[0] = ' ';             // Zahl ist positiv

  for(i=10; i>=1; i--) {
    string[i]=(zahl % 10) +'0';     // Modulo rechnen, dann den ASCII-Code von '0' addieren
    zahl /= 10;
  }
}

Allerdings ist diese Vorgehesweise durch die Modulo- und anschliessende Divisionsoperation etwas ineffizient. Es werden tatsächlich zwei Divisionen durchgeführt, obwohl nur eine notwendig wäre. Der Optimizer (-Os auf der Kommandozeile von avr-gcc) „erkennt“ diese Situation und setzt die Funktion div() ein, die in einem Rutsch Quotient und Rest ermittelt. Was der Optimizer nicht „sieht“ ist dass zahl hier unsigned ist und generiert viel unnützen Kode für die Vorzeichenbehandlung.

Eigenes printf

Eine komfortable Lösung des Problems ist eine eigene printf-Implementierung, und die ist gar nicht so schwer realisierbar! Diese unterstützt dann Formatstrings der Form "%.1d °C", d.h. durch Angabe einer (bei Integerzahlen unüblichen) Präzisionsangabe spuckt die Dezimalzahlausgabe einen Dezimalpunkt an der richtigen Stelle aus. Die übergebene Integerzahl ist in Zehntelgrad zu übergeben.

Bei dieser Gelegenheit lässt man gleich noch das Komma als Alternative zu, um sich dem Punkt-Komma-Problem zu entledigen: Ein Formatstring der Form "%,1d °C" spuckt ein Dezimalkomma aus.

Und wenn schon Punkt oder Komma angegeben wurde, wozu dann noch die '1'? Eine Nachkommastelle will man ja wohl mindestens haben, und der Formatstring wird zu "%,d °C". Ungewöhnlich, aber kurz und lesbar.

Links mit Leerzeichen aufgefüllte (rechtsbündige) Ausgaben erreicht man in bekannter Manier mit der Feldbreiten-Angabe, etwa "%5,d °C", die für "-99,9 °C" bis "999,9 °C" Platz freihält.

Beispiele

Digitaler Temperatursensor

Anstatt eines ADC-Wertes werden oft auch Digitalwerte von Temperatursensoren verarbeitet. Der Weg ist hier identisch. Allerdings hat man keine Referenzspannung oder Referenztemperatur. Das ist aber kein Beinbruch. Z.B. der LM74 hat eine Auflösung von 1/16°C = 0,0625°C. Um das Messergebnis ohne Verlust von Auflösung auszugeben könnte man als erstes den Digitalwert auf 1/100 °C umrechnen. Das geschieht mit der Multiplikation mit 6,25. Doch Stop, das ist ja schon wieder ne Gleitkommazahl. Doch kein Problem, wir wissen ja wie wir das Problem lösen. Wir schieben das Komma um zwei Stellen nach rechts und multiplizieren mit 625 und wissen, dass das Ergbniss nun in 1/10000°C vorliegt. Über den physikalischen Sinn dieser Auflösung müssen wir nicht nachdenken, wichtig ist für uns nur, dass jetzt die Zahl einfach per itoa() umwandelbar ist. Allgemein kann man folgenden Ablauf zur Berechnung des Korrekturfaktors angeben

  • Den Korrekturfaktor K mit vollen Kommastellen berechnen, dabei ist die neue Auflösung sinnvollerweise eine Dezimalzahl ( 0,1; 0,001 etc.) und kleiner als die alte Auflösung.
[math]\displaystyle{ K = \frac{\mathrm{alte\ Aufl\ddot osung}}{\mathrm{neue\ Aufl\ddot osung}} }[/math]
  • Den berechneten Korrekturfaktor solange mit 10 multiplizieren bis alle Nachkommastellen verschwunden sind bzw. durch Rundung ein akzeptabler Fehler entsteht. Für jede Multiplikation des Faktors mit 10 muss die neue Auflösung durch 10 dividiert werden.
[math]\displaystyle{ \text{Temperatur} = \text{Sensorwert} \cdot K }[/math]

Beispiel:

Alte Auflösung: 1/32°C
Neue Auflösung: 1/100°C

[math]\displaystyle{ K = \frac{\frac{1}{32}}{\frac{1}{100}} = 3{,}125 }[/math]

Der Korrekturfaktor 3,125 kann zweimal mit 10 multipliziert werden und dann auf 312 gerundet werden, das entspricht einem Rundungsfehler von gerade mal 1/624 = 0,16% ! Die neue Auflösung beträgt 1/10000°C. Wenn der Sensor nur 7 Bit Werte liefert kann das Ergebnis in einer 16 Bit Variablen gespeichert werden, darüber hinaus ist eine 32 Bit Variable notwendig.

ADC allgemein

Wenn man das obige ADC-Beispiel allgemein beschreiben will, dann gilt folgender Ablauf:

  • Korrekturfaktor mit vollen Kommastellen berechnen
[math]\displaystyle{ \mathrm{alte\ Aufl\ddot osung} = \frac{\text{Referenzspannung}}{2^{\text{Bitbreite}_\text{ADC}}} }[/math]
[math]\displaystyle{ K = \frac{\mathrm{alte\ Aufl\ddot osung}}{\mathrm{neue\ Aufl\ddot osung}} }[/math]
  • Den berechneten Korrekturfaktor K solange mit 10 multiplizieren, bis alle Nachkommastellen verschwunden sind bzw. durch Rundung ein akzeptabler Fehler entsteht. Für jede Multiplikation des Faktors mit 10 muss die neue Auflösung durch 10 dividiert werden.
[math]\displaystyle{ \text{Eingangsspannung} = \text{ADC-Wert} \cdot K }[/math]

Beispiel:

Referenzspannung: 5 V
ADC-Bitbreite: 10

[math]\displaystyle{ \mathrm{alte\ Aufl\ddot osung} = \frac{5\,\mathrm V}{2^{10}} = \frac{5\,\mathrm V}{1024} = 0{,}0048828125\,\mathrm V }[/math]

neue Auflösung = 0,001 V = 1 mV

[math]\displaystyle{ K = \frac{0{,}0048828125\,\mathrm V}{0{,}001\,\mathrm V} = 4{,}8828125 }[/math]

Der Korrekturfaktor 4,8828125 wird zweimal mit 10 multipliziert und dann auf 488 gerundet, das entspricht einem Rundungsfehler von gerade mal 0,05 %! Die neue Auflösung ist 0,00001 V = 10 µV. Das Ergebnis muss in einer 32-Bit-Variablen gespeichert werden, denn der größte Messwert ergibt 1023 · 488 = 499.224. Der Vorteil dieses allgemeinen Ansatzes ist vor allem, daß nur eine Multiplikation benötigt wird, im Gegensatz zu unserem allerersten Beispiel, welches eine Multiplikation und eine Division benötigt. Das spart einiges an Rechenzeit und Programmspeicherplatz.

Lösung 2: Mehrfachabtastung (oversampling) mit Korrekturrechnung
Beispiel, um vom 10-bit-ADC-Wert 1024 auf 5000 zu kommen:
5 mal abtasten und aufsummieren, dann vom Ergebnis 1/32 subtrahieren und 1/128 addieren:
5 * 1024 = 5120
5120 / 32 = 160

160 /  4 =  40

5120 - 160 + 40 = 5000

Division durch Konstanten

Manchmal muss man in in einer Formel durch eine Konstante dividieren, z.B. a = b / 0.5432. Das kann man zunächst mathematisch korrekt umformen zu a = b * ( 1 / 0.5432) = b * 1.8409426. Die Divison ist identisch mit der Multiplikation des Kehrwertes. Auch hier kann man wieder auf das bewährte Prinzip zurückgreifen. Und zwar solange mit 10 multiplizieren, bis alle Nachkommastellen verschwunden sind oder der Fehler vernachlässigbar klein ist. In diesem Beispiel könnte man z.B. rechnen a = b * 18409 / 10000. Dabei kann man zu einem weiteren Trick greifen, um die echte Division durch 10000, welche einiges an Zeit und Programmspeicher kostet, zu sparen. Und zwar kann man anstatt einer Zehnerpotenz (10,100, 1000 etc.) auf eine Zweierpotenz ausweichen. Man multipliziert den Faktor also in unserem Beispiel mit 2^13=8192. Die Rechnung lautet nun a = b * 15081 / 8192. Die Division durch 8192 kann man aber deutlich schneller, nämlich als eine Bitverschiebung um 13 Stellen nach rechts, ausführen. Einige Compiler sind schlau genug das automatisch zu erkennen und umzusetzen (aber nur bei vorzeichenlosen Datentypen, siehe „Achtung“ unten), bei anderen muss man es explizit hinschreiben. Auch hier muss man wie immer dafür sorgen, daß es in den Zwischenergebnissen nicht zu Überläufen kommt.

int32_t a,b;

a = b / 0.5432;         // direkte Formel, Division durch Konstante
                        // mit Fließkommaarithmetik
a = b * 1,8409;         // Multiplikation mit 1/x
                        // mit Fließkommaarithmetik
a = b * 18409 / 10000;  // Umformung in Kehrwert und Festkommaarthimetik
                        // mit Zehnerpotenzen
a = b * 15081 / 8192;   // Festkommaarithmetik mit Zweierpotenzen.
a = (b * 15081) >> 13;  // Division explizit ausgeführt als Schiebeoperation
                        // für nicht so schlaue Compiler

Wenn es die Bitbreite der Zwischenergebnisse erlaubt ist es beim 8-Bit-AVR vorteilhaft, den Rechtsschiebewert als ganzes Vielfaches von 8 festzulegen. Dann werden die Lo-Bytes einfach weggelassen, und nichts wird geschoben. Ansonsten compiliert das Schieben zu einer Schleife.

Die gängigen 32-Bit-Prozessoren enthalten einen Barrel-Schifter und können in 1 Takt um beliebig viele Bit schieben.

Achtung: Im Gegensatz zur Division, die gegen 0 rundet, rundet das Rechtsschieben gegen -∞! Das erleichtert das Runden insofern, dass das letzte herausgeschobene Bit auf das Ergebnis zu addieren ist. Also etwa:

int32_t b;
int16_t a = (int16_t)(b>>16);
if (b & 1L<<15) ++a;

Ergebnis runden

Nach der Umrechung des ADC/Sensor-Wertes und dem Aufruf von my_uitoa() kann man sehr einfach eine Rundung durchführen. Dazu muss nur die erste Stelle, welche durch Rundung wegfallen soll, geprüft werden. Ist sie kleiner als der ASCII-Code von '5' (0x35) dann muss abgerundet werden, sprich alles bleibt wie es ist. Im anderen Fall muss aufgerundet werden, was bedeutet dass die letzte angezeigte Dezimalstelle um eins erhöht werden muss. Doch aufgepasst! Wenn z. B. in unserem String die Zahl 1995 steht und die letzte Stelle durch Rundung wegfallen soll, kommte es zum Übertrag. Die letzte 9 wird zur 0 + Übertrag. Die nächste linksstehende Stelle muss erhöht werden. Das ist "dummerweise" auch eine 9, also wieder ein Übertrag. Die letzte Ziffer ist 1, die wird nur auf zwei erhöht und der Übertrag endet.

Basierend auf unserer Funktion my_uitoa() soll hier eine einfache Rundungsfunktion gezeigt werden. Auch diese arbeitet mit einem 11 Byte String, welcher vorher durch my_uitoa erzeugt wurde.

#include <stdint.h>
/*

Funktion zur Rundung einer vorzeichenlosen 32 Bit Zahl im Stringformat

Parameter:

char* string:  Zeiger auf String, welcher mit my_uitoa() erzeugt wurde
uint8_t digit: Offset im String, zeigt auf die Stelle welche zur Rundung ausgewertet werden soll
               gültiger Wertebereich ist 1..9 !
               Der Offset von 1 zeigt auf die zweite Stelle von links
               Der Offset von 9 zeigt auf die letzte Stelle von links

*/

void my_round(char* string, uint8_t digit) {
  uint8_t i;

  if (string[digit]>='5') {         // Aufrunden?
    for(i=(digit-1); i>=0; i--) {
      string[i] += 1;               // Aufrunden
      if (string[i]<='9')
        break;                      // kein Übertrag, schleife verlassen
      else
        string[i]='0';              // Übertrag und Überlauf
    }
  }

  for(i=digit; i<10; i++) string[i] ='0';   // gerundete Stellen auf Null setzen

}

Die Funktion ist auch auf einen String anwendbar, welcher mit my_itoa() erzeugt wurde (also vorzeichenbehaftete Zahlen). Allerdings muss der Funktion ein Pointer auf das zweite Byte (char) übergeben werden, da im ersten das Vorzeichen gespeichert ist. Das geschieht am einfachsten so.

my_round(my_string+1, 5);

Etwas einfacher ist es hier übrigens, zur letzten Stelle die "weggerundet" werden soll, einfach fünf zu addieren. Die Rundung wird dann durch eine simple Division durch die entsprechende Zehnerpotenz ersetzt. Wenn die letzte wegfallende Stelle vorher größer oder gleich 5 war, wurde durch die Addition die vorletzte Stelle um eins erhöht, also aufgerundet. Wenn die letzte Stelle kleiner als 5 war, dann spielt die Veränderung keine Rolle und die vorletzte Stelle bleibt gleich. Durch die Division wird dann also abgerundet.

Rundungsregel

Die hier angegebenen Rundungen runden allesamt 1,50 auf 2 und 2,50 auf 3. Das ist „Kaufmännisches Runden“ und einfach zu implementieren. Wurde im geteilten Deutschland in West-Schulen gelehrt.

Hingegen „Bankers Rounding“ rundet glatte Hälften auf gerade Zahlen, also 1,50 auf 2 und 2,50 auf 2. Wurde im geteilten Deutschland in Ost-Schulen gelehrt und da einfach „Runden“ genannt. Ist schwieriger zu implementieren, deshalb nimmt man's im AVR-Bastelbereich eher nie, ist jedoch Vorschrift für IEEE-konforme Gleitkommazahlen-Arithmetik sowie immer wenn's um Geld geht. Der Hintergedanke ist, dass — statistisch gesehen — niemand durch Runden gewinnt oder verliert.

Ergebnis ausgeben

Nun haben wir unseren Messwert in eine Zahl mit der richtigen Einheit umgewandelt und gerundet. Jetzt folgt der letzte Schritt, die Ausgabe auf einen UART oder LCD. Dazu ist es meist wünschenswert führende Nullen nicht anzuzeigen. Also anstatt 0095,89 will man 95,89 ausgeben. Bei negativen Zahlen kommt noch das Vorzeichen hinzu.

Ausgabe auf LCD (HD44780 und ähnliche)

Zunächst wollen wir unsere Zahl auf einem LCD ausgeben. Dazu braucht man die entsprechende Funktion lcd_data(), wie sie z. B. im AVR-GCC-Tutorial#LCD_Ansteuerung zu finden ist.

#include <stdint.h>
/*

Funktion zur Anzeige einer 32 Bit Zahl im Stringformat
auf einem LCD mit HD44780 Controller

Parameter:

char* string  : Zeiger auf String, welcher mit my_itoa() erzeugt wurde
uint8_t start : Offset im String, ab der die Zahl ausgegeben werden soll,
                das ist notwenig wenn Zahlen mit begrenztem Zahlenbereich
                ausgegeben werden sollen
                Vorzeichenlose Zahlen      : 0..10
                Vorzeichenbehaftete zahlen : 1..11
uint8_t komma : Offset im String, zeigt auf die Stelle an welcher das virtuelle
                Komma steht (erste Nachkommastelle)
                komma muss immer grösser oder gleich start sein !

uint8_t frac  : Anzahl der Nachkommastellen

*/

void my_print_LCD(char* string, uint8_t start, uint8_t komma, uint8_t frac) {

  uint8_t i;            // Zähler
  uint8_t flag=0;       // Merker für führende Nullen

  // Vorzeichen ausgeben  
  if (string[0]=='-') lcd_data('-'); else lcd_data(' ');

  // Vorkommastellen ohne führende Nullen ausgeben
  for(i=start; i<komma; i++) {
    if (flag==1 || string[i]!='0') {
      lcd_data(string[i]);
      flag = 1;
    }
    else lcd_data(' ');         // Leerzeichen
  }

  lcd_data(',');                // Komma ausgeben

  // Nachkommastellen ausgeben
  for(; i<(komma+frac); i++) lcd_data(string[i]);

}

Die Funktion gibt zunächst das Vorzeichen aus, das ist einfach. Danach werden die Vorkommastellen ausgegeben. Aber es wird nur dann ein Zeichen ausgegeben, wenn vorher schon mal eins ausgegeben wurde (flag ==1) oder das aktuelle Zeichen keine '0' ist. Danach werden ganz normal die Nachkommastellen ausgegeben, hier ist eine Unterdrückung führender Nullen sogar mathematisch falsch!

Genutzt wird die Funktion beispielsweise so, um dreistellige Zahlen bis 999 mit zwei Nachkommastellen auszugeben. Die Zahl selber hat sechs Nachkommastellen.

char my_string[12]="-0034567891\0";
my_print_LCD(my_string, 2, 5, 2);

Auf dem LCD erscheint dann

- 34.56

Diese Funktion kann auch verwendet werden, um vorzeichenlose Zahlen auszugeben, welche mit my_uitoa() erzeugt wurden. Da an der ersten Stelle nie ein '-' steht, wird auch nie ein '-' ausgegeben.

Ausgabe auf UART

Die Ausgabe auf einen UART ist nahezu identisch. Auch hier braucht man eine Funktion putc(), welche ein einzelnes Zeichen auf den UART schreiben kann wie z. B. im AVR-GCC-Tutorial/Der UART beschrieben ist. Der Unterschied zur LCD-Ausgabe besteht darin, dass zwischen dem Vorzeichen und der Zahl keinerlei Leerzeichen eingefügt werden.

#include <stdint.h>
/*

Funktion zur Ausgabe einer 32 Bit Zahl im Stringformat
auf den UART

Parameter:

char* string  : Zeiger auf String, welcher mit my_itoa() erzeugt wurde
uint8_t start : Offset im String, ab der die Zahl ausgegeben werden soll,
                das ist notwenig wenn Zahlen mit begrenztem Zahlenbereich
                ausgegeben werden sollen
                Vorzeichenlose Zahlen      : 0..10
                Vorzeichenbehaftete zahlen : 1..11
uint8_t komma : Offset im String, zeigt auf die Stelle an welcher das virtuelle
                Komma steht (erste Nachkommastelle);
                komma muss immer grösser oder gleich start sein !

uint8_t frac  : Anzahl der Nachkommastellen

*/

void my_print_UART(char* string, uint8_t start, uint8_t komma, uint8_t frac) {

  uint8_t i;            // Zähler
  uint8_t flag=0;       // Merker für führende Nullen

  // Vorkommastellen ohne führende Nullen ausgeben
  for(i=start; i<komma; i++) {
    if (flag==1 || string[i]!='0') {
      if (flag==0 && string[0]=='-') putc('-');     // negatives Vorzeichen ausgeben
      putc(string[i]);
      flag = 1;
    }
  }

  putc(',');                // Komma ausgeben

  // Nachkommastellen ausgeben
  for(; i<(komma+frac); i++) putc(string[i]);
}

Diese Funktion kann auch verwendet werden, um vorzeichenlose Strings auszugeben, welche mit my_uitoa() erzeugt wurden. Da an der ersten Stelle nie ein '-' steht, wird auch nie ein '-' ausgegeben.

Siehe auch