poniedziałek, 29 stycznia 2018

BME280 Czujnik wilgotności, ciśnienia i temperatury

BME280 to chyba jedyny cyfrowy czujnik posiadający możliwość pomiaru aż trzech parametrów powietrza: wilgotności, ciśnienia atmosferycznego oraz temperatury. Jak inne moje moduły, tak i ten, ma racjonalną cenę, zaczynającą się od 2,35 € na AliExpress. Można tam znaleźć także moduły z czujnikiem BME680, mierzącym dodatkowo zawartość rozmaitych gazów w powietrzu, a ogólnie jakość powietrza. Cena jednak w jego przypadku jest o wiele większa i  zaczyna się od 15 €.

Moduł BME280

Jak widać ze zdjęcia, na tej samej płytce producent modułu montuje BME280 lub BMP280 (wersja bez pomiaru wilgotności). Co więcej, nie miałem oznaczonej wersji czujnika (puste białe pola/kwadraciki). Jednak przykładowy kod (podany poniżej) wzięty z użytej biblioteki (2) obsługującej ten czujnik, wykrył BME280. Widać to na screenie z terminala portu szeregowego (zamieszczonym na końcu posta).

Specyfikacja
  • interfejs I²C i SPI
  • napięcie interfejsu max 3,9V (moduł ma wbudowany konwerter poziomów i umożliwia pracę z napięciem 5V)
  • domyślny adres I²C zależy od stanu na pinie SDO:
    • stan niski 0x76
    • stan wysoki 0x77
  • napięcie zasilania 1,2 do 3,6V. Sam moduł może być zasilany do 5V (wbudowany regulator napięcia 662K)
  • pobór prądu 3,6uA
  • parametry pracy -40…+85 °C, 0…100 %RH, 300…1100 hPa

Druga strona modułu

Moduł ma wyprowadzone tylko piny magistrali I²C.

Schemat elektryczny ze strony sprzedawcy
Uruchomienie

Do uruchomienia układu posłużył mi tradycyjnie przykład Environment_Calculations.ino z biblioteki finitespace/BME280 (2). Pierwsza zmiana, to szybkość transferu szeregowego na 9600 (stała SERIAL_BAUD). Taką wartość akurat mam ustawioną w terminalu używanego Atmel Studio. Przykład przeredagowałem wydzielając wyznaczanie parametrów powietrza przez czujnik do funkcji printBasicData(), natomiast pozostałe obliczenia środowiskowe do funkcji printEnvironmentCalculations(). Poprawiłem też czytelność wyników prezentowanych na terminalu, dodając jeden wiersz nagłówków (funkcja printHeader()). Nawiasem mówiąc funkcja Altitude() zwracała jakieś kosmiczne wyniki, ale pomogła zmiana wartości referencePressure z hPa na Pa  (obliczenia w tej funkcji są w Pa-skalach).

Schemat blokowy połączeń
Reszta obliczeń na pierwszy rzut oka się zgadza, choć przyznam, że nie specjalnie znam się na tych rzeczach (w kodzie samej biblioteki i w pliku pomocy pojawia się wiele dziwnych skrótów i pojęć). Druga sprawa, to to że dodatkowe obliczenia środowiskowe wymagają wiedzy na temat innych czynników, takich jak lokalne ciśnienie zredukowane do poziomu morza (zmienna referencePressure), temperatura zewnętrzna (outdoorTemp) oraz wysokość położenia czujnika nad poziomem morza (barometerAltitude). Dane te brałem z serwisu pogodowego http://meteo.org.pl/ i  https://www.wysokosciomierz.pl. Dwa pierwsze parametry zmieniają się w czasie, tak więc aby je monitorować, trzeba by mieć dostęp do API jakiegoś serwisu pogodowego i pobierać je na bieżąco.

/*
Connecting the BME280 Sensor:
Sensor              ->  Board
-----------------------------
Vin (Voltage In)    ->  5V
Gnd (Ground)        ->  Gnd
SDA (Serial Data)   ->  A4 on Uno/Pro-Mini, 20 on Mega2560/Due, 2 Leonardo/Pro-Micro
SCK (Serial Clock)  ->  A5 on Uno/Pro-Mini, 21 on Mega2560/Due, 3 Leonardo/Pro-Micro
*/

#include <BME280I2C.h>
#include <EnvironmentCalculations.h>
#include <Wire.h>

#define SERIAL_BAUD 9600

BME280I2C bme;    // Default : forced mode, standby time = 1000 ms
// Oversampling = pressure ×1, temperature ×1, humidity ×1, filter off,

float temp(NAN), hum(NAN), pres(NAN);

// Assumed environmental values:
float referencePressure = 101900;     // Pa local QFF (official meteor-station reading)
float outdoorTemp = 10.0;           // °C  measured local outdoor temp.
float barometerAltitude = 80;      // meters ... map readings + barometer position

BME280::TempUnit tempUnit(BME280::TempUnit_Celsius);
BME280::PresUnit presUnit(BME280::PresUnit_Pa);
EnvironmentCalculations::AltitudeUnit envAltUnit  =  EnvironmentCalculations::AltitudeUnit_Meters;
EnvironmentCalculations::TempUnit     envTempUnit =  EnvironmentCalculations::TempUnit_Celsius;

void setup()
{
    Serial.begin(SERIAL_BAUD);

    while (!Serial) {} // Wait

    Wire.begin();

    while (!bme.begin()) {
        Serial.println("Could not find BME280 sensor!");
        delay(1000);
    }

    switch (bme.chipModel()) {
        case BME280::ChipModel_BME280:
            Serial.println("Found BME280 sensor! Success.");
            break;
        case BME280::ChipModel_BMP280:
            Serial.println("Found BMP280 sensor! No Humidity available.");
            break;
        default:
            Serial.println("Found UNKNOWN sensor! Error!");
    }

    Serial.print("Assumed outdoor temperature: ");
    Serial.print(outdoorTemp);
    Serial.write(0xC2);
    Serial.write(0xB0);
    Serial.print("C\nAssumed reduced sea level Pressure: ");
    Serial.print(referencePressure);
    Serial.print("Pa\nAssumed barometer altitude: ");
    Serial.print(barometerAltitude);
    Serial.println("m\n***************************************");

    printHeader(&Serial);
}

void loop()
{
    printBasicData(&Serial);
    printEnvironmentCalculations(&Serial);
    delay(5000);
}

void printHeader(Stream* client)
{
    client->print("Temp: ");
    client->print("\t\tHumidity: ");
    client->print("\tPressure: ");
    client->print("\tAltitude: ");
    client->print("\tDew point: ");
    client->print("\tEquivalent Sea Level Pressure: ");
    client->print("\tHeat Index: ");
    client->println("\tAbsolute Humidity: ");
}

void printBasicData(Stream* client)
{
    bme.read(pres, temp, hum, tempUnit, presUnit);

    //Temperature
    client->print(temp);
    client->write(0xC2);
    client->write(0xB0);
    client->print(String(tempUnit == BME280::TempUnit_Celsius ? 'C' :'F'));
    client->print("\t\t");

    //Humidity
    client->print(hum);
    client->print("% RH");
    client->print("\t");

    //Pressure
    client->print(pres);
    client->print("Pa");
    client->print("\t");
}

void printEnvironmentCalculations(Stream* client)
{
    float altitude = EnvironmentCalculations::Altitude(pres, envAltUnit, referencePressure, outdoorTemp, envTempUnit);
    float dewPoint = EnvironmentCalculations::DewPoint(temp, hum, envTempUnit);
    float seaLevel = EnvironmentCalculations::EquivalentSeaLevelPressure(barometerAltitude, temp, pres, envAltUnit, envTempUnit);
    float absHum = EnvironmentCalculations::AbsoluteHumidity(temp, hum, envTempUnit);
    float heatIndex = EnvironmentCalculations::HeatIndex(temp, hum, envTempUnit);

    //Altitude
    client->print(altitude);
    client->print((envAltUnit == EnvironmentCalculations::AltitudeUnit_Meters ? "m" : "ft"));
    client->print("\t\t");

    //Dew point
    client->print(dewPoint);
    client->write(0xC2);
    client->write(0xB0);
    client->print(String(envTempUnit == EnvironmentCalculations::TempUnit_Celsius ? "C" :"F"));
    client->print("\t\t");

    //Equivalent Sea Level Pressure
    client->print(seaLevel);
    client->print(String( presUnit == BME280::PresUnit_hPa ? "hPa" :"Pa")); // expected hPa and Pa only
    client->print("\t\t\t");

    //Heat Index
    client->print(heatIndex);
    client->write(0xC2);
    client->write(0xB0);
    client->print(String(envTempUnit == EnvironmentCalculations::TempUnit_Celsius ? "C" :"F"));
    client->print("\t\t");

    //Absolute Humidity
    client->println(absHum);
}

Powyższy kod można także znaleźć na bitbukecie w repo gitowym.

Za pomocą funkcji tempUnit można ustawić odczyt wartości temperatury w jednej z dwóch jednostek:
  • TempUnit_Celsius - Celsjusz
  • TempUnit_Fahrenheit - Fahrenheit

Za pomocą funkcji presUnit() można ustawić odczyt wartości ciśnienia w następujących jednostkach:
  • PresUnit_Pa - paskal
  • PresUnit_hPa - hektopaskal
  • PresUnit_inHg - cal słupa rtęci
  • PresUnit_atm - atmosfera
  • PresUnit_bar - bar
  • PresUnit_torr - tor (milimetr słupa rtęci)
  • PresUnit_psi - funt-siła na cal kwadratowy

Użyta biblioteka dostarcza dodatkowo funkcje (zdefiniowane w pliku nagłówkowym EnvironmentCalculations.h) przeznaczone do wyliczania innych parametrów powietrza:
  • wysokość nad poziomem morza - funkcja Altitude() z parametrami:
    • pressure - ciśnienie wskazywane przez czujnik
    • AltitudeUnit - jednostka wysokości (domyślnie w metrach)
    • seaLevelPressure - lokalne ciśnienie zredukowane do poziomu morza. Domyślna wartość wynosi 1013.25 hPa i jest to średnia wielkości ciśnienia atmosferycznego na Ziemi na poziomie morza.
    • outsideTemp - temperatura powietrza na zewnątrz (domyślnie 15 stopni Celsjusza)
    • TempUnit - jednostka temperatury (domyślnie Celsjusz)
  • punkt rosy - funkcja DewPoint() z parametrami:
    • temp - temperatura wskazywana przez czujnik
    • hum - wilgotność względna wskazywana przez czujnik
    • TempUnit - jednostka temperatury (domyślnie Celsjusz)
  • ciśnienie dla poziomu morza - funkcja EquivalentSeaLevelPressure() z parametrami:
    • barometerAltitude - wysokość czujnika (wysokość nad poziomem morza + wysokość lokalizacji czujnika)
    • temp - temperatura wskazywana przez czujnik
    • pres - ciśnienie wskazywane przez czujnik
    • envAltUnit - jednostka wysokości (domyślnie metr)
    • envTempUnit - jednostka temperatury (domyślnie Celsjusz)
  • wilgotność bezwzględna - funkcja AbsoluteHumidity() z parametrami:
    • temp - temperatura wskazywana przez czujnik
    • hum - wilgotność względna wskazywana przez czujnik
    • envTempUnit - jednostka temperatury (domyślnie Celsjusz)
  • wskaźnik ciepła (temperatura odczuwana) - funkcja HeatIndex() z parametrami:
    • temperature - temperatura wskazywana przez czujnik
    • humidity - wilgotność względna wskazywana przez czujnik
    • tempUnit - jednostka temperatury (domyślnie Celsjusz)

Wzory dla wykonywanych obliczeń i linki do źródeł można znaleźć w pliku źródłowym EnvironmentCalculations.cpp i w pliku README.

Kończąc temat uruchomienia, trzeba jeszcze wspomnieć o możliwościach konfiguracji czujnika, jakie daje użyta biblioteka. Domyślną konfigurację przedstawia przykład BME280_Modes.ino.

BME280I2C::Settings settings(
   BME280::OSR_X1,
   BME280::OSR_X1,
   BME280::OSR_X1,
   BME280::Mode_Forced,
   BME280::StandbyTime_1000ms,
   BME280::Filter_Off,
   BME280::SpiEnable_False,
   BME280I2C::I2CAddr_0x76 // I2C address. I2C specific.
);

BME280I2C bme(settings);

Stałe powyżej określają pracę czujnika.

Pierwsze trzy określają tzw. oversampling (nadpróbkowanie) odpowiednio dla temperatury, wilgotności i ciśnienia. Możliwe wartości to: OSR_Off, OSR_X1, OSR_X2, OSR_X4, OSR_X8, OSR_X16. Im wyższy jest oversampling tym większa jest rozdzielczość i mniejsze szumy pomiaru. Użycie OSR_Off oznacza wyłączenie pomiaru danego parametru, co oznacza, że można mierzyć np. tylko temperaturę.

Czwarty parametr (w przykładzie przyjmuje wartość BME280::Mode_Forced określa tryb pracy czujnika. Możliwe są trzy wartości: Mode_Sleep, Mode_Forced, Mode_Normal i opisane w rozdziale "Sensor modes" dokumentacji czujnika. W opisie jest też diagram przejść między tymi trybami. W skrócie Mode_Sleep oznacza brak wykonywania jakichkolwiek operacji i najniższy możliwy pobór mocy. Mode_Forced oznacza wykonanie jednego pomiaru i przejście do stanu uśpienia (Mode_Sleep). Kolejny pomiar, jak mniemam, uruchamia funkcja bme.read(). Ostatni z trybów, Mode_Normal powoduje cykliczne wykonywanie pomiarów po których czujnik przechodzi w stan uśpienia. O częstotliwości pomiarów decyduje piąty parametr, w przykładzie wyżej jest to StandbyTime_1000ms. Do wyboru mamy stałe/wartości: StandbyTime_500us, StandbyTime_62500us, StandbyTime_125ms, StandbyTime_250ms, StandbyTime_50ms, StandbyTime_1000ms, StandbyTime_10ms, StandbyTime_20ms.

Szósty parametr określa, czy włączony jest filtr  IIR (filtr o nieskończonej odpowiedzi impulsowej), który służy do wygładzania wyników pomiaru  temperatury i ciśnienia.Można powiedzieć, że spełnia funkcję filtra dolnoprzepustowego. Filter_Off oznacza wyłączenie filtru. Inne możliwe wartości konfigurują nam filtr (filter coefficient): Filter_1, Filter_2, Filter_4, Filter_8, Filter_16. Im większa wartość tym sygnał wyjściowy wolniej podąża za zmianami sygnału wejściowego (wyniku pomiaru). Zgodnie z dokumentacją filtr może być użyty przy pomiarze ciśnienia i temperatury, które mogą się gwałtownie zmieniać na skutek chwilowych zdarzeń takich jak przeciąg, czy na moment otwarte drzwi wejściowe na zewnątrz domu. Z kolei wilgotność, z tego co można wyczytać w dokumentacji, nie ulega już takim wahaniom i nie wymaga używania filtra IIR.

Pozostałych dwóch parametrów już nie rozpatrywałem. Używałem tu magistrali I2C i adres czujnika się zgadzał z wartością domyślną.

W rozdziale "Recommended modes of operation" dokumentacji czujnika producent przestawia różne możliwości zastosowania czujnika i propozycje konfiguracji dla każdego z wymienionych zastosowań. Domyślna konfiguracja czujnika użyta w omawianej bibliotece odpowiada pracy czujnika w stacji pogody. Poza tym można tam jeszcze znaleźć sposób konfiguracji czujnika w przypadku użycia go jako monitora wilgotności, wewnętrznej nawigacji (cokolwiek to znaczy) lub w przypadku gier. W dwóch ostatnich przypadkach jest wzmianka o konieczności dokładanego pomiaru wysokości nad poziomem morza, więc zalecany jest "wysoki" oversampling i włączony filtr IIR z dużym (Filter_16) współczynnikiem filtra.

Uruchomienie czujnika
Wyniki i obliczenia na terminalu

Artykuł ze źródła (3) zawiera porównanie różnych tanich czujników w tym BME280. Okazuje się, że jest on zwycięzcą tego zestawienia. W szczególności wyprzedza opisywany czujnik DHT11 i co więcej, DHT22. Moim zdaniem jest najlepszym wyborem do pomiaru najbardziej podstawowych parametrów powietrza. Można się ewentualnie zastanawiać nad dodaniem DS18B20, który oferuje lepszy pomiar temperatury (dokładność ±0.5°C w przedziale od -10°C do +85°C). W przypadku BME280 jest to ±1.0°C w przedziale od 0°C do +65°C.

Czujnik użyłem praktycznie przy realizacji własnej Stacji pogody

3 komentarze:

  1. Może rozbudujesz kod do obliczenia DevPoint i wstawisz możliwość pokazywania ciśnienia takiego jakie jest na wysokości morza.

    OdpowiedzUsuń
    Odpowiedzi
    1. Sposób wyznaczania tych wielkości znajdziesz w przykładach do użytej tu biblioteki pod linkiem https://github.com/finitespace/BME280/blob/master/examples/Environment_Calculations/Environment_Calculations.ino. Samą rozbudowę przykładu na tej stronie dodaję do długiej listy TODO.

      Usuń
    2. Kod rozbudowałem o dodatkowe obliczenia (ze wspomnianego Environment_Calculations.ino) w tym DevPoint.

      Usuń