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
floattemperatur;
2
int16_traw;// nimmt erst mal unsere zwei temperaturbytes auf
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.
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);
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.
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...
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.
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.
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
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
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.
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.
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.
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?
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.
> 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:
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.