Introduction: Library for BMP280 and BME280

About: I am a retired professional engineer, now farmer. Taking an interest in all things technological and in building devices useful on the farm.

Introduction

I did not set out to write this library. It "happened" as a side-effect of a project I started that uses a BMP280. That project is not yet finished, but I think the library is ready to share with others.
Subsequently I had a need to use a BME280, which adds humidity measurement to the pressure and temperature capability of the BMP280. The BME280 is "backward-compatible" with the BMP280 - that is, all the registers and the steps needed to read pressure and temperature from the BME280 are the same as those used for the BMP280. There are additional registers and steps needed to read humidity, applicable to the BME280 only. This raises the question, one library for both, or two separate libraries. The hardware for the two device types is fully interchangeable. Even many of the modules being sold (for example on Ebay and AliExpress) are labelled BME/P280. To find out which type it is, you have to look at the (miniscule) writing on the sensor itself, or test the device ID byte. I decided to go for a single library. It seems to have worked out OK.

Feedback, especially any suggestions for improvements, will be appreciated.

Library features and capabilities

A library is a piece of software that provides an Application Programming Interface (API) for a programmer to exercise the capabilities of the device, without necessarily having to deal with all the fine-grain details. Desirably, the API should be easy for a beginner with simple requirements to get started, while providing for full exploitation of the device capabilities. Desirably the library should follow any specific guidelines from the device manufacturer, as well as general software good practice. I have endeavoured to achieve all of these.
When starting out with the BMP280, I found 3 different libraries for it: Adafruit_BMP280; Seeed_BMP280; and one called BMP280 from the device manufacturer. Neither Adafruit nor Seeed provided extended capabilities, although they worked well and were easy to use for basic applications. I could not figure our how to use the one produced by the device manufacturer (Bosch Sensortec). This may be my deficiency, rather than theirs. However the library was much more complicated than the other two, I could not find any instructions or examples of use (I subsequently found examples were in the file "bmp280_support.c", however these were not particularly helpful to me).

As a result of these factors, I decided to write my own library for the BMP280.

Looking into the library situation for the BME280, I found separate libraries Adafruit_BME280, Seed_BME280 and another one BME280_MOD-1022 written by Embedded Adventures. None of them combined the functions for BMP280 in a library capable of using the BME280. None of them explicitly supported the capability of the devices to store a few bits of data while the device and its controlling microprocessor are sleeping (this capability is evident in the datasheet and supported in the library I have written and described here).

A combined library should have support for all the capabilities of the BME280, but when used with a BMP280 it should not impose any overhead from the unused functions. Benefits of a combined library include fewer library files to manage, easy mix-and-match of different devices in the same project, and simplified changes for maintenance or upgrades which only have to be done in one place rather than two. These are probably all quite minor, even insignificant, but ...

Device capabilities

The BMP280 and BME280 are surface-mount devices about 5mm square and 1 mm high. There are 8 interface pads, including 2 separate power input pads and two Ground pads. They are available on eBay as a module with either 4 or 6 pins brought out.
The 4-pin module has a fixed I2C address and cannot be configured to use the SPI protocol.

The 6-pin module or the bare device can be used with either I2C or SPI protocols. In I2C mode it can have two different addresses, achieved by connecting the SDO pin either to Ground (for base address = 0x76) or to Vdd (for base address +1 = 0x77). In SPI mode it has the usual arrangement of 1 clock, 2 data (one for each direction) and a device select pin (CS).

The library I wrote and describe here only supports I2C. The Adafruit_BMP280 and the BME_MOD-1022 libraries have support for both i2C and SPI.

The library can be downloaded here:

https://github.com/farmerkeith/BMP280-library

Step 1: Setting Up the Hardware

Before the library can be useful it is necessary to connect a microcontroller to the BMP280 (or to two of them if you wish).

I used a WeMos D1 mini pro, so I will show its connections. Other microcontrollers will be similar, you just need to get the SDA and SCL pins connected correctly.

In the case of the WeMos D1 mini pro, the connections are:

Function         WeMos pin    BMP280 pin     Notes
SDA               D2           SDA
SCL               D1           SCL
Vdd               3V3          Vin          Nominal 3.3V
Ground                         GND
Address control                SDO          Ground or Vdd 
I2C select                     CSB          Vdd (GND selects SPI)

Note that the SDO pin on some of the MP280 modules is labelled SDD, and the Vdd pin may be labelled VCC.
Note: SDA and SCL lines should have pull-up resistors between the line and the Vin pin. Typically a value of 4.7K should be OK. Some BMP280 and BME280 modules have 10K pull-up resistors included in the module (which is not good practice, since putting multiple devices on the I2C bus may load it excessively). However using 2 BME/P280 modules each with a 10K resistor should not be a problem in practice so long as there are not too many other devices on the same bus also with pull-up resistors.

Once you have the hardware connected, you can easily check whether your device is a BMP280 or a BME280 by running the sketch I2CScan_ID which you can find here: https://github.com/farmerkeith/I2CScanner

You can also check whether you have a BMP280 or BME280 by looking at the device itself. I found it necessary to use a digital microscope to do this, but if your eyesight is very good you may be able to do it without any aids. There are two lines of printing on the casing of the device. The key is the first letter on the second line, which in the case of BMP280 devices is a "K" and in the case of BME280 devices is a "U".

Step 2: APIs Provided by the Library

Including the library in a sketch

The library is included in a sketch in the standard way using the statement

#include "farmerkeith_BMP280.h"

This statement needs to be included in the early part of the sketch prior to the start of the setup() function.

Creating a BME or BMP software object

There are 3 levels for creating the BMP280 software object. The simplest is just

bme280 objectName; <br>or
bmp280 objectName; 

for example, BMP280 bmp0;

This creates a software object with the default address of 0x76 (ie for SDO connected to ground).

The next level for creating a BME280 or BMP280 software object has a parameter of either 0 or 1, as follows:

bme280 objectNameA(0); 
bmp280 objectNameB(1);

The parameter (0 or 1) is added to the I2C base address, so that two BME280 or BMP280 devices can be used on the same I2C bus (including one of each).

The third level for creating a BME or BMP280 software object has two parameters. The first parameter, which is either 0 or 1, is for the address, as for the previous case. The second parameter controls debug printing. If it is set to 1, each transaction with the software object results in Serial.print outputs that enables the programmer to see the details of the transaction. For example:

bmp280 objectNameB(1,1);

If the debug printing parameter is set to 0, the software object reverts to normal behaviour (no printing).

This statement or statements needs to be included after the #include and before the setup() function.

Initialising the BME or BMP software object

Before being used, it is necessary to read the calibration parameters from the device, and to configure it for whatever measurement mode, oversampling, and filter settings are appropriate.

For a simple, general purpose initialisation, the statement is:

objectName.begin();

This version of begin() reads the calibration parameters from the device and sets osrs_t=7 (16 temperature measurements), osrs_p=7 (16 pressure measurements), mode=3 (continuous, Normal), t_sb=0 (0.5 ms sleep between measurement sets), filter=0 (K=1, so no filtering) and spiw_en=0 (SPI disabled, so use I2C). In the case of the BME280, there is an extra parameter osrs_h=7 for 16 humidity measurements.

There is another version of begin() that takes all six (or 7) parameters. The equivalent of the above statement is

objectName.begin(7,7,3,0,0,0); // osrs_t, osrs_p, mode, t_sb, filter, spiw_en
or 
objectName.begin(7,7,3,0,0,0,7); // osrs_t, osrs_p, mode, t_sb, filter, spiw_en, osrs_h

The full list of codes and their meanings is in the BME280 and BMP280 data sheet, and also in the comments in the .cpp file in the library.

Simple temperature and pressure measurement

To get a temperature measurement the simplest way is

double temperature=objectName.readTemperature (); // measure temperature

To get a pressure measurement the simplest way is

double pressure=objectName.readPressure (); // measure pressure

To get a humidity measurement the simplest way is

double humidity=objectName.readHumidity (); // measure humidity (BME280 only)

To get both temperature and pressure the above two statements can be used one after the other, but there is another option, which is:

double temperature;
double pressure=objectName.readPressure (temperature); // measure pressure and temperature

This statement reads the data from the BME280 or BMP280 device only once, and returns both temperature and pressure. This is slightly more efficient use of the I2C bus and ensures that the two readings correspond to the same measurement cycle.

For the BME 280, a combined statement that gets all three values (humidity, temperature and pressure) is:

double temperature, pressure;<br>double humidity=objectName.readHumidity (temperature, pressure); // measure humidity, pressure and temperature 

This statement reads the data from the BMP280 device only once, and returns all three values. This is slightly more efficient use of the I2C bus and ensures that the three readings correspond to the same measurement cycle. Note that the names of the variables can be changed to anything the user likes, but their order is fixed - temperature comes first, and pressure comes second.

These use cases are covered in example sketches provided with the library, being basicTemperature.ino, basicPressure.ino, basicHumidity.ino, basicTemperatureAndPressure.ino and basicHumidityAndTemperatureAndPressure.ino.

More sophisticated temperature and pressure measurement

Although the above series of statements will work without problems, there are a couple of issues:

  1. the device is running continuously, and therefore is consuming power at its maximum level. If the energy is coming from a battery, it may be necessary to reduce this.
  2. due to the power consumed, the device will experience warming, and therefore the measured temperature will be higher than the ambient temperature. I will cover this more in a later step.

A result that uses less power, and gives a temperature that is closer to ambient, can be obtained by using begin() with parameters that put it to sleep (eg mode=0). For example:

objectName.begin(1,1,0,0,0,0[,1]); // osrs_t, osrs_p, mode, t_sb, filter, spiw_en [,osrs_h]

Then, when a measurement is wanted, wake up the device with a configuration command to registers F2 (if required) and F4 that sets the appropriate values of osrs_h, osrs_t and osrs_p, plus mode=1 (single shot mode). For example:

[objectName.updateF2Control(1);]    // osrs_h - never needed for BMP280, 
// and not needed for BME280 if the No. of measurements is not being changed 
// from the value provided in begin().
objectName.updateF4Control(1,1,1); // osrs_t, osrs_p, mode

Having woken up the device, it will start measuring, but the result will not be available for some milliseconds - at least 4 ms, maybe up to 70 ms or more, depending on the number of measurements that have been specified. If the read command is sent immediately, the device will return the values from the previous measurement - which may be acceptable in some applications, but in most cases it is probably better to delay until the new measurement is available.

This delay can be done in several ways.

  1. wait a fixed amount of time to cover the longest expected delay
  2. wait an amount of time calculated from the maximum measurement time per measurement (ie 2.3ms) times the number of measurements, plus overhead, plus a margin.
  3. wait a shorter amount of time calculated as above, but using the nominal measurement time (ie 2 ms) plus overhead, and then start checking the "I am measuring" bit in the status register. When the status bit reads 0 (ie, not measuring), get the temperature and pressure readings.
  4. immediately start checking the status register, and get the temperature and pressure readings when the status bit reads 0,

I will show an example of one way of doing this a bit later.

Configuration register operations

To make all this happen, we need several tools that I have not yet introduced. They are:

byte readRegister(reg) 
void updateRegister(reg, value)

Each of these has several derived commands in the library, which make the software for specific actions a bit simpler.

The example powerSaverPressureAndTemperature.ino uses method No. 3. The line of code that does the repeated checking is

while (bmp0.readRegister(0xF3)>>3); // loop untl F3bit 3 ==0

Note that this sketch is for an ESP8266 microcontroller. I used a WeMos D1 mini pro. The sketch will not work with Atmega microcontrollers, which have different instructions for sleeping. This sketch exercises several other commands, so I will introduce all of them before describing that sketch in more detail.

When the microcontoller is sleeping in parallel with the BMP280 sensor, the configuration of the sensor for the required measurements can be done in the begin() command, using the 6 parameters. However if the microcontroller is not sleeping, but the sensor is, then at the time of measurement the sensor has to be woken up and told its measurement configuration. This can be done directly with

updateRegister(reg, value)

but is a bit easier with the following three commands:

updateF2Control(osrs_h); // BME280 only
updateF4Control(osrs_t, osrs_p, mode); 
updateF5Config(t_sb, filter, spi3W_en);

After the measurement is done, if the mode used is Single shot (Forced mode), then the device will automatically go back to sleep. However, if the measurement set involves multiple measurements using continuous (Normal) mode then the BMP280 will need to be put back to sleep. This can be done with either of the two following commands:

updateF4Control16xSleep();       
updateF4ControlSleep(value);

Both of these set the mode bits to 00 (ie sleep mode). However the first sets the osrs_t and osrs_p to 111 (ie 16 measurements) while the second one stores the low 6 bits from "value" into bits 7:2 of the 0xF4 register.

Similarly the following statement stores the low six bits of "value" into bits 7:2 of the 0xF5 register.

updateF5ConfigSleep(value); 

The use of these latter commands enable storage of 12 bits of information in the BMP280 registers F4 and F5. At least in the case of the ESP8266, when the microcontroller wakes up after a period of sleep, it starts at the beginning of the sketch with no knowledge of its state prior to the sleep command. To store knowledge of its state prior to the sleep command, data can be stored in flash memory, using either the EEPROM functions or by writing a file using SPIFFS. However flash memory has a limitation of the number of write cycles, of the order of 10,000 to 100,000. This means that if the microcontroller is going through a sleep-wake cycle every few seconds, it can exceed the allowable memory write limit in a few months. Storing a few bits of data in the BMP280 has no such limitation.

The data stored in registers F4 and F5 can be recovered when the microcontroller wakes up using the commands

readF4Sleep();
readF5Sleep();

These functions read the corresponding register, shift the contents to remove the 2 LSBs and return the remaining 6 bits. These functions are used in the example sketch powerSaverPressureAndTemperatureESP.ino as follows:

// read value of EventCounter back from bmp0
byte bmp0F4value= bmp0.readF4Sleep(); // 0 to 63
byte bmp0F5value= bmp0.readF5Sleep(); // 0 to 63
eventCounter= bmp0F5value*64+bmp0F4value; // 0 to 4095

These functions read the corresponding register, shift the contents to remove the 2 LSBs and return the remaining 6 bits. These functions are used in the example sketch powerSaverPressureAndTemperature.ino as follows:

// read value of EventCounter back from bmp1
byte bmp1F4value= bmp1.readF4Sleep(); // 0 to 63
byte bmp1F5value= bmp1.readF5Sleep(); // 0 to 63
eventCounter= bmp1F5value*64+bmp1F4value; // 0 to 4095

Raw temperature and pressure functions

The basic readTemperature, readPressure and readHumidity functions have two components. First the raw 20-bit temperature and pressure values are obtained from the BME/P280, or the raw 16-bit humidity value is obtained from the BME280. Then the compensation algorithm is used to generate the output values in degrees Celsius, hPa or %RH.

The library provides separate functions for these components, so that the raw temperature, presssure and humidity data can be obtained, and perhaps manipulated in some way. The algorithm to derive the temperature, pressure and humidity from these raw values is also provided. In the library these algorithms are implemented using double length floating point arithmetic. It works well on the ESP8266 which is a 32-bit processor and uses 64 bits for "double" float variables. Making these functions accessible may be useful for assessing and possibly changing the calculation for other platforms.

These functions are:

readRawPressure (rawTemperature); // reads raw pressure and temperature data from BME/P280<br>readRawHumidity (rawTemperature, rawPressure); // reads raw humidity, temperature and pressure data from BME280
calcTemperature (rawTemperature, t_fine);
calcPressure (rawPressure, t_fine);
calcHumidity (rawHumidity, t_fine)

The "t-fine" argument to these functions is worth a bit of explanation. Both pressure and humidity compensation algorithms include a temperature dependent component which is achieved through the t_fine variable. The calcTemperature function writes a value in t_fine based on the temperature compensation algorithm logic, which is then used as an input in both calcPressure and calcHumidity.

An example of the use of these functions can be found in the example sketch rawPressureAndTemperature.ino, and also in the code for the readHumidity() function in the .cpp file of the library.

Altitude and Sea Level pressure

There is a known relationship between atmospheric pressure and altitude. The weather also influences pressure. When the weather organisations publish atmospheric pressure information, they usually adjust it for altitude and so the "synoptic chart" shows isobars (lines of constant pressure) standardised to mean sea level. So really there are 3 values in this relationship, and knowing two of them enables derivation of the third one. The 3 values are:

  • altitude above sea level
  • actual air pressure at that altitude
  • equivalent air pressure at sea level (more strictly, mean sea level, because instantaneous sea level constantly changes)

This library provides two functions for this relationship, as follows:

calcAltitude (pressure, seaLevelhPa);
calcNormalisedPressure (pressure, altitude);

There is also a simpllified version, which assumes the standard sea level pressure of 1013.15 hPa.

calcAltitude (pressure); // standard seaLevelPressure assumed

Step 3: BMP280 Device Details

Hardware capabilities

The BMP280 has 2 bytes of configuration data (at register addresses 0xF4 and 0xF5) which is used to control multiple measurement and data output options. It also provides 2 bits of status information, and 24 bytes of calibration parameters which are used in converting the raw temperature and pressure values into conventional temperature and pressure units.
The BME280 has additional data as follows:

  • 1 extra byte of configuration data at register address 0xF2 used to control multiple humidity measurements;
  • 8 extra bytes of calibration parameters used in converting the raw humidity value into relative humidity percentage.

The temperature, pressure and status registers for the BME280 are the same as for the BMP280 with minor exceptions as follows:

  • the "ID" bits of the BME280 are set to 0x60, so it can be distinguished from BMP280 which may be 0x56, 0x57 or 0x58
  • the sleep time control (t_sb) is changed so that the two long times in the BMP280 (2000 ms and 4000 ms) are replaced in the BME280 with short times of 10 ms and 20 ms. The maximum sleep time in the BME280 is 1000 ms.
  • In the BME280 the temperature and pressure raw values are always 20 bits if filtering is applied. The use of 16 to 19 bit values is limited to cases with no filtering (ie filter=0).

Temperature and pressure are each 20 bit values, which need to be converted into conventional temperature and pressure via a rather complex algorithm using 3 16 bit calibration parameters for temperature, and 9 16 bit calibration parameters plus the temperature for pressure. The granulatity of the temperature measurement is 0.0003 degrees Celsius for a least significant bit change (20 bit readout), increasing to 0.0046 degrees Celsius if the 16 bit readout is used.

Humidity is a 16 bit value which needs to be converted into relative humidity via another complex algorithm using 6 calibration parameters which are a mix of 8, 12 and 16 bits.

The data sheet shows the absolute accuracy of the temperature readout as +-0.5 C at 25 C and +-1 C over the range 0 to 65 C.

The granularity of the pressure measurement is 0.15 Pascals (ie 0.0015 hectoPascals) at 20 bit resolution, or 2.5 Pascals at 16 bit resolution. The raw pressure value is affected by the temperature, so that around 25C, an increase in temperature of 1 degree C decreases the measured pressure by 24 Pascals. The temperature sensitivity is accounted for in the calibration algorithm, so the delivered pressure values should be accurate at different temperatures.

The data sheet shows the absolute accuracy of the pressure readout as +-1 hPa for temperatures between 0 C and 65 C.

The accuracy of the humidity is given in the data sheet as +-3% RH, and +-1% hysteresis.

How it works

The 24 bytes of temperature and pressure calibration data, and also in the case of the BME280 the 8 bytes of humidity calibration data, have to be read from the device and stored in variables. These data are individually programmed into the device in the factory, so different devices have different values - at least for some of the parameters.
A BME/P280 can be in one of two states. In one state it is measuring. In the other state it is waiting (sleeping).

Which state it is in can be checked by looking at bit 3 of register 0xF3.

The results of the most recent measurement can be obtained at any time by reading the corresponding data value, irrespective of whether the device is sleeping or measuring.

There are also two ways of operating the BME/P280. One is Continuous mode (called Normal mode in the data sheet) which repeatedly cycles between Measuring and Sleeping states. In this mode the device performs a set of measurements, then goes to sleep, then wakes up for another set of measurements, and so on. The number of individual measurements and the duration of the sleep part of the cycle can all be controlled through the configuration registers.

The other way of operating the BME/P280 is Single Shot mode (called Forced mode in the data sheet). In this mode the device is woken from sleep by a command to measure, it does a set of measurements, then goes back to sleep. The number of individual measurements in the set is controlled in the configuration command that wakes up the device.

In the BMP280, if a single measurement is made, the 16 most significant bits in the value are populated, and the four least significant bits in the value readout are all zeros. The number of measurements can be set to 1, 2, 4, 8 or 16 and as the number of measurements is increased, the number of bits populated with data increases, so that with 16 measurements all 20 bits are populated with measurement data. The data sheet refers to this process as oversampling.

In the BME280, the same arrangement applies so long as the result is not being filtered. If filtering is used, the values are always 20 bits, irrespective of how many measurements are taken in each measurement cycle.

Each individual measurement takes about 2 milliseconds (typical value; maximum value is 2.3 ms). Add to this a fixed overhead of about 2 ms (usually a bit less) means that a measurement sequence, which can consist of from 1 to 32 individual measurements, can take from 4 ms up to 66 ms.

The data sheet provides a set of recommended combinations of temperature and pressure oversampling for various applications.

Configuration control registers

The two configuration control registers in the BMP280 are at register addresses 0xF4 and 0xF5, and are mapped onto 6 individual configuration control values. 0xF4 consists of:

  • 3 bits osrs_t (measure temperature 0, 1, 2, 4, 8 or 16 times);
  • 3 bits osrs_p (measure pressure 0, 1, 2, 4, 8 or 16 times); and
  • 2 bits Mode (Sleep, Forced (ie Single Shot), Normal (ie continuous).

0xF5 consists of:

  • 3 bits t_sb (standby time, 0.5ms to 4000 ms);
  • 3 bits filter (see below); and
  • 1 bit spiw_en which selects SPI or I2C.

The filter parameter controls a type of exponential decay algorithm, or Infinite Impulse Response (IIR) filter, applied to the raw pressure and temperature measurement values (but not to the humidity values). The equation is given in the data sheet. Another presentation is:

Value(n) = Value(n-1) * (K-1)/K + measurement(n) / K

where (n) indicates the most recent measurement and output value; and K is the filter parameter. The filter parameter K and can be set to 1,2,4,8 or 16. If K is set to 1 the equation just becomes Value(n) = measurement(n). The coding of the filter parameter is:

  • filter = 000, K=1
  • filter = 001, K=2
  • filter = 010, K=4
  • filter = 011, K=8
  • filter = 1xx, K=16

The BME 280 adds a further configuration control register at address 0xF2, "ctrl_hum" with a single 3-bit parameter osrs_h (measure humidity 0, 1, 2, 4, 8 or 16 times).

Step 4: Measurement and Readout Timing

I plan to add this later, showing the timing of commands and measurement responses.

Iddt - current at temperature measurement. Typical value 325 uA

Iddp - current at pressure measurement. Typical value 720 uA, max 1120 uA

Iddsb - current in standby mode. Typical value 0.2 uA, max 0.5 uA

Iddsl - current in sleep mode. Typical value 0.1 uA, max 0.3 uA


Step 5: Software Guidelines

I2C Burst mode

The BMP280 data sheet provides guidance about data readout (section 3.9). It says "it is strongly recommended to use a burst read and not address every register individually. This will prevent a possible mix-up of bytes belonging to different measurements and reduce interface traffic."
No guidance is given regarding the reading of the compensation/calibration parameters. Presumably these are not an issue because they are static and do not change.

This library reads all contiguous values in a single read operation - 24 bytes in the case of the temperature and pressure compensation parameters, 6 bytes for temperature and pressure combined, and 8 bytes for humidity, temperature and pressure combined. When temperature alone is checked, only 3 bytes are read.

Use of macros (#define etc.)

There are no macros in this library other than the usual library "include guard" macro which prevents duplication.

All constants are defined using the const keyword, and debug printing is controlled with standard C functions.

It has been the source of some uncertainty for me, but the advice I get from reading many posts on this subject is that use of #define for declaration of constants (at least) and (probably) debug printing control is unnecessary and undesirable.

The case for the use of const rather than #define is pretty clear - const uses the same resources as #define (ie nil) and the resultant values follow the scoping rules, thereby reducing the chance of errors.

The case for debug printing control is a bit less clear, because the way I have done it means that the final code contains the logic for the debug printing statements, even though they are never exercised. If the library is to be used in a big project on a microcontroller with very limited memory, this may become an issue. Since my development was on an ESP8266 with a large flash memory, this did not seem to be an issue for me.

Step 6: Temperature Performance

I plan to add this later.

Step 7: Pressure Performance

I plan to add this later.