OLED Display with ESP32 and ESP8266 in MicroPython

In this user guide, we learn about the SSD1306 0.96-inch I2C OLED display and how to connect it with ESP32 and ESP8266 by using MicroPython firmware. An OLED (organic light-emitting diode) is used frequently in displaying texts, bitmap images, shapes, and different types of clocks. They offer good view angles and pixel density in a cost-effective manner. At first, we will take a look at the pinout of the OLED display and then connect it with the ESP boards using MicroPython and then display a simple message on the screen.

MicroPython OLED Display with ESP32 and ESP8266

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.

SSD1306 0.96inch OLED Display

Although there are several types of OLED displays available in the market the one which we will be using is the SSD1306 0.96-inch OLED display. The main component of all different types of OLED displays is an SSD1306 controller which uses I2C or SPI protocol to communicate with the microcontrollers. The OLED displays can vary in size, color, and shape but primarily programmed in a similar way.

Let us take a look at the OLED display which we will be using in this article. It is called SSD 1306 0.96-inch OLED display which has 128×64 pixels and communicates only via I2C protocol with the ESP development boards. It is cheap and readily available in the market.

Below you can see the pinout of this OLED Display.

OLED pinout diagram

SSD1306 OLED Pinout

There are four pins in this display. Imprinted as VCC, GND, SCL, and SDA respectively. The VCC and GND pins will power the OLED display and will be connected with the ESP board’s power supply pins as they require a driving voltage of 3.3-5V. The SCL and SDA pins are necessary for generating the clock signal and in the transmission of data respectively. Both of these pins will be connected with the I2C pins of the ESP32 and ESP8266 boards.

Now let us have a look at the specifications for this model:

Size0.96 inch
Terminals4
Pixels128×64
Communication typeI2C only
VCC3.3-5V
Operating Temperature-40℃ to +80℃
SSD 1306 OLED Display Specifications

Connecting SSD1306 OLED Display with ESP32 and ESP8266

As we have seen above, the OLED display has 4 terminals which we will connect with the ESP boards. As the OLED display requires an operating voltage in the range of 3.3-5V hence we will connect the VCC terminal with 3.3V which will be in common with the ESP board. SCL of the display will be connected with the SCL pin of the module and the SDA of the display will be connected with the SDA of the module. By default, the I2C pin in ESP32 for SDA is GPIO21, and for SCL is GPIO22. Whereas in ESP8266 the default I2C pin for SDA is GPIO4 and for SCL is GPIO5. The ground of both the devices will be held common. The connections between the two devices can be seen below.

ESP32ESP8266SSD1306 OLED Display
VCC=3.3V3.3VVCC
GPIO21(I2C SDA)GPIO4 (D2)SDA
GPIO22 (I2C SCL)GPIO5 (D1)SCL
GROUNDGROUNDGND

The I2C pins stated above are set in default. If you want to use any GPIO pins for I2C, you will have to set it in code using SoftI2C().

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:https://3905944c7706de1a323751faf164f33e.safeframe.googlesyndication.com/safeframe/1-0-38/html/container.htmlAd by Valueimpression

ESP8266 I2C Pins
ESP8266 I2C Pins

More information on ESP32 and ESP8266 pins is available here:

Components Required

We will need the following components to connect our ESP board with the OLED Display.

  1. ESP32/ESP8266
  2. SSD1306 OLED Display
  3. Connecting Wires
  4. Breadboard

Schematic ESP32 with OLED

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 OLED as shown in the schematic diagram below:

OLED interfacing with ESP32
Schematic diagram for ESP32 and OLED Display

Schematic ESP8266 with OLED

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

OLED interfacing with ESP8266 Nodemcu
Schematic Diagram for ESP8266 and OLED Display

SSD1306 OLED MicroPython Library

MicroPython does not contain the library for the OLED display so we would have to upload it ourselves. The library can be found on GitHub which was developed by Adafruit. We will copy the program code for the library from there and upload it on our ESP boards by saving it as ssd1306_oled.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()

Uploading OLED Library with uPyCraft IDE

Now, we will look at how to upload the SSD1306 OLED library to ESP32 and ESP8266. This step is required because the MicroPython implementation itself does not contain the OLED 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 OLED library in that file.
  2. Name the file ssd1306.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 ssd1306.py file lists under the device menu.

Uploading OLED Library with uPyCraft IDE

You have now successfully uploaded the ssd1306 OLED library to ESP32/ESP8266 using uPyCraft IDE. After that, we can use the above library functions to display text, draw shapes, lines, etc, on OLED. You can use a similar procedure to upload files using Thonny IDE.

Uploading Wi-Fi Manager in Thonny IDE

Open a new file and write the OLED library code into that file. Click on the Save button and set the file name as ssd1306.py

micropython wifi manager library upload thonny IDE 1

When you click on the save button, you will see the following window will appear. Select, MicroPython device to upload the library to ESP32 or ESP8266.

Note: The Device options in the menu bar to upload scripts are available in the previous versions of Thonny IDE.

micropython wifi manager library upload thonny IDE 2

After following these steps, we have successfully uploaded the ssd1306.py library to ESP32 or ESP8266 NodeMCU using Thonny IDE.

MicroPython Script: Displaying a simple message on OLED Display

As we have already uploaded the SSD1306 OLED Display Library in our respective ESP boards. Let’s now take a look at the MicroPython code. This code will display a message on the OLED display.

Open a new file and save the file as main.py. Copy the code given below in that file. Make sure both your ssd1306.py and main.py are uploaded to ESP32 or ESP8266.

from machine import Pin, I2C
import ssd1306
from time import sleep


i2c = I2C(-1, scl=Pin(22), sda=Pin(21)) #For ESP32: pin initializing
#i2c = I2C(-1, scl=Pin(5), sda=Pin(4))  #For ESP8266: pin initializing
 
oled_width = 128
oled_height = 64
oled = ssd1306.SSD1306_I2C(oled_width, oled_height, i2c)

oled.text('Welcome to Lab!', 0, 0)
oled.text('Welcome to Lab!', 0, 16)
oled.text('Welcome to Lab!', 0, 32)
        
oled.show()

How the Code works?

Importing I2C and OLED Library

We start off by importing the pin and the I2C classes from the machine module. Because we have to interact with the GPIO pins and transmit data through the I2C protocol hence both these classes are necessary. We will also import the ssd1306 which is the OLED display library that we uploaded earlier. This will help us in accessing all the functions defined inside it.

from machine import Pin, I2C
import ssd1306

Selecting I2C Pins for OLED

Next, we will initialize the I2C GPIO pins for SCL and SDA respectively. We have used the default SCL and SDA pins for both the ESP boards.

We have created an I2C() method which takes in three parameters. The first parameter is -1 indicating that it a software I2C, not a hardware one. The second parameter specifies the I2C GPIO pin of the ESP board which is connected to the SCL line. The third parameter specifies the I2C GPIO pin of the ESP board which is connected to the SDA line.

In case if we are using ESP32, we are setting the SCL on pin 22 and the SDA on pin 21.

i2c = I2C(-1, scl=Pin(22), sda=Pin(21))

 In case if we are using ESP8266, then uncomment the following line and comment the line above which was configuring the pins for ESP32. We are setting the SCL on pin 5 and the SDA on pin 4.

#i2c = I2C(-1, scl=Pin(5), sda=Pin(4))  #For ESP8266: pin initializing

Initialize OLED  

Keep in mind that we are using default I2C pins for both the ESP boards in our script. This data gets saved in the object ‘i2c’ which will connect to the I2C bus and help in the communication between the two devices

We will define the width and the height which are the horizontal and the vertical resolution of the display (128×64) as follows:

oled_width = 128
oled_height = 64

Now, we will create an object ‘oled’ of SSD1306_I2C which uses the width, height, and the i2c object as parameters.

oled = ssd1306.SSD1306_I2C(oled_width, oled_height, i2c) #Initializing the OLED display

Displaying Text on OLED

Next, we used the text() function on the oled object which we initialized before. The text() function takes in three arguments.

  • First argument to text() is the text which we want to display. This should be in the form of ‘string.’ We will be displaying ‘Welcome to Lab’.
  • The second argument is the horizontal position where the text starts. This is known as the ‘X position.’ We will specify it as 0.
  • The third argument is the ‘Y position’ which is the vertical position from where the text starts.

We will specify it first as 0, then 16, and lastly as 32 to view the three messages which we will display in three rows, one after another. The horizontal position remains the same in all. The fourth argument is the color of the text. We are not specifying any fourth argument so it will be set as default which is white. You can choose between black and white. For white, we specify 1 and for black, we specify 0.

To display text just use the text() fucntion on the oled object. The following code displays the message: ‘Welcome to Lab’ on x=0 and y=0.

oled.text('Welcome to Lab!', 0, 0)

Next line of code displays the message on the next line as x=0 and y=16.

oled.text('Welcome to Lab!', 0, 16)
oled.text('Welcome to Lab!', 0, 32)

Lastly, we will use the show() method on the oled object to successfully display the messages on the OLED Display.

oled.show()

Demonstration

Now upload both ssd1306.py and main.py to ESP32 or ESP8266 NodeMCU, you will see this message on OLED:

OLED Display with ESP32 and ESP8266 in MicroPython

MicroPython: Displaying Sensor Value on OLED

In this section, we will see how to display sensor values on OLED. For that, we will take the example of the BME680 sensor and display its values on OLED using ESP32 and ESP8266.

BME680 MicroPython Library

To use the BME680 sensor with ESP32 or ESP8266, we need to upload bme680.py file to ESP boards. You should follow the same procedure to upload the library as we did for ssd1306.py.

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

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:

Leave a Comment