MicroPython: BME680 with ESP32 and ESP8266 (Gas, Pressure, Temperature, Humidity)

In the user guide, we will look upon the BME680 environmental sensor, which is used to measure ambient temperature, barometric pressure, relative humidity, and gas (VOC) or Indoor air quality (IAQ). We will learn how to interface it with ESP32 and ESP8266 NodeMCU using MicroPython firmware. For demonstration, firstly, we will a simple MicroPython program for reading the BME680 sensor data and displaying it on the shell console of uPyCraft IDE and Thonny IDE. Furthermore, we will also see an example to display BME680 sensor readings on an OLED display.

MicroPython BME680 with ESP32 and ESP8266 Gas Pressure Temperature Humidity

Prerequisites

Before we start this lesson make sure you are familiar with and have the latest version of MicroPython firmware installed in your ESP boards and have a running Integrated Development Environment (IDE) in which we will be doing the programming such as uPyCraft IDE or Thonny IDE.

BME680 Introduction

BME680 is a four in one low power-driven sensor, which has integrated temperature, pressure, humidity and gas detection sensors. It runs on an operating voltage of 1.8-3.8V and communicates with other microcontrollers through I2C and SPI protocol. This sensor is used in areas such as tracking the quality of air, humidity indicators, weather trends, home automation and controlling and GPS enhancements.

Operating Range and Accuracy

Some key features of BME680 include:

  • Temperature measurement: measures the ambient temperature with an accuracy of ±1.0°C and an operating range of -40 to 85 ºC.
  • Relative Humidity measurement: measures relative humidity with a fast response rate and an accuracy of ±3% and an operating range of 0-100%.
  • Pressure measurement: measures barometric pressure with ±1 hPa absolute accuracy and altitude with an accuracy of ±1 meter. The operating range of pressure ranges from 300-1100 hPa.
AccuracyOperating Range
Temperature±1.0°C-40 to 85 ºC
Humidity±3%0-100%
Pressure±1 hPa300-1100 hPa
  • Gas measurement: detects a wide range of gases including volatile organic compounds (VOCs) thus determining the indoor air quality.
  • Due to its compact size and low power operation, it is suitable in mobile applications, smartwatches, and navigation systems.

As compared to BME280, BME680 is a unique and updated sensor in the sense that it contains a small-sized MOX (metal oxide) sensor.

Gas Sensor

BME680 sensor can determine the number of pollutants/VOCs in the environment such as carbon monoxide, Ethane, Isoprene /2-methyl-1,3 Butadiene, Acetone, and Ethanol. The VOCs are detected by the adsorption of oxygen molecules onto the metal oxide layer. Its actual detection is done by the principle of changing the resistance of the MOX sensor. Whenever the MOX gets in contact with a pollutant in the air, the resistance of the sensor changes with the concentration of the pollutants present. This means that the higher number of pollutants in the air leads to significantly lower resistance of the sensor. Likewise, with the lower concentration of VOCs in the air, the resistance of the sensor is significantly higher.

bme680 gas sensor resistance output and working

Note: The BME680 gas sensor is not a CO2 or ethanol measurement sensor. It gets the relative idea of CO2 from VOC in the air But we can not use it for direct measurement of CO2.

Like all other gas sensors, BME680 also gives variable results each time. To ensure greater accuracy, we have to calibrate it against a known source and then plot its curve. As the sensitivity levels are not constant during the initial use of the sensor, it is initially recommended to run it for forty-eight hours which can later be tuned down to thirty minutes before each use.

BME680 Modules

Several types of BME680 sensors with different sizes and numbers of terminals are available in the market. But all modules provide data through SPI and I2C interfaces and we can use the same MicroPython script to get sensor readings. Some of them are shown below:

You can choose the module according to your convenience and the type of microcontroller you want to connect it with. In this article, we will use the BME680 sensor with 6 terminals, which is shown below. It has 4 SPI and 2 I2C terminals.

different bme680 sensor modules
BME680 Sensor Modules

BME680 Sensor Module

Pinout Diagram

The following figure shows the pinout of BME680 sensor:

bme680 pinout
BME680 Pinout
Pin NameDescription
VCCThis pin powers up the sensor.
GNDThis is the common ground pin for power and logic.
SCLWhen using I2C protocol this pin acts as SCL or serial clock pin and when using SPI protocol this pin acts as SCK or the serial pin for SPI. It is the input to the chip.
SDAWhen using I2C protocol this pin acts as SDA or Serial Data pin and when using SPI protocol this pin acts as SDI or Serial Data In also known as MOSI (‘Master out Slave in’). This is used as the I2C/SPI input to the sensor.
SDOThis is the SDO (Slave data out) /MISO (Master in slave out) pin for SPI communication. This pin is used as the SPI output from the sensor.
CSThis is the Chip Select pin used in SPI communication. Acts as an input to the sensor.

BME680 I2C and SPI Interface

In this section, we will see how to interface the BME680 sensor module with ESP32. As discussed earlier, the BME680 offers two interfaces such as I2C and SPI. We can use either SPI or I2C interface t connect a sensor with the ESP32 or ESP8266 NodeMCU. We will show the connections for both scenarios in the next section.

I2C Interface with ESP32/ESP8266

If you want to interface BME680 sensor with ESP32 or ESP8266 NodeMCU using I2C interface, you should make the connections of ESP boards with sensor accoridng to these SCL and SDA pins.

BME680ESP32ESP8266 NodeMCU
VCC VCC=3.3V VCC=3.3V
GNDGNDGND
SCLGPIO22GPIO5
SDAGPIO21GPIO4

Connect the VCC pin of ESP32 and ESP8266 NodeMCU with the 3.3V VCC pin of the module to power up. Both grounds of the two devices will be connected in common. The SCL pin of BME680 will be connected with the default SCL pin of ESP32/ESP8266. Likewise, the SDA pin will be connected with the default SDA pin of ESP32/ESP8266.

Note: If we use the softI2C library of MicroPython, we can define any GPIO pins of ESP32 and ESP8266 as SCL and SDA pins. We don’t have to use default I2C pins.

ESP32 I2C Pins

The I2C pins stated above are set in default. If we want to change the GPIO pins we have to set them in code. The diagrams below show the pinout for the ESP32 and Esp8266 respectively.

ESP32 I2C Pins
ESP32 I2C Pins

ESP8266 I2C Pins

The following figure shows the I2C pins of ESP8266 NodeMCU:

ESP8266 I2C Pins
ESP8266 I2C Pins

More information on ESP32 and ESP8266 pins is available here:

SPI Interface with ESP32/ESP8266

If you want to interface the BME680 sensor with ESP32 or ESP8266 NodeMCU using SPI interface, you should make the connections of ESP boards with the sensor according to these MOSI, MISO, and SCK pins. Because SPI protocol uses MOSI/SDI, MISO/SDO, and SCK pins.

BME680ESP32ESP8266
VCCVCC=3.3VVCC=3.3V
GNDGNDGND
SCL (SCK for SPI)GPIO18GPIO14 (D5)
SDA (SDI / MOSI for SPI)GPIO23GPIO13 (D7)
SDO / MISOGPIO19GPIO12 (D6)
CSGPIO5Any GPIO pin or GND

The VCC pin will be connected with the 3.3V from the ESP32 module to power up. Both the grounds of the two devices will be connected in common. The SCL pin of BME680 will be connected with the default SCK/CLK pin of ESP32. Likewise, the SDA pin will be connected with the default MOSI pin of ESP32.  The SDO pin will be connected with the default MISO pin of ESP32 and the CS pin of the sensor will also be connected with the default CS pin of ESP32.

Note: The above table lists the default SPI pins of ESP32 and ESP8266. But if you want to use any other pins, you need to define a softSPI library of MicroPython

As you can see, we will use the default I2C and default SPI pins of the ESP32 module to connect with the respective pins of the sensor. If you want to change the pins according to your preference that can be handled in the program code.

Which Interface to use? 

So far we have discussed SPI and I2C interfaces of BME680 and learned to interface the sensor with ESP32 or ESP8266 using both interfaces. But the question is which communication protocol should we use to get reading from BME680. As we have seen SPI requires four pins to communicate. On the contrary, I2C requires two pins. However, SPI is generally faster in speed as compared to I2C. Hence, it’s a tradeoff between speed and the number of pins. In short, it depends on your choice and application. We will use the I2C interface to get sensor readings with default I2C pins of ESP32 and ESP8266 NodeMCU. 

Required Hardware

We will need the following components to connect our ESP board with the BME680 sensor.

MicroPython BME680 Sensor with ESP32 and ESP8266
  1. ESP32/ESP8266
  2. BME680 Module
  3. Connecting Wires
  4. Breadboard
  5. 0.96 inch OLED Display

Schematic Diagrams ESP32 and ESP8266

Follow the schematic diagrams below for both the ESP modules and connect them accordingly. If you are using ESP32 for this project, connect the ESP32 device with BME680 as shown in the schematic diagram below:

bme680 with esp32 schematic diagram

Similarly, if you are using ESP8266 NodeMCU for this project, connect the ESP8266 device with BME680 as shown in the schematic diagram below:

bme680 with esp8266 nodemcu schematic diagram

BME680 MicroPython Library

By default, MicroPython does not have an implementation of the BME680 library. But, MicroPyhon provides I2C API of ESP32 and ESP8266 which can be used to read values from the BME680 sensor. Fortunately, there is one library available which is developed by Adafruit and can be downloaded from this link. Download the following library and upload it to ESP32/ESP8266 board with the name of bme680.py using uPyCraft or Thonny IDE.

import time
import math
from micropython import const
from ubinascii import hexlify as hex
try:
  import struct
except ImportError:
  import ustruct as struct
_BME680_CHIPID = const(0x61)
_BME680_REG_CHIPID = const(0xD0)
_BME680_BME680_COEFF_ADDR1 = const(0x89)
_BME680_BME680_COEFF_ADDR2 = const(0xE1)
_BME680_BME680_RES_HEAT_0 = const(0x5A)
_BME680_BME680_GAS_WAIT_0 = const(0x64)
_BME680_REG_SOFTRESET = const(0xE0)
_BME680_REG_CTRL_GAS = const(0x71)
_BME680_REG_CTRL_HUM = const(0x72)
_BME280_REG_STATUS = const(0xF3)
_BME680_REG_CTRL_MEAS = const(0x74)
_BME680_REG_CONFIG = const(0x75)
_BME680_REG_PAGE_SELECT = const(0x73)
_BME680_REG_MEAS_STATUS = const(0x1D)
_BME680_REG_PDATA = const(0x1F)
_BME680_REG_TDATA = const(0x22)
_BME680_REG_HDATA = const(0x25)
_BME680_SAMPLERATES = (0, 1, 2, 4, 8, 16)
_BME680_FILTERSIZES = (0, 1, 3, 7, 15, 31, 63, 127)
_BME680_RUNGAS = const(0x10)
_LOOKUP_TABLE_1 = (2147483647.0, 2147483647.0, 2147483647.0, 2147483647.0, 2147483647.0,
  2126008810.0, 2147483647.0, 2130303777.0, 2147483647.0, 2147483647.0,
  2143188679.0, 2136746228.0, 2147483647.0, 2126008810.0, 2147483647.0,
  2147483647.0)
_LOOKUP_TABLE_2 = (4096000000.0, 2048000000.0, 1024000000.0, 512000000.0, 255744255.0, 127110228.0,
  64000000.0, 32258064.0, 16016016.0, 8000000.0, 4000000.0, 2000000.0, 1000000.0,
  500000.0, 250000.0, 125000.0)
def _read24(arr):
  ret = 0.0
  for b in arr:
    ret *= 256.0
    ret += float(b & 0xFF)
  return ret
class Adafruit_BME680:
  def __init__(self, *, refresh_rate=10):
    self._write(_BME680_REG_SOFTRESET, [0xB6])
    time.sleep(0.005)
    chip_id = self._read_byte(_BME680_REG_CHIPID)
    if chip_id != _BME680_CHIPID:
      raise RuntimeError('Failed 0x%x' % chip_id)
    self._read_calibration()
    self._write(_BME680_BME680_RES_HEAT_0, [0x73])
    self._write(_BME680_BME680_GAS_WAIT_0, [0x65])
    self.sea_level_pressure = 1013.25
    self._pressure_oversample = 0b011
    self._temp_oversample = 0b100
    self._humidity_oversample = 0b010
    self._filter = 0b010
    self._adc_pres = None
    self._adc_temp = None
    self._adc_hum = None
    self._adc_gas = None
    self._gas_range = None
    self._t_fine = None
    self._last_reading = 0
    self._min_refresh_time = 1000 / refresh_rate
  @property
  def pressure_oversample(self):
    return _BME680_SAMPLERATES[self._pressure_oversample]
  @pressure_oversample.setter
  def pressure_oversample(self, sample_rate):
    if sample_rate in _BME680_SAMPLERATES:
      self._pressure_oversample = _BME680_SAMPLERATES.index(sample_rate)
    else:
      raise RuntimeError("Invalid")
  @property
  def humidity_oversample(self):
    return _BME680_SAMPLERATES[self._humidity_oversample]
  @humidity_oversample.setter
  def humidity_oversample(self, sample_rate):
    if sample_rate in _BME680_SAMPLERATES:
      self._humidity_oversample = _BME680_SAMPLERATES.index(sample_rate)
    else:
      raise RuntimeError("Invalid")
  @property
  def temperature_oversample(self):
      return _BME680_SAMPLERATES[self._temp_oversample]
  @temperature_oversample.setter
  def temperature_oversample(self, sample_rate):
    if sample_rate in _BME680_SAMPLERATES:
      self._temp_oversample = _BME680_SAMPLERATES.index(sample_rate)
    else:
      raise RuntimeError("Invalid")
  @property
  def filter_size(self):
    return _BME680_FILTERSIZES[self._filter]
  @filter_size.setter
  def filter_size(self, size):
    if size in _BME680_FILTERSIZES:
      self._filter = _BME680_FILTERSIZES[size]
    else:
      raise RuntimeError("Invalid")
  @property
  def temperature(self):
    self._perform_reading()
    calc_temp = (((self._t_fine * 5) + 128) / 256)
    return calc_temp / 100
  @property
  def pressure(self):
    self._perform_reading()
    var1 = (self._t_fine / 2) - 64000
    var2 = ((var1 / 4) * (var1 / 4)) / 2048
    var2 = (var2 * self._pressure_calibration[5]) / 4
    var2 = var2 + (var1 * self._pressure_calibration[4] * 2)
    var2 = (var2 / 4) + (self._pressure_calibration[3] * 65536)
    var1 = (((((var1 / 4) * (var1 / 4)) / 8192) *
      (self._pressure_calibration[2] * 32) / 8) +
      ((self._pressure_calibration[1] * var1) / 2))
    var1 = var1 / 262144
    var1 = ((32768 + var1) * self._pressure_calibration[0]) / 32768
    calc_pres = 1048576 - self._adc_pres
    calc_pres = (calc_pres - (var2 / 4096)) * 3125
    calc_pres = (calc_pres / var1) * 2
    var1 = (self._pressure_calibration[8] * (((calc_pres / 8) * (calc_pres / 8)) / 8192)) / 4096
    var2 = ((calc_pres / 4) * self._pressure_calibration[7]) / 8192
    var3 = (((calc_pres / 256) ** 3) * self._pressure_calibration[9]) / 131072
    calc_pres += ((var1 + var2 + var3 + (self._pressure_calibration[6] * 128)) / 16)
    return calc_pres/100
  @property
  def humidity(self):
    self._perform_reading()
    temp_scaled = ((self._t_fine * 5) + 128) / 256
    var1 = ((self._adc_hum - (self._humidity_calibration[0] * 16)) -
      ((temp_scaled * self._humidity_calibration[2]) / 200))
    var2 = (self._humidity_calibration[1] *
      (((temp_scaled * self._humidity_calibration[3]) / 100) +
       (((temp_scaled * ((temp_scaled * self._humidity_calibration[4]) / 100)) /
         64) / 100) + 16384)) / 1024
    var3 = var1 * var2
    var4 = self._humidity_calibration[5] * 128
    var4 = (var4 + ((temp_scaled * self._humidity_calibration[6]) / 100)) / 16
    var5 = ((var3 / 16384) * (var3 / 16384)) / 1024
    var6 = (var4 * var5) / 2
    calc_hum = (((var3 + var6) / 1024) * 1000) / 4096
    calc_hum /= 1000
    if calc_hum > 100:
      calc_hum = 100
    if calc_hum < 0:
      calc_hum = 0
    return calc_hum
  @property
  def altitude(self):
    pressure = self.pressure
    return 44330 * (1.0 - math.pow(pressure / self.sea_level_pressure, 0.1903))
  @property
  def gas(self):
    self._perform_reading()
    var1 = ((1340 + (5 * self._sw_err)) * (_LOOKUP_TABLE_1[self._gas_range])) / 65536
    var2 = ((self._adc_gas * 32768) - 16777216) + var1
    var3 = (_LOOKUP_TABLE_2[self._gas_range] * var1) / 512
    calc_gas_res = (var3 + (var2 / 2)) / var2
    return int(calc_gas_res)
  def _perform_reading(self):
    if (time.ticks_diff(self._last_reading, time.ticks_ms()) * time.ticks_diff(0, 1)
        < self._min_refresh_time):
      return
    self._write(_BME680_REG_CONFIG, [self._filter << 2])
    self._write(_BME680_REG_CTRL_MEAS,
      [(self._temp_oversample << 5)|(self._pressure_oversample << 2)])
    self._write(_BME680_REG_CTRL_HUM, [self._humidity_oversample])
    self._write(_BME680_REG_CTRL_GAS, [_BME680_RUNGAS])
    ctrl = self._read_byte(_BME680_REG_CTRL_MEAS)
    ctrl = (ctrl & 0xFC) | 0x01
    self._write(_BME680_REG_CTRL_MEAS, [ctrl])
    new_data = False
    while not new_data:
      data = self._read(_BME680_REG_MEAS_STATUS, 15)
      new_data = data[0] & 0x80 != 0
      time.sleep(0.005)
    self._last_reading = time.ticks_ms()
    self._adc_pres = _read24(data[2:5]) / 16
    self._adc_temp = _read24(data[5:8]) / 16
    self._adc_hum = struct.unpack('>H', bytes(data[8:10]))[0]
    self._adc_gas = int(struct.unpack('>H', bytes(data[13:15]))[0] / 64)
    self._gas_range = data[14] & 0x0F
    var1 = (self._adc_temp / 8) - (self._temp_calibration[0] * 2)
    var2 = (var1 * self._temp_calibration[1]) / 2048
    var3 = ((var1 / 2) * (var1 / 2)) / 4096
    var3 = (var3 * self._temp_calibration[2] * 16) / 16384
    self._t_fine = int(var2 + var3)
  def _read_calibration(self):
    coeff = self._read(_BME680_BME680_COEFF_ADDR1, 25)
    coeff += self._read(_BME680_BME680_COEFF_ADDR2, 16)
    coeff = list(struct.unpack('<hbBHhbBhhbbHhhBBBHbbbBbHhbb', bytes(coeff[1:39])))
    coeff = [float(i) for i in coeff]
    self._temp_calibration = [coeff[x] for x in [23, 0, 1]]
    self._pressure_calibration = [coeff[x] for x in [3, 4, 5, 7, 8, 10, 9, 12, 13, 14]]
    self._humidity_calibration = [coeff[x] for x in [17, 16, 18, 19, 20, 21, 22]]
    self._gas_calibration = [coeff[x] for x in [25, 24, 26]]
    self._humidity_calibration[1] *= 16
    self._humidity_calibration[1] += self._humidity_calibration[0] % 16
    self._humidity_calibration[0] /= 16
    self._heat_range = (self._read_byte(0x02) & 0x30) / 16
    self._heat_val = self._read_byte(0x00)
    self._sw_err = (self._read_byte(0x04) & 0xF0) / 16
  def _read_byte(self, register):
    return self._read(register, 1)[0]
  def _read(self, register, length):
    raise NotImplementedError()
  def _write(self, register, values):
    raise NotImplementedError()
class BME680_I2C(Adafruit_BME680):
  def __init__(self, i2c, address=0x77, debug=False, *, refresh_rate=10):
    self._i2c = i2c
    self._address = address
    self._debug = debug
    super().__init__(refresh_rate=refresh_rate)
  def _read(self, register, length):
    result = bytearray(length)
    self._i2c.readfrom_mem_into(self._address, register & 0xff, result)
    if self._debug:
      print("\t${:x} read ".format(register), " ".join(["{:02x}".format(i) for i in result]))
    return result
  def _write(self, register, values):
    if self._debug:
      print("\t${:x} write".format(register), " ".join(["{:02x}".format(i) for i in values]))
    for value in values:
      self._i2c.writeto_mem(self._address, register, bytearray([value & 0xFF]))
      register += 1

Uploading BME680 Library with uPyCraft IDE

Now, we will look at how to upload the BME680 library to ESP32 and ESP8266. This step is required because MicroPython itself does not contain the sensor’s library. Follow the steps in order to successfully upload the library to ESP32/ESP8266 in uPyCraft IDE:

  1. First, in the Tools section, click on the NEW FILE button and open a file.
uPyCraft IDE create new file
  1. Then replicate the following BME680 library in that file.
  1. Name the file bme680.py and save it by choosing your desired directory.
upycraft IDE save file
  1. Now press the button DOWNLOAD AND RUN in the tools section.
upycraft IDE download and run code

As soon as you click on the download and run button, you will get the message that states “download ok”. Moreover, you will also see bme680.py file lists under device menue.

upload bme680 library to esp32 esp8266 nodemcu upycraft ide

You have now successfully uploaded the BME680 library to ESP32/ESP8266 using uPyCraft IDE. After that, we can use the above library functions to read the Gas (VOC), Pressure, Temperature, Humidity from a sensor. You can use a similar procedure to upload files using Thonny IDE.

Uploading BME680 Library in Thonny IDE

If you are using Thonny IDE , open a new file and copy the code as we did in uPyCraft IDE.

  • Save the file as bm680.py
  • In addition, head over to Device> Upload Current Script with Current Name

In the latest version of Thonny IDE, you will get the option to save the library to a device when you click on the save as option.

You have successfully uploaded BME680 library to ESP32/ESP8266 using Thonny IDE.

MicroPython BME680: Getting Temperature, Humidity, Pressure, and Gas Air Quality Values

As we have already uploaded the BME680 library to ESP32/ESP8266 boards. Now we can use the functions available in the BME680 library to get sensor readings.

Let’s now look at an example to show the working of the sensor. We will connect our BME680 sensor with the ESP module via the I2C protocol as shown above in the connection diagrams. We will see a MicroPython script code and after uploading it to our ESP boards, we will get readings of temperature, humidity, pressure, and Gas Air Quality printed on the MicroPython shell terminal.

BME680 MicroPython Code

Now let’s look at the MicroPython script for BME680 to get sensor readings. Copy the following code to the main.py file and upload the main.py file to ESP32/ESP8266 by following the same process we followed to upload the library file. This microPython script reads Temperature, Humidity, Pressure, and Gas Air Quality Values values from BME680 over I2C bus and prints them on the MicroPython shell console.

from machine import Pin, SoftI2C
from time import sleep
from bme680 import *

# ESP32 Pins for SoftI2C 
i2c = SoftI2C(scl=Pin(22), sda=Pin(21))
# ESP8266 Pins for SoftI2C
#i2c = I2C(scl=Pin(5), sda=Pin(4))

bme = BME680_I2C(i2c=i2c)

while True:
  try:
    temperature = str(round(bme.temperature, 2)) + ' C'
    #temp = (bme.temperature) * (9/5) + 32
    #temp = str(round(temp, 2)) + 'F'
    
    humidity = str(round(bme.humidity, 2)) + ' %'
    
    pressure = str(round(bme.pressure, 2)) + ' hPa'
    
    gas = str(round(bme.gas/1000, 2)) + ' KOhms'

    print('Temperature:', temperature)
    print('Humidity:', humidity)
    print('Pressure:', pressure)
    print('Gas:', gas)
    print('-------')
  except OSError as e:
    print('Failed to read bme680 sensor.')
 
  sleep(5)

How the Code Works?

Importing Libraries

Firstly, we will be importing the Pin class and SoftI2C class from the machine module. This is because we have to specify the pins for I2C communication. We also import the sleep module so that we will be able to add a delay of 5 seconds in between our readings. Also, import the BME680 library which we have previously uploaded to ESP32 or ESP8266.

from machine import Pin, SoftI2C
from time import sleep
from bme680 import *

The reason we have using SoftI2C class. Because I2C library has deprecated in MicroPython. We can define any GPIO pin to be used as SDA and SCL pins for I2C communication.

Defining ESP32/ESP8266 I2C Pins for BME680

Now, we initialize the I2C method by giving it three arguments. The first argument specifies the GPIO pin for SCL. This is given as GPIO22 for ESP32 and GPIO5 for ESP8266. The second parameter specifies the GPIO pin for the SDA. This is given as GPIO21 for ESP32 and GPIO4 for ESP8266. Keep in mind, these are the default I2C pins for SCL and SDA which we have used for both the ESP boards respectively.

# ESP32 Pins for SoftI2C 
i2c = SoftI2C(scl=Pin(22), sda=Pin(21))
# ESP8266 Pins for SoftI2C
#i2c = I2C(scl=Pin(5), sda=Pin(4)

Now, create an object of BME680_I2C() class from bme860 module with the name of “bme”.

bme = BME680_I2C(i2c=i2c)

Getting Sensor Values

Inside the while loop, get the sensor reading by using an object “bme” on temperature, humidity, pressure, and gas methods.

 temperature = str(round(bme.temperature, 2)) + ' C'
 #temp = (bme.temperature) * (9/5) + 32
 #temp = str(round(temp, 2)) + 'F'
    
 humidity = str(round(bme.humidity, 2)) + ' %'
 pressure = str(round(bme.pressure, 2)) + ' hPa'
    
 gas = str(round(bme.gas/1000, 2)) + ' KOhms'

After that print the values on the micropython shell console.

print('Temperature:', temperature)
print('Humidity:', humidity)
print('Pressure:', pressure)
print('Gas:', gas)
print('-------')

Demo

To test the MicroPython script for BME680 with ESP32 and ESP8266, upload the main.py file to ESP32/ESP8266. After uploading the MicroPython script, click on Enable/Reset button of ESP32 or ESP8266:

ESP32 enable button

You will see the Temperature, Humidity, Pressure, and Gas Air Quality values on shell console as follows:

MicroPython BME680 measuring Temperature Humidity Pressure Gas Air Quality

MicroPython: Displaying BME680 Sensor values on OLED Display

In this section, we will see how to display Gas, Pressure, Temperature, Humidity values on a 0.96 SSD1306 OLED display using MicroPython and ESP32/ESP8266. 

SSD1306 OLED Display MicroPython Library

We have already uploaded BME680 MicroPython library to ESP32 and ESP8266 NodeMCU. For an OLED display, we will also need to upload library to ESP boards. 

Copy the following code which is a MicroPython library of OLED and upload library using either uPycraft or Thonny IDE by giving it a name of ssd1306.py. You should follow the same procedure to upload library as we did for bme680.py.

# MicroPython SSD1306 OLED driver, I2C and SPI interfaces
from micropython import const
import time
import framebuf
import sys

currentBoard=""
if(sys.platform=="esp8266"):
  currentBoard="esp8266"
elif(sys.platform=="esp32"):
  currentBoard="esp32"
elif(sys.platform=="pyboard"):
  currentBoard="pyboard"
  import pyb
# 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)
class SSD1306:
  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)
    self.framebuf = framebuf.FrameBuffer(self.buffer, self.width, self.height, framebuf.MVLSB)
    self.poweron()
    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.height == 32 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 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)
  def fill(self, col):
    self.framebuf.fill(col)
  def pixel(self, x, y, col):
    self.framebuf.pixel(x, y, col)
  def scroll(self, dx, dy):
    self.framebuf.scroll(dx, dy)
  def text(self, string, x, y, col=1):
    self.framebuf.text(string, x, y, col)
  def hline(self, x, y, w, col):
    self.framebuf.hline(x, y, w, col)
  def vline(self, x, y, h, col):
    self.framebuf.vline(x, y, h, col)
  def line(self, x1, y1, x2, y2, col):
    self.framebuf.line(x1, y1, x2, y2, col)
  def rect(self, x, y, w, h, col):
    self.framebuf.rect(x, y, w, h, col)
  def fill_rect(self, x, y, w, h, col):
    self.framebuf.fill_rect(x, y, w, h, col)
  def blit(self, fbuf, x, y):
    self.framebuf.blit(fbuf, x, y)

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)
    super().__init__(width, height, external_vcc)
  def write_cmd(self, cmd):
    self.temp[0] = 0x80 # Co=1, D/C#=0
    self.temp[1] = cmd
    #IF SYS  :
    global currentBoard
    if currentBoard=="esp8266" or currentBoard=="esp32":
      self.i2c.writeto(self.addr, self.temp)
    elif currentBoard=="pyboard":
      self.i2c.send(self.temp,self.addr)
    #ELSE:
          
  def write_data(self, buf):
    self.temp[0] = self.addr << 1
    self.temp[1] = 0x40 # Co=0, D/C#=1
    global currentBoard
    if currentBoard=="esp8266" or currentBoard=="esp32":
      self.i2c.start()
      self.i2c.write(self.temp)
      self.i2c.write(buf)
      self.i2c.stop()
    elif currentBoard=="pyboard":
      #self.i2c.send(self.temp,self.addr)
      #self.i2c.send(buf,self.addr)
      self.i2c.mem_write(buf,self.addr,0x40)
  def poweron(self):
    pass

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
    super().__init__(width, height, external_vcc)
  def write_cmd(self, cmd):
    global currentBoard
    if currentBoard=="esp8266" or currentBoard=="esp32":
      self.spi.init(baudrate=self.rate, polarity=0, phase=0)
    elif currentBoard=="pyboard":
      self.spi.init(mode = pyb.SPI.MASTER,baudrate=self.rate, polarity=0, phase=0)
    self.cs.high()
    self.dc.low()
    self.cs.low()
    global currentBoard
    if currentBoard=="esp8266" or currentBoard=="esp32":
      self.spi.write(bytearray([cmd]))
    elif currentBoard=="pyboard":
      self.spi.send(bytearray([cmd]))
    self.cs.high()
  def write_data(self, buf):
    global currentBoard
    if currentBoard=="esp8266" or currentBoard=="esp32":
      self.spi.init(baudrate=self.rate, polarity=0, phase=0)
    elif currentBoard=="pyboard":
      self.spi.init(mode = pyb.SPI.MASTER,baudrate=self.rate, polarity=0, phase=0)
    self.cs.high()
    self.dc.high()
    self.cs.low()
    global currentBoard
    if currentBoard=="esp8266" or currentBoard=="esp32":
      self.spi.write(buf)
    elif currentBoard=="pyboard":
      self.spi.send(buf)
    self.cs.high()
  def poweron(self):
    self.res.high()
    time.sleep_ms(1)
    self.res.low()
    time.sleep_ms(10)
    self.res.high()

Schematic – OLED with ESP32 and BME680

The table below shows the terminals of the three devices which would be connected together.

OLED DisplayESP32BME680
VCCVCC=3.3VVCC
GNDGNDGND
SCLGPIO22SCL
SDAGPIO21SDA

Assemble the circuit as shown in the schematic diagram below:

bme680 with oled and esp32 schematic diagram

As you can see above, we have connected all the VCC terminals with a 3.3V power supply. The SCL terminals are connected with GPIO22 and the SDA terminals are connected with GPIO21. The grounds are also common.

MicroPython Code: Displaying BME680 readings on OLED Display


from machine import Pin,I2C
from time import sleep
import ssd1306
from time import sleep
from bme680 import *
i2c = I2C(scl=Pin(22), sda=Pin(21))
lcd=ssd1306.SSD1306_I2C(128,64,i2c)
bme = BME680_I2C(i2c=i2c)

while True:
  lcd.fill(0)
  try:
    temp = str(round(bme.temperature, 2)) + ' C'
    #temp = (bme.temperature) * (9/5) + 32
    #temp = str(round(temp, 2)) + 'F'
    
    hum = str(round(bme.humidity, 2)) + ' %'
    
    pres = str(round(bme.pressure, 2)) + ' hPa'
    
    gas = str(round(bme.gas/1000, 2)) + ' KOhms'

    print('Temperature:', temp)
    print('Humidity:', hum)
    print('Pressure:', pres)
    print('Gas:', gas)
    print('-------')
    lcd.text("Temp. "+temp, 0, 0)
    lcd.text("PA "+pres, 0, 16)
    lcd.text("Hum. "+hum, 0,32)
    lcd.text("gas. "+gas, 0, 48)

  except OSError as e:
    print('Failed to read sensor.')
  lcd.show()                          #display pix
  sleep(5)

How Does the Code Work?

In the section, we only explained the Micropython code part which is used to display sensor values on OLED. Because reset of the code is the same as we have used in previous sections to display sensor readings on the shell console. 

Import SSD1306 OLED library.

import ssd1306

Create an instance of the SSD1306_I2C() method by giving it the name of “lcd”. The first two arguments to SSD1306_I2C() are the size of OLED that is the number of columns and rows. A last argument is an object of I2C pins which we define with SoftI2C(). 

lcd=ssd1306.SSD1306_I2C(128,64,i2c)

Clears the OLED display with led.fill() routine. 

lcd.fill(0)

Finally display the text along with sensor readings on OLED. 

lcd.text("Temp. "+temp, 0, 0)
lcd.text("PA "+pres, 0, 16)
lcd.text("Hum. "+hum, 0,32)
lcd.text("gas. "+gas, 0, 48)

At the end, call the show() method on lcd method for changes to show on OLED. 

 lcd.show()   #display pix

Demonstration

Upload the above code as main.py file along with bme680.py and ssd1306.py to ESP32 or ESP8266. After that press the reset button on your ESP board. You will get sensor readings on OLED display as follows:

MicroPython Displaying BME680 Sensor values on OLED Display

Video Demo:

If you want to display BME680 sensor values on a web page, you can check our BME680 web server project here:

We have other guides with popular sensors:

Leave a Comment