BMP280 / BME280 uPython

RP2040 based microcontroller boards running MicroPython.
Target audience: MicroPython users with an RP2040 boards.
This does not include conventional Linux-based Raspberry Pi boards.
User avatar
Roberthh
Posts: 3667
Joined: Sat May 09, 2015 4:13 pm
Location: Rhineland, Europe

Re: BMP280 / BME280 uPython

Post by Roberthh » Tue Sep 06, 2022 11:58 am

So I have a modified code here. It can be loaded. But I cannot tell if it works, since I have given away my BME280 sensor.
The mode parameter is now either a single int or a tuple of three ints with (mode_hum, mode_temp, mode_pressure).

Code: Select all

# Authors: Paul Cunnane 2016, Peter Dahlebrg 2016
#
# This module borrows from the Adafruit BME280 Python library. Original
# Copyright notices are reproduced below.
#
# Those libraries were written for the Raspberry Pi. This modification is
# intended for the MicroPython and esp8266 boards.
#
# Copyright (c) 2014 Adafruit Industries
# Author: Tony DiCola
#
# Based on the BMP280 driver with BME280 changes provided by
# David J Taylor, Edinburgh (www.satsignal.eu)
#
# Based on Adafruit_I2C.py created by Kevin Townsend.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

import time
from ustruct import unpack, unpack_from
from array import array

# BME280 default address.
BME280_I2CADDR = 0x76

# Operating Modes
BME280_OSAMPLE_1 = 1
BME280_OSAMPLE_2 = 2
BME280_OSAMPLE_4 = 3
BME280_OSAMPLE_8 = 4
BME280_OSAMPLE_16 = 5

BME280_REGISTER_CONTROL_HUM = 0xF2
BME280_REGISTER_CONTROL = 0xF4


class BME280:

    def __init__(self,
                 mode=BME280_OSAMPLE_1,
                 address=BME280_I2CADDR,
                 i2c=None,
                 **kwargs):
        # Check that mode is valid.
        if type(mode) is tuple and len(mode) == 3:
            self._mode_hum, self._mode_temp, self._mode_press = mode
        elif type(mode) == int:
            self._mode_hum, self._mode_temp, self._mode_press = mode, mode, mode
        else:
            raise ValueError("Wrong type for the mode parameter, must be int or a 3 element tuple")

        for mode in (self._mode_hum, self._mode_temp, self._mode_press):
            if mode not in [BME280_OSAMPLE_1, BME280_OSAMPLE_2, BME280_OSAMPLE_4,
                            BME280_OSAMPLE_8, BME280_OSAMPLE_16]:
                raise ValueError(
                    'Unexpected mode value {0}. Set mode to one of '
                    'BME280_ULTRALOWPOWER, BME280_STANDARD, BME280_HIGHRES, or '
                    'BME280_ULTRAHIGHRES'.format(mode))

        self.address = address
        if i2c is None:
            raise ValueError('An I2C object is required.')
        self.i2c = i2c

        # load calibration data
        dig_88_a1 = self.i2c.readfrom_mem(self.address, 0x88, 26)
        dig_e1_e7 = self.i2c.readfrom_mem(self.address, 0xE1, 7)
        self.dig_T1, self.dig_T2, self.dig_T3, self.dig_P1, \
            self.dig_P2, self.dig_P3, self.dig_P4, self.dig_P5, \
            self.dig_P6, self.dig_P7, self.dig_P8, self.dig_P9, \
            _, self.dig_H1 = unpack("<HhhHhhhhhhhhBB", dig_88_a1)

        self.dig_H2, self.dig_H3 = unpack("<hB", dig_e1_e7)
        e4_sign = unpack_from("<b", dig_e1_e7, 3)[0]
        self.dig_H4 = (e4_sign << 4) | (dig_e1_e7[4] & 0xF)

        e6_sign = unpack_from("<b", dig_e1_e7, 5)[0]
        self.dig_H5 = (e6_sign << 4) | (dig_e1_e7[4] >> 4)

        self.dig_H6 = unpack_from("<b", dig_e1_e7, 6)[0]

        self.i2c.writeto_mem(self.address, BME280_REGISTER_CONTROL,
                             bytearray([0x3F]))
        self.t_fine = 0

        # temporary data holders which stay allocated
        self._l1_barray = bytearray(1)
        self._l8_barray = bytearray(8)
        self._l3_resultarray = array("i", [0, 0, 0])

    def read_raw_data(self, result):
        """ Reads the raw (uncompensated) data from the sensor.

            Args:
                result: array of length 3 or alike where the result will be
                stored, in temperature, pressure, humidity order
            Returns:
                None
        """

        self._l1_barray[0] = self._mode_hum
        self.i2c.writeto_mem(self.address, BME280_REGISTER_CONTROL_HUM,
                             self._l1_barray)
        self._l1_barray[0] = self._mode_temp << 5 | self._mode_press << 2 | 1
        self.i2c.writeto_mem(self.address, BME280_REGISTER_CONTROL,
                             self._l1_barray)

        sleep_time = 1250 + 2300 * (1 << self._mode_temp)
        sleep_time = sleep_time + 2300 * (1 << self._mode_hum) + 575
        sleep_time = sleep_time + 2300 * (1 << self._mode_press) + 575
        time.sleep_us(sleep_time)  # Wait the required time

        # burst readout from 0xF7 to 0xFE, recommended by datasheet
        self.i2c.readfrom_mem_into(self.address, 0xF7, self._l8_barray)
        readout = self._l8_barray
        # pressure(0xF7): ((msb << 16) | (lsb << 8) | xlsb) >> 4
        raw_press = ((readout[0] << 16) | (readout[1] << 8) | readout[2]) >> 4
        # temperature(0xFA): ((msb << 16) | (lsb << 8) | xlsb) >> 4
        raw_temp = ((readout[3] << 16) | (readout[4] << 8) | readout[5]) >> 4
        # humidity(0xFD): (msb << 8) | lsb
        raw_hum = (readout[6] << 8) | readout[7]

        result[0] = raw_temp
        result[1] = raw_press
        result[2] = raw_hum

    def read_compensated_data(self, result=None):
        """ Reads the data from the sensor and returns the compensated data.

            Args:
                result: array of length 3 or alike where the result will be
                stored, in temperature, pressure, humidity order. You may use
                this to read out the sensor without allocating heap memory

            Returns:
                array with temperature, pressure, humidity. Will be the one from
                the result parameter if not None
        """
        self.read_raw_data(self._l3_resultarray)
        raw_temp, raw_press, raw_hum = self._l3_resultarray
        # temperature
        var1 = ((raw_temp >> 3) - (self.dig_T1 << 1)) * (self.dig_T2 >> 11)
        var2 = (((((raw_temp >> 4) - self.dig_T1) *
                  ((raw_temp >> 4) - self.dig_T1)) >> 12) * self.dig_T3) >> 14
        self.t_fine = var1 + var2
        temp = (self.t_fine * 5 + 128) >> 8

        # pressure
        var1 = self.t_fine - 128000
        var2 = var1 * var1 * self.dig_P6
        var2 = var2 + ((var1 * self.dig_P5) << 17)
        var2 = var2 + (self.dig_P4 << 35)
        var1 = (((var1 * var1 * self.dig_P3) >> 8) +
                ((var1 * self.dig_P2) << 12))
        var1 = (((1 << 47) + var1) * self.dig_P1) >> 33
        if var1 == 0:
            pressure = 0
        else:
            p = 1048576 - raw_press
            p = (((p << 31) - var2) * 3125) // var1
            var1 = (self.dig_P9 * (p >> 13) * (p >> 13)) >> 25
            var2 = (self.dig_P8 * p) >> 19
            pressure = ((p + var1 + var2) >> 8) + (self.dig_P7 << 4)

        # humidity
        h = self.t_fine - 76800
        h = (((((raw_hum << 14) - (self.dig_H4 << 20) -
                (self.dig_H5 * h)) + 16384)
              >> 15) * (((((((h * self.dig_H6) >> 10) *
                            (((h * self.dig_H3) >> 11) + 32768)) >> 10) +
                          2097152) * self.dig_H2 + 8192) >> 14))
        h = h - (((((h >> 15) * (h >> 15)) >> 7) * self.dig_H1) >> 4)
        h = 0 if h < 0 else h
        h = 419430400 if h > 419430400 else h
        humidity = h >> 12

        if result:
            result[0] = temp
            result[1] = pressure
            result[2] = humidity
            return result

        return array("i", (temp, pressure, humidity))

    @property
    def values(self):
        """ human readable values """

        t, p, h = self.read_compensated_data()

        p = p // 256
        pi = p // 100
        pd = p - pi * 100

        hi = h // 1024
        hd = h * 100 // 1024 - hi * 100
        return ("{}C".format(t / 100), "{}.{:02d}hPa".format(pi, pd),
                "{}.{:02d}%".format(hi, hd))

User avatar
Roberthh
Posts: 3667
Joined: Sat May 09, 2015 4:13 pm
Location: Rhineland, Europe

Re: BMP280 / BME280 uPython

Post by Roberthh » Tue Sep 06, 2022 12:34 pm

I updates as well my variant of the BME280 driver at https://github.com/robert-hh/BME280. The bme280_int.py version is based on the same driver as the one you use, with a small bug fix in processing the calibration data. But that has only rarely a minor effect on the results.

Rissy
Posts: 116
Joined: Sun Aug 14, 2022 8:15 am

Re: BMP280 / BME280 uPython

Post by Rissy » Tue Sep 06, 2022 5:00 pm

Thank you for having a go at that for me Roberthh! much appreciated.

I can see some of the changes you've made when i compare the new _float version with what was there beforehand, but I need an example on how I need to write code to allow me to configure the oversampling I wish to use within my own code for each variable.

I'd like to make the following settings from my code:

Code: Select all

mode='normal'
temperature_oversampling=2
pressure_oversampling=16
humidity_oversampling=1
temperature_standby=500
This is the BME280 portions of my overall script. Can you mark this up with the changes I need to be able to set the the above oversampling with the readings?

Code: Select all

#bme280 sensor addresses
bme0_i2c_address = 0x77
bme1_i2c_address = 0x76

#Define the interface of the BME280 Temp/Pressure/Humidity sensor with the Pico W
i2c=SoftI2C(sda=Pin(4), scl=Pin(5), freq=50000) #default frequency is 400,000 for short cables.  For longer cables, use reduced frequency value
bme0 = bme280.BME280(i2c=i2c, address=bme0_i2c_address)
bme1 = bme280.BME280(i2c=i2c, address=bme1_i2c_address)

#read in values direct from sensor
t0, p0, h0 = bme0.read_compensated_data()
t1, p1, h1 = bme1.read_compensated_data()

at0 = t0/100.0
ap0 = p0/25600.0
ah0 = h0/1024.0
at1 = t1/100.0
ap1 = p1/25600.0
ah1 = h1/1024.0
    
rt0 = round(at0,1)
rp0 = round(ap0,1)
rh0 = round(ah0,1)
rt1 = round(at1,1)
rp1 = round(ap1,1)
rh1 = round(ah1,1)
Sorry to put this on you. I just cant do it myself because I dont know what im doing without a guide or example. Once i know through example, i'm fine. But i need that initial kick to get me going.

User avatar
Roberthh
Posts: 3667
Joined: Sat May 09, 2015 4:13 pm
Location: Rhineland, Europe

Re: BMP280 / BME280 uPython

Post by Roberthh » Tue Sep 06, 2022 7:34 pm

Could be like that below. The driver requires for oversampling the log2 + 1 of the number.
x1: 1
x2: 2
x4: 3
x8: 4
x16: 5
That is calculated with the log2i() function.

Code: Select all


def log2i(n):
	result = 1
	while n > 1:
		result += 1
		n >>= 1
	return result

#bme280 sensor addresses
bme0_i2c_address = 0x77
bme1_i2c_address = 0x76
temperature_oversampling=2
pressure_oversampling=16
humidity_oversampling=1

#Define the interface of the BME280 Temp/Pressure/Humidity sensor with the Pico W
i2c=SoftI2C(sda=Pin(4), scl=Pin(5), freq=50000) #default frequency is 400,000 for short cables.  For longer cables, use reduced frequency value
mode = (log2i(humidity_oversampling), log2i(temperature_oversampling), log2i(pressure_oversampling))

bme0 = bme280.BME280(i2c=i2c, address=bme0_i2c_address, mode=mode)
bme1 = bme280.BME280(i2c=i2c, address=bme1_i2c_address, mode=mode)

#read in values direct from sensor
t0, p0, h0 = bme0.read_compensated_data()
t1, p1, h1 = bme1.read_compensated_data()

at0 = t0/100.0
ap0 = p0/25600.0
ah0 = h0/1024.0
at1 = t1/100.0
ap1 = p1/25600.0
ah1 = h1/1024.0
    
rt0 = round(at0,1)
rp0 = round(ap0,1)
rh0 = round(ah0,1)
rt1 = round(at1,1)
rp1 = round(ap1,1)
rh1 = round(ah1,1)

Rissy
Posts: 116
Joined: Sun Aug 14, 2022 8:15 am

Re: BMP280 / BME280 uPython

Post by Rissy » Wed Sep 07, 2022 6:29 pm

Hi Roberthh, thanks very much for doing this for me. We can work together to get it working since you have the brains and I have the kit available in front of me! :lol:

So I made your changes to the code here and tried to run it in Thonny.

I get the following:

Code: Select all

>>> %Run -c $EDITOR_CONTENT
Traceback (most recent call last):
  File "<stdin>", line 21
SyntaxError: invalid syntax
>>> 
This is from the following function from your post:

Code: Select all

def log2i(n):
    result = 1
    while n > 1:
        result += 1
        n >> = 1			<-------------This is Line 21
    return result
What does this function do?

While the oversampling number submitted is greater than 1, then increment "result" by 1, initially from the value of 1, then I don't know what "n >>=1" means, then return the newly incremented "result" (which goes where?)

User avatar
Roberthh
Posts: 3667
Joined: Sat May 09, 2015 4:13 pm
Location: Rhineland, Europe

Re: BMP280 / BME280 uPython

Post by Roberthh » Wed Sep 07, 2022 7:02 pm

Must be n >>= 1.

Rissy
Posts: 116
Joined: Sun Aug 14, 2022 8:15 am

Re: BMP280 / BME280 uPython

Post by Rissy » Wed Sep 07, 2022 7:42 pm

Ah ok, I just copied and pasted your text.

Right. Got passed that error.

Now i'm getting:

Code: Select all

%Run -c $EDITOR_CONTENT
Traceback (most recent call last):
  File "<stdin>", line 47, in <module>
  File "bme280.py", line 116, in __init__
AttributeError: 'BME280' object has no attribute '_mode'
That's this line:

Code: Select all

        self._l1_barray[0] = self._mode << 5 | self._mode << 2 | MODE_SLEEP

User avatar
Roberthh
Posts: 3667
Joined: Sat May 09, 2015 4:13 pm
Location: Rhineland, Europe

Re: BMP280 / BME280 uPython

Post by Roberthh » Wed Sep 07, 2022 7:55 pm

I updated the repository. Should be:

Code: Select all

        self._l1_barray[0] = self._mode_temp << 5 | self._mode_press << 2 | MODE_SLEEP

Rissy
Posts: 116
Joined: Sun Aug 14, 2022 8:15 am

Re: BMP280 / BME280 uPython

Post by Rissy » Wed Sep 07, 2022 8:11 pm

Ah! I'm getting something now.

Except i think the values might be out by a factor of 10 and 100.

BME0 Temperature: 2.3ºC (This should be closer to 23)
BME0 Humidity: 5.1% (This should be closer to 51)
BME0 Pressure: 17.8% (This should be closer to 1005)
BME1 Temperature: 2.0ºC (This should be closer to 20)
BME1 Humidity: 6.6% (This should be closer to 66)
BME1 Pressure: 17.2% (This should be closer to 1005)

I have to go now unfortunately, so we can pick this up maybe tomorrow? (gives you time to think about the scaling of the values here)

Thanks so far though, I'm still VERY MUCH appreciative of your help! :)

User avatar
Roberthh
Posts: 3667
Joined: Sat May 09, 2015 4:13 pm
Location: Rhineland, Europe

Re: BMP280 / BME280 uPython

Post by Roberthh » Thu Sep 08, 2022 6:05 am

The float version of my driver return the values in the proper range. So these lines are not needed, if you use the float version of the driver:

Code: Select all

at0 = t0/100.0
ap0 = p0/25600.0
ah0 = h0/1024.0
at1 = t1/100.0
ap1 = p1/25600.0
ah1 = h1/1024.0

Post Reply