Den Sensor BMP280 als Thermometer und Barometer nutzen und über den I2C-Bus auslesen

Ich habe meinen Sensoren-Zoo ein wenig ausgebaut. So ganz genau habe ich wieder nicht gewusst, was ich bekommen werde, wenn ich beim freundlichen Chinesen das BMP280 Druck Sensor Replace BMP180 High Precision Atmosphärisch BAF - Modul bestelle, aber bei dem Preis (1 €) kann man ruhig mal probieren. Wer keine Lust hat, drei bis sechs Wochen zu warten, der kann sich so ein Teil auch und hat es mit ein bisschen Glück schon am nächsten Tag.

Bevor ich mir erst das Datenblatt durchlese, dann 3 Wochen warte und dann alles wieder vergessen habe, ist die Reihenfolge gewesen: bestellen, warten und dann mit beschäftigen.


Nun ist das gute Teil also da. Hier die technischen Daten: BMP 280 von Bosch Maße: 2.0 x 2.5 mm min. VDD: 1.71 V min. VDDIO: 1.20 V max. VDD/VDDIO: 4.25 (also nur für 3.3V, nicht für 5V geeignet!) Leistungsaufnahme: 2.7 µA Luftdruck Bereich: 300 bis 1100 hPa (-500 bis +9000 m), abs. max. 20.000 hPa Luftdruck Auflösung: 0.16 Pa Luftdruck Genauigkeit: ± 0.12 hPa (1 m) (bei 950 bis 1050 hPa bei 25 °C), min. ± 1 hPa Temperatur Auflösung: 0.01 °C Temperatur Bereich: -40 bis +85 °C Interface: I2C und SPI Messrate: bis zu 157 Hz Eine Temperaturauflösung von nur einem Hunderstel Grad! Das nenne ich mal genau. Dem Chip kann man im mehrfach Oversampling betreiben, dann sind Genauigkeiten von ±0.0003 °C möglich. Das ist in Sachen Genauigkeit Spitze, und das zu dem Preis. Da schmeiss ich den NTC-Thermistor doch gleich weg und auch der LM35 mit einem Viertel Grad Genauigkeit kann da einpacken.


Der BMP280 hat aber auch noch ein Barometer an Bord. Damit kann man den Luftdruck messen und auch den mit einer immensen Genauigkeit. Den könnte man z. B. hervorragend als Höhenmesser benutzen. Aber dieses Exemplar soll indoor bleiben und mein Always-On-Display mit Daten versorgen.

Angesprochen wird das edle Stück über den I2C-Bus oder alternativ auch über den SPI-Bus. Da ich mich mit dem I2C-Bus schon ein wenig auskenne und die I2C-Tools auf der Kommandozeile schätze, habe ich ihn daran angeschlossen. Einfach parallel zum schon am Bus hängenden PCF8591-AD-Wandler eingesteckt, funktioniert ohne Probleme.







Ich habe den Header so angelötet, dass ich die Pin-Bezeichnungen oben habe und noch lesen kann und zudem das Modul aufs Breadboard stecken kann oder mir so ein paar Kabel für den Headeranschluss spare.

Dabei hängt der Sensor kopfüber. Das dürfte dem aber nichts ausmachen und für die Messung der Temperatur wird das irrelevant sein. Evtl. hat es sogar ein Gutes, wenn der Sensor jetzt nicht hart auf dem Breadboard aufliegt, sondern in der Luft hängt, denn Erschütterungen können die Messung des Luftdrucks verfälschen.


Als erstes einmal müssen wir das neue Modul auf dem I2C-Bus suchen. Dazu benutzen wir wieder das Tools i2cdetect : pi@raspberrypi:~ $ i2cdetect -y 1 0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 40: -- -- -- -- -- -- -- -- 48 -- -- -- -- -- -- -- 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 70: -- -- -- -- -- -- 76 -- 0x48 war das PCF8591-Modul, dann gehört die 0x76 dem BMP280. Wollen wir doch gleich mal schauen, was es zu sagen hat... pi@raspberrypi:~ $ i2cdump -y 1 0x76 No size specified (using byte-data access) 0 1 2 3 4 5 6 7 8 9 a b c d e f 0123456789abcdef 00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 40: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 80: 9f 6c 90 38 27 34 6d 00 76 69 92 66 32 00 10 92 ?l?8'4m.vi?f2.?? 90: 81 d7 d0 0b 17 1a fe ff f9 ff 8c 3c f8 c6 70 17 ???????.?.?<??p? a0: 00 44 44 00 00 00 00 00 00 00 00 00 33 00 00 c0 .DD.........3..? b0: 00 54 00 00 00 00 60 02 00 01 ff 44 13 60 03 00 .T....`?.?.D?`?. c0: 00 00 00 ff 00 00 00 00 00 00 00 00 00 00 00 00 ................ d0: 58 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 X............... e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ f0: 00 00 00 00 00 00 00 80 00 00 80 00 00 00 00 00 .......?..?..... Komisch. Keine Daten, die sich bei verändernder Temperatur ändern würde. Da hilft dann doch nur ein Blick ins umfangreiche Datenblatt.

Aha! In der letzten Zeile (f0:...) sollten die Temperatur und der Luftdruck stehen. Da steht aber nur jeweils 80 00 00 drin, weil wird dem Chip noch nicht mitgeteilt haben, wie er messen soll.

Dazu sind folgende Speicherstellen zuständig: pi@raspberrypi:~ $ i2cdump -y 1 0x76 No size specified (using byte-data access) 0 1 2 3 4 5 6 7 8 9 a b c d e f 0123456789abcdef 00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 40: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 80: 9f 6c 90 38 27 34 6d 00 76 69 92 66 32 00 10 92 ?l?8'4m.vi?f2.?? 90: 81 d7 d0 0b 17 1a fe ff f9 ff 8c 3c f8 c6 70 17 ???????.?.?<??p? a0: 00 44 44 00 00 00 00 00 00 00 00 00 33 00 00 c0 .DD.........3..? b0: 00 54 00 00 00 00 60 02 00 01 ff 44 13 60 03 00 .T....`?.?.D?`?. c0: 00 00 00 ff 00 00 00 00 00 00 00 00 00 00 00 00 ................ d0: 58 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 X............... ^^ 0xD0: "id": Chip-ID-Nummer, immer 0x58 e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ ^^ 0xE0: "reset": wird hierher 0xB6 geschrieben, führt der Chip einen Reset aus f0: 00 00 00 00 00 00 00 80 00 00 80 00 00 00 00 00 .......?..?..... ^^ 0xF3: "status": Bit 3 wird auf 1 gestzt, wenn eine Kommunikation stattfindet. Bit 0 wird während des NVM-Kopiervorganges auf 1 gesetzt. ^^ 0xF4: "ctrl_meas": Bit 7, 6, 5: Oversampling Temp. Daten "osrs_t" 000: skip, output immer 0x80000 001: 1-fach Oversampling ~±0.0050 °C 010: 2-fach ~±0.0025 °C 011: 4-fach ~±0.0012 °C 100: 8-fach ~±0.0006 °C 101: 16-fach (auch 110, 111) ~±0.0003 °C Bit 4, 3, 2: Oversampling Druck Daten "osrs_p" 000: skip, output immer 0x80000 001: 1-fach Oversampling ~±2.62 Pa 010: 2-fach ~±1.31 Pa 011: 4-fach ~±0.66 Pa 100: 8-fach ~±0.33 Pa 101: 16-fach (auch 110, 111) ~±0.16 Pa Bit 1, 0: Power mode 00: sleep mode 01: forced mode 11: normal mode ^^ 0xF5: "config": "t_sb", "filter" und "spi3w_en", siehe Datenblatt In 0xF4 müssen wir dem BMP280 mitteilen, wie genau unser Ergebnis sein soll. Ich glaube, 1-fach Oversampling ist für den Innenraumbetrieb genau genug, macht 001 001. Den Power mode setzen wir auf normal, schließlich gibt es etwas zu tun, macht zusammen 001 001 11 oder in Hex 0x27. Schreiben wir diesen Wert also in die entsprechende Speicherzelle des BMP280: pi@raspberrypi:~ $ i2cset -y 1 0x76 0xf4 0x27 Und lesen dann neu aus. Jetzt sollten echte Werte (statt nur 80 00 00) vorhanden sein. pi@raspberrypi:~ $ i2cdump -y 1 0x76 No size specified (using byte-data access) 0 1 2 3 4 5 6 7 8 9 a b c d e f 0123456789abcdef 00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 40: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 80: 9f 6c 90 38 27 34 6d 00 76 69 92 66 32 00 10 92 ?l?8'4m.vi?f2.?? 90: 81 d7 d0 0b 17 1a fe ff f9 ff 8c 3c f8 c6 70 17 ???????.?.?<??p? a0: 00 44 44 00 00 00 00 00 00 00 00 00 33 00 00 c0 .DD.........3..? b0: 00 54 00 00 00 00 60 02 00 01 ff 44 8c 4e 08 00 .T....`?.?.D?N?. c0: 00 40 27 ff 00 00 00 00 00 00 00 00 00 00 00 00 .@'............. d0: 58 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 X............... e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ f0: 00 00 00 0c 27 00 00 57 b2 00 7d 78 00 00 00 00 ...?'..W?.}x.... ^^ 0xF7: "press_msb": MSB part of raw press data ^^ 0xF8: "press_lsb": LSB part of raw press data ^^ 0xF9: "press_xlsb": (Bit 7-4) XLSB part of raw press data ^^ 0xFA: "temp_msb" ^^ 0xFB: "temp_lsb" ^^ 0xFC: "temp_xlsb" - wie bei "press_ ..." Siehe da: echte Werte. Und zwar mit 20 Bit Auflösung. Aufgeteilt auf je 2 ½ Bytes. MSB meint übrigens most significant bits und LSB least significant bits. Unser Wert für den Druck ist demnach 0x57b20 oder dezimal 359200. Und für die Temperaturk 0x7d780 oder dezimal 513920.

Das sind aber erst die Rohwerte. Denn jeder Sensor wird bei Bosch kalibriert, denn jeder Sensor liefert aufgrund seiner physikalischen Eigenschaften leicht andere Messwerte. Diese Kalibrierungswerte werden dann in dem BMP280 gebrannt, müssen von dort ausgelesen werden und in eine Rechnung einbezogen werden, damit am Ende ein genauer Wert herauskommt.

Womit wir zur Software kommen. Hier der Teil zur Temperaturmessung: def getLocalTempBMP280(): # Temperatur vom Bosch BMP 280, Genauigkeit ~±0.0050 °C w1=readI2c(i2cBMP280, 0xfa) # most significant w2=readI2c(i2cBMP280, 0xfb) w3=readI2c(i2cBMP280, 0xfc) >> 4 # least significant #gesamt 20 bit wert = w1*2**12 + w2*2**4 + w3 # Kompensationsvariablen aus ROM des BMP280 lesen data = i2c.read_i2c_block_data(i2cBMP280, 0x88, 6) # Block von 6 Bytes ab 0x88 lesen digT = [0, 0, 0, 0]; digT[1] = data[1] * 256 + data[0] digT[2] = data[3] * 256 + data[2] if digT[2] > 32767: # signed short digT[2] -= 65536 digT[3] = data[5] * 256 + data[4] if digT[3] > 32767: # signed short digT[3] -= 65536 adcT = float(wert) var1 = ( adcT/16384. - digT[1]/1024. ) * digT[2] var2 = adcT/131072. - digT[1]/8192. var2 = var2**2 * digT[3] tFine = var1+var2 temp = tFine / 5120. gradC= temp return gradC Wie man sieht, gibt es drei Kalibrierungswerte für den Temperatursensor: digT[1] bis digT[3]. Die Formel habe ich so aus dem Datenblatt abgeschrieben, allerdings um ein paar überflüssige und verwirrende Klammern erleichtert. Zum Schluss kommt eine genaue Temperatur in Grad Celsius heraus und wird zurückgegben.

Bei der Berechnung des Luftdrucks wird noch einer draufgesetzt: def getAbsPressureBMP280(): w1=readI2c(i2cBMP280, 0xf7) # most significant w2=readI2c(i2cBMP280, 0xf8) w3=readI2c(i2cBMP280, 0xf9) >> 4 # least significant #gesamt 20 bit wert = w1*2**12 + w2*2**4 + w3 # Kompensationsvariablen aus ROM des BMP280 lesen data = i2c.read_i2c_block_data(i2cBMP280, 0x8e, 18) # Block von 18 Bytes ab 0x8e lesen digP = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; digP[1] = data[1] * 256 + data[0] # als einzige P-Var. unsigned for p in range(2,10): digP[p] = data[(p-1)*2+1] * 256 + data[(p-1)*2] if digP[p] > 32767: # signed short digP[p] -= 65536 tFine = getLocalTempBMP280() * 5120. adcP=wert var1 = tFine/2.-64000. var2 = var1*var1*digP[6]/32768. var2 = var2+var1*digP[5]*2. var2 = var2/4. + digP[4]*65536. var1 = ( digP[3]*var1*var1/524288. + digP[2]*var1 ) / 524288. var1 = (1. + var1/32768.) * digP[1] p = 1048576. - adcP p = (p - var2/4096.)*6250./var1 var1 = digP[9]*p*p/2147483648. var2 = p * digP[8]/32768. p = p + (var1 + var2 + digP[7])/16. return p/100. Hier gibt es gleich 9 Kompensationswerte, wobei nur der erste unsigned, also immer positiv ist. Der Rest ist signed, teilt sich also in zwei Hälften auf: die untere von 0-32767, die positiv ist und die obere von 32768-65535, die die negativen Zahlen repräsentiert. In der For-Schleife wird dies entsprechend umgerechnet.

Die Temperatur wird auch noch mit in die Rechnung mit einbezogen, da der Luftdruck auch von der Temperatur abhängig ist. Die Funktion haben wir ja bereits geschrieben und wir müssen sie nur noch aufrufen. Und danach noch einmal mit 5120 multiplizieren, damit wir auf tFine kommen, den die Rechnung gerne hätte.

Danach wird mit den Werten fröhlich gerechnet. Auch hier habe ich mich möglichst nah an das Datenblatt gehalten und auch die Rechnung einmal mit den dort aufgeführten Beispielwerten durchgeführt, um sicher zu gehen, dass alles stimmt.

Am Schluss kommt der absolute, lokale Wert des Luftdrucks heraus. Mich interessiert aber eher der Wert, der in der Meteorologie angegeben wird. Das ist nämlich ein Wert, wo der Luftdruck in Relation zur Meereshöhe NN gesetzt wird, die Höhe sozusagen heraus gerechnet. Denn die Höhe hat einen gewaltigen Einfluss auf den Luftdruck: je tiefer man sich befindet, desto höher stapelt sich die Luft über einem - und die übt einen ziemlichen Druck aus.

Also habe ich eine zweite Funktion dafür geschrieben: def getRelPressureBMP280(): # relativ zur Meereshöhe altitude = 330 pLocal = getAbsPressureBMP280() * 100. pNN = pLocal/pow(1 - altitude/44330.0, 5.255) #print pNN/100. return pNN/100. altitude ist dabei die Höhe, in der ich mich befinde, also so ungefähr. Google Earth meint hier etwas anderes als mein GPS-Empfänger (der natürlich auch nur ein Barometer zur Höhenberechnung nutzt). Hier machen schon ein paar Meter etwas aus. Ich habe mich darum am aktuellen Hektopascal-Wert eines Internet-Wetterdienstes orientiert. Obwohl das 10 Meter weniger sind, als Google Earth meint - und mein GPS-Höhenmeter müsste ich wohl mal wieder kalibrieren, meinte das doch gleich 360 m. Evtl. werde ich hier noch ein paar Meter hin oder her schieben müssen.

Der Norm-Luftdruck auf Normal Null beträgt 101325 Pascal (oder 1.01325 Bar oder 1013.25 Hektopascal (hPa) oder Millibar (mbar)). Eigentlich interessant ist, wie sehr der aktuelle Druck von diesem Normdruck aufweicht. So kann man leichter erkennen, ob und wie sehr der Luftdruck gerade steigt oder fällt. Dafür habe ich folgende Funktion geschrieben: def getDeltaPressureBMP280(): # relativ zu 101.325 Pa (Normaldruck auf Meereshöhe (NN)) paDelta= getRelPressureBMP280()-1013.25 return paDelta
Diesen Wert lasse ich dann auch auf meinem Monitoring-LCD anzeigen. Gaaaanz grob gesagt: befindet sich der Wert weit im Plus-Bereich (+25 bis +50 Pa), dann wird oder bleibt das Wetter schön, fällt er plötzlich in den Minusbereich (-20 bis -50), dann spricht das für aufkommendes schlechtes Wetter. Um die Null bleibt das Wetter wechselhaft.

Achja, zum Zeitpunkt, als das Foto vom Display gemacht wurde, tröpfelte es ein wenig und das Wetter war wechselhaft. Darum ist der Wert mit -11.6 Pa durchaus realistisch. Die Wettervorhersage im Internet sagt für morgen den ganzen Tag Sonne voraus. Ich bin mal gespannt, ob sich das in einem stabilen Hochdruck ausdrücken wird.

Am nächsten Tag: Die Internetvorhersage zeigt einen aktuellen Wert von 1019.2 hPa. Meine Anzeige meint +595.28 Pa, was gar nicht mehr aufs LCD passt. Etwas über 6 hPa über Norm. Passt zum Wetterdienst. Ich habe die Anzeige jetzt übrigens auf Hektopascal und eine Nachkommastelle gekürzt. Das ist immer noch genau genug.

Die Internet-Vorhersage auf dem Smartphone ist natürlich genauer, weil es noch viel mehr andere Faktoren miteinbeziehen kann, aber ich finde es mal interessant, den Luftdruck im Auge zu behalten und meine Schlüsse zu ziehen, wie das Wetter in Relation zum Luftdruck steht.

Der erste Screen der Anzeige ist ja nun prall gefüllt. Wird Zeit, dass ich mich bald mal an das Umschalten per Fernbedienung mache, um weitere, noch zu programmierende Screens auch erreichbar zu machen. Das wird sicher eines meiner nächsten Projekte.

Den Fotowiderstand vom PCF8591-Modul habe ich übrigens auch ausgelesen und als mittleren Wert angezeigt. Allerdings ist der verbaute Teilwiderstand schlecht dimensioniert und der Wertebereich unnötig beschnitten. Evtl. werde ich dies in einem baldigen Projekt korrigieren.

Es gibt übrigens auch ein BME280-Modul, dass fast identisch dem BMP280-Modul aussieht. Es unterscheidet sich nur durch den Sensor-Chip von Bosch. Der BME hat zusätzlich einen Feuchtigkeitssensor verbaut und ist etwas größer und quadratischer und zur Zeit nicht unter 3 Euro aus Fernost zu bekommen. Dort wird aber vielfach ein BMP als BME für weniger Geld angeboten. Die Enttäuschung ist dann natürlich vorprogrammiert, wenn man vergebens versucht, aus einem imaginären BME (in Wahrheit BMP280) die Feuchtigkeitsdaten auszulesen.

Ein BMP280 hat die ID 0x58 an Adresse 0xd0 bzw. für Entwicklungsmuster auch 0x56 oder 0x57. Ein BME280 hat immer die ID 0x60 an Adresse 0xd0.