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. 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.
As a result of these factors, I decided to write my own.
The BMP280 is a surface-mount device about 5mm square and 1 mm high. It has 8 interface pads, including 2 separate power input pads and two Ground pads. It is available on eBay as a module with 6 pins brought out.
The 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 library has support for both i2C and SPI.
The library can be downloaded here:
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.
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
This statement needs to be included in the early part of the sketch prior to the start of the setup() function.
Creating the BMP software object
There are 3 levels for creating the BMP280 software object. The simplest is just
for example, BMP280 bmp0;
This creates the software object with the default address of 0x76 (ie for SDO connected to ground).
The next level for creating the BMP280 software object has a parameter of either 0 or 1, as follows:
BMP280 objectNameA(0); BMP280 objectNameB(1);
The parameter (0 or 1) is added to the I2C base address, so that two BMP280 devices can be used on the same I2C bus.
The third level for creating the BMP280 software object has two parameters. The first paramter, 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:
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 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:
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).
There is another version of begin() that takes all six 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
The full list of codes and their meanings is in the 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 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 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.
These use cases are covered in three example sketches provided with the library, being basicTemperature.ino, basicPressure.ino, and basicTemperatureAndPressure.ino.
More sophisticated temperature and pressure measurement
Although the above series of statements will work without problems, there are a couple of issues:
- 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.
- 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); // osrs_t, osrs_p, mode, t_sb, filter, spiw_en
Then, when a measurement is wanted, wake up the device with a configuration command to register F4 that sets the appropriate values of osrs_t and osrs_p, plus mode=1 (single shot mode). For example:
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.
- wait a fixed amount of time to cover the longest expected delay
- 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.
- 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.
- 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
but is a bit easier with the following two commands:
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 eiher of the two following commands:
Both of these set the mode bits to 00 (ie sleep mode). However the first of sets the osrs_t and osrs_p to 111 (ie 16 measurements) whle the second one stores the low 6 bits from value into bits 2:7 of the 0xF4 register.
Similarly the following statement stores the low six bits of value into bits 2:7 of the 0xF5 register.
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
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 and readPressure functions have two components. First the raw 20-bit temperature and pressure values are obtained from the BMP280. Then the compensation algorithm is used to generate the output values in degrees Celsius and hPa.
The library provides separate functions for these two components, so that the raw temperature and presssure data can be obtained, and perhaps manipulated in some way. The algorithm to derive the temperature and pressure 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. Making these functions accessible may be useful for assessing and possibly changing the calculation for other platforms.
These functions are:
readRawPressure (rawTemperature); calcPressure (rawPressure, rawTemperature, temperature); calcTemperature (rawTemperature);
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
The device 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.
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.
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.
How it works
The 24 bytes of 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 BMP280 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 BMP280. 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 BMP280 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,
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.
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 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 measurement 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
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 values in a single read operation - 24 bytes in the case of the compensation parameters, and 6 bytes for 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.