ESP-IDF Weather Station with BME280 C++

Create a Weather station in the ESP-IDF with the BME280 and C++

The BME280 is a very popular temperature, barometric pressure, and relative humidity sensor. The sensor has good tolerances and accuracies at low power usage.

Unfortunately, the BME280 is one of the more expensive sensors and requires some compensation formulas which takes away from the power efficiency since the MCU will need to be awake for longer in some applications.


I’ve been thinking about how I was going to approach the I2C and SPI driver tutorials, because unlike the previous tutorials where we write the tutorial and do a simple demonstration, SPI and I2C cannot easily be demonstrated using an LED or some output to the serial terminal.

So, I decided to use the BME280. The BME280 supports both SPI and I2C, making it the perfect candidate to demonstrate both SPI and I2C.

In this tutorial, we are going to create the base class for the BME280 sensor including all methods but excluding the comms implementations. We will then use this project to create both the SPI and I2C drivers and then create child classes for the BME280 to implement SPI and I2C communication.


ESP-IDF BME280 C++ Example

Parts used in this tutorial

BME280 Module
NodeMCU32s
Breadboard
Breadboard Link Wires

In the tutorial, we are going to extensively use the BME280 datasheet which can be found on the manufacturer’s website here.

Create a new Project

If you are unsure about how to create a new project and modify it to use C++ then please follow the steps here.

Let’s name this project BME280. Be sure to create the components directory in the BME280 folder.

Create a new Component

Create a new folder in the components folder and name it bme280_common.

Navigate into the bme280_common folder and create two new folders named src and include. In this same bme280_common folder create a file named CMakeLists.txt.

Create two more bme280_common folders, one in include and one in src, and then finally create bme280_common.cpp and bme280_common.h

BME280 common component structure
BME280_common component structure

Configure CMakeLists.txt

Open CMakeLists.txt and insert the following:

set(SOURCES src/bme280_common/bme280_common.cpp)
            
idf_component_register(SRCS ${SOURCES}
                    INCLUDE_DIRS include)

This will tell the build system where to find the component header and source file.

Define the Class in the header file

Open bme280_common.h and add the following code:

#pragma once

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

namespace CPPBME280
{
    class BME280
    {
        // Registers
        constexpr static uint8_t HUM_LSB = 0xFE;
        constexpr static uint8_t HUM_MSB = 0xFD;
        constexpr static uint8_t TEMP_XLSB = 0xFC;
        constexpr static uint8_t TEMP_LSB = 0xFB;
        constexpr static uint8_t TEMP_MSB = 0xFA;
        constexpr static uint8_t PRESS_XLSB = 0xF9;
        constexpr static uint8_t PRESS_LSB = 0xF8;
        constexpr static uint8_t PRESS_MSB = 0xF7;
        constexpr static uint8_t CONFIG = 0xF5;
        constexpr static uint8_t CTRL_MEAS = 0xF4;
        constexpr static uint8_t STATUS = 0xF3;
        constexpr static uint8_t CTRL_HUM = 0xF2;
        constexpr static uint8_t RESET = 0xE0;
        constexpr static uint8_t ID = 0xD0;

        // Settings
        constexpr static uint8_t pressureSensorDisable = 0x00 << 2;
        constexpr static uint8_t pressureOversamplingX1 = 0x01 << 2;
        constexpr static uint8_t pressureOversamplingX2 = 0x02 << 2;
        constexpr static uint8_t pressureOversamplingX4 = 0x03 << 2;
        constexpr static uint8_t pressureOversamplingX8 = 0x04 << 2;
        constexpr static uint8_t pressureOversamplingX16 = 0x05 << 2;
        constexpr static uint8_t temperatureSensorDisable = 0x00 << 5;
        constexpr static uint8_t temperatureOversamplingX1 = 0x01 << 5;
        constexpr static uint8_t temperatureOversamplingX2 = 0x02 << 5;
        constexpr static uint8_t temperatureOversamplingX4 = 0x03 << 5;
        constexpr static uint8_t temperatureOversamplingX8 = 0x04 << 5;
        constexpr static uint8_t temperatureOversamplingX16 = 0x05 << 5;
        constexpr static uint8_t sensorSleepMode = 0x00;
        constexpr static uint8_t sensorForcedMode = 0x01;
        constexpr static uint8_t sensorNormalMode = 0x03;

        constexpr static uint8_t configStandby0_5ms = 0x00 << 5;
        constexpr static uint8_t configStandby62_5ms = 0x01 << 5;
        constexpr static uint8_t configStandby125ms = 0x02 << 5;
        constexpr static uint8_t configStandby250ms = 0x03 << 5;
        constexpr static uint8_t configStandby500ms = 0x04 << 5;
        constexpr static uint8_t configStandby1000ms = 0x05 << 5;
        constexpr static uint8_t configStandby10ms = 0x06 << 5;
        constexpr static uint8_t configStandby20ms = 0x07 << 5;
        constexpr static uint8_t configFilterOff = 0x00 << 2;
        constexpr static uint8_t configFilter2 = 0x01 << 2;
        constexpr static uint8_t configFilter4 = 0x02 << 2;
        constexpr static uint8_t configFilter8 = 0x03 << 2;
        constexpr static uint8_t configFilter16 = 0x04 << 2;

        constexpr static uint8_t humiditySensorDisable = 0x00;
        constexpr static uint8_t humidityOversamplingX1 = 0x01;
        constexpr static uint8_t humidityOversamplingX2 = 0x02;
        constexpr static uint8_t humidityOversamplingX4 = 0x03;
        constexpr static uint8_t humidityOversamplingX8 = 0x04;
        constexpr static uint8_t humidityOversamplingX16 = 0x05;

    private:
        struct SensorRawData
        {
            long temperature = 0;
            unsigned long humididty = 0;
            unsigned long pressure = 0;
        };

        uint8_t _humidityOversamplingValue = humidityOversamplingX1;    // Default to 1X over sampling
        uint8_t _pressureOversamplingValue = pressureOversamplingX1;    // Default to 1X over sampling
        uint8_t _temperatureOversamplingValue = temperatureOversamplingX1; // Default to 1X over sampling
        uint8_t _sensorModeValue = sensorForcedMode;              // Default to forced mode

        // Calibration Data
        unsigned short  dig_t1 = 0;
        signed short    dig_t2 = 0;
        signed short    dig_t3 = 0;
        signed long     t_fine = 0;
        unsigned short  dig_p1 = 0;
        signed short    dig_p2 = 0;
        signed short    dig_p3 = 0;
        signed short    dig_p4 = 0;
        signed short    dig_p5 = 0;
        signed short    dig_p6 = 0;
        signed short    dig_p7 = 0;
        signed short    dig_p8 = 0;
        signed short    dig_p9 = 0;
        uint8_t         dig_h1 = 0;
        signed short    dig_h2 = 0;
        uint8_t         dig_h3 = 0;
        signed short    dig_h4 = 0;
        signed short    dig_h5 = 0;
        signed char     dig_h6 = 0;

        int getStatus();
        int getCalibrateData();
        int getSensorData(SensorRawData *resultRaw);
        float compensateTemp(const signed long adc_T);
        float compensatePressure(const unsigned long adc_P);
        int compensateHumidity(const unsigned long adc_H);

    protected:
        virtual esp_err_t writeByteData(const uint8_t reg, const uint8_t value);
        virtual int readByteData(const uint8_t reg);
        virtual int readWordData(const uint8_t reg);
        virtual esp_err_t readBlockData(const uint8_t reg, uint8_t *buf, const int length);
        
    public:
        struct BME280ResultData
        {
            float temperature = 0.0;
            int humididty = 0;
            float pressure = 0.0;
        } results;

        esp_err_t Init(const uint8_t humidityOversampling = humidityOversamplingX1,
                       const uint8_t temperatureOversampling = temperatureOversamplingX1,
                       const uint8_t pressureOversampling = pressureOversamplingX1,
                       const uint8_t sensorMode = sensorForcedMode);
        //esp_err_t Close(void);
        int GetDeviceID(void);
        esp_err_t SetConfig(const uint8_t config);
        esp_err_t SetConfigStandbyT(const uint8_t standby);   // config bits 7, 6, 5  page 30
        esp_err_t SetConfigFilter(const uint8_t filter);      // config bits 4, 3, 2
        esp_err_t SetCtrlMeas(const uint8_t ctrlMeas);
        esp_err_t SetTemperatureOversampling(const uint8_t tempOversampling);     // ctrl_meas bits 7, 6, 5   page 29
        esp_err_t SetPressureOversampling(const uint8_t pressureOversampling);    // ctrl_meas bits 4, 3, 2
        esp_err_t SetOversampling(const uint8_t tempOversampling, const uint8_t pressureOversampling);
        esp_err_t SetMode(const uint8_t mode);                                    // ctrl_meas bits 1, 0
        esp_err_t SetCtrlHum(const int humididtyOversampling);                    // ctrl_hum bits 2, 1, 0    page 28
        esp_err_t GetAllResults(BME280ResultData *results);
        esp_err_t GetAllResults(float *temperature, int *humidity, float *pressure);
        float GetTemperature(void);    // Preferable to use GetAllResults()
        float GetPressure(void);       
        int GetHumidity(void);       
        bool StatusMeasuringBusy(void); // check status (0xF3) bit 3
        bool ImUpdateBusy(void);        // check status (0xF3) bit 0
        esp_err_t Reset(void);                // write 0xB6 into reset (0xE0)
    };
} // namespace CPPBME280

That’s quite the mouth full, but don’t let it intimidate you, let’s look at all the parts;

#pragma once

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

namespace CPPBME280
{
    class BME280
    {

This is only the include guard, includes, namespace, and class declaration.

        // Registers
        constexpr static uint8_t HUM_LSB = 0xFE;
        constexpr static uint8_t HUM_MSB = 0xFD;
        constexpr static uint8_t TEMP_XLSB = 0xFC;
        constexpr static uint8_t TEMP_LSB = 0xFB;
        constexpr static uint8_t TEMP_MSB = 0xFA;
        constexpr static uint8_t PRESS_XLSB = 0xF9;
        constexpr static uint8_t PRESS_LSB = 0xF8;
        constexpr static uint8_t PRESS_MSB = 0xF7;
        constexpr static uint8_t CONFIG = 0xF5;
        constexpr static uint8_t CTRL_MEAS = 0xF4;
        constexpr static uint8_t STATUS = 0xF3;
        constexpr static uint8_t CTRL_HUM = 0xF2;
        constexpr static uint8_t RESET = 0xE0;
        constexpr static uint8_t ID = 0xD0;

Here we define all the register addresses. In pure C or older style C++, this would have been done with the #define keyword. However, with constexpr, you are able to strongly type the variables which should provide more useful information when debugging.

For more information on the registers, please look at chapter 5 of the datasheet.

        // Settings
        constexpr static uint8_t pressureSensorDisable = 0x00 << 2;
        constexpr static uint8_t pressureOversamplingX1 = 0x01 << 2;
        constexpr static uint8_t pressureOversamplingX2 = 0x02 << 2;
        constexpr static uint8_t pressureOversamplingX4 = 0x03 << 2;
        constexpr static uint8_t pressureOversamplingX8 = 0x04 << 2;
        constexpr static uint8_t pressureOversamplingX16 = 0x05 << 2;
        constexpr static uint8_t temperatureSensorDisable = 0x00 << 5;
        constexpr static uint8_t temperatureOversamplingX1 = 0x01 << 5;
        constexpr static uint8_t temperatureOversamplingX2 = 0x02 << 5;
        constexpr static uint8_t temperatureOversamplingX4 = 0x03 << 5;
        constexpr static uint8_t temperatureOversamplingX8 = 0x04 << 5;
        constexpr static uint8_t temperatureOversamplingX16 = 0x05 << 5;
        constexpr static uint8_t sensorSleepMode = 0x00;
        constexpr static uint8_t sensorForcedMode = 0x01;
        constexpr static uint8_t sensorNormalMode = 0x03;

        constexpr static uint8_t configStandby0_5ms = 0x00 << 5;
        constexpr static uint8_t configStandby62_5ms = 0x01 << 5;
        constexpr static uint8_t configStandby125ms = 0x02 << 5;
        constexpr static uint8_t configStandby250ms = 0x03 << 5;
        constexpr static uint8_t configStandby500ms = 0x04 << 5;
        constexpr static uint8_t configStandby1000ms = 0x05 << 5;
        constexpr static uint8_t configStandby10ms = 0x06 << 5;
        constexpr static uint8_t configStandby20ms = 0x07 << 5;
        constexpr static uint8_t configFilterOff = 0x00 << 2;
        constexpr static uint8_t configFilter2 = 0x01 << 2;
        constexpr static uint8_t configFilter4 = 0x02 << 2;
        constexpr static uint8_t configFilter8 = 0x03 << 2;
        constexpr static uint8_t configFilter16 = 0x04 << 2;

        constexpr static uint8_t humiditySensorDisable = 0x00;
        constexpr static uint8_t humidityOversamplingX1 = 0x01;
        constexpr static uint8_t humidityOversamplingX2 = 0x02;
        constexpr static uint8_t humidityOversamplingX4 = 0x03;
        constexpr static uint8_t humidityOversamplingX8 = 0x04;
        constexpr static uint8_t humidityOversamplingX16 = 0x05;

Here we are defining all the available settings and modes for the BME280 device. Of the possible settings and options were obtained from chapter 5 in the datasheet.

private:
        struct SensorRawData
        {
            long temperature = 0;
            unsigned long humididty = 0;
            unsigned long pressure = 0;
        };

The first of the private members will be a struct to hold all of the raw ADC values as received from the device. I package them as a struct because then I only need to pass a single address for all three variables when I pass a pointer to the struct to the compensation methods.

        uint8_t _humidityOversamplingValue = humidityOversamplingX1;    // Default to 1X over sampling
        uint8_t _pressureOversamplingValue = pressureOversamplingX1;    // Default to 1X over sampling
        uint8_t _temperatureOversamplingValue = temperatureOversamplingX1; // Default to 1X over sampling
        uint8_t _sensorModeValue = sensorForcedMode;              // Default to forced mode

The next 4 private variables will hold the internal states of some of the settings and set initial values for the settings.

        // Calibration Data
        unsigned short  dig_t1 = 0;
        signed short    dig_t2 = 0;
        signed short    dig_t3 = 0;
        signed long     t_fine = 0;
        unsigned short  dig_p1 = 0;
        signed short    dig_p2 = 0;
        signed short    dig_p3 = 0;
        signed short    dig_p4 = 0;
        signed short    dig_p5 = 0;
        signed short    dig_p6 = 0;
        signed short    dig_p7 = 0;
        signed short    dig_p8 = 0;
        signed short    dig_p9 = 0;
        uint8_t         dig_h1 = 0;
        signed short    dig_h2 = 0;
        uint8_t         dig_h3 = 0;
        signed short    dig_h4 = 0;
        signed short    dig_h5 = 0;
        signed char     dig_h6 = 0;

Because all sensors are a little bit different, it is necessary to calibrate each sensor to get accurate measurements. Each BME280 device contains a set of calibration registers that hold the calibration data. These are the variables that will hold the calibration data. This calibration data will then be used in the compensation methods to calculate the temperature, humidity, and pressure.

The API places this data into a struct and passes them to the compensation methods by reference. Because we are using C++, we can declare the variables inside of a class making them always available and unique to a given instance of the class. For this reason, I’m not creating a struct for the calibration data, the compensation methods will access the private variables directly.

        int getStatus();
        int getCalibrateData();
        int getSensorData(SensorRawData *resultRaw);
        float compensateTemp(const signed long adc_T);
        float compensatePressure(const unsigned long adc_P);
        int compensateHumidity(const unsigned long adc_H);

These are the private methods that will only ever be used internally by the class. These are private methods and will not be visible to the end-user.

  • getStatus Indicated whether the sensor is busy taking measurements or updating registers.
  • getCalibrateData Populates the calibration variables with the device-specific factory preprogrammed calibration data used in the compensation formulas.
  • getSensorData Read the raw sensor data of the device.
  • compensateTemp Uses the raw sensor data and calibration data to calculate the temperature in degrees C. Returns a floating-point with a resolution of 0.01.
  • compensatePressure Uses the raw sensor data and calibration data to calculate the pressure in Pa. Returns a floating point.
  • compensateHumidity Uses the raw sensor data and calibration data to calculate the relative humidity as a percentage. The relative humidity is returned as an integer.
    protected:
        virtual esp_err_t writeByteData(const uint8_t reg, const uint8_t value);
        virtual int readByteData(const uint8_t reg);
        virtual int readWordData(const uint8_t reg);
        virtual esp_err_t readBlockData(const uint8_t reg, uint8_t *buf, const int length);

These are the protected methods. Protected methods are basically the same as private methods with the exception that protected methods can be inherited whereas private cannot.

You’ll also notice that both methods are virtual. This is actually a beautiful example to demonstrate where you want to use the virtual keyword. Simply put a virtual method is declared here, so it tells the compiler that the method does exist but will be defined later. This enables us to use the virtual methods in our implementation as though the classes exist without getting compiler errors.

We will later create SPI and I2C implementations of these methods, The virtual method will then be replaced by the implementation that we use. I’ve prepared exampled for both I2C and SPI.

    public:
        struct BME280ResultData
        {
            float temperature = 0.0;
            int humididty = 0;
            float pressure = 0.0;
        } results;

Now to move on to the public section. Firstly we create a struct to hold the temperature, humidity, and pressure. This is used if you want to declare a struct to hold the measurements in the main project.

        esp_err_t Init(const uint8_t humidityOversampling = humidityOversamplingX1,
                       const uint8_t temperatureOversampling = temperatureOversamplingX1,
                       const uint8_t pressureOversampling = pressureOversamplingX1,
                       const uint8_t sensorMode = sensorForcedMode);

The initinilazion method. We are setting default values for all of the arguments. This means that if we don’t provide arguments when calling this method then the defaults will be used.

        int GetDeviceID(void);
        esp_err_t SetConfig(const uint8_t config);
        esp_err_t SetConfigStandbyT(const uint8_t standby);   // config bits 7, 6, 5  page 30
        esp_err_t SetConfigFilter(const uint8_t filter);      // config bits 4, 3, 2
        esp_err_t SetCtrlMeas(const uint8_t ctrlMeas);
        esp_err_t SetTemperatureOversampling(const uint8_t tempOversampling);     // ctrl_meas bits 7, 6, 5   page 29
        esp_err_t SetPressureOversampling(const uint8_t pressureOversampling);    // ctrl_meas bits 4, 3, 2
        esp_err_t SetOversampling(const uint8_t tempOversampling, const uint8_t pressureOversampling);
        esp_err_t SetMode(const uint8_t mode);                                    // ctrl_meas bits 1, 0
        esp_err_t SetCtrlHum(const int humididtyOversampling);                    // ctrl_hum bits 2, 1, 0    page 28
        esp_err_t GetAllResults(BME280ResultData *results);
        esp_err_t GetAllResults(float *temperature, int *humidity, float *pressure);
        float GetTemperature(void);    // Preferable to use GetAllResults()
        float GetPressure(void);       
        int GetHumidity(void);       
        bool StatusMeasuringBusy(void); // check status (0xF3) bit 3
        bool ImUpdateBusy(void);        // check status (0xF3) bit 0
        esp_err_t Reset(void);                // write 0xB6 into reset (0xE0)

The last lot of public methods. The method names describe what the methods will do. I’ll point out some nuances when we do the implementations in the next section.

Implement the class in the source file

Open bme280_common.cpp and add the following code:

#include <memory>
#include "bme280_common/bme280_common.h"

namespace CPPBME280
{
    int BME280::getStatus()
    {
        return readByteData(STATUS);
    }

    int BME280::getCalibrateData()
    {
        //============================== Temperature Calibration Data ===========================
        dig_t1 = static_cast<unsigned short>(readWordData(0x88));
        dig_t2 = static_cast<signed short>(readWordData(0x8A));
        dig_t3 = static_cast<signed short>(readWordData(0x8C));
        //=======================================================================================
        //============================== Pressure Calibration Data ==============================
        dig_p1 = static_cast<unsigned short>(readWordData(0x8E));
        dig_p2 = static_cast<signed short>(readWordData(0x90));
        dig_p3 = static_cast<signed short>(readWordData(0x92));
        dig_p4 = static_cast<signed short>(readWordData(0x94));
        dig_p5 = static_cast<signed short>(readWordData(0x96));
        dig_p6 = static_cast<signed short>(readWordData(0x98));
        dig_p7 = static_cast<signed short>(readWordData(0x9A));
        dig_p8 = static_cast<signed short>(readWordData(0x9C));
        dig_p9 = static_cast<signed short>(readWordData(0x9E));
        //=======================================================================================
        //============================== Humidity Calibration Data ==============================
        dig_h1 = static_cast<unsigned char>(readByteData(0xA1));
        dig_h2 = static_cast<signed short>(readWordData(0xE1));
        dig_h3 = static_cast<unsigned char>(readByteData(0xE3));
        int8_t digH4Msb = static_cast<int8_t>(readByteData(0xE4));
        int8_t digH4H5Shared = static_cast<int8_t>(readByteData(0xE5)); // this register hold parts of the values of dig_H4 and dig_h5
        int8_t digH5Msb = static_cast<int8_t>(readByteData(0xE6));
        dig_h6 = static_cast<int8_t>(readByteData(0xE7));

        dig_h4 = static_cast<signed short>(digH4Msb << 4 | (digH4H5Shared & 0x0F));        // split and shift the bits appropriately.
        dig_h5 = static_cast<signed short>(digH5Msb << 4 | ((digH4H5Shared & 0xF0) >> 4)); // split and shift the bits appropriately.
        //=======================================================================================

        return 0;
    }

    int BME280::getSensorData(SensorRawData *resultRaw)
    {
        esp_err_t status = ESP_OK;
        std::unique_ptr<uint8_t[]> buff = std::make_unique<uint8_t[]>(8);

        if (_sensorModeValue == sensorForcedMode)
        {
            SetMode(sensorForcedMode);
            while (StatusMeasuringBusy() || ImUpdateBusy())
            {
                vTaskDelay(pdMS_TO_TICKS(50));
            }
        }

        status = readBlockData(PRESS_MSB, buff.get(), 8);

        uint8_t pressMsb = buff[0];
        uint8_t pressLsb = buff[1];
        uint8_t pressXlsb = buff[2];
        uint8_t tempMsb = buff[3];
        uint8_t tempLsb = buff[4];
        uint8_t tempXlsb = buff[5];
        uint8_t humMsb = buff[6];
        uint8_t humLsb = buff[7];

        resultRaw->temperature = tempMsb << 12 | tempLsb << 4 | tempXlsb >> 4;
        resultRaw->pressure = pressMsb << 12 | pressLsb << 4 | pressXlsb >> 4;
        resultRaw->humididty = humMsb << 8 | humLsb;

        return status;
    }

    float BME280::compensateTemp(const signed long adc_T)
    {
        int32_t var1;
        int32_t var2;
        int32_t temperature;
        int32_t temperature_min = -4000;
        int32_t temperature_max = 8500;

        var1 = (int32_t)((adc_T / 8) - ((int32_t)dig_t1 * 2));
        var1 = (var1 * ((int32_t)dig_t2)) / 2048;
        var2 = (int32_t)((adc_T / 16) - ((int32_t)dig_t1));
        var2 = (((var2 * var2) / 4096) * ((int32_t)dig_t3)) / 16384;
        t_fine = var1 + var2;
        temperature = (t_fine * 5 + 128) / 256;

        if (temperature < temperature_min)
        {
            temperature = temperature_min;
        }
        else if (temperature > temperature_max)
        {
            temperature = temperature_max;
        }

        float returnTemperature = temperature;

        return returnTemperature / 100;
    }

    float BME280::compensatePressure(const unsigned long adc_P)
    {
        int64_t var1;
        int64_t var2;
        int64_t var3;
        int64_t var4;
        uint32_t pressure;
        uint32_t pressure_min = 3000000;
        uint32_t pressure_max = 11000000;

        var1 = ((int64_t)t_fine) - 128000;
        var2 = var1 * var1 * (int64_t)dig_p6;
        var2 = var2 + ((var1 * (int64_t)dig_p5) * 131072);
        var2 = var2 + (((int64_t)dig_p4) * 34359738368);
        var1 = ((var1 * var1 * (int64_t)dig_p3) / 256) + ((var1 * ((int64_t)dig_p2) * 4096));
        var3 = ((int64_t)1) * 140737488355328;
        var1 = (var3 + var1) * ((int64_t)dig_p1) / 8589934592;

        /* To avoid divide by zero exception */
        if (var1 != 0)
        {
            var4 = 1048576 - adc_P;
            var4 = (((var4 * INT64_C(2147483648)) - var2) * 3125) / var1;
            var1 = (((int64_t)dig_p9) * (var4 / 8192) * (var4 / 8192)) / 33554432;
            var2 = (((int64_t)dig_p8) * var4) / 524288;
            var4 = ((var4 + var1 + var2) / 256) + (((int64_t)dig_p7) * 16);
            pressure = (uint32_t)(((var4 / 2) * 100) / 128);

            if (pressure < pressure_min)
            {
                pressure = pressure_min;
            }
            else if (pressure > pressure_max)
            {
                pressure = pressure_max;
            }
        }
        else
        {
            pressure = pressure_min;
        }

        float returnPressure = pressure;

        return returnPressure / 100;
    }

    int BME280::compensateHumidity(const unsigned long adc_H)
    {
        int32_t var1;
        int32_t var2;
        int32_t var3;
        int32_t var4;
        int32_t var5;
        uint32_t humidity;
        uint32_t humidity_max = 102400;

        var1 = t_fine - ((int32_t)76800);
        var2 = (int32_t)(adc_H * 16384);
        var3 = (int32_t)(((int32_t)dig_h4) * 1048576);
        var4 = ((int32_t)dig_h5) * var1;
        var5 = (((var2 - var3) - var4) + (int32_t)16384) / 32768;
        var2 = (var1 * ((int32_t)dig_h6)) / 1024;
        var3 = (var1 * ((int32_t)dig_h3)) / 2048;
        var4 = ((var2 * (var3 + (int32_t)32768)) / 1024) + (int32_t)2097152;
        var2 = ((var4 * ((int32_t)dig_h2)) + 8192) / 16384;
        var3 = var5 * var2;
        var4 = ((var3 / 32768) * (var3 / 32768)) / 128;
        var5 = var3 - ((var4 * ((int32_t)dig_h1)) / 16);
        var5 = (var5 < 0 ? 0 : var5);
        var5 = (var5 > 419430400 ? 419430400 : var5);
        humidity = (uint32_t)(var5 / 4096);

        if (humidity > humidity_max)
        {
            humidity = humidity_max;
        }

        return humidity/1024;
    }

    esp_err_t BME280::Init(const uint8_t humidityOversampling,
                           const uint8_t temperatureOversampling,
                           const uint8_t pressureOversampling,
                           const uint8_t sensorMode)
    {
        _humidityOversamplingValue = humidityOversampling;
        _pressureOversamplingValue = pressureOversampling;
        _temperatureOversamplingValue = temperatureOversampling;
        _sensorModeValue = sensorMode;

        esp_err_t status = ESP_OK;

        status |= writeByteData(CONFIG, 0); // Enable SPI 4-wire
        status |= getCalibrateData();
        status |= writeByteData(CTRL_HUM, _humidityOversamplingValue);
        status |= writeByteData(CTRL_MEAS, _pressureOversamplingValue | _temperatureOversamplingValue | _sensorModeValue);

        return status;
    }

    int BME280::GetDeviceID()
    {
        return readByteData(ID);
    }

    esp_err_t BME280::SetConfig(const uint8_t config)
    {
        return writeByteData(CONFIG, config);
    }

    esp_err_t BME280::SetConfigStandbyT(const uint8_t standby) // config bits 7, 6, 5  page 30
    {
        uint8_t temp = readByteData(CONFIG) & 0b00011111;

        return writeByteData(CONFIG, temp | standby);
    }

    esp_err_t BME280::SetConfigFilter(const uint8_t filter) // config bits 4, 3, 2
    {
        uint8_t temp = readByteData(CONFIG);
        temp = temp & 0b11100011;
        temp = temp | filter << 2;

        return writeByteData(CONFIG, temp);
    }

    esp_err_t BME280::SetCtrlMeas(const uint8_t ctrlMeas)
    {
        _pressureOversamplingValue = 0 | (ctrlMeas & 0b11100011);
        _temperatureOversamplingValue = 0 | (ctrlMeas & 0b00011111);
        _sensorModeValue = 0 | (ctrlMeas & 0b11111100);

        return writeByteData(CTRL_MEAS, ctrlMeas);
    }

    esp_err_t BME280::SetTemperatureOversampling(const uint8_t tempOversampling) // ctrl_meas bits 7, 6, 5   page 29
    {
        uint8_t temp = readByteData(CTRL_MEAS) & 0b00011111;
        _temperatureOversamplingValue = tempOversampling;

        return writeByteData(CTRL_MEAS, temp | tempOversampling);
    }

    esp_err_t BME280::SetPressureOversampling(const uint8_t pressureOversampling) // ctrl_meas bits 4, 3, 2
    {
        uint8_t temp = readByteData(CTRL_MEAS) & 0b11100011;
        _pressureOversamplingValue = pressureOversampling;

        return writeByteData(CTRL_MEAS, temp | pressureOversampling);
    }

    esp_err_t BME280::SetOversampling(const uint8_t tempOversampling, const uint8_t pressureOversampling)
    {
        _pressureOversamplingValue = 0 | pressureOversampling;
        _temperatureOversamplingValue = 0 | tempOversampling;

        return writeByteData(CTRL_MEAS, tempOversampling | pressureOversampling | _sensorModeValue);
    }

    esp_err_t BME280::SetMode(const uint8_t mode) // ctrl_meas bits 1, 0
    {
        uint8_t temp = readByteData(CTRL_MEAS) & 0b11111100;
        _sensorModeValue = mode;

        return writeByteData(CTRL_MEAS, temp | mode);
    }

    esp_err_t BME280::SetCtrlHum(const int humididtyOversampling) // ctrl_hum bits 2, 1, 0    page 28
    {
        _humidityOversamplingValue = humididtyOversampling;
        
        return writeByteData(CTRL_HUM, humididtyOversampling);
    }

    esp_err_t BME280::GetAllResults(BME280ResultData *results)
    {
        esp_err_t status = ESP_OK;
        SensorRawData resultRaw{};

        status = getSensorData(&resultRaw);

        results->temperature = compensateTemp(resultRaw.temperature);
        results->humididty = compensateHumidity(resultRaw.humididty);
        results->pressure = compensatePressure(resultRaw.pressure);

        return status;
    }

    esp_err_t BME280::GetAllResults(float *temperature, int *humidity, float *pressure)
    {
        esp_err_t status = ESP_OK;
        SensorRawData resultRaw{};

        status = getSensorData(&resultRaw);

        *temperature = compensateTemp(resultRaw.temperature);
        *humidity = compensateHumidity(resultRaw.humididty);
        *pressure = compensatePressure(resultRaw.pressure);

        return status;
    }

    float BME280::GetTemperature(void) // Preferable to use GetAllResults()
    {
        BME280ResultData results{};

        GetAllResults(&results);

        return results.temperature; // compensateTemp(resultRaw.temperature);
    }

    float BME280::GetPressure(void)
    {
        BME280ResultData results{};

        GetAllResults(&results);

        return results.pressure;
    }

    int BME280::GetHumidity(void)
    {
        BME280ResultData results{};

        GetAllResults(&results);

        return results.humididty;
    }

    bool BME280::StatusMeasuringBusy(void) // check status (0xF3) bit 3
    {
        return ((readByteData(STATUS) & 8) == 8) ? true : false;
    }

    bool BME280::ImUpdateBusy(void) // check status (0xF3) bit 0
    {
        return ((readByteData(STATUS) & 1) == 1) ? true : false;
    }
    
    esp_err_t BME280::Reset(void) // write 0xB6 into reset (0xE0)
    {
        return writeByteData(RESET, 0xB6);
    }

} // namespace CPPBME280

Let’s look at the individual parts.

    int BME280::getStatus()
    {
        return readByteData(STATUS);
    }

This method returns the value of the STATUS register.

    int BME280::getCalibrateData()
    {
        //============================== Temperature Calibration Data ===========================
        dig_t1 = static_cast<unsigned short>(readWordData(0x88));
        dig_t2 = static_cast<signed short>(readWordData(0x8A));
        dig_t3 = static_cast<signed short>(readWordData(0x8C));
        //=======================================================================================
        //============================== Pressure Calibration Data ==============================
        dig_p1 = static_cast<unsigned short>(readWordData(0x8E));
        dig_p2 = static_cast<signed short>(readWordData(0x90));
        dig_p3 = static_cast<signed short>(readWordData(0x92));
        dig_p4 = static_cast<signed short>(readWordData(0x94));
        dig_p5 = static_cast<signed short>(readWordData(0x96));
        dig_p6 = static_cast<signed short>(readWordData(0x98));
        dig_p7 = static_cast<signed short>(readWordData(0x9A));
        dig_p8 = static_cast<signed short>(readWordData(0x9C));
        dig_p9 = static_cast<signed short>(readWordData(0x9E));
        //=======================================================================================
        //============================== Humidity Calibration Data ==============================
        dig_h1 = static_cast<unsigned char>(readByteData(0xA1));
        dig_h2 = static_cast<signed short>(readWordData(0xE1));
        dig_h3 = static_cast<unsigned char>(readByteData(0xE3));
        int8_t digH4Msb = static_cast<int8_t>(readByteData(0xE4));
        int8_t digH4H5Shared = static_cast<int8_t>(readByteData(0xE5)); // this register hold parts of the values of dig_H4 and dig_h5
        int8_t digH5Msb = static_cast<int8_t>(readByteData(0xE6));
        dig_h6 = static_cast<int8_t>(readByteData(0xE7));

        dig_h4 = static_cast<signed short>(digH4Msb << 4 | (digH4H5Shared & 0x0F));        // split and shift the bits appropriately.
        dig_h5 = static_cast<signed short>(digH5Msb << 4 | ((digH4H5Shared & 0xF0) >> 4)); // split and shift the bits appropriately.
        //=======================================================================================

        return 0;
    }

This method reads the factory programmed calibration data and populates it into the calibration data variables.

The casting of these values is very important. If the variables are cast incorrectly then the compensation formulas are going to return incorrect results.

More information on this can be found in section 4.2 in the datasheet.

    int BME280::getSensorData(SensorRawData *resultRaw)
    {
        esp_err_t status = ESP_OK;
        std::unique_ptr<uint8_t[]> buff = std::make_unique<uint8_t[]>(8);

        if (_sensorModeValue == sensorForcedMode)
        {
            SetMode(sensorForcedMode);
            while (StatusMeasuringBusy() || ImUpdateBusy())
            {
                vTaskDelay(pdMS_TO_TICKS(50));
            }
        }

        status = readBlockData(PRESS_MSB, buff.get(), 8);

        uint8_t pressMsb = buff[0];
        uint8_t pressLsb = buff[1];
        uint8_t pressXlsb = buff[2];
        uint8_t tempMsb = buff[3];
        uint8_t tempLsb = buff[4];
        uint8_t tempXlsb = buff[5];
        uint8_t humMsb = buff[6];
        uint8_t humLsb = buff[7];

        resultRaw->temperature = tempMsb << 12 | tempLsb << 4 | tempXlsb >> 4;
        resultRaw->pressure = pressMsb << 12 | pressLsb << 4 | pressXlsb >> 4;
        resultRaw->humididty = humMsb << 8 | humLsb;

        return status;
    }

This method reads the sensor data from the BME280 device.

Let’s take a closer look at how this is done:

std::unique_ptr<uint8_t[]> buff = std::make_unique<uint8_t[]>(8);

This is essentially a safer way to create uint8_t buff[8]{};

        if (_sensorModeValue == sensorForcedMode)
        {
            SetMode(sensorForcedMode);
            while (StatusMeasuringBusy() || ImUpdateBusy())
            {
                vTaskDelay(pdMS_TO_TICKS(50));
            }
        }

If the sensor is configured to operate in forced mode, then the sensor should be in standby mode at this stage.

We then set the device to forced mode to signal the device to make measurements.

We then check if the measurements are completed and the registers updated every 50ms. The 50ms time is an arbitrary number I selected, you may be able to optimize this for better performance.

If the device is configured for continuous mode then this code block is not executed.

status = readBlockData(PRESS_MSB, buff.get(), 8);

Here we read 8 bytes of data into the buff variable created earlier. We start the read at the PRESS_MSB register.

        uint8_t pressMsb = buff[0];
        uint8_t pressLsb = buff[1];
        uint8_t pressXlsb = buff[2];
        uint8_t tempMsb = buff[3];
        uint8_t tempLsb = buff[4];
        uint8_t tempXlsb = buff[5];
        uint8_t humMsb = buff[6];
        uint8_t humLsb = buff[7];

Here I’m placing the bytes in buff into more logically named variables. This step is not needed but it makes for more readable code. It should be optimized out during compilation.

        resultRaw->temperature = tempMsb << 12 | tempLsb << 4 | tempXlsb >> 4;
        resultRaw->pressure = pressMsb << 12 | pressLsb << 4 | pressXlsb >> 4;
        resultRaw->humididty = humMsb << 8 | humLsb;

Using some bitwise operators to populate the variables in the restultRaw struct.

    float BME280::compensateTemp(const signed long adc_T)
    {
        int32_t var1;
        int32_t var2;
        int32_t temperature;
        int32_t temperature_min = -4000;
        int32_t temperature_max = 8500;

        var1 = (int32_t)((adc_T / 8) - ((int32_t)dig_t1 * 2));
        var1 = (var1 * ((int32_t)dig_t2)) / 2048;
        var2 = (int32_t)((adc_T / 16) - ((int32_t)dig_t1));
        var2 = (((var2 * var2) / 4096) * ((int32_t)dig_t3)) / 16384;
        t_fine = var1 + var2;
        temperature = (t_fine * 5 + 128) / 256;

        if (temperature < temperature_min)
        {
            temperature = temperature_min;
        }
        else if (temperature > temperature_max)
        {
            temperature = temperature_max;
        }

        float returnTemperature = temperature;

        return returnTemperature / 100;
    }

    float BME280::compensatePressure(const unsigned long adc_P)
    {
        int64_t var1;
        int64_t var2;
        int64_t var3;
        int64_t var4;
        uint32_t pressure;
        uint32_t pressure_min = 3000000;
        uint32_t pressure_max = 11000000;

        var1 = ((int64_t)t_fine) - 128000;
        var2 = var1 * var1 * (int64_t)dig_p6;
        var2 = var2 + ((var1 * (int64_t)dig_p5) * 131072);
        var2 = var2 + (((int64_t)dig_p4) * 34359738368);
        var1 = ((var1 * var1 * (int64_t)dig_p3) / 256) + ((var1 * ((int64_t)dig_p2) * 4096));
        var3 = ((int64_t)1) * 140737488355328;
        var1 = (var3 + var1) * ((int64_t)dig_p1) / 8589934592;

        /* To avoid divide by zero exception */
        if (var1 != 0)
        {
            var4 = 1048576 - adc_P;
            var4 = (((var4 * INT64_C(2147483648)) - var2) * 3125) / var1;
            var1 = (((int64_t)dig_p9) * (var4 / 8192) * (var4 / 8192)) / 33554432;
            var2 = (((int64_t)dig_p8) * var4) / 524288;
            var4 = ((var4 + var1 + var2) / 256) + (((int64_t)dig_p7) * 16);
            pressure = (uint32_t)(((var4 / 2) * 100) / 128);

            if (pressure < pressure_min)
            {
                pressure = pressure_min;
            }
            else if (pressure > pressure_max)
            {
                pressure = pressure_max;
            }
        }
        else
        {
            pressure = pressure_min;
        }

        float returnPressure = pressure;

        return returnPressure / 100;
    }

    int BME280::compensateHumidity(const unsigned long adc_H)
    {
        int32_t var1;
        int32_t var2;
        int32_t var3;
        int32_t var4;
        int32_t var5;
        uint32_t humidity;
        uint32_t humidity_max = 102400;

        var1 = t_fine - ((int32_t)76800);
        var2 = (int32_t)(adc_H * 16384);
        var3 = (int32_t)(((int32_t)dig_h4) * 1048576);
        var4 = ((int32_t)dig_h5) * var1;
        var5 = (((var2 - var3) - var4) + (int32_t)16384) / 32768;
        var2 = (var1 * ((int32_t)dig_h6)) / 1024;
        var3 = (var1 * ((int32_t)dig_h3)) / 2048;
        var4 = ((var2 * (var3 + (int32_t)32768)) / 1024) + (int32_t)2097152;
        var2 = ((var4 * ((int32_t)dig_h2)) + 8192) / 16384;
        var3 = var5 * var2;
        var4 = ((var3 / 32768) * (var3 / 32768)) / 128;
        var5 = var3 - ((var4 * ((int32_t)dig_h1)) / 16);
        var5 = (var5 < 0 ? 0 : var5);
        var5 = (var5 > 419430400 ? 419430400 : var5);
        humidity = (uint32_t)(var5 / 4096);

        if (humidity > humidity_max)
        {
            humidity = humidity_max;
        }

        return humidity/1024;
    }

These are the methods to get the compensated measurement data.

The formulas were copied from the vendor API which can be found on their web page. I only made the following changes:

  • Removed the struct for the calibration data.
  • Changed the return type for temperature and pressure to float and return the API return value divided by 100. The methods now return usable Temperature and Pressure values.
    esp_err_t BME280::Init(const uint8_t humidityOversampling,
                           const uint8_t temperatureOversampling,
                           const uint8_t pressureOversampling,
                           const uint8_t sensorMode)
    {
        _humidityOversamplingValue = humidityOversampling;
        _pressureOversamplingValue = pressureOversampling;
        _temperatureOversamplingValue = temperatureOversampling;
        _sensorModeValue = sensorMode;

        esp_err_t status = ESP_OK;

        status |= writeByteData(CONFIG, 0); // Enable SPI 4-wire
        status |= getCalibrateData();
        status |= writeByteData(CTRL_HUM, _humidityOversamplingValue);
        status |= writeByteData(CTRL_MEAS, _pressureOversamplingValue | _temperatureOversamplingValue | _sensorModeValue);

        return status;
    }

Initialize the BME280 device. If no arguments are provided then defaults are used.

    int BME280::GetDeviceID()
    {
        return readByteData(ID);
    }

    esp_err_t BME280::SetConfig(const uint8_t config)
    {
        return writeByteData(CONFIG, config);
    }

    esp_err_t BME280::SetConfigStandbyT(const uint8_t standby) // config bits 7, 6, 5  page 30
    {
        uint8_t temp = readByteData(CONFIG) & 0b00011111;

        return writeByteData(CONFIG, temp | standby);
    }

    esp_err_t BME280::SetConfigFilter(const uint8_t filter) // config bits 4, 3, 2
    {
        uint8_t temp = readByteData(CONFIG);
        temp = temp & 0b11100011;
        temp = temp | filter << 2;

        return writeByteData(CONFIG, temp);
    }

    esp_err_t BME280::SetCtrlMeas(const uint8_t ctrlMeas)
    {
        _pressureOversamplingValue = 0 | (ctrlMeas & 0b11100011);
        _temperatureOversamplingValue = 0 | (ctrlMeas & 0b00011111);
        _sensorModeValue = 0 | (ctrlMeas & 0b11111100);

        return writeByteData(CTRL_MEAS, ctrlMeas);
    }

    esp_err_t BME280::SetTemperatureOversampling(const uint8_t tempOversampling) // ctrl_meas bits 7, 6, 5   page 29
    {
        uint8_t temp = readByteData(CTRL_MEAS) & 0b00011111;
        _temperatureOversamplingValue = tempOversampling;

        return writeByteData(CTRL_MEAS, temp | tempOversampling);
    }

    esp_err_t BME280::SetPressureOversampling(const uint8_t pressureOversampling) // ctrl_meas bits 4, 3, 2
    {
        uint8_t temp = readByteData(CTRL_MEAS) & 0b11100011;
        _pressureOversamplingValue = pressureOversampling;

        return writeByteData(CTRL_MEAS, temp | pressureOversampling);
    }

    esp_err_t BME280::SetOversampling(const uint8_t tempOversampling, const uint8_t pressureOversampling)
    {
        _pressureOversamplingValue = 0 | pressureOversampling;
        _temperatureOversamplingValue = 0 | tempOversampling;

        return writeByteData(CTRL_MEAS, tempOversampling | pressureOversampling | _sensorModeValue);
    }

    esp_err_t BME280::SetMode(const uint8_t mode) // ctrl_meas bits 1, 0
    {
        uint8_t temp = readByteData(CTRL_MEAS) & 0b11111100;
        _sensorModeValue = mode;

        return writeByteData(CTRL_MEAS, temp | mode);
    }

    esp_err_t BME280::SetCtrlHum(const int humididtyOversampling) // ctrl_hum bits 2, 1, 0    page 28
    {
        _humidityOversamplingValue = humididtyOversampling;
        
        return writeByteData(CTRL_HUM, humididtyOversampling);
    }

A few utility methods to set some setup options etc.

    esp_err_t BME280::GetAllResults(BME280ResultData *results)
    {
        esp_err_t status = ESP_OK;
        SensorRawData resultRaw{};

        status = getSensorData(&resultRaw);

        results->temperature = compensateTemp(resultRaw.temperature);
        results->humididty = compensateHumidity(resultRaw.humididty);
        results->pressure = compensatePressure(resultRaw.pressure);

        return status;
    }

    esp_err_t BME280::GetAllResults(float *temperature, int *humidity, float *pressure)
    {
        esp_err_t status = ESP_OK;
        SensorRawData resultRaw{};

        status = getSensorData(&resultRaw);

        *temperature = compensateTemp(resultRaw.temperature);
        *humidity = compensateHumidity(resultRaw.humididty);
        *pressure = compensatePressure(resultRaw.pressure);

        return status;
    }

This is a method with overloaded variables depending on how the user wants the received measured data to be presented.

The main part to take away is that the measurement values are returned by a reference.

    float BME280::GetTemperature(void) // Preferable to use GetAllResults()
    {
        BME280ResultData results{};

        GetAllResults(&results);

        return results.temperature; // compensateTemp(resultRaw.temperature);
    }

    float BME280::GetPressure(void)
    {
        BME280ResultData results{};

        GetAllResults(&results);

        return results.pressure;
    }

    int BME280::GetHumidity(void)
    {
        BME280ResultData results{};

        GetAllResults(&results);

        return results.humididty;
    }

These are the methods to return only one of the measured results.

Note that all of these methods call GetAllResults even though only one is needed. I do it this way because we always need the temperature measurements to be taken even if they will not be used. We use some of the variables used in the temperature compensation method.

    bool BME280::StatusMeasuringBusy(void) // check status (0xF3) bit 3
    {
        return ((readByteData(STATUS) & 8) == 8) ? true : false;
    }

    bool BME280::ImUpdateBusy(void) // check status (0xF3) bit 0
    {
        return ((readByteData(STATUS) & 1) == 1) ? true : false;
    }

    esp_err_t BME280::Reset(void) // write 0xB6 into reset (0xE0)
    {
        return writeByteData(RESET, 0xB6);
    }

These are some utility methods.

Create I2C and SPI interface components for the BME280

Create bme280_i2c component

This process is the same as creating the bme280_common component earlier, only the name changes.

Create a new folder in the components folder and name it bme280_i2c.

Navigate into the bme280_i2c folder and create two new folders named src and include. In this same bme280_i2c folder create a file named CMakeLists.txt.

Create two more bme280_i2c folders, one in include and one in src, and then finally create bme280_i2c.cpp and bme280_i2c.h

bme280_i2c component folder structure
bme280_i2c component folder structure

At this point, you might be wondering why we are creating a new component when we can just add the new header and source files in the existing bme280_common component folder. The reason is that bme280_i2c and bme280_spi will have different dependencies. We don’t want to have to include the SPI component if we are only going to use the I2C component for example.

Configure CMakeLists.txt

Open CMakeLists.txt and insert the following:

set(SOURCES src/bme280_i2c/bme280_i2c.cpp)
            
idf_component_register(SRCS ${SOURCES}
                    INCLUDE_DIRS include
                    REQUIRES bme280_common CPPI2C)

This will tell the build system where to find the component header and source file.

Notice that we now have one additional line, REQUIRES. This tells the build system that this component is dependent on the bme280_common and CPPI2C components and will fail to compile (linker error) if the required components cannot be found.

Define the Class in the header file

Open bme280_i2c.h and add the following code:

#pragma once

#include "bme280_common/bme280_common.h"
#include "CPPI2C/cppi2c.h"

namespace CPPBME280
{
    class BME280I2C : public BME280
    {
    private:
        CPPI2C::I2c *i2c;
        uint8_t _devAddress{};

    protected:
        esp_err_t writeByteData(const uint8_t reg, const uint8_t value);
        int readByteData(const uint8_t reg);
        int readWordData(const uint8_t reg);
        esp_err_t readBlockData(const uint8_t reg, uint8_t *buf, const int length);

    public:
        void InitI2c(CPPI2C::I2c *i_i2c, const uint8_t dev_addr = 0x76);
    }; // namespace CPPBME280
} // namespace CPPBME280

The I2C and SPI interface child classes for the bme280 are very short. All we are doing is implementing the virtual classes defined in the bme280_common parent class and adding some support variables and initialization methods to facilitate the communication protocol.

Let’s look at the different parts.

namespace CPPBME280

We are going to use the same namespace for all the BME280-related classes.

class BME280I2C : public BME280

The BME280I2C class will inherit from the BME280 class. We want all of the public methods in the BME280 class to also be public for the BME280I2C class, so we add the public keyword.

    private:
        CPPI2C::I2c *i2c;
        uint8_t _devAddress{};

We have 2 private variables:

  • * i2c This is a pointer to our I2C driver object. This has to be a pointer because we are going to pass in a reference to the I2C object to the BME280I2C class. This way we can create the I2C object outside of this class which will enable us to pass it by reference to several other device drivers to be used.
  • _devAddress This variable will hold the address of the BME280. The BME280 address can be configured to be either 0x76 or 0x77.
    protected:
        esp_err_t writeByteData(const uint8_t reg, const uint8_t value);
        int readByteData(const uint8_t reg);
        int readWordData(const uint8_t reg);
        esp_err_t readBlockData(const uint8_t reg, uint8_t *buf, const int length);

These are copies of the virtual methods defined in the BME280 class. The implementations here will be called wherever these methods are called in the parent class.

    public:
        void InitI2c(CPPI2C::I2c *i_i2c, const uint8_t dev_addr = 0x76);

The only public method this class adds is to receive the device address and a valid address to an I2C object.

Implement the class in the source file

Open bme280_i2c.cpp and add the following code:

#include "bme280_i2c/bme280_i2c.h"

namespace CPPBME280
{
    esp_err_t BME280I2C::writeByteData(const uint8_t reg, const uint8_t value)
    {
        return i2c->WriteRegister(_devAddress, reg, value);
    }

    int BME280I2C::readByteData(const uint8_t reg)
    {
        return i2c->ReadRegister(_devAddress, reg);
    }

    int BME280I2C::readWordData(const uint8_t reg)
    {
        uint8_t buff[2];
        i2c->ReadRegisterMultipleBytes(_devAddress, reg, buff, 2);
        return buff[1] << 8 | buff[0];
    }

    esp_err_t BME280I2C::readBlockData(const uint8_t reg, uint8_t *buf, const int length)
    {
        return i2c->ReadRegisterMultipleBytes(_devAddress, reg, buf, length);
    }

    void BME280I2C::InitI2c(CPPI2C::I2c *i_i2c, const uint8_t dev_addr)
    {
        i2c = i_i2c;
        _devAddress = dev_addr;
    }
} // namespace CPPBME280

All this interface class really does is add the device address and then call the appropriate read or write method.

Create bme280_spi component

This is exactly the same as the bme280_i2c detailed earlier in this tutorial.

Create a new folder in the components folder and name it bme280_spi.

Navigate into the bme280_spi folder and create two new folders named src and include. In this same bme280_spi folder create a file named CMakeLists.txt.

Create two more bme280_spi folders, one in include and one in src, and then finally create bme280_spi.cpp and bme280_spi.h

BME280_spi component folder structure
BME280_spi component folder structure

Configure CMakeLists.txt

Open CMakeLists.txt and insert the following:

set(SOURCES src/bme280_spi/bme280_spi.cpp)
            
idf_component_register(SRCS ${SOURCES}
                    INCLUDE_DIRS include
                    REQUIRES bme280_common CPPSPI)

Instead of adding CPPI2C to the REQUIRES list, we this time add CPPSPI.

Define the Class in the header file

Open bme280_spi.h and add the following code:

#pragma once

#include "bme280_common/bme280_common.h"
#include "CPPSPI/cppspi.h"

namespace CPPBME280
{
    constexpr static uint8_t MODE = 0;
    constexpr static uint8_t ADDR_BITS = 7;
    constexpr static uint8_t CMD_BITS = 1;

    constexpr static uint8_t SPI_WRITE = 0;
    constexpr static uint8_t SPI_READ = 1;

    class BME280SPI : public BME280
    {
    private:
        CPPSPI::Spi *spi;

    protected:
        esp_err_t writeByteData(const uint8_t reg, const uint8_t value);
        int readByteData(const uint8_t reg);
        int readWordData(const uint8_t reg);
        esp_err_t readBlockData(const uint8_t reg, uint8_t *buf, const int length);

    public:
        esp_err_t InitSpi(CPPSPI::Spi *i_spi, const int ss);
    }; // namespace CPPBME280
}

The SPI method looks nearly identical to the I2C interface class. Let’s only discuss the differences.

    constexpr static uint8_t MODE = 0;
    constexpr static uint8_t ADDR_BITS = 7;
    constexpr static uint8_t CMD_BITS = 1;

Here we define some SPI-specific constants.

  • MODE defines what SPI mode will be used. The BME280 support SPI mode 0 and 3
  • ADDR_BITS defines the number of bits used for register addresses
  • CMD_BITS defines the number of bits used to indicate the SPI command. The BME280 only supports a read and write command.
    constexpr static uint8_t SPI_WRITE = 0;
    constexpr static uint8_t SPI_READ = 1;

Here we define the SPI commands.

  • SPI_WRITE the command to initiate a write operation into the indicated register address.
  • SPI_READ the command to initiate a read operation from the indicated register address.
    private:
        CPPSPI::Spi *spi;

The only private variable is a pointer to a valid SPI object. SPI does not need an address, the individual devices are selected using an electrical connection (the ss pin) that will enable or disable the SPI communications of the connected slave device.

    public:
        esp_err_t InitSpi(CPPSPI::Spi *i_spi, const int ss);

Even the initialization method looks the same, only the SPI method accepts the ss pin number while the I2C method accepts the device address.

Implement the class in the source file

Open bme280_spi.cpp and add the following code:

#include "bme280_spi/bme280_spi.h"

namespace CPPBME280
{
    esp_err_t BME280SPI::writeByteData(const uint8_t reg, const uint8_t value)
    {
        return spi->WriteRegister(reg, value, SPI_WRITE);
    }

    int BME280SPI::readByteData(const uint8_t reg)
    {
        return spi->ReadRegister(reg, SPI_READ);
    }

    int BME280SPI::readWordData(const uint8_t reg)
    {
        uint8_t buff[2];
        spi->ReadRegisterMultipleBytes(reg, buff, 2, SPI_READ);
        return buff[1] << 8 | buff[0];
    }

    esp_err_t BME280SPI::readBlockData(const uint8_t reg, uint8_t *buf, const int length)
    {
        return spi->ReadRegisterMultipleBytes(reg, buf, length, SPI_READ);
    }

    esp_err_t BME280SPI::InitSpi(CPPSPI::Spi *i_spi, const int ss)
    {
        spi = i_spi;

        return spi->RegisterDevice(MODE, ss, ADDR_BITS, CMD_BITS);
    }
} // namespace CPPBME280

All this interface class really does is select the correct command and then call the appropriate read or write method.

One method of note is:

    esp_err_t BME280SPI::InitSpi(CPPSPI::Spi *i_spi, const int ss)
    {
        spi = i_spi;

        return spi->RegisterDevice(MODE, ss, ADDR_BITS, CMD_BITS);

Here the SPI slave device is registered to the SPI object with its unique settings. Notably is the ss or Slave Select pin. This is the pin that will enable or disable the SPI communication port on the BME280 device. The ss pin must be connected to a GPIO pin on the ESP32.

Read sensor data from the BME280 using I2C and SPI

Now that all the hard work is done writing the device drivers, let’s get some sensor data and print it to the terminal.

Add the SPI and I2C components to the project

The BME280 driver is dependent on either I2C or SPI drivers. Luckily we already created the drivers in the following tutorials:

The I2C and SPI components can also be downloaded from GitHub here.

To use these components, copy them into the components folder. You might have to do a full clear and rebuild the project if you’ve compiled the project earlier.

Now with all the components added to the project, the project folder structure should look like the illustration.

BME280-Project-Folder-Structures
BME280 Project Folder Structures

Configure CMakeLists.txt

Open CMakeLists.txt in the project root folder and add the following code:

# For more information about build system see
# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html
# The following five lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.8)
set(CMAKE_CXX_STANDARD 17)
set(EXTRA_COMPONENT_DIRS components)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(BME280)

This is the same boilerplate that must be added to all projects to use C++.

The part I want to draw your attention to is:

set(EXTRA_COMPONENT_DIRS components)

This line tells the build system to look for extra component directories in the component folder. Because we set up our components in a specific way with a CMakeLists.txt for each, we only have to copy the component into the components folders and the project will be able to find and compile it.

Connect the BME280 modules to the ESP32

For this demonstration, I used 2 BME280 modules to demonstrate both I2C and SPI communication modes.

Connect the modules as per the schematic below:

BME280 Schematic
BME280 I2C and SPI connections

for more information, please check out sections 7.2 and 7.3 of the BME280 datasheet.

The pin numbers used on the ESP32 will become apparent in the code.

main.cpp

Open main.cc in the main folder and add the following code:

#include <iostream>

#include "CPPSPI/cppspi.h"
#include "CPPI2C/cppi2c.h"
#include "bme280_spi/bme280_spi.h"
#include "bme280_i2c/bme280_i2c.h"

constexpr static int SPI_3_MISO = 19;
constexpr static int SPI_3_MOSI = 23;
constexpr static int SPI_3_SCLK = 18;
constexpr static int BME280_SS_PIN = 5;

constexpr static int I2C_SDA = 17;
constexpr static int I2C_SCL = 16;
constexpr static uint32_t I2C_CLK_SPEED_HZ = 400000;

CPPI2C::I2c i2c {I2C_NUM_0};
CPPSPI::Spi spi3;

extern "C" void app_main(void)
{    
    CPPBME280::BME280SPI bme280spi;
    CPPBME280::BME280I2C bme280i2c;

    // Initialise the I2C
    i2c.InitMaster(I2C_SDA, I2C_SCL, I2C_CLK_SPEED_HZ, true, true);

    // Initialize the SPI
    spi3.Init(SPI3_HOST, SPI_3_MISO, SPI_3_MOSI, SPI_3_SCLK);

    // Register BME280 device as SPI device using GPIO5 as the select pin
    bme280spi.InitSpi(&spi3, BME280_SS_PIN);
    // Initialize the BME280SPI device
    bme280spi.Init();
    bme280spi.SetMode(1);
    bme280spi.SetConfigFilter(1);

    // Initialize the BME280I2C device
    bme280i2c.InitI2c(&i2c, 0x76);
    bme280i2c.Init();
    bme280i2c.SetMode(1);
    bme280i2c.SetConfigFilter(1);

    float spiTemperature{};
    float spiPressure{};
    int spiHumidity{};
    int spiId{};

    float i2cTemperature{};
    float i2cPressure{};
    int i2cHumidity{};
    int i2cId{};

    while(true)
    {
        spiId = bme280spi.GetDeviceID();
        i2cId = bme280i2c.GetDeviceID();
        bme280spi.GetAllResults(&spiTemperature, &spiHumidity, &spiPressure);
        bme280i2c.GetAllResults(&i2cTemperature, &i2cHumidity, &i2cPressure);
        std::cout << "==================================================\n";
        std::cout << "SPI Temperature: " << spiTemperature << "c\n";
        std::cout << "SPI Humidity   : " << spiHumidity << "%\n";
        std::cout << "SPI Pressure   : " << spiPressure << "Pa\n";
        std::cout << "SPI ID         : " << spiId << '\n';

        std::cout << "--------------------------------------------------\n";

        std::cout << "I2C Temperature: " << i2cTemperature << "c\n";
        std::cout << "I2C Humidity   : " << i2cHumidity << "%\n";
        std::cout << "I2C Pressure   : " << i2cPressure << "Pa\n";
        std::cout << "I2C ID         : " << i2cId << '\n';
        std::cout << "==================================================\n";

        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

Let’s take a look at the different parts of the main program:

#include <iostream>

Here we include iostream to give us access to std::cout which allows us to write to the serial port.

#include "CPPSPI/cppspi.h"
#include "CPPI2C/cppi2c.h"
#include "bme280_spi/bme280_spi.h"
#include "bme280_i2c/bme280_i2c.h"

These are the includes to all of our components. This is all we need to do to use the components.

constexpr static int SPI_3_MISO = 19;
constexpr static int SPI_3_MOSI = 23;
constexpr static int SPI_3_SCLK = 18;
constexpr static int BME280_SS_PIN = 5;

Here we define the pins used for the SPI. Use this as a guide to connecting the BME280 module to the ESP32 device. You may also select different pins here, however, I would advise keeping to these specific pins for SPI, changing the pins will half the maximum available speed for the SPI3 module.

constexpr static int I2C_SDA = 17;
constexpr static int I2C_SCL = 16;
constexpr static uint32_t I2C_CLK_SPEED_HZ = 400000;

Here we define the pins used for the I2C. Use this as a reference to connect the BME280 module to the ESP32 device. You may choose different available pins.

We also define the I2C speed as 400000, or 400kbps. This is known as fast mode. It is possible to increase this speed, however, faster speeds might not be supported by all the devices on the I2C bus. You also need to pay attention to bus capacitance and pay extra special attention to the value of the pull-up resistors when increasing the speed.

CPPI2C::I2c i2c {I2C_NUM_0};
CPPSPI::Spi spi3;

Here we are creating our I2C and SPI objects. We do this outside of the classes that will use these objects so that we can pass pointers to these objects to many different peripheral objects. For instance, you might have an I2C BME280 sensor as well as an I2C OLED display. We want to be able to drive both using the same I2C driver.

extern "C" void app_main(void)
{    

app_main is the entry point for any program written for the ESP32. We add the extern “C” directive to tell the compiler not to change the function name during compilation. This is only necessary for C++.

    CPPBME280::BME280SPI bme280spi;
    CPPBME280::BME280I2C bme280i2c;

Here we create objects for both the I2C and SPI versions of the BME280 driver. Notice that we never reference bme280_common.h. That is because the BME280SPI and BME280I2C classes inherited all of the methods and variables from BME280 class defined in bme280_common.h and bme280_common.cpp

    // Initialise the I2C
    i2c.InitMaster(I2C_SDA, I2C_SCL, I2C_CLK_SPEED_HZ, true, true);

    // Initialize the SPI
    spi3.Init(SPI3_HOST, SPI_3_MISO, SPI_3_MOSI, SPI_3_SCLK);

Here we are initializing the I2C and SPI divers. Notice that I’m enabling the internal pull-up resistors for the I2C. I’m doing this to simplify the breadboard circuit, however, in real-world applications, it’s usually better to use external pull-up resistors where you can more precisely choose the values. This becomes especially important when the number of devices on the bus and the speed increase.

I’m not adding the ss pin to the SPI driver, that is because the ss pin is slave specific and each slave will get its own. We will define the ss pin when we add the BME280 device as an SPI slave.

    // Register BME280 device as SPI device using GPIO5 as the select pin
    bme280spi.InitSpi(&spi3, BME280_SS_PIN);
    // Initialize the BME280SPI device
    bme280spi.Init();
    bme280spi.SetMode(1);
    bme280spi.SetConfigFilter(1);

Here we initialize the SPI for the BME280 device. Initialize is actually the wrong word, we are passing a pointer of the SPI driver to the BME280_SPI object and registering the BME280 to that SPI driver.

The ampersand (&) that we add before the spi3 object means that we are passing the memory address of the spi3 object rather than the actual spi3 object to the InitSpi method.

bme280spi.Init() initializes the BME280 device. I’m calling bme280spi.SetMode(1) and bme280spi.SetConfigFilter(1) to test some features. Feel free to play around with the settings of the BME280 device. It’s bad practice to put natural numbers as arguments to methods or functions like I did here, in a few months’ time when someone revisits the code no one is going to know what the 1 means. Always define natural numbers with a logical name. The proper way to do it would be bme280spi.SetMode(BME280_FORCED_MODE) and define a constexpr static variable named BME280_FORCED_MODE as an integer with the value one.

    // Initialize the BME280I2C device
    bme280i2c.InitI2c(&i2c, 0x76);
    bme280i2c.Init();
    bme280i2c.SetMode(1);
    bme280i2c.SetConfigFilter(1);

InitI2c accepts a point to an I2C object and the I2C address of a slave device. The above code is actually bad practice, I should have defined the I2C address to a logical name. I’m leaving it like this in the tutorial to point out what not to do.

Other than the InitI2C method, this code is exactly the same as the for the bme280spi. It’s actually calling the exact same methods defined and implemented in bme280_common.

    float spiTemperature{};
    float spiPressure{};
    int spiHumidity{};
    int spiId{};

    float i2cTemperature{};
    float i2cPressure{};
    int i2cHumidity{};
    int i2cId{};

These are zero-initialized variables that will hold the various reading that we are going to receive from our BME280 devices.

    while(true)
    {

This is the main loop of the application.

        spiId = bme280spi.GetDeviceID();
        i2cId = bme280i2c.GetDeviceID();

These methods read the register that holds the device ID and returns it. You will notice that the value that we receive (96) is different from the value specified in the datasheet (0x60). The reason is that we are printing the value in decimal representation. 0x60 is the same as 96.

I could have formatted the output to display the hexadecimal value, however, I wanted to point out this pitfall.

        bme280spi.GetAllResults(&spiTemperature, &spiHumidity, &spiPressure);
        bme280i2c.GetAllResults(&i2cTemperature, &i2cHumidity, &i2cPressure);

Here we are calling the method to get the sensor data. Notice the ampersand (&) before each variable. That means we are sending the memory address of the variable rather than the variable itself as an argument. This way any changes made to the variables in the method will reflect everywhere the variable is used. This can be dangerous in some circumstances, however, in this case, it allows us to get all three sensor values using a single call.

        std::cout << "==================================================\n";
        std::cout << "SPI Temperature: " << spiTemperature << "c\n";
        std::cout << "SPI Humidity   : " << spiHumidity << "%\n";
        std::cout << "SPI Pressure   : " << spiPressure << "Pa\n";
        std::cout << "SPI ID         : " << spiId << '\n';

        std::cout << "--------------------------------------------------\n";

        std::cout << "I2C Temperature: " << i2cTemperature << "c\n";
        std::cout << "I2C Humidity   : " << i2cHumidity << "%\n";
        std::cout << "I2C Pressure   : " << i2cPressure << "Pa\n";
        std::cout << "I2C ID         : " << i2cId << '\n';
        std::cout << "==================================================\n";

        vTaskDelay(pdMS_TO_TICKS(1000));

Here we print the results to the serial port, wait for one second and repeat.

You can now compile the project and you should see the following output.

If you receive an error that some files cannot be found. Then make sure all the components are in the components folder and do a full clean and rebuild

Terminal Output
BME280 C++ Terminal Output

Notice the sensor data is slightly different, this is normal tolerances between devices and is within specifications defined in the datasheet.

Conclusion

In this tutorial, we learned a number of things.

  1. How virtual methods can be used to create interfaces
  2. That we can use the same code for different communication implementations
  3. How to add and use custom components in a project
  4. How to create a driver for the BME280 device to work in both SPI and I2C mode

The SPI and I2C drivers used are some of the most commonly used interfaces that you are likely to come across. A large number of devices, modules etc. use either I2C or SPI. Having robust and easy-to-use libraries for both of these protocols is sure to open many doors.

Thanks for reading.

Similar Posts