BMЕ680 и ESP32 метеорологична станция за дома

Какво е BMЕ680

BMЕ680 е сензор за налягане, температура, влажност и въздушно качество, разработен от Bosch Sensortec. Той предоставя прецизни измервания на тези параметри и е използван в различни области като метеорология, промишлени приложения и IoT (Интернет на нещата). BMP680 е възможно да се използва за измерване на налягане от околната среда, както и за разработване на различни видове устройства, които изискват прецизни измервания на атмосферните условия. Днес ние ще видим, как може да си направим метеорологична станция с него в комбинация с ESP32 борд. Повече информация за сензора може да се намери тук. Сензора, който ще ползваме е този и ESP32 борда е NodeMCU-32S – този (версия с micro usb).

Какъв е плана?

BME680 поддържа I2C и SPI комуникация. Плана е да свържем сензора с ESP32 борда и да четем измерванията на определен интервал. След като ги прочетем да ги покажем на серийната   конзола и да разберем измерванията.

Какво е I2C

I2C (Inter-Integrated Circuit) е комуникационен протокол, който се използва за свързване на различни компоненти в електронни устройства. Той позволява на тези компоненти да обменят данни и команди помежду си чрез двуправна серийна връзка, като използва две жици: данни (SDA – Serial Data) и часовник (SCL – Serial Clock).

I2C е много популярен за връзката на микроконтролери, сензори, актуатори и други периферни устройства в системи, където се изисква комуникация между различни компоненти. Едно от предимствата на I2C е, че позволява свързването на множество устройства към една и съща линия, като използва различни адреси за идентификация на всяко от тях. Това прави I2C подходящ за изграждане на сложни системи с множество периферни устройства.

Свързване

Нека да проверим сензора и да намерим SDA и SCL пиновете. Обикновено те са написани на сензора. Както може да видим от картината и надписите на сензора, това са 3тия и 4тия пин от ляво на дясно.

Сега трябва да намерим тези пинове и на ESP32 борда. Това може да видим в описанието на конкретния борд или от диаграмата му за пиновете. Нека намерим тези на тази диаграма.

Това са 3-тия и 6-тия пин от горе в дясно. Към тези входове трябва да свържем сензора. За да работи, на BME680  му трябва и захранване. Това можем да вземем от 3.3 V от 1рвия пин от горе в ляво и земя 1 пин в дясно. Нека ги навържем.

Да конфигурираме Arduino IDE 1

За да можем да четем от сензора, трябва първо да конфигурираме средата за програмиране. Затова трябва да инсталираме първо някои библиотеки.

Нека да отидем на Sketch -> Include Library -> Manage Libraries

И нека там потърсим „adafruit bme680“, изберем и инсталираме.

След това трябва да инсталираме и сензорните библиотеки.

Нека да отидем пак на Sketch -> Include Library -> Manage Libraries, да потърсим “Adafruit Unified Sensor” и да го инсталираме.

Ако сте ползвали тази статия за конфигуриране на Arduino IDE-то, трябва да сменим борда, защото в момента ползваме NodeMCU-32S. Нека отидем на Tools -> Board -> Boards manager и нека изберем NodeMCU-32S. За да имаме това, трябва преди това да семе инсталирали esp32 от Espressif Systems библиотеката (показана на по-следващата снимка).

Сега вече сме готови да напишем малко код.

Кода

Кодът, който използваме е взет от тук.

/***
  Read Our Complete Guide: https://RandomNerdTutorials.com/esp32-bme680-sensor-arduino/
  Designed specifically to work with the Adafruit BME680 Breakout ----> http://www.adafruit.com/products/3660 These sensors use I2C or SPI to communicate, 2 or 4 pins are required to interface. Adafruit invests time and resources providing this open source code, please support Adafruit and open-source hardware by purchasing products from Adafruit! Written by Limor Fried & Kevin Townsend for Adafruit Industries. BSD license, all text above must be included in any redistribution
***/

#include <Wire.h>
#include <SPI.h>
#include <Adafruit_Sensor.h>
#include "Adafruit_BME680.h"

/*#define BME_SCK 18
#define BME_MISO 19
#define BME_MOSI 23
#define BME_CS 5*/

#define SEALEVELPRESSURE_HPA (1013.25)

Adafruit_BME680 bme; // I2C
//Adafruit_BME680 bme(BME_CS); // hardware SPI
//Adafruit_BME680 bme(BME_CS, BME_MOSI, BME_MISO, BME_SCK);

void setup() {
  Serial.begin(115200);
  while (!Serial);
  Serial.println(F("BME680 async test"));

  if (!bme.begin()) {
    Serial.println(F("Could not find a valid BME680 sensor, check wiring!"));
    while (1);
  }

  // Set up oversampling and filter initialization
  bme.setTemperatureOversampling(BME680_OS_8X);
  bme.setHumidityOversampling(BME680_OS_2X);
  bme.setPressureOversampling(BME680_OS_4X);
  bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
  bme.setGasHeater(320, 150); // 320*C for 150 ms
}

void loop() {
  // Tell BME680 to begin measurement.
  unsigned long endTime = bme.beginReading();
  if (endTime == 0) {
    Serial.println(F("Failed to begin reading :("));
    return;
  }
  Serial.print(F("Reading started at "));
  Serial.print(millis());
  Serial.print(F(" and will finish at "));
  Serial.println(endTime);

  Serial.println(F("You can do other work during BME680 measurement."));
  delay(50); // This represents parallel work.
  // There's no need to delay() until millis() >= endTime: bme.endReading()
  // takes care of that. It's okay for parallel work to take longer than
  // BME680's measurement time.

  // Obtain measurement results from BME680. Note that this operation isn't
  // instantaneous even if milli() >= endTime due to I2C/SPI latency.
  if (!bme.endReading()) {
    Serial.println(F("Failed to complete reading :("));
    return;
  }
  Serial.print(F("Reading completed at "));
  Serial.println(millis());

  Serial.print(F("Temperature = "));
  Serial.print(bme.temperature);
  Serial.println(F(" *C"));

  Serial.print(F("Pressure = "));
  Serial.print(bme.pressure / 100.0);
  Serial.println(F(" hPa"));

  Serial.print(F("Humidity = "));
  Serial.print(bme.humidity);
  Serial.println(F(" %"));

  Serial.print(F("Gas = "));
  Serial.print(bme.gas_resistance / 1000.0);
  Serial.println(F(" KOhms"));

  Serial.print(F("Approx. Altitude = "));
  Serial.print(bme.readAltitude(SEALEVELPRESSURE_HPA));
  Serial.println(F(" m"));

  Serial.println();
  delay(2000);
}

След като компилираме кода и го запишем на бода, може да отворим серийната конзола и ще видим следното:

По този начин можем да прочетем стойностите на сензора. Повече подробности за кода можете да прочетете в оригиналната статия, от която той е взет, тук. Но нека обърнем малко повече внимание на стойностите.

Стойностите

Както видяхме сензора може да измери температура, надморско налягане, влажност и газ.

Сензора мери доста точно температурното изменение, но стойността на температурата не е точна. Ако прочетем някои от оригиналните документи на производителя, примерно този тук, ще намерим следното описание:

 „2.7 Температурни отмествания, дължащи се на източници на топлина върху платката

Нека да разгледаме стойностите на температурата и влажността, които получаваме от платката. Температура от над тридесет градуса и толкова ниска относителна влажност изглеждат нередни. Поглеждайки към еталонен термометър, можем да видим, че

температурата ни наистина е с няколко градуса по-висока. Означава ли това, че температурният сензор в BME680 е неточен?

Всъщност не, той много точно измерва температурата точно там, където е разположен, на платката. Нашата платка, както повечето устройства, съдържа някои източници на топлина (например MCU, WiFi чип, дисплей, …). Това означава, че температурата на там  всъщност е по-висока от температурата на околната среда. Тъй като абсолютното количество на вода във въздуха е приблизително постоянно, това води и до по-ниска относителна влажност на борда, отколкото на на друго място в помещението.“

Това за нас означава, че трябва да вземем референтна стойност с друг термометър и да настроим отместването на температурата спрямо него. Това можем да направим като извадим или съберем отместването към градусите в програмата. Същото важи и за влажността.

Газ и VOC

Когато четем стойността на газ, ние получаваме стойност на съпротивление на сензора, но какво означава това и какво е VOC.

VOC стои за „волатилни органични съединения“ (Volatile Organic Compounds) и се използва за описване на различни химични съединения, които имат висока паропропускливост и се лесно изпаряват при обикновени условия на температура и налягане. Те често се свързват с промишлени процеси, боядисване, лепене, почистване и други дейности, които могат да предизвикат замърсяване на въздуха и да имат вредно въздействие върху здравето на хората и околната среда.

Когато VOC се свързват с газове, съпротивлението на сензора се увеличава, което намалява електропроводимостта, а когато VOC намаляват, съпротивлението намалява, което увеличава електропроводимостта. Така чрез измерване на съпротивлението можем да открием VOC-газовете. Накратко, когато съпротивлението се увеличава и наличието на газове се увеличава, а когато намалява и газовете намаляват.

Но за да можем да покажем разбираеми стойности, ще използваме библиотеката bsec.

BSEC

Нека я инсталираме. Нека да отидем пак на Sketch -> Include Library -> Manage Libraries, да потърсим “bsec” и да го инсталираме.

Нека да копираме следният код, който може да намерим тук с повече описание.

#include <EEPROM.h>
#include "bsec.h"
/* Configure the BSEC library with information about the sensor
    18v/33v = Voltage at Vdd. 1.8V or 3.3V
    3s/300s = BSEC operating mode, BSEC_SAMPLE_RATE_LP or BSEC_SAMPLE_RATE_ULP
    4d/28d = Operating age of the sensor in days
    generic_18v_3s_4d
    generic_18v_3s_28d
    generic_18v_300s_4d
    generic_18v_300s_28d
    generic_33v_3s_4d
    generic_33v_3s_28d
    generic_33v_300s_4d
    generic_33v_300s_28d
*/
const uint8_t bsec_config_iaq[] = {
#include "config/generic_33v_3s_4d/bsec_iaq.txt"
};

#define BME68X_I2C_ADDR 0x77

#define STATE_SAVE_PERIOD  UINT32_C(360 * 60 * 1000) // 360 minutes - 4 times a day

// Helper functions declarations
void checkIaqSensorStatus(void);
void errLeds(void);
void loadState(void);
void updateState(void);

// Create an object of the class Bsec
Bsec iaqSensor;
uint8_t bsecState[BSEC_MAX_STATE_BLOB_SIZE] = {0};
uint16_t stateUpdateCounter = 0;

String output;

// Entry point for the example
void setup(void)
{
  EEPROM.begin(BSEC_MAX_STATE_BLOB_SIZE + 1);
  Serial.begin(115200);
  delay(1000);
  pinMode(LED_BUILTIN, OUTPUT);
  iaqSensor.begin(BME68X_I2C_ADDR, Wire);
  output = "\nBSEC library version " + String(iaqSensor.version.major) + "." + String(iaqSensor.version.minor) + "." + String(iaqSensor.version.major_bugfix) + "." + String(iaqSensor.version.minor_bugfix);
  Serial.println(output);
  checkIaqSensorStatus();

  iaqSensor.setConfig(bsec_config_iaq);
  checkIaqSensorStatus();

  loadState();

  bsec_virtual_sensor_t sensorList[13] = {
    BSEC_OUTPUT_IAQ,
    BSEC_OUTPUT_STATIC_IAQ,
    BSEC_OUTPUT_CO2_EQUIVALENT,
    BSEC_OUTPUT_BREATH_VOC_EQUIVALENT,
    BSEC_OUTPUT_RAW_TEMPERATURE,
    BSEC_OUTPUT_RAW_PRESSURE,
    BSEC_OUTPUT_RAW_HUMIDITY,
    BSEC_OUTPUT_RAW_GAS,
    BSEC_OUTPUT_STABILIZATION_STATUS,
    BSEC_OUTPUT_RUN_IN_STATUS,
    BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE,
    BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY,
    BSEC_OUTPUT_GAS_PERCENTAGE
  };

  iaqSensor.updateSubscription(sensorList, 13, BSEC_SAMPLE_RATE_LP);
  checkIaqSensorStatus();

 }

// Function that is looped forever
void loop(void)
{
  unsigned long time_trigger = millis();
  if (iaqSensor.run()) { // If new data is available
    digitalWrite(LED_BUILTIN, HIGH);
    output = "Timestamp [ms]: " + String(time_trigger) + "\n";
    output += "Raw temperature [°C]: " + String(iaqSensor.rawTemperature) + "\n";
    output += "Pressure [hPa]: " + String(iaqSensor.pressure/100) + "\n";
    output += "Raw relative humidity [%]: " + String(iaqSensor.rawHumidity) + "\n";
    output += "Gas resistance [Ohm]: " + String(iaqSensor.gasResistance/1000) +"\n";
    output += "IAQ: " + String(iaqSensor.iaq) + "\n";
    output += "IAQ accuracy: " + String(iaqSensor.iaqAccuracy) + "\n";
    output += "Temperature [°C]: " + String(iaqSensor.temperature) + "\n";
    output += "Relative humidity [%]: " + String(iaqSensor.humidity) + "\n";
    output += "Static IAQ: " + String(iaqSensor.staticIaq) + "\n";
    output += "CO2 equivalent: " + String(iaqSensor.co2Equivalent) + "\n";
    output += "Breath VOC equivalent: " + String(iaqSensor.breathVocEquivalent) + "\n";
    Serial.println(output);

    digitalWrite(LED_BUILTIN, LOW);
    updateState();
  } else {
    checkIaqSensorStatus();
  }
}

// Helper function definitions
void checkIaqSensorStatus(void)
{
  if (iaqSensor.bsecStatus != BSEC_OK) {
    if (iaqSensor.bsecStatus < BSEC_OK) {
      output = "BSEC error code : " + String(iaqSensor.bsecStatus);
      Serial.println(output);
      for (;;)
        errLeds(); /* Halt in case of failure */
    } else {
      output = "BSEC warning code : " + String(iaqSensor.bsecStatus);
      Serial.println(output);
    }
  }

  if (iaqSensor.bme68xStatus != BME68X_OK) {
    if (iaqSensor.bme68xStatus < BME68X_OK) {
      output = "BME68X error code : " + String(iaqSensor.bme68xStatus);
      Serial.println(output);
      for (;;)
        errLeds(); /* Halt in case of failure */
    } else {
      output = "BME68X warning code : " + String(iaqSensor.bme68xStatus);
      Serial.println(output);
    }
  }
}

void errLeds(void)
{
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH);
  delay(100);
  digitalWrite(LED_BUILTIN, LOW);
  delay(100);
}

void loadState(void)
{
  if (EEPROM.read(0) == BSEC_MAX_STATE_BLOB_SIZE) {
    // Existing state in EEPROM
    Serial.println("Reading state from EEPROM");

    for (uint8_t i = 0; i < BSEC_MAX_STATE_BLOB_SIZE; i++) {
      bsecState[i] = EEPROM.read(i + 1);
      Serial.println(bsecState[i], HEX);
    }

    iaqSensor.setState(bsecState);
    checkIaqSensorStatus();
  } else {
    // Erase the EEPROM with zeroes
    Serial.println("Erasing EEPROM");

    for (uint8_t i = 0; i < BSEC_MAX_STATE_BLOB_SIZE + 1; i++)
      EEPROM.write(i, 0);

    EEPROM.commit();
  }
}

void updateState(void)
{
  bool update = false;
  if (stateUpdateCounter == 0) {
    /* First state update when IAQ accuracy is >= 3 */
    if (iaqSensor.iaqAccuracy >= 3) {
      update = true;
      stateUpdateCounter++;
    }
  } else {
    /* Update every STATE_SAVE_PERIOD minutes */
    if ((stateUpdateCounter * STATE_SAVE_PERIOD) < millis()) {
      update = true;
      stateUpdateCounter++;
    }
  }

  if (update) {
    iaqSensor.getState(bsecState);
    checkIaqSensorStatus();

    Serial.println("Writing state to EEPROM");

    for (uint8_t i = 0; i < BSEC_MAX_STATE_BLOB_SIZE ; i++) {
      EEPROM.write(i + 1, bsecState[i]);
      Serial.println(bsecState[i], HEX);
    }

    EEPROM.write(0, BSEC_MAX_STATE_BLOB_SIZE);
    EEPROM.commit();
  }
}

В кода трябва да конфигурираме BME68X_I2C_ADDR. В случая той е 0x77. Също така можем да напаснем и конфигурацията на BSEC библиотеката. В кода тя е конфигурирана с generic_33v_3s_4d. Повече информация за библиотеката може да намерим тук.

Когато качим кода на борда, се виждат следните стойности в серийната конзола

Нека да разгледаме стойностите по-отблизо

Timestamp [ms]: 343045 – Това е времето, в което е почнало измерването

Raw temperature [°C]: 25.82 – Това е температурата прочетена от сензора

Pressure [hPa]: 949.18 – Това е надморското налягане прочетено от сензора (не на нивото на морето, а на актуалната височина)

Raw relative humidity [%]: 49.55 – Относителната влажност прочетена от сензора. Или това е съотношение, изразено в проценти, на количеството влага налична в атмосферна, спрямо количеството, което би било налично, ако въздухът е наситен.

Gas resistance [Ohm]: 84.00 – Съпротивлението, което се получава при измерването на VOC.

IAQ: 50.00 – Indoor air quality или Индекс на качеството на въздуха

IAQ accuracy: 1 – Този флаг е интересен и информация за него може да получим тук, но на кратко

Точността на IAQ всъщност отразява текущото състояние на процеса на калибриране на фона, като например:

  • IAQ Accuracy=0 може да означава: BSEC току-що е стартирал и сензорът се стабилизира (това обикновено трае 5 мин. в режим LP или 20 мин. в режим ULP), е имало нарушение на времето (т.е. BSEC е бил извикан твърде рано или твърде късно), което трябва да бъде сигнализирано от BSEC чрез предупреждение/флаг за грешка,
  • IAQ Accuracy=1 означава, че фоновата история на BSEC е несигурна. Това обикновено означава, че данните от газовия сензор са били твърде стабилни, за да може BSEC да определи ясно своите референтни стойности,
  • IAQ Accuracy=2 означава, че BSEC е намерил нови данни за калибриране и в момента се калибрира,
  • IAQ Accuracy=3 означава, че BSEC се е калибрирал успешно.

Temperature [°C]: 25.71 – Предполагаемата температура на въздуха

Relative humidity [%]: 49.87 – Предполагаемата относителната влажност на въздуха

Static IAQ: 50.00 – Статичен IAQ: Основната разлика между IAQ и статичния IAQ (sIAQ) се състои в мащабиращия фактор, изчислен въз основа на скорошната история на сензора. Изходът на sIAQ е оптимизиран за стационарни приложения (напр. стационарни устройства на закрито), докато изходът на IAQ е идеален за мобилни приложения (напр. преносими устройства).

CO2 equivalent: 600.00 – Еквивалентната концентрацията на CO2 (CO2eq) [ppm] в околната среда. Тя също се изчислява въз основа на изхода на sIAQ и се извлича от измервания на ЛОС и корелация от полеви проучвания.

Breath VOC equivalent: 0.50 – Еквивалента концентрация на VOC в дъха (bVOCeq) оценява общата концентрация на VOC [ppm] в околната среда. Тя се изчислява въз основа на изхода на sIAQ и се получава от лабораторни тестове.

Доста подробно обяснение на стойностите и какво може да се прави със сензора може да се намери тук.

Заключение

В тази статия видяхме, как може да се програмира сензора BMP680 и да се предават данните му на борд с ESP32 процесор. Информацията събран по този начин ни дава доста точна представа за нашият дом. По този начин може да открием фактори, застрашаващи нашето здраве, да разберем комфортната температура и влажност, да пазим история за измерените данни и да реагираме на тези промени.

Весело програмиране!

Comments

No comments yet. Why don’t you start the discussion?

Вашият коментар

Вашият имейл адрес няма да бъде публикуван. Задължителните полета са отбелязани с *