04 Wir brauchen eine Fließkomma-Variable

Um die Nennspannung eines Akkus sinnvoll auszulesen, brauchen wir eine höhere Auflösung, als sie uns die Variable Integer liefern kann. Wir brauchen Dezimalzahlen (Gleitkommazahlen). Der Grund: Bei voller Kapazität hat die NiMH-Batterie eine Spannung von 1,2Volt und sinkt auf die Entladeschlussspannung von 1,0Volt. Das ist eine Differenz von 0,2Volt. Also benötigen wir Zwischenwerte wie 1,2 / 1,1 / 1,0.

Zur genauen Auswertung (Kommawert) der Spannung brauchen wir eine float-Variable. Float belegt im Arduino ganze 4Byte an Speicherplatz. Ihr Wertebereich reicht von –3.4028235E+38 bis +3.4028235E+38. Bei diesen beiden Werten handelt es sich um eine Exponentialschreibweise. Ausgeschrieben ergeben sich beeindruckende Zahlenlängen:

–3.4028235E+38 = –340.282.350.000.000.000.000.000.000.000.000.000.000
+ 3.4028235E+38 = +340.282.350.000.000.000.000.000.000.000.000.000.000

Auch wenn das gigantische Zahlen sind, ist die Genauigkeit nicht unbedingt hoch. Wir dürfen nicht vergessen, dass im Wertebereich Kommazahlen enthalten sind! Hier ein Beispiel, was gemeint ist:


1,000008
1,000009
1,000010
1,000011
1,000012

Aus diesem Grund ergeben sich so viele Werte innerhalb von float. Beachte, dass im Englischen anstatt eines Kommas ein Punkt verwendet wird. Auf folgende Weise ist es möglich eine float-Variable in einen Integer zu verwandeln, aber die Konvertierung in eine Ganzzahl (int) führt zu Kürzungen:

void setup()
{
Serial.begin(9600);
float x = 2.9; //eine float-Variable 2.9

int y = x;  //das Ergebnis ist jetzt der Integer 2
                //0,9 abgeschnitten und fällt weg
Serial.println(y);
}

Float-Variablen belegen nicht nur viel Speicherplatz, sie sind bei mathematischen Berechnungen auch um einiges langsamer. Falls es um zeitkritische Anwendungen geht, wo es um schnelle Berechnungen geht, sollten floats vermieden werden.

Vorsicht vor der Fließkomma-Variablen!

Floats haben eine signifikante Genauigkeit von nur 6-7 Dezimalstellen. Eigentlich sollte die Fließkomma-Variable double, wie man es aus anderen Mikrocontroller-Systemen kennt, die doppelte Genauigkeit von float liefern. Aber in der Arduinoplattform sind float und double absolut identisch. Eine Ausnahme bildet das Arduino Due, die tatsächlich eine zweifache Genauigkeit hat (8Byte/64Bit).

Mit folgendem Sketch kannst du dir die Nachkommastellen der float-Variable anzeigen lassen.

void setup()
{Serial.begin(9600);}

voidloop()
{
float number =2.1234567;
Serial.println(number,7);
}

Standardmäßig werden bei float zwei Kommastellen angezeigt. Mit der Zahl im zweiten Argument können im Serial.Println() jeweils die gewünschten Nachkommastellen angezeigt werden. Wenn hier eine 3 steht, werden im seriellen Monitor drei Stellen angezeigt usw. Man kann sich auch 100 Stellen anzeigen lassen, aber nur 6 bis 7 Stellen nach dem Komma ergeben sinnvolle Ergebnisse.

Serial.println(number,7);

Auch wenn 6-7 Stellen nach dem Komma sehr präzise erscheinen, ist dies in Wirklichkeit nicht der Fall. Werte aus float-Berechnungen können Rundungsfehler aufweisen, die zu gewichtigen Fehlern führen, insbesondere wenn Ergebnisse auf mehreren Summenberechnungen basieren. Wir müssen float-Werte eher als eine Annäherung an einen bestimmten Wert betrachten. Auf keinen Fall sollten float-Werte auf exakte Übereinstimmung getestet werden, sondern ob sie sich innerhalb bestimmter Toleranzbereiche aufhalten. Folgender Sketch wird dir zeigen, was eigentlich gemeint ist.

Der Sketch

Der Sketch subtrahiert eine Variable in der loop()-Schleife. Sobald die Variable den Wert 0 erreicht, soll der serielle Monitor eine Meldung ausgeben. Überraschenderweise tut er das aber nicht.

float floatValue =1.1;

voidsetup()
{Serial.begin(9600);}

voidloop()
{
floatValue = floatValue – 0,1;
if(floatValue == 0)
	{
	Serial.println("Der Wert ist genau 0");
	}
else
Serial.println(floatValue);
delay(500);}

Die float-Variable wird innerhalb der loop()-Schleife bei jedem Durchgang um 0,1 kleiner und weiter unten am seriellen Monitor ausgegeben.

floatValue -=0.1;
...
Serial.println(floatValue);

Sobald der Wert der Variable floatValue den Wert 0 erreicht, sollte die Kondition der if()-Abfrage sich erfüllen (floatValue ==0) und die Nachricht “Der Wert ist genau 0“ als Beweis an den seriellen Monitor geschickt werden.

if(floatValue == 0)
  {
    Serial.println("Der Wert ist genau 0"); 
  }

Der Inhalt der if()-Abfrage wird nicht aufgerufen, obwohl wir nachweislich eine 0 (–0.00) ausgedruckt bekommen.

Wie schon eingangs erwähnt, können sich bei mathematischen Berechnungen Rundungsfehler ergeben. Zeigen wir alle sieben signifikanten Nullstellen darstellen lassen, sehen wir im seriellen Monitor, eine nicht exakte Nulldarstellung: –0,0000001

Ein Übereinstimmungstest (floatValue == 0) ist somit sinnlos.

Wir werden das Dilemma mit einer externen Funktion lösen, in dem wir bestimmen, wann für uns (floatValue == 0) ist.

float floatValue = 1.1;

void setup(){Serial.begin(9600);}

void loop() 
{
  floatValue -= 0.1; 
  
  if(floatValue == 0)
  {
    Serial.println("Der Wert ist genau 0"); 
  }
  
  else if(towardsZero(floatValue))
  {
    Serial.print("Der Wert ");
    Serial.print(floatValue,7); // 7 Dezimalstellen ausgeben 
    Serial.println(" geht gegen 0");
  } 
  
  else
  Serial.println(floatValue);
  delay(500); 
}

bool towardsZero(float valueToCheck)
{
  const float difference = .00001; // Max. Differenz, die noch "gleich" ist 

  return valueToCheck <= difference;
}

Die Lösung ist eine else-if-Abfrage, die alle floatValue Ergebnisse an eine externe Funktion namens towardsZero schickt und dort überprüft, wie nahe das Ergebnis an der Null ist. Entspricht der Rückgabewert der externen Funktion unserer Einstellung, dann wird die else-if-Abfrage wahr und der Inhalt wird ausgeführt. In unserem Fall bestätigt der serielle Monitor, dass der Wert floatValue gegen null geht, also sehr nahe an unserer Nulldefinition ist.

else if(towardsZero(floatValue))
  {
    Serial.print("Der Wert ");
    Serial.print(floatValue,7); // 7 Dezimalstellen ausgeben 
    Serial.println(" geht gegen 0");
  }

Die externe Funktion mit Rückgabewert ist ein bool, d.h. sie sendet entweder true oder false zurück. Der Übergabewert wird hier als float valueCheck deklariert. Die Variable difference ist unsere Tolleranzeinstellung. Hier definieren wir, ab wann wir eine Null als eine Null sehen: die maximale Differenz, die noch eine Null darstellt. Hier können wir die Tolleranz einstellen, wie eng das Ergebnis sein soll: const float difference = 0,00001;

bool towardsZero(float valueToCheck)
{
  const float difference = 0.00001; // Max. Differenz, die noch "gleich" ist 
...
}

Der Wert valueToCheck und unsere Toleranzeinstellung difference werden miteinander verglichen. Sobald valueToCheck kleiner gleich 0,00001 wird, haben wir unsere Nulldefinition und return sendet ein true zurück. Die Nachricht “Der Wert –0.00 geht gegen Null“ wird am seriellen Monitor ausgegeben.

returnvalueToCheck <=difference;

Wir sind aber noch nicht fertig. Der Blick auf den seriellen Monitor zeigt, dass ALLE Werte unterhalb von 0 als Null definiert wird. Also auch z.B. –1.0000001. Das ist natürlich richtig, dass eine negative Zahl kleiner Null ist. Wir wollen aber nur die Null.

Hier zu bedienen wir uns eines kleinen Tricks. Wir berechnen mit Hilfe des Befehls fabs() den Absolutwert von valueToCheck. Der Absolutwert bewirkt, dass das Vorzeichen eines Wertes wegfällt. Alle negativen Zahlen werden zu positiven Werten. Dadurch nehmen wir alle Werte unterhalb von Null heraus. Unserer Ergebnis ist nun punktuell auf die Null gerichtet.

returnfabs(valueToCheck)<=difference;

Hier der vollständige Sketch:

float floatValue = 1.1;

void setup(){Serial.begin(9600);}

void loop() 
{
  floatValue -= 0.1; 
  
  if(floatValue == 0)
  {
    Serial.println("Der Wert ist genau 0"); 
  }
  
  else if(towardsZero(floatValue))
  {
    Serial.print("Der Wert ");
    Serial.print(floatValue,7); // 7 Dezimalstellen ausgeben 
    Serial.println(" geht gegen 0");
  } 
  
  else
  Serial.println(floatValue);
  delay(500); 
}

bool towardsZero(float valueToCheck)
{
  const float difference = .00001; // Max. Differenz, die noch "gleich" ist 

  return fabs(valueToCheck) <= difference;
}

Am seriellen Ausdruck sehen wir, dass die if-Abfrage die Null als Wert erkennt.