Forum: Mikrocontroller und Digitale Elektronik Verstehe Umwandlung von 16-Bit signed integer nach float nicht


von hans_1 (Gast)


Lesenswert?

Hallo,

ich habe hier den DS18B20 Temperatursensor, vermutlich bekannt.

Er speichert den Temperaturwert in einer 16Bit Sign-Extended 
Zweier-Komplement-darstellung, verteilt auf zwei Bytes ab:
1
LowByte:  
2
Bitnummer 7    6    5    4    3     2     1     0
3
          2^3  2^2  2^1  2^0  2^-1  2^-2  2^-3  2^-4
4
5
Hibyte:
6
Bitnummer 15   14   13   12    11   10    9     8
7
          S     S   S     S    S    2^6   2^5   2^4
8
9
S ist das Vorzeichenbit: 0 positiv, 1 negativ
Jetzt habe ich folgenden Code (AVR-gcc) zur Umwandlung des
Tempwertes aus dem 16-Bit-Typ in einen float, der tut was er soll aber 
mir nicht klar ist warum:
1
float temperatur;
2
int16_t raw;       // nimmt erst mal unsere zwei temperaturbytes auf
3
raw = ( hibyte << 8 ) | lowbyte;   // soweit klar
4
temperatur =  ((float) raw)/16.0;  //Wieso funktioniert das?
Wenn ich mir anschaue wie float in C definiert ist:
http://en.wikipedia.org/wiki/Single-precision_floating-point_format
frage ich mich warum das oben funktioniert.

Die Wandlung von int16_t nach float müsste doch eigentlich den 
gebrochenen Teil in der 16-Bit Darstellung unter den Tisch fallen oder 
anders interpretieren, die Typkonvertierung geht doch von einem reinen 
Ganzzahltyp aus, die weiss doch gar nicht dass die unteren 5 Bits den 
gebrochenen Teil darstellen.

: Bearbeitet durch User
von (prx) A. K. (prx)


Lesenswert?

hans_1 schrieb:
> Die Wandlung von int16_t nach float müsste doch eigentlich den
> gebrochenen Teil in der 16-Bit Darstellung unter den Tisch fallen

Der Rohwert in "raw" hat aus Sicht der Sprache C keinen gebrochenen 
Teil, sondern ist eine Ganzzahl, die das 16fache der Temperatur in °C 
darstellt. Deshalb dividierst du ja hinterher. So ginge es wirklich in 
die Hose: temperatur = (float) (raw/16);

: Bearbeitet durch User
von Christian K. (the_kirsch)


Lesenswert?

Der Compiler erzeugt eine Routine, welche den Ganzzahlwert in eine 
Floatwert umwandelt, nach der Umwandlung gibt es eine Fließkomadivision.


Noch was:
Der AVR hat keine Hardware (Floatingpointunit) für solche Berechnungen, 
das wird alles in Software gemacht, und das ist langsam.

von c-hater (Gast)


Lesenswert?

hans_1 schrieb:

> Die Wandlung von int16_t nach float müsste doch eigentlich den
> gebrochenen Teil in der 16-Bit Darstellung unter den Tisch fallen

Nein. Aus Sicht des Compilers gibt es dort garkeinen fraktionalen Teil. 
Für den ist das eine schlichte Ganzzahl. Allerdings eine um das 16fache 
zu große, aber das weiß er nicht, kann es nicht wissen.
Menschen wissen es aber und korrigieren den Fehler, indem sie einfach 
hinterher, nach der Wandlung der um das 16fache zu großen Zahl in die 
Gleitkommadarstellung einfach das Ergebnis nochmal explizit durch 16.0 
teilen.

Noch schlauere Menschen ersparen allerdings der MCU solch 
rechenzeitintensiven Vollquatsch und machen die Wandlung und Korrektur 
in einem Schritt "von Hand" in einem klitzekleinen Bruchteil der Zeit, 
die der Compiler für diese beiden Aufgaben benötigt. Das geht sogar in C 
einigermaßen effizient, erfordert dafür allerdings die C-übliche extreme 
Syntaxblähung für trivialste Sachverhalte und ist dann kaum noch lesbar, 
noch viel weniger als die von dir schon nicht verstandene Variante.

Naja, nicht so schlimm, die schlaueste Variante ist ja in 99,9% der 
Fälle sowieso, auf Gleitkommazahlen ganz zu verzichten und einfach die 
fixed point-Darstellungen beizubehalten und erst "am Ende" in was an 
genau diesem Ende Benutzbares zu wandeln. Und das ist praktisch niemals 
ein Gleitkomma-Format...

von W.S. (Gast)


Lesenswert?

hans_1 schrieb:
> raw = ( hibyte << 8 ) | lowbyte;   // soweit klar
> temperatur =  ((float) raw)/16.0;  //Wieso funktioniert das?

Ach so.

Du willst nur wisen, wie man eine Zahl in C durch 16 teilt.
Anfangs dachte ich schon, du wolltest wissen, wie man tatsächlich eine 
Int zu Float konvertiert.

Dazu hättest du die zugehörige Float-Darstellung kennen müssen, die 
Integerzahl in die Mantisse laden, soweit verschieben, bis das 
tatsächliche MSB in die Hiddenbit-Position gelangt und gleichzeitig den 
Exponenten entsprechend korrigieren, dann das Vorzeichen auf's MSB 
schreiben und fertig. Sowas geht recht fix und braucht auch bloß ein 
paar simple Assemblerbefehle.

W.S.

von hans_1 (Gast)


Lesenswert?

Ich kapiers immer noch nicht.

Angenommen in raw steht:
0000 0000 0001 1000
Das wäre nach dem Datenformat des Sensors +1,5°C
für den C-Compiler aber 24 (dec)
Wenn er das umwandelt müsste doch 24.0 rauskommen oder?
Oder erwartet (float) bei einem int16_t sowas wie oben, das eben ein 
Teil des int16_t schon als ganzahl und ein teil als frac interpretiert 
wird, ok dann wäre es klar.

Wie geht das denn einfacher ohne float? Ich brauche nur den Wert zur 
Ausgabe, also den Ganzzahlteil und den gebrochenen Teil, ich rechne 
nicht damit. Den Ganzzahlteil bekomme ich durch geshifte und 
Vorzeichencheck selber hin. Beim Gebrochenen Teil dachte ich, ich klopfe 
jedes Bit ab und Addiere dann 0.5 0.125 ... aber dann muss der AVR 
wieder mit floats rechnen obwohl er das nicht kann, das ist irgendwie 
wie von hinten durch die Brust ins Auge. Deshalb habe ich das wie oben 
bisher übernommen, wusste aber schon damals dass das auch irgendwie 
Murks ist da die AVRs keine FPU haben und das in Software nachgebildetet 
wird, vom Code her ist es eben schön kurz.

von (prx) A. K. (prx)


Lesenswert?

hans_1 schrieb:
> Wenn er das umwandelt müsste doch 24.0 rauskommen oder?

Ja, direkt nach der Umwandlung in float. Da danach durch 16.0 dividiert 
wird steht in "temperatur" anschliessend 1,5.

raw = ( hibyte << 8 ) | lowbyte; // 24
temperatur = (float) raw;        // 24.0
temperatur = temperatur / 16.0;  // 1,5

: Bearbeitet durch User
von (prx) A. K. (prx)


Lesenswert?

hans_1 schrieb:
> Wie geht das denn einfacher ohne float?

Weder einfacher noch kürzer, was deinen Code angeht. Aber schneller und 
kürzer was das erzeugte Programm angeht. Macht ein paar KB im ROM aus.

Es gibt hier im Forum einige Leute, die Fliesskommaverarbeitung meiden 
wie der Teufel das Weihwasser. Nicht immer ist das nachvollziehbar. 
Festkommarechnung ist zwar schneller und kompakter, aber wenn Platz und 
Zeit kein Problem sind, dann muss man sich nicht unbedingt einen dabei 
abbrechen, zwanghaft Festkommarechnung zu verwenden.

Irgendwann wirst du es vielleicht auch mal brauchen. Oder die Controller 
mit Fliesskommaeinheit sind in einigen Jahren dann so verbreitet wie die 
AVRs heute und niemand versteht diese Fliesskommallergie noch.

Beschrieben ist das hier: 
http://www.mikrocontroller.net/articles/Festkommaarithmetik

: Bearbeitet durch User
von c-hater (Gast)


Lesenswert?

hans_1 schrieb:

> Ich kapiers immer noch nicht.
>
> Angenommen in raw steht:
> 0000 0000 0001 1000
> Das wäre nach dem Datenformat des Sensors +1,5°C

Jepp.

> für den C-Compiler aber 24 (dec)

Jepp.

> Wenn er das umwandelt müsste doch 24.0 rauskommen oder?

Jepp, kommen ja auch raus. Allerdings wird das ja dann noch durch 16.0 
geteilt, womit sich eben die korrekten 1.5 ergeben.

> Wie geht das denn einfacher ohne float? Ich brauche nur den Wert zur
> Ausgabe

Mit wie vielen Nachkommastellen? Das ist der Schlüssel zur Einfachheit.

von hans_1 (Gast)


Lesenswert?

c-hater schrieb:
> Mit wie vielen Nachkommastellen? Das ist der Schlüssel zur Einfachheit.
Zwei Dezimalstellen.

von hans_1 (Gast)


Lesenswert?

A. K. schrieb:
> Ja, direkt nach der Umwandlung in float. Da danach durch 16.0 dividiert
> wird steht in "temperatur" anschliessend 1,5.
> temperatur = temperatur / 16.0;  // 1,5

Also ist "/ 16.0" sowas wie ein Shift Right um 4 Stellen, was den 4-Bit 
für den gebrochenen Teil entspricht. Jetzt weiss ich auch warum dort 
kein ">> 4" steht, weil es mit float nicht geht.
So braucht man sich also gar nicht mit VZ, Mantisse, Basis und Exponent 
rumplagen.

von (prx) A. K. (prx)


Lesenswert?

hans_1 schrieb:
> Also ist "/ 16.0" sowas wie ein Shift Right um 4 Stellen, was den 4-Bit
> für den gebrochenen Teil entspricht.

Richtig.

Bei Integers zu beachten: Bei negativen Integers und Rest != 0 kommen 
Divison und Shift zu unterschiedlichen Ergebnissen.

: Bearbeitet durch User
von c-hater (Gast)


Lesenswert?

hans_1 schrieb:

> c-hater schrieb:
>> Mit wie vielen Nachkommastellen? Das ist der Schlüssel zur Einfachheit.
> Zwei Dezimalstellen.

Du hast nur vier binäre Nachkommastellen, damit können sich exakt 16 
verschiedene Varianten bezüglich der dezimalen Nachkommastellen ergeben, 
nämlich:

bin   dez   dez (gerundet)
0000 .0     .00
0001 .0625  .06
0010 .125   .12
0011 .1875  .19
0100 .25    .25
0101 .3125  .31
0110 .375   .38
usw.

Erkennst du jetzt vielleicht schon eigenständig (mindestens) einen 
Lösungsansatz?

von hans_1 (Gast)


Lesenswert?

A. K. schrieb:
> Beschrieben ist das hier:
> http://www.mikrocontroller.net/articles/Festkommaarithmetik

Sehr interessant, genau sowas hatte ich als nächstes vor: Das ganze 
selber in einen String umzuwandeln, ohne fertige Funktionen. Wie das bei 
Integer -> ASCII-Zeichen ohne sprint,itoc geht wusste ich schon, die 
Idee mit dieser Festkommaarithmetik ist simpel und genial.

von foobar (Gast)


Lesenswert?

> Den Ganzzahlteil bekomme ich durch geshifte und
> Vorzeichencheck selber hin. Beim Gebrochenen Teil dachte ich, ich klopfe
> jedes Bit ab und Addiere dann 0.5 0.125 ... aber dann muss der AVR
> wieder mit floats rechnen obwohl er das nicht kann, das ist irgendwie
> wie von hinten durch die Brust ins Auge.

Genau ;-)  Deshalb skalierst du die Rechnerei.  Der kleinste Wert ist 
1/16 = 0.0625.  Man braucht also 4 Nachkommastellen, also alles mit 
10000 multiplizieren damit es Ganzzahlig wird - statt mit 1/16tel mit 
10000/16tel arbeiten:
1
    x = 10000/16 * (positiv_raw & 15);

Danach hast du in x 4 Nachkommastellen (0 = .0000, 625 = .0625, ..., 
9375 = .9375).  Das Runden auf zwei Stellen überlasse ich dir ;-)

Alternativ, wenn du direkt die einzelnen Ziffern brauchst, kannst du 
auch "Tabellen" benutzen:
1
    const char durch16_10tel[]  = "00112..........9";
2
    const char durch16_100tel[] = "06395..........4";
3
    char a,b;
4
    
5
    a = durch16_10tel[positiv_raw & 15];
6
    b = durch16_100tel[positiv_raw & 15];

Nicht sehr flexibel, aber schnell.

von hans_1 (Gast)


Lesenswert?

c-hater schrieb:
> Erkennst du jetzt vielleicht schon eigenständig (mindestens) einen
> Lösungsansatz?
Ne Tabelle geht immer. Aber wenn man mehr Stellen will wird schnell der 
Platz knapp.

von hans_1 (Gast)


Lesenswert?

.. und festkommaarithmetik, das wurde ja schon erwähnt. Oder gibts noch 
einen Weg?

von Joe F. (easylife)


Lesenswert?

1
#include <stdio.h>
2
#include <math.h>
3
4
int main(int argc, char **argv)
5
{
6
  unsigned data;
7
  
8
  unsigned ganzzahl;
9
  unsigned nachkomma_x10;
10
  unsigned nachkomma_x100;
11
  unsigned nachkomma_x1000;
12
  unsigned nachkomma_x10000;
13
  
14
  for (data=0; data<=32; data++)
15
  {
16
    ganzzahl         = data >> 4;
17
    nachkomma_x10    = ((data & 0xf) * 10 + 8) / 16;
18
    nachkomma_x100   = ((data & 0xf) * 100 +8) / 16;
19
    nachkomma_x1000  = ((data & 0xf) * 1000 +8) / 16;
20
    nachkomma_x10000 = ((data & 0xf) * 10000 +8) / 16;
21
    
22
    printf("data: %2d 0x%04x float: %2.4f ", data, data, (float)(data)/16.0);
23
    printf("%2d.%01d ", ganzzahl, nachkomma_x10);
24
    printf("%2d.%02d ", ganzzahl, nachkomma_x100);
25
    printf("%2d.%03d ", ganzzahl, nachkomma_x1000);
26
    printf("%2d.%04d ", ganzzahl, nachkomma_x10000);
27
    printf("\n");
28
  }
29
  
30
  return 0;
31
}

Ergebnis:
1
data:  0 0x0000 float: 0.0000  0.0  0.00  0.000  0.0000 
2
data:  1 0x0001 float: 0.0625  0.1  0.06  0.063  0.0625 
3
data:  2 0x0002 float: 0.1250  0.1  0.13  0.125  0.1250 
4
data:  3 0x0003 float: 0.1875  0.2  0.19  0.188  0.1875 
5
data:  4 0x0004 float: 0.2500  0.3  0.25  0.250  0.2500 
6
data:  5 0x0005 float: 0.3125  0.3  0.31  0.313  0.3125 
7
data:  6 0x0006 float: 0.3750  0.4  0.38  0.375  0.3750 
8
data:  7 0x0007 float: 0.4375  0.4  0.44  0.438  0.4375 
9
data:  8 0x0008 float: 0.5000  0.5  0.50  0.500  0.5000 
10
data:  9 0x0009 float: 0.5625  0.6  0.56  0.563  0.5625 
11
data: 10 0x000a float: 0.6250  0.6  0.63  0.625  0.6250 
12
data: 11 0x000b float: 0.6875  0.7  0.69  0.688  0.6875 
13
data: 12 0x000c float: 0.7500  0.8  0.75  0.750  0.7500 
14
data: 13 0x000d float: 0.8125  0.8  0.81  0.813  0.8125 
15
data: 14 0x000e float: 0.8750  0.9  0.88  0.875  0.8750 
16
data: 15 0x000f float: 0.9375  0.9  0.94  0.938  0.9375 
17
data: 16 0x0010 float: 1.0000  1.0  1.00  1.000  1.0000 
18
data: 17 0x0011 float: 1.0625  1.1  1.06  1.063  1.0625 
19
data: 18 0x0012 float: 1.1250  1.1  1.13  1.125  1.1250 
20
data: 19 0x0013 float: 1.1875  1.2  1.19  1.188  1.1875 
21
data: 20 0x0014 float: 1.2500  1.3  1.25  1.250  1.2500 
22
data: 21 0x0015 float: 1.3125  1.3  1.31  1.313  1.3125 
23
data: 22 0x0016 float: 1.3750  1.4  1.38  1.375  1.3750 
24
data: 23 0x0017 float: 1.4375  1.4  1.44  1.438  1.4375 
25
data: 24 0x0018 float: 1.5000  1.5  1.50  1.500  1.5000 
26
data: 25 0x0019 float: 1.5625  1.6  1.56  1.563  1.5625 
27
data: 26 0x001a float: 1.6250  1.6  1.63  1.625  1.6250 
28
data: 27 0x001b float: 1.6875  1.7  1.69  1.688  1.6875 
29
data: 28 0x001c float: 1.7500  1.8  1.75  1.750  1.7500 
30
data: 29 0x001d float: 1.8125  1.8  1.81  1.813  1.8125 
31
data: 30 0x001e float: 1.8750  1.9  1.88  1.875  1.8750 
32
data: 31 0x001f float: 1.9375  1.9  1.94  1.938  1.9375 
33
data: 32 0x0020 float: 2.0000  2.0  2.00  2.000  2.0000

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.