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.
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.
- Getting Started with uPyCraft IDE on ESP32 and ESP8266
- Getting Started with Thonny MicroPython IDE for ESP32 and ESP8266
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.
Accuracy | Operating Range | |
---|---|---|
Temperature | ±1.0°C | -40 to 85 ºC |
Humidity | ±3% | 0-100% |
Pressure | ±1 hPa | 300-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.
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.
BME680 Sensor Module
Pinout Diagram
The following figure shows the pinout of BME680 sensor:
Pin Name | Description |
---|---|
VCC | This pin powers up the sensor. |
GND | This is the common ground pin for power and logic. |
SCL | When 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. |
SDA | When 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. |
SDO | This 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. |
CS | This 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.
BME680 | ESP32 | ESP8266 NodeMCU |
---|---|---|
VCC | VCC=3.3V | VCC=3.3V |
GND | GND | GND |
SCL | GPIO22 | GPIO5 |
SDA | GPIO21 | GPIO4 |
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.
ESP8266 I2C Pins
The following figure shows the I2C pins of ESP8266 NodeMCU:
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.
BME680 | ESP32 | ESP8266 |
---|---|---|
VCC | VCC=3.3V | VCC=3.3V |
GND | GND | GND |
SCL (SCK for SPI) | GPIO18 | GPIO14 (D5) |
SDA (SDI / MOSI for SPI) | GPIO23 | GPIO13 (D7) |
SDO / MISO | GPIO19 | GPIO12 (D6) |
CS | GPIO5 | Any 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.
- ESP32/ESP8266
- BME680 Module
- Connecting Wires
- Breadboard
- 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:
Similarly, if you are using ESP8266 NodeMCU for this project, connect the ESP8266 device with BME680 as shown in the schematic diagram below:
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:
- First, in the Tools section, click on the NEW FILE button and open a file.
- Then replicate the following BME680 library in that file.
- Name the file bme680.py and save it by choosing your desired directory.
- Now press the button DOWNLOAD AND RUN in the tools section.
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.
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:
You will see the Temperature, Humidity, Pressure, and Gas Air Quality values on shell console as follows:
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 Display | ESP32 | BME680 |
---|---|---|
VCC | VCC=3.3V | VCC |
GND | GND | GND |
SCL | GPIO22 | SCL |
SDA | GPIO21 | SDA |
Assemble the circuit as shown in the schematic diagram below:
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:
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:
- MPU-6050 with ESP32/ESP8266 (Accelerometer, Gyroscope, and temperature)
- ESP32/ESP8266: Send Sensor Readings via Email (IFTTT)
- BME280 with ESP32 and ESP8266 – Measure Temperature, Humidity, and Pressure
- BME280 Web Server with ESP32/ESP8266 (Weather Station)
- MicroPython: DS18B20 Web Server with ESP32/ESP8266(Weather Station)
- MicroPython: DHT11/DHT22 Web Server with ESP32/ESP8266 (Weather Station)