nextpcb
Raspberry Pi Pico Projects

Interface BME280 with Raspberry Pi Pico using MicroPython

BME280 & OLED Display with Pi Pico to measure Temperature Pressure & Humidity

Overview: BME280 with Raspberry Pi Pico

In this tutorial, we will learn how to interface a BME280 sensor with a Raspberry Pi Pico using MicroPython. The BME280 is a popular environmental sensor from BOSCH that can measure temperature, humidity, pressure, and altitude. We will use the BME280 library to develop a MicroPython code in Thonny IDE to read data using the Raspberry Pi Pico.

The tutorial contains circuits, connections, and code to communicate with the BME280 sensor. We will connect the BME280 sensor to the Raspberry Pi Pico via the I2C pins and use a breadboard for assembly and testing. The MicroPython code is divided into two parts: bme280.py and main.py. The main code requires a library for BME280. It contains all the necessary modules and registers details to extract data from the sensor. The first example will display the BME280 sensor reading on the shell and in the second example, we will use a 0.96″ I2C OLED display to display BME280 parameters.


Components Required

The components required for this tutorial include a Raspberry Pi Pico, a BME280 sensor, a 0.96″ OLED display, jumper wires, a breadboard, and a Micro-USB cable. You can easily purchase them from the links provided below.

S.NCOMPONENTS NAMEQUANTITYPURCHASE LINKS
1Raspberry Pi Pico Basic Starter Kit1 Amazon | AliExpress
2BME280 Sensor1 Amazon | AliExpress
3Raspberry Pi Pico1 Amazon | AliExpress
40.96" I2C OLED Display1 Amazon | AliExpress
5Breadboard1 Amazon | AliExpress
6Jumper Wires10 Amazon | AliExpress

BME280 Barometric Pressure Sensor

The BME280 is a Barometric Pressure Sensor that can measure temperature, humidity, and atmospheric pressure.

BME280

It operates on an I2C bus and a 3.3V power supply and is designed with high linearity and high accuracy sensors for low current consumption, long-term stability, and high EMC robustness. It is particularly accurate for measuring humidity with ±3% accuracy, barometric pressure with ±1 hPa absolute accuracy, and temperature with ±1.0°C accuracy, and can also be used as an altimeter with ±1 meter or better accuracy due to its high accuracy pressure measurements.


Some other helpful tutorials


Interfacing BME280 Sensor with Raspberry Pi Pico

To connect the BME280 Sensor with a Raspberry Pi Pico, simply connect the VCC, GND, SCL, and SDA pins of the BME280 Sensor to the 3.3V, GND, GP21, and GP20 pins of the Raspberry Pi Pico respectively. This can be easily achieved by using a breadboard for assembly and testing. The connection process is straightforward.

BME280 Raspberry Pi Pico Connection

MicroPython Code: BME280 & Pi Pico Interfacing

The process of interfacing the BME280 Sensor with the Raspberry Pi Pico requires two parts of code: bme280.py and main.py. The main code uses a library for BME280, which includes all the necessary modules and register details to collect data from the sensor.

bme280.py

To use the bme280.py code, open the Thonny IDE and paste the code into the Thonny Editor. Then, save the file on the Raspberry Pi Pico and name it bme280.py.

# 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 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._mode = 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
        self.i2c.writeto_mem(self.address, BME280_REGISTER_CONTROL_HUM,
                             self._l1_barray)
        self._l1_barray[0] = self._mode << 5 | self._mode << 2 | 1
        self.i2c.writeto_mem(self.address, BME280_REGISTER_CONTROL,
                             self._l1_barray)
 
        sleep_time = 1250 + 2300 * (1 << self._mode)
        sleep_time = sleep_time + 2300 * (1 << self._mode) + 575
        sleep_time = sleep_time + 2300 * (1 << self._mode) + 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))

main.py

Open another tab on Thonny IDE and paste the following code. Save the file as main.py on Raspberry Pi Pico.

from machine import Pin, I2C        #importing relevant modules & classes
from time import sleep
import bme280       #importing BME280 library
 
i2c=I2C(0,sda=Pin(20), scl=Pin(21), freq=400000)    #initializing the I2C method 
 
 
while True:
  bme = bme280.BME280(i2c=i2c)          #BME280 object created
  print(bme.values)
  sleep(10)           #delay of 10s

After completing the above steps, execute the main.py file. If the hardware connections have been made correctly, the output of the shell will show the following results.

BME280 Python Shell

Interfacing BME280 Sensor with Raspberry Pi Pico & OLED Display

Now, we will cover the process of displaying the sensor readings on a 0.96″ OLED display. The connections are fairly simple to connect the BME280 sensor to the Raspberry Pi Pico and OLED display. Connect the VCC, GND, SCL, and SDA pins of the OLED display to the 3.3V, GND, GP21, and GP20 of the Raspberry Pi Pico respectively. You can use a custom PCB to assemble the circuit and use this project as a mini weather station.

Interfacing BME280 with Raspberry Pi Pico using MicroPython
Interface BME280 with Raspberry Pi Pico using MicroPython

Project PCB Gerber File & PCB Ordering Online

If you don’t want to assemble the circuit on a breadboard and you want PCB for the project, then here is the PCB for you. The PCB Board for this project looks something like below.

PCB for interfacing BME280 with Raspberry Pi Pico and OLED Display

The Gerber File for the PCB is given below. You can simply download the Gerber File and order the PCB from PCBWay.com.

Now you can visit the PCBWay official website by clicking here: PCBWay.com. So you will be directed to the PCBWay website.

You can now upload the Gerber File to the Website and place an order. The PCB quality is superb & high standard. That is why most people trust PCBWay for PCB & PCBA Services.

  • PCB for BME280 OLED Display and Raspberry Pi Pico
  • RPI Pico with BME280 PCB
  • PCB for interfacing BME280 with Raspberry Pi Pico and OLED Display

Now, you can assemble the components on the PCB Board.


MicroPython Code: BME280, OLED Display & Pi Pico Interfacing

The process of interfacing the BME280 Sensor with the Raspberry Pi Pico and OLED Display requires three parts of code:

  1. bme280.py
  2. ssd1306.py
  3. main.py.

The main code uses libraries for BME280 and SSD1306 OLED Display. These include all the necessary modules and register details to collect data from the BME280 Sensor and display the reading on a 0.96″ OLED Display. To use the bme280 library, copy the code from above and save it on the Raspberry Pi Pico as bme280.py.


ssd1306.py

Additionally, we also need a library for the SSD1306 OLED Display. Open a new tab on the Thonny IDE and paste the code, then save the file as ssd1306.py on the Raspberry Pi Pico.

# MicroPython SSD1306 OLED driver, I2C and SPI interfaces
 
from micropython import const
import framebuf
 
# register definitions
SET_CONTRAST = const(0x81)
SET_ENTIRE_ON = const(0xA4)
SET_NORM_INV = const(0xA6)
SET_DISP = const(0xAE)
SET_MEM_ADDR = const(0x20)
SET_COL_ADDR = const(0x21)
SET_PAGE_ADDR = const(0x22)
SET_DISP_START_LINE = const(0x40)
SET_SEG_REMAP = const(0xA0)
SET_MUX_RATIO = const(0xA8)
SET_COM_OUT_DIR = const(0xC0)
SET_DISP_OFFSET = const(0xD3)
SET_COM_PIN_CFG = const(0xDA)
SET_DISP_CLK_DIV = const(0xD5)
SET_PRECHARGE = const(0xD9)
SET_VCOM_DESEL = const(0xDB)
SET_CHARGE_PUMP = const(0x8D)
 
# Subclassing FrameBuffer provides support for graphics primitives
# http://docs.micropython.org/en/latest/pyboard/library/framebuf.html
class SSD1306(framebuf.FrameBuffer):
    def __init__(self, width, height, external_vcc):
        self.width = width
        self.height = height
        self.external_vcc = external_vcc
        self.pages = self.height // 8
        self.buffer = bytearray(self.pages * self.width)
        super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB)
        self.init_display()
 
    def init_display(self):
        for cmd in (
            SET_DISP | 0x00,  # off
            # address setting
            SET_MEM_ADDR,
            0x00,  # horizontal
            # resolution and layout
            SET_DISP_START_LINE | 0x00,
            SET_SEG_REMAP | 0x01,  # column addr 127 mapped to SEG0
            SET_MUX_RATIO,
            self.height - 1,
            SET_COM_OUT_DIR | 0x08,  # scan from COM[N] to COM0
            SET_DISP_OFFSET,
            0x00,
            SET_COM_PIN_CFG,
            0x02 if self.width > 2 * self.height else 0x12,
            # timing and driving scheme
            SET_DISP_CLK_DIV,
            0x80,
            SET_PRECHARGE,
            0x22 if self.external_vcc else 0xF1,
            SET_VCOM_DESEL,
            0x30,  # 0.83*Vcc
            # display
            SET_CONTRAST,
            0xFF,  # maximum
            SET_ENTIRE_ON,  # output follows RAM contents
            SET_NORM_INV,  # not inverted
            # charge pump
            SET_CHARGE_PUMP,
            0x10 if self.external_vcc else 0x14,
            SET_DISP | 0x01,
        ):  # on
            self.write_cmd(cmd)
        self.fill(0)
        self.show()
 
    def poweroff(self):
        self.write_cmd(SET_DISP | 0x00)
 
    def poweron(self):
        self.write_cmd(SET_DISP | 0x01)
 
    def contrast(self, contrast):
        self.write_cmd(SET_CONTRAST)
        self.write_cmd(contrast)
 
    def invert(self, invert):
        self.write_cmd(SET_NORM_INV | (invert & 1))
 
    def show(self):
        x0 = 0
        x1 = self.width - 1
        if self.width == 64:
            # displays with width of 64 pixels are shifted by 32
            x0 += 32
            x1 += 32
        self.write_cmd(SET_COL_ADDR)
        self.write_cmd(x0)
        self.write_cmd(x1)
        self.write_cmd(SET_PAGE_ADDR)
        self.write_cmd(0)
        self.write_cmd(self.pages - 1)
        self.write_data(self.buffer)
 
 
class SSD1306_I2C(SSD1306):
    def __init__(self, width, height, i2c, addr=0x3C, external_vcc=False):
        self.i2c = i2c
        self.addr = addr
        self.temp = bytearray(2)
        self.write_list = [b"\x40", None]  # Co=0, D/C#=1
        super().__init__(width, height, external_vcc)
 
    def write_cmd(self, cmd):
        self.temp[0] = 0x80  # Co=1, D/C#=0
        self.temp[1] = cmd
        self.i2c.writeto(self.addr, self.temp)
 
    def write_data(self, buf):
        self.write_list[1] = buf
        self.i2c.writevto(self.addr, self.write_list)
 
 
class SSD1306_SPI(SSD1306):
    def __init__(self, width, height, spi, dc, res, cs, external_vcc=False):
        self.rate = 10 * 1024 * 1024
        dc.init(dc.OUT, value=0)
        res.init(res.OUT, value=0)
        cs.init(cs.OUT, value=1)
        self.spi = spi
        self.dc = dc
        self.res = res
        self.cs = cs
        import time
 
        self.res(1)
        time.sleep_ms(1)
        self.res(0)
        time.sleep_ms(10)
        self.res(1)
        super().__init__(width, height, external_vcc)
 
    def write_cmd(self, cmd):
        self.spi.init(baudrate=self.rate, polarity=0, phase=0)
        self.cs(1)
        self.dc(0)
        self.cs(0)
        self.spi.write(bytearray([cmd]))
        self.cs(1)
 
    def write_data(self, buf):
        self.spi.init(baudrate=self.rate, polarity=0, phase=0)
        self.cs(1)
        self.dc(1)
        self.cs(0)
        self.spi.write(buf)
        self.cs(1)

main.py

Now, open a new tab on the Thonny IDE and paste the main.py code. Save the file as main.py on the Raspberry Pi Pico.

from machine import Pin, I2C
from ssd1306 import SSD1306_I2C
import time
import bme280
 
i2c=I2C(0,sda=Pin(20), scl=Pin(21), freq=400000)    #initializing the I2C method 
 
WIDTH =128 
HEIGHT= 64
oled = SSD1306_I2C(WIDTH,HEIGHT,i2c)
 
while True:
    oled.fill(0)
    bme = bme280.BME280(i2c=i2c)
    
    print(bme.values)
    oled.text("BME280 SENSOR:", 10, 0)
    oled.text("Temp:", 0, 18)
    oled.text(str(bme.values[0]), 50, 18)
    oled.text("PA:", 0, 35)
    oled.text(str(bme.values[1]), 50, 35)
    oled.text("Humi:", 0, 55)
    oled.text(str(bme.values[2]), 50, 55)
    oled.show()
    time.sleep(1)

Testing

Once the main.py code has been saved, execute it. The OLED display will instantly show the values of temperature, pressure, and humidity. If you are building a weather station, having a display to view the live data is crucial, and the OLED display is an ideal choice for this purpose.

Interface BME280 with Raspberry Pi Pico and OLED Display using MicroPython
Interface BME280 with Raspberry Pi Pico using MicroPython

Conclusion

In conclusion, interfacing the BME280 sensor with the Raspberry Pi Pico and OLED Display is a straightforward process that requires three parts of code: bme280.py, ssd1306.py and main.py. These codes use libraries for BME280 and SSD1306 OLED Display that include all the necessary modules and register details to collect data from the BME280 sensor and display the readings on a 0.96″ OLED Display. This project can be a great solution for building a weather station as it provides live data readings of temperature, pressure, and humidity. Hence, the OLED display is an ideal choice for this purpose.


Related Articles

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button