7.0 🚧 Numerik bei µC für C(++) 🤯
Synopsis: 📖 Numerische Mathematik 📖 Datentypen in C
Immer wieder Ärger mit der Rechnerei. Bei Arduino gibt es auch noch 8Bit µC mit wenig RAM-Speicher bei denen geschickter Umgang mit geeigneten Datentypen eine Rolle spielen kann. Mit modernen 32Bit Controllern wie STM32, ESP32 usw. entspannt sich die Lage aber grundlegendes Verständnis sollte vorhanden sein.
Bei praktisch allen neueren ITG Abi-Prüfungen wird im Bereich Hardware dieses Thema tangiert: AD-Wandlerwerte werden in Fließkommazahlen umgewandelt, Sensordaten werden byte-weise eingelesen und in Fließkommawerte umgewandelt.
Betrachte: 1.1d C Datentypen für Arduino
Problemfelder
- Ganzahldivision: Nachkommastellen werden abgeschnitten
- Vorzeichenbehaftete ganzzahlige Datentypen und Shift nach Rechts: MSB wird mitgenommen, damit das Vorzeichen erhalten bleibt
- Konvertierung von Datentypen bei Zuweisung…
- Geschickte Anordnung von Operationen für maximale Genauigkeit.
🧠 Byte-Werte zusammen setzen und weiter verarbeiten
Typischer Fall, ein Temperaturwert wird von einem I2C Sensor gelesen (Wire.read() gibt int zurück), dabei werden zwei Bytes übertragen und müssen danach zusammengesetzt werden. Hier mein Versuchscode zur Analyse und meine Interpretation was genau passiert (gerne Rückmeldung geben wenn ich falsch liege):
void setup() {
Serial.begin(115200);
Serial.println();
char c1 = 0b10101011;
Serial.printf("\tchar c1 = 0b10101011 \t-> %#x\n",c1);
signed char sc1 = 0b10101011;
Serial.printf("signed \tchar sc1 = 0b10101011 \t-> %#x\n",sc1);
char c2 = sc1;
Serial.printf("\tchar c2 = sc1 \t-> %#x\n",c2);
signed char sc2 = c1;
Serial.printf("signed \tchar sc2 = c1 \t-> %#x\n",sc2);
char c3 = sc1>>1;
Serial.printf("\tchar c3 = sc1>>1 \t-> %#x\n",c3);
signed char sc3 = c1>>1;
Serial.printf("signed \tchar sc3 = c1>>1 \t-> %#x\n",sc3);
int i1 = c1;
Serial.printf("\tint i1 = c1 \t-> %#x\n",i1);
int i2 = sc1;
Serial.printf("\tint i2 = sc1 \t-> %#x\n",i2);
int i3 = sc1>>1;
Serial.printf("\tint i3 = sc1>>1 \t-> %#x\n",i3);
int i4 = (unsigned char)sc1>>1;
Serial.printf("\tint i4 = (unsigned char)sc1>>1 \t-> %#x\n",i4);
int i5 = 0b110101011;
Serial.printf("\tint i5 = 0b110101011 \t-> %#x\n",i5);
char c4 = i5;
Serial.printf("\tchar c4 = i5 \t-> %#x\n",c4);
signed char sc4 = i5;
Serial.printf("signed \tchar sc4 = i5 \t-> %#x\n",sc4);
}
void loop() {}
Mit Serial.printf(..) wird über den Seriellen Monitor ausgegeben, die Ganzzahl-Parameter werden offensichtlich in int (bei STM32 int32_t) umgewandelt, um die Bitmuster zu sehe habe ich Hexadezimalformat gewählt. Vor der Ausgabe erfolgt implizit eine Umwandung von Datentyp der Variablen -> int.
Beim STM32 werden die Variablen <=32 Bit als 32Bit passend der Architektur gespeichert (alignment).
Hinweis: Es gibt arithmetischen Shift Right und logischen Shift Right. Beim arithmetischen Shift Right wird das Vorzeichen bei behalten das MSB weiter kopiert: Damit -2>>1 auch -1 wird. Beim logischen Shift Right wird kein Vorzeichen beachtet. Vorzeichenbehaftete Datentypen werden arithmetisch und vorzeichenlose logisch geschoben.
| Ausgabe | Erklärung, meine Interpretation |
|---|---|
| char c1 = 0b10101011 -> 0xab | char ist Vorzeichenlos und wird einfach als untere 8 Bit eingespeichert |
| signed char sc1 = 0b10101011 -> 0xffffffab | signed char hat Vorzeichen und beim Einspeichern wird das MSB des Bytes beachtet, daher wird der Wert als 32Bit negative Zahl gespeichert. |
| char c2 = sc1 -> 0xab | Die unteren 8Bit werden einfach kopiert. |
| signed char sc2 = c1 -> 0xffffffab | Die unteren 8Bit werden kopiert aber das Vorzeichen Bit 7 wird beachtet und bei den restlichen Bits angepasst. |
| char c3 = sc1>>1 -> 0xd5 | Vorzeichenbehaftete negative Zahl sc1 wird nach rechts verschoben, dabei das Vorzeichen mit genommen, das Ergebnis in ein char gepresst. |
| signed char sc3 = c1>>1 -> 0x55 | Vorzeichenlose Zahl c1 wird nach rechts verschoben dabei wird kein Vorzeichen mit genommen. |
| int i1 = c1 -> 0xab | Vorzeichenloser Wert aus c1 wird in i1 kopiert. |
| int i2 = sc1 -> 0xffffffab | Vorzeichenbehafteter Wert aus sc1 wird in i2 gespeichert, dabei wir das MSB von sc1 beachtet. |
| int i3 = sc1>>1 -> 0xffffffd5 | sc1 wird nach rechts geschoben, dabei wird das negative Vorzeichen mit genommen. |
| int i4 = (unsigned char)sc1>>1 -> 0x55 | sc1 wird als vorzeichenloser 8Bit Wert interpretiert und dann um 1 Bit nach rechts verschoben. |
| int i5 = 0b110101011 -> 0x1ab | Bitwert wird in Variable kopiert. |
| char c4 = i5 -> 0xab | Die unteren 8 Bits werden kopiert der obere Rest wird abgeschnitten. |
| signed char sc4 = i5 -> 0xffffffab | Die unteren 8 Bits werden kopiert das MSB wird dabei als Vorzeichen berücksichtigt. |
🔨 Nun aus dem Experiment eine leicht merkbare Erkenntnis formulieren..
Beispiele aus der Praxis und Abi-Prüfungen
LM75A I2C Temperatursensor
Beim 🔗LM75A wird die Temperatur mit zwei Bytes übertragen. Zuerst das MSB mit dem Vorzeichenbehafteten Wert in °C, dann das LSB mit den Nachkommawerten.
| MSByte | LSByte | Temperatur | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | |
| LM75 | D8 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | x | x | x | x | x | x | x | D * 0,5°C |
| LM75A | D10 | D9 | D8 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | x | x | x | x | x | D * 0,125°C |
Diese beiden Bytes müssen zusammengesetzt und mit der Auflösung multipliziert werden. Auf Seite 8 des Datenblatts gibt es netterweise eine Tabelle mit Beispielen.
Test mit STM32 und „Newlib Nano + Float Printf“
signed char msb = 0b11111111; // zum Testen ob -0.125 laut Doku rauskommt
char lsb = 0b11100000;
float temp = ((msb<<3) | (lsb>>5))*0.125; // LM75A mit 0.125 Grad Auflösung
Serial.printf("temp = %f °C\n",temp);
Damit Printf float ausgeben kann Newlib Nano + Float Printf auswählen. Ergebnis stimmt dann.
Test mit Arduino Uno (8Bit ATmega328)
void setup() {
char mybuffer[80]; // notwendig für snprintf
char fstr[16]; // Puffer für dtostrf-Funktion
Serial.begin(115200);
Serial.println();
signed char msb = 0b11111111; // zum Testen ob -0.125 laut Doku rauskommt
unsigned char lsb = 0b11100000;
int dwert = msb;
dwert = (msb << 3) | (lsb>>5);
//float temp = dwert * 0.125;
float temp = ((msb<<3) | (lsb>>5))*0.125; // LM75A mit 0.125 Grad Auflösung
dtostrf(temp,0,3,fstr); // 3 Nachkommastellen
snprintf(mybuffer,sizeof(mybuffer),"dwert = %#x temp = %s °C\n",dwert,fstr);
Serial.print(mybuffer);
msb = 0b01111110; // zum Testen ob +126.875 laut Doku rauskommt
lsb = 0b11100000;
dwert = msb;
dwert = (msb << 3) | (lsb>>5);
//temp = dwert * 0.125;
temp = ((msb<<3) | (lsb>>5))*0.125; // LM75A mit 0.125 Grad Auflösung
dtostrf(temp,0,3,fstr); // 3 Nachkommastellen
snprintf(mybuffer,sizeof(mybuffer),"dwert = %#x temp = %s °C\n",dwert,fstr);
Serial.print(mybuffer);
msb = 0b11001001; // zum Testen ob -54,875 laut Doku rauskommt
lsb = 0b00100000;
dwert = msb;
dwert = (msb << 3) | (lsb>>5);
//temp = dwert * 0.125;
temp = ((msb<<3) | (lsb>>5))*0.125; // LM75A mit 0.125 Grad Auflösung
dtostrf(temp,0,3,fstr); // 3 Nachkommastellen
snprintf(mybuffer,sizeof(mybuffer),"dwert = %#x temp = %s °C\n",dwert,fstr);
Serial.print(mybuffer);
}
void loop() {}
Hier sind zwei Puffer nötig, einer für snprintf(..) und einer für die Umwandlung von float in einen String mit Funktion dtostrf(..), da snprintf(..) nicht einfach %f kann.
Achtung: char ist bei Arduino Uno signed! Es muss unsigned für lsb-Verarbeitung vorangestellt werden.
Überrascht hat mich, dass ((msb<<3) | (lsb>>5))*0.125 funktioniert, obgleich die Operanden 8-Bit Datentypen sind und der µC 8 Bit hat.
SHT30 HP25 I2C Temperatur- und Feuchtigkeitssensor
Beim 🔗SHT30 wird die Temperatur als aus 2 Byte zusammengesetzte 16-Bit Zahl St übertragen. Die Temperatur in °C wird mit dieser Formel daraus berechnet:
temp = -45 + 175* St/(216 -1). St ist offensichtlich nicht vorzeichenbehaftet!
uint8_t tempMSB = Wire.read(); // lies aus Puffer und konvertiere in uint8_t
uint8_t tempLSB = Wire.read();
int temp = (tempMSB << 8) | tempLSB; // setze die Werte in 16 Bit zusammen
float tempC = -45 + 175 * temp / 65535.0; // Beachte die Reihenfolge der Ausführung
Bei der tempC-Berechnung wird 175 mit temp als Ganzzahl multipliziert und dann in eine Fließkommazahl konvertiert um durch 65535.0 geteilt zu werden. -45 wird in eine Fließkommazahl konvertiert und addiert.
