BME280 with ESP32 ESP-IDF and Display Readings on OLED

In this tutorial, we will learn about one of the most commonly used sensor called BME280 to get temperature, pressure, and humidity readings using ESP32 and ESP-IDF. This guide will include a brief description of the sensor, connection with ESP32 board and then setting up a project in VS Code with ESP-IDF extension to acquire the sensor data and display it both on the console and SSD1306 OLED.

Before we move ahead, make sure you have the latest version of VS Code installed on your system with the ESP-IDF extension configured.

BME280 Sensor Introduction

BME280 sensor is widely used in electronic projects to measure ambient temperature, barometric pressure, and relative humidity where low power consumption is required. Both I2C and SPI communication protocols can be used to send data over to the microcontroller. Although the BME280 sensor comes in various versions, we will use I2C communication to communicate the sensor data with the ESP32 board. This protocol is based on a synchronous, multi-master multi-slave system where the ESP32 board acts as the master and the BME280 sensor acts as the slave.

The diagram below shows the pinout of the BME280 sensor that we will use.

BME280 Sensor Pinout

As you can see in the diagram above, the BME280 sensor consists of four pins. Two are power supply pins and two are I2C communication pins.

VINThis is the power supply pin which is connected with 3.3V
GNDThis is the ground pin
SCLThis is the serial clock pin which will produce the clock signal
SDAThis is the serial data pin which is used for sending and receiving data

ESP32 I2C Pins

The diagram below shows the pinout for the ESP32, where the default I2C pins are highlighted in red. These are the default I2C pins of the ESP32 DEVKIT module. However, if you want to use other GPIO pins as I2C pins then you will have to set them in code.

ESP32 Pinout I2C Pins
ESP32 I2C Pins

Refer to the following article to know more about ESP32 GPIO pins:

Interfacing BME280 with ESP32

We will need the following components to connect our ESP32 board with the I2C LCD.

  1. ESP32 board
  2. BME280 Sensor
  3. Connecting Wires
  4. Breadboard

The connection of I2C LCD with the ESP32 board is straightforward as it just involves the connection of the 4 pins (GND, VCC, SDA, and SCL) with ESP32. We have to connect the VCC terminal with 3.3V pin, ground with the ground (common ground), SCL of the sensor with SCL of ESP32 board, and SDA of the sensor with the SDA pin of the ESP32 board.

In ESP32, the default I2C pin for SDA is GPIO21 and for SCL is GPIO22.

The connections between the devices are specified in the table below:

BME280 SensorESP32
GNDGND
VCC3.3V
SDAGPIO21(I2C SDA)
SCLGPIO22 (I2C SCL)
ESP32 with BME280 sensor connection diagram

Measure BME280 Readings with ESP32 and ESP-IDF

We will build and create a project in VS Code with ESP-IDF extension. The code will read temperature, humidity, and pressure values by using the I2C master driver provided by ESP-IDF. We will use BME280 library written by BoschSensortec.

Create Example Project

Open your VS Code and head over to View > Command Palette. Type ESP-IDF: New Project in the search bar and press enter.

Specify the project name and directory. We have named our project ‘BME280.’ For the ESP-IDF board, we have chosen the custom board option. For ESP-IDF target, we have chosen ESP32 module. Click ‘Choose Template’ button to proceed forward.

In the Extension, select ESP-IDF option:

ESP-IDF in VS Code New Project 2

We will click the ‘sample_project’ under the get-started tab. Now click ‘Create project using template sample_project.’

ESP-IDF in VS Code New Project 3

You will get a notification that the project has been created. To open the project in a new window, click ‘Yes.’

This opens the project that we created inside the EXPLORER tab. There are several folders inside our project folder. This is the same for every project which you will create through ESP-IDF Explorer.

  • First, let’s add the necessary header files for the libraries required for this project. Create a new folder called ‘components/bme280’ and add the following files present at this link. This includes all the files listed below:
ESP-IDF BME280 Add Libraries
  • Now head over to the main.c file. The main folder contains the source code meaning the main.c file will be found here. Go to main > main.c and open it. Copy the code given below in that file and save it.

ESP32 BME280 Display Readings Code

#include "driver/gpio.h"
#include "driver/i2c.h"
#include "esp_err.h"
#include "esp_log.h"
#include "freertos/task.h"

#include "sdkconfig.h" // generated by "make menuconfig"

#include "bme280.h"

#define SDA_PIN GPIO_NUM_21
#define SCL_PIN GPIO_NUM_22

#define TAG_BME280 "BME280"

#define I2C_MASTER_ACK 0
#define I2C_MASTER_NACK 1

void i2c_master_init()
{
	i2c_config_t i2c_config = {
		.mode = I2C_MODE_MASTER,
		.sda_io_num = SDA_PIN,
		.scl_io_num = SCL_PIN,
		.sda_pullup_en = GPIO_PULLUP_ENABLE,
		.scl_pullup_en = GPIO_PULLUP_ENABLE,
		.master.clk_speed = 1000000
	};
	i2c_param_config(I2C_NUM_0, &i2c_config);
	i2c_driver_install(I2C_NUM_0, I2C_MODE_MASTER, 0, 0, 0);
}

s8 BME280_I2C_bus_write(u8 dev_addr, u8 reg_addr, u8 *reg_data, u8 cnt)
{
	s32 iError = BME280_INIT_VALUE;

	esp_err_t espRc;
	i2c_cmd_handle_t cmd = i2c_cmd_link_create();

	i2c_master_start(cmd);
	i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_WRITE, true);

	i2c_master_write_byte(cmd, reg_addr, true);
	i2c_master_write(cmd, reg_data, cnt, true);
	i2c_master_stop(cmd);

	espRc = i2c_master_cmd_begin(I2C_NUM_0, cmd, 10/portTICK_PERIOD_MS);
	if (espRc == ESP_OK) {
		iError = SUCCESS;
	} else {
		iError = FAIL;
	}
	i2c_cmd_link_delete(cmd);

	return (s8)iError;
}

s8 BME280_I2C_bus_read(u8 dev_addr, u8 reg_addr, u8 *reg_data, u8 cnt)
{
	s32 iError = BME280_INIT_VALUE;
	esp_err_t espRc;

	i2c_cmd_handle_t cmd = i2c_cmd_link_create();

	i2c_master_start(cmd);
	i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_WRITE, true);
	i2c_master_write_byte(cmd, reg_addr, true);

	i2c_master_start(cmd);
	i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_READ, true);

	if (cnt > 1) {
		i2c_master_read(cmd, reg_data, cnt-1, I2C_MASTER_ACK);
	}
	i2c_master_read_byte(cmd, reg_data+cnt-1, I2C_MASTER_NACK);
	i2c_master_stop(cmd);

	espRc = i2c_master_cmd_begin(I2C_NUM_0, cmd, 10/portTICK_PERIOD_MS);
	if (espRc == ESP_OK) {
		iError = SUCCESS;
	} else {
		iError = FAIL;
	}

	i2c_cmd_link_delete(cmd);

	return (s8)iError;
}

void BME280_delay_msek(u32 msek)
{
	vTaskDelay(msek/portTICK_PERIOD_MS);
}

void bme280_reader_task(void *ignore)
{
	struct bme280_t bme280 = {
		.bus_write = BME280_I2C_bus_write,
		.bus_read = BME280_I2C_bus_read,
		.dev_addr = BME280_I2C_ADDRESS1,
		.delay_msec = BME280_delay_msek
	};

	s32 com_rslt;
	s32 v_uncomp_pressure_s32;
	s32 v_uncomp_temperature_s32;
	s32 v_uncomp_humidity_s32;

	com_rslt = bme280_init(&bme280);

	com_rslt += bme280_set_oversamp_pressure(BME280_OVERSAMP_16X);
	com_rslt += bme280_set_oversamp_temperature(BME280_OVERSAMP_2X);
	com_rslt += bme280_set_oversamp_humidity(BME280_OVERSAMP_1X);

	com_rslt += bme280_set_standby_durn(BME280_STANDBY_TIME_1_MS);
	com_rslt += bme280_set_filter(BME280_FILTER_COEFF_16);

	com_rslt += bme280_set_power_mode(BME280_NORMAL_MODE);
	if (com_rslt == SUCCESS) {
		while(true) {
			vTaskDelay(40/portTICK_PERIOD_MS);

			com_rslt = bme280_read_uncomp_pressure_temperature_humidity(
				&v_uncomp_pressure_s32, &v_uncomp_temperature_s32, &v_uncomp_humidity_s32);

			if (com_rslt == SUCCESS) {
				ESP_LOGI(TAG_BME280, "%.2f degC / %.3f hPa / %.3f %%",
					bme280_compensate_temperature_double(v_uncomp_temperature_s32),
					bme280_compensate_pressure_double(v_uncomp_pressure_s32)/100, // Pa -> hPa
					bme280_compensate_humidity_double(v_uncomp_humidity_s32));
			} else {
				ESP_LOGE(TAG_BME280, "measure error. code: %d", com_rslt);
			}
		}
	} else {
		ESP_LOGE(TAG_BME280, "init or setting error. code: %d", com_rslt);
	}

	vTaskDelete(NULL);
}

void app_main(void)
{
	i2c_master_init();
	xTaskCreate(&bme280_reader_task, "bme280_reader_task",  2048, NULL, 6, NULL);
}

How the Code Works?

Firstly, we will start by including the necessary libraries for this project. This includes the i2c driver, gpio driver, ESP logging library, BME280 library and FreeRTOS library. The driver/i2c.h library will enable the ESP32 to communicate with other I2C devices, in our case it is the BME280 sensor. 

#include "driver/gpio.h"
#include "driver/i2c.h"
#include "esp_err.h"
#include "esp_log.h"
#include "freertos/task.h"

#include "sdkconfig.h" // generated by "make menuconfig"

#include "bme280.h"

Next, we have defined the SCL and SDA pins for I2C communication. Use these GPIO pins to connect the two devices together. You may change these pins as well.

#define SDA_PIN GPIO_NUM_21
#define SCL_PIN GPIO_NUM_22

This code uses Informational Logging. The log function takes in two arguments. The first argument is the tag and the second argument is a formatted string. Therefore this global variable will be useful while calling the ESP_LOGI() functions. Here, “BME280″ is the tag that will be used while logging.

#define TAG_BME280 "BME280"

Initializing I2C Master (ESP32)

The following function is responsible for initializing the I2C master.

void i2c_master_init()
{
	i2c_config_t i2c_config = {
		.mode = I2C_MODE_MASTER,
		.sda_io_num = SDA_PIN,
		.scl_io_num = SCL_PIN,
		.sda_pullup_en = GPIO_PULLUP_ENABLE,
		.scl_pullup_en = GPIO_PULLUP_ENABLE,
		.master.clk_speed = 1000000
	};
	i2c_param_config(I2C_NUM_0, &i2c_config);
	i2c_driver_install(I2C_NUM_0, I2C_MODE_MASTER, 0, 0, 0);
}

First, the parameters of the i2c_config_t structure are defined. This includes the mode of operation which is master in this case, the I2C communication pins, configure the internal pull-up resistors of ESP32 and setting the I2C clock speed.

	i2c_config_t i2c_config = {
		.mode = I2C_MODE_MASTER,
		.sda_io_num = SDA_PIN,
		.scl_io_num = SCL_PIN,
		.sda_pullup_en = GPIO_PULLUP_ENABLE,
		.scl_pullup_en = GPIO_PULLUP_ENABLE,
		.master.clk_speed = 1000000
	};

After the parameters are set, then the I2C configuration is initialized for the particular port. This is achieved by calling the function i2c_param_config() which takes in two parameters. The first parameter is the I2C port and the second parameter is the pointer to a constant configuration structure of type i2c_config_t. At this point, the I2C port gets configured.

i2c_param_config(I2C_NUM_0, &i2c_config);

The next step is to install the I2C driver. It takes in several parameters which are listed below:

  • The I2C port number.
  • The configuration mode that we set (master/slave)
  • RX buffer disable. This is the receiving buffer size which is applicable for the slave.
  • TX buffer disable. This is the sending buffer size which is also applicable for the slave.
  • The last parameter is ‘intr_alloc_flags’ which is the flags for the allocated interrupt. It is ‘0’ in our case as we are not using interrupts in this example.
i2c_driver_install(I2C_NUM_0, I2C_MODE_MASTER, 0, 0, 0);

BME280 I2C Bus Write/Read Functions

s8 BME280_I2C_bus_write(u8 dev_addr, u8 reg_addr, u8 *reg_data, u8 cnt)
{
	s32 iError = BME280_INIT_VALUE;

	esp_err_t espRc;
	i2c_cmd_handle_t cmd = i2c_cmd_link_create();

	i2c_master_start(cmd);
	i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_WRITE, true);

	i2c_master_write_byte(cmd, reg_addr, true);
	i2c_master_write(cmd, reg_data, cnt, true);
	i2c_master_stop(cmd);

	espRc = i2c_master_cmd_begin(I2C_NUM_0, cmd, 10/portTICK_PERIOD_MS);
	if (espRc == ESP_OK) {
		iError = SUCCESS;
	} else {
		iError = FAIL;
	}
	i2c_cmd_link_delete(cmd);

	return (s8)iError;
}

Inside the BME280_I2C_bus_write() function, we start off by creating and initializing the I2C command link using i2c_cmd_link_create() function. This function will return an I2C command link handler.

i2c_cmd_handle_t cmd = i2c_cmd_link_create();

After we create the command link, we will call i2c_master_start() and pass the command link as a parameter inside it. This will enable the I2C master to generate a starting signal. It will transmit all queued commands.

Note: This function is only called when the device acts as a master.

i2c_master_start(cmd);

Then we call i2c_master_write_byte() function is used to specify the slave address. This function takes in three parameters. The I2C command link, I2C one byte command which will be written to the bus and the enable ack check for the master device. Here “(dev_addr << 1) | I2C_MASTER_WRITE” is the salve address that specifies the master will write to the slave.

i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_WRITE, true);

Next, we call i2c_master_write() function for the I2C master to write the buffer to the I2C bus. This function takes in four parameters which are the I2C command handler, the data to be transferred, the length of the data and enable ack check for the master device.

i2c_master_write_byte(cmd, reg_addr, true);
i2c_master_write(cmd, reg_data, cnt, true);

Then we call the function i2c_master_cmd_begin() which will enable the I2C master to start sending the queued commands. This function takes in three parameters. The first parameter is the I2C port number, the second parameter is the I2C command handler and the third parameter is the maximum wait ticks which is set to 0.01 second in this case.

	espRc = i2c_master_cmd_begin(I2C_NUM_0, cmd, 10/portTICK_PERIOD_MS);
	if (espRc == ESP_OK) {
		iError = SUCCESS;
	} else {
		iError = FAIL;
	}

Afterwards, we will free the command link when we are done sending the commands. This is achieved by calling the i2c_cmd_link() function and passing the I2C command handler as a parameter inside it.

i2c_cmd_link_delete(cmd);

Inside the BME280_I2C_bus_read() function, all the same steps are followed to create the link and start the I2C master bus and write byte just as we did for BME280_I2C_bus_write(). This time the command link sent by the master to read data from the slave contains the address ESP_SLAVE_ADDR << 1) | I2C_MASTER_READ which is specified as the second parameter in the i2c_master_write_byte() function will However, now we will populate the command link inside the if condition which checks if the length of data is greater than 1, then it calls i2c_master_read() to read multiple bytes on the I2C bus. It takes in four parameters. The first parameter is the I2C command handler. The second parameter is the data to be transferred. The third parameter is the length of the data buffer in bytes. Lastly, the fourth parameter is to enable ACK signal for the master.

Next, call i2c_master_read_byte() to read a single byte on the I2C bus. Also, it is configured such that the master does not provide the ACK bit. After that queue a “STOP signal” to the given commands list by calling i2c_master_stop().

Then we call the function i2c_master_cmd_begin() which will enable the I2C master to start sending the queued commands. Afterwards, we will free the command link when we are done sending the commands.

s8 BME280_I2C_bus_read(u8 dev_addr, u8 reg_addr, u8 *reg_data, u8 cnt)
{
	s32 iError = BME280_INIT_VALUE;
	esp_err_t espRc;

	i2c_cmd_handle_t cmd = i2c_cmd_link_create();

	i2c_master_start(cmd);
	i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_WRITE, true);
	i2c_master_write_byte(cmd, reg_addr, true);

	i2c_master_start(cmd);
	i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_READ, true);

	if (cnt > 1) {
		i2c_master_read(cmd, reg_data, cnt-1, I2C_MASTER_ACK);
	}
	i2c_master_read_byte(cmd, reg_data+cnt-1, I2C_MASTER_NACK);
	i2c_master_stop(cmd);

	espRc = i2c_master_cmd_begin(I2C_NUM_0, cmd, 10/portTICK_PERIOD_MS);
	if (espRc == ESP_OK) {
		iError = SUCCESS;
	} else {
		iError = FAIL;
	}

	i2c_cmd_link_delete(cmd);

	return (s8)iError;
}

Read from BME280

Inside the bme280_reader_task() function, the BME280 sensor is initialized and sensor data is acquired and printed on the ESP-IDF serial terminal continuously.

void bme280_reader_task(void *ignore)
{
	struct bme280_t bme280 = {
		.bus_write = BME280_I2C_bus_write,
		.bus_read = BME280_I2C_bus_read,
		.dev_addr = BME280_I2C_ADDRESS1,
		.delay_msec = BME280_delay_msek
	};

	s32 com_rslt;
	s32 v_uncomp_pressure_s32;
	s32 v_uncomp_temperature_s32;
	s32 v_uncomp_humidity_s32;

	com_rslt = bme280_init(&bme280);

	com_rslt += bme280_set_oversamp_pressure(BME280_OVERSAMP_16X);
	com_rslt += bme280_set_oversamp_temperature(BME280_OVERSAMP_2X);
	com_rslt += bme280_set_oversamp_humidity(BME280_OVERSAMP_1X);

	com_rslt += bme280_set_standby_durn(BME280_STANDBY_TIME_1_MS);
	com_rslt += bme280_set_filter(BME280_FILTER_COEFF_16);

	com_rslt += bme280_set_power_mode(BME280_NORMAL_MODE);
	if (com_rslt == SUCCESS) {
		while(true) {
			vTaskDelay(40/portTICK_PERIOD_MS);

			com_rslt = bme280_read_uncomp_pressure_temperature_humidity(
				&v_uncomp_pressure_s32, &v_uncomp_temperature_s32, &v_uncomp_humidity_s32);

			if (com_rslt == SUCCESS) {
				ESP_LOGI(TAG_BME280, "%.2f degC / %.3f hPa / %.3f %%",
					bme280_compensate_temperature_double(v_uncomp_temperature_s32),
					bme280_compensate_pressure_double(v_uncomp_pressure_s32)/100, // Pa -> hPa
					bme280_compensate_humidity_double(v_uncomp_humidity_s32));
			} else {
				ESP_LOGE(TAG_BME280, "measure error. code: %d", com_rslt);
			}
		}
	} else {
		ESP_LOGE(TAG_BME280, "init or setting error. code: %d", com_rslt);
	}

	vTaskDelete(NULL);
}

First, the parameters of the bme280_t structure are defined. This includes the I2C write, I2C read, slave address and delay.

	struct bme280_t bme280 = {
		.bus_write = BME280_I2C_bus_write,
		.bus_read = BME280_I2C_bus_read,
		.dev_addr = BME280_I2C_ADDRESS1,
		.delay_msec = BME280_delay_msek
	};

Declare the following variables to holds the BME280 sensor data:

	s32 com_rslt;
	s32 v_uncomp_pressure_s32;
	s32 v_uncomp_temperature_s32;
	s32 v_uncomp_humidity_s32;

Initialize the BME280 sensor by calling bme280_init() and specifying the pointer to the bme280_t structure as a parameter inside it.

com_rslt = bme280_init(&bme280);

Next, we set the pressure oversampling setting in the register 0xF4 bits from 2 to 4 by calling bme280_set_overamp_pressure(). This function takes in a single parameter inside it which is the value of the pressure over sampling. It can take any value listed below:

  • Skipped: value is 0x00
  • BME280_OVERSAMP_1X: Value is 0x01
  • BME280_OVERSAMP_2X: Value is 0x02
  • BME280_OVERSAMP_4X: Value is 0x03
  • BME280_OVERSAMP_8X: Value is 0x04
  • BME280_OVERSAMP_16X: Value is 0x05, 0x06 and 0x07
com_rslt += bme280_set_oversamp_pressure(BME280_OVERSAMP_16X);

Similarly, we set the temperature oversampling setting in the register 0xF4 bits from 5 to 7 by calling bme280_set_overamp_temperature(). This function takes in a single parameter inside it which is the value of the temperature over sampling. It can take any value listed below:

  • Skipped: value is 0x00
  • BME280_OVERSAMP_1X: Value is 0x01
  • BME280_OVERSAMP_2X: Value is 0x02
  • BME280_OVERSAMP_4X: Value is 0x03
  • BME280_OVERSAMP_8X: Value is 0x04
  • BME280_OVERSAMP_16X: Value is 0x05, 0x06 and 0x07
com_rslt += bme280_set_oversamp_temperature(BME280_OVERSAMP_2X);

Likewise, we set the humidity oversampling setting in the register 0xF2 bits from 0 to 2 by calling bme280_set_overamp_humidity(). This function takes in a single parameter inside it which is the value of the humidity over sampling. It can take any value listed below:

  • Skipped: value is 0x00
  • BME280_OVERSAMP_1X
  • BME280_OVERSAMP_2X
  • BME280_OVERSAMP_4X
  • BME280_OVERSAMP_8X
  • BME280_OVERSAMP_16X
com_rslt += bme280_set_oversamp_humidity(BME280_OVERSAMP_1X);

After that we will write the standby duration time from the sensor in the register 0xF5 bit 5 to 7 by calling bme280_set_standby_durn(). This function takes in a single parameter which is the value of the standby duration time. It can take any value listed below:

  • BME280_STANDBY_TIME_1_MS
  • BME280_STANDBY_TIME_63_MS
  • BME280_STANDBY_TIME_125_MS
  • BME280_STANDBY_TIME_250_MS
  • BME280_STANDBY_TIME_500_MS
  • BME280_STANDBY_TIME_1000_MS
  • BME280_STANDBY_TIME_2000_MS
  • BME280_STANDBY_TIME_4000_MS
com_rslt += bme280_set_standby_durn(BME280_STANDBY_TIME_1_MS);

Then we will specify the filter setting in the register 0xF5 bit 3 and 4 by calling bme280_set_filter() function. This function also takes in a single parameter which is the value of IIR filter coefficient. It can take any value listed below:

  • BME280_FILTER_COEFF_OFF
  • BME280_FILTER_COEFF_2
  • BME280_FILTER_COEFF_4
  • BME280_FILTER_COEFF_8
  • BME280_FILTER_COEFF_16
com_rslt += bme280_set_filter(BME280_FILTER_COEFF_16);

Finally, we will set the power mode of the sensor by calling bme280_set_power_mode(). This function takes in a single parameter which denotes the operational mode in the register 0xF4 bit 0 and 1. It can take any value listed below:

  • BME280_SLEEP_MODE
  • BME280_FORCED_MODE
  • BME280_NORMAL_MODE
com_rslt += bme280_set_power_mode(BME280_NORMAL_MODE);

If all the configurations of the sensor are set successfully, then we will start the infinite loop which reads and prints the sensor data on the console. To read the uncompensated pressure, temperature and humidity readings we call the function bme280_read_uncomp_pressure_temperature_humidity(). This function takes in three parameters which are the value of the uncompensated pressure, temperature and humidity respectively. If the BME280 sensor is measuring correctly, the sensor readings will be logged on the terminal. The bme280_compensate_temperature_double() outputs the actual temperature from uncompensated temperature. The bme280_compensate_pressure_double() outputs the actual pressure from the uncompensated pressure. Similarly, the bme280_compensate_humidity_double() outputs the actual humidity from the uncompensated humidity. Incase, of an error in measuring or initialization of the sensor, a relevant message will be printed.

	if (com_rslt == SUCCESS) {
		while(true) {
			vTaskDelay(40/portTICK_PERIOD_MS);

			com_rslt = bme280_read_uncomp_pressure_temperature_humidity(
				&v_uncomp_pressure_s32, &v_uncomp_temperature_s32, &v_uncomp_humidity_s32);

			if (com_rslt == SUCCESS) {
				ESP_LOGI(TAG_BME280, "%.2f degC / %.3f hPa / %.3f %%",
					bme280_compensate_temperature_double(v_uncomp_temperature_s32),
					bme280_compensate_pressure_double(v_uncomp_pressure_s32)/100, // Pa -> hPa
					bme280_compensate_humidity_double(v_uncomp_humidity_s32));
			} else {
				ESP_LOGE(TAG_BME280, "measure error. code: %d", com_rslt);
			}
		}
	} else {
		ESP_LOGE(TAG_BME280, "init or setting error. code: %d", com_rslt);
	}

After that we will delete the task by using the vTaskDelete() function. Passing NULL as the parameter, will cause the calling task to be deleted.

vTaskDelete(NULL);

app_main()

Inside the app_main() function, we will call the function i2c_master_init() to initialize the I2C master. Then use xTaskCreate() to create the bme280 reader task that will obtain BME280 sensor readings and print them on the console continuously.

void app_main(void)
{
	i2c_master_init();
	xTaskCreate(&bme280_reader_task, "bme280_reader_task",  2048, NULL, 6, NULL);
}

Compiling the Sketch

To flash your chip, type the following command in the serial terminal. Remember to replace the COM port with the one through which your board is connected.

idf.py -p COMX flash monitor

After the code is successfully flashed, you can view the temperature, pressure and humidity readings on the console.

ESP-IDF BME280 Display Sensor Data on Console

ESP32 ESP-IDF Display BME280 Readings on OLED

Now let us create another project where we will program our ESP32 board to acquire BME280 sensor readings and display them on a 0.96 SSD1306 OLED display.

Create Project

Follow all the same steps to create a similar project as we did before. This time, however, we have to add library for the OLED as well.

  • First, let’s add the necessary header files for the libraries required for this project. After adding the BME280 libraries let us proceed to add ssd1306 libraries. Under the components folder, create a new folder called ‘ssd1306’ and add all the files present at this link. This includes all the files listed below:
ESP-IDF BME280 with OLED Add Libraries

OLED MenuConfig Settings ESP-IDF

Let’s first head over to the menuconfig. Open the ESP-IDF SDK Configuration Editor. Scroll down and open the SSD1306 Configuration. Here we can set the SSD1306 configuration parameter according to our needs. This includes the UART interface, panel type, SCL GPIO pin, SDA GPIO pin, and the reset GPIO pin. Here you can view that by default, ESP-IDF is using the interface as I2C, panel type as 128×64, SCL GPIO pin as 22, SDA GPIO pin as 21 and Reset GPIO pin as 33. You can alter these parameters according to your OLED display and then click the Save button found at the top. We will leave the configuration settings as default as they match our module.

As our BME280 sensor also communicates via I2C interface hence same communication pins will be used for both devices.

ESP32 OLED using ESP-IDF TextDemo project SDK configuration editor
  • Now head over to the main.c file. The main folder contains the source code meaning the main.c file will be found here. Go to main > main.c and open it. Copy the code given below in that file and save it.

ESP32 BME280 Display Readings on OLED Code

#include "driver/gpio.h"
#include "driver/i2c.h"
#include "esp_err.h"
#include "esp_log.h"
#include "freertos/task.h"

#include "sdkconfig.h" // generated by "make menuconfig"

#include "bme280.h"
#include "ssd1306.h"


#define TAG_BME280 "BME280"

#define I2C_MASTER_ACK 0
#define I2C_MASTER_NACK 1

s8 BME280_I2C_bus_write(u8 dev_addr, u8 reg_addr, u8 *reg_data, u8 cnt)
{
	s32 iError = BME280_INIT_VALUE;

	esp_err_t espRc;
	i2c_cmd_handle_t cmd = i2c_cmd_link_create();

	i2c_master_start(cmd);
	i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_WRITE, true);

	i2c_master_write_byte(cmd, reg_addr, true);
	i2c_master_write(cmd, reg_data, cnt, true);
	i2c_master_stop(cmd);

	espRc = i2c_master_cmd_begin(I2C_NUM_0, cmd, 10/portTICK_PERIOD_MS);
	if (espRc == ESP_OK) {
		iError = SUCCESS;
	} else {
		iError = FAIL;
	}
	i2c_cmd_link_delete(cmd);

	return (s8)iError;
}

s8 BME280_I2C_bus_read(u8 dev_addr, u8 reg_addr, u8 *reg_data, u8 cnt)
{
	s32 iError = BME280_INIT_VALUE;
	esp_err_t espRc;

	i2c_cmd_handle_t cmd = i2c_cmd_link_create();

	i2c_master_start(cmd);
	i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_WRITE, true);
	i2c_master_write_byte(cmd, reg_addr, true);

	i2c_master_start(cmd);
	i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_READ, true);

	if (cnt > 1) {
		i2c_master_read(cmd, reg_data, cnt-1, I2C_MASTER_ACK);
	}
	i2c_master_read_byte(cmd, reg_data+cnt-1, I2C_MASTER_NACK);
	i2c_master_stop(cmd);

	espRc = i2c_master_cmd_begin(I2C_NUM_0, cmd, 10/portTICK_PERIOD_MS);
	if (espRc == ESP_OK) {
		iError = SUCCESS;
	} else {
		iError = FAIL;
	}

	i2c_cmd_link_delete(cmd);

	return (s8)iError;
}

void BME280_delay_msek(u32 msek)
{
	vTaskDelay(msek/portTICK_PERIOD_MS);
}

void bme280_oled_task(void *ignore)
{
		SSD1306_t dev;
		i2c_master_init(&dev, CONFIG_SDA_GPIO, CONFIG_SCL_GPIO, CONFIG_RESET_GPIO);
		
		ssd1306_init(&dev, 128, 64);
		ssd1306_clear_screen(&dev, false);
		ssd1306_contrast(&dev, 0xff);
		

	struct bme280_t bme280 = {
		.bus_write = BME280_I2C_bus_write,
		.bus_read = BME280_I2C_bus_read,
		.dev_addr = BME280_I2C_ADDRESS1,
		.delay_msec = BME280_delay_msek
	};

	s32 com_rslt;
	s32 v_uncomp_pressure_s32;
	s32 v_uncomp_temperature_s32;
	s32 v_uncomp_humidity_s32;
	

	com_rslt = bme280_init(&bme280);

	com_rslt += bme280_set_oversamp_pressure(BME280_OVERSAMP_16X);
	com_rslt += bme280_set_oversamp_temperature(BME280_OVERSAMP_2X);
	com_rslt += bme280_set_oversamp_humidity(BME280_OVERSAMP_1X);

	com_rslt += bme280_set_standby_durn(BME280_STANDBY_TIME_1_MS);
	com_rslt += bme280_set_filter(BME280_FILTER_COEFF_16);

	com_rslt += bme280_set_power_mode(BME280_NORMAL_MODE);
	if (com_rslt == SUCCESS) {
		while(true) {
			vTaskDelay(1000/portTICK_PERIOD_MS);

			com_rslt = bme280_read_uncomp_pressure_temperature_humidity(
				&v_uncomp_pressure_s32, &v_uncomp_temperature_s32, &v_uncomp_humidity_s32);

				   double temp = bme280_compensate_temperature_double(v_uncomp_temperature_s32);
				   char temperature[12];
                   sprintf(temperature, "%.2f degC", temp);

				   double press = bme280_compensate_pressure_double(v_uncomp_pressure_s32)/100; // Pa -> hPa
				   char pressure[10];
                   sprintf(pressure, "%.2f hPa", press);

					double hum = bme280_compensate_humidity_double(v_uncomp_humidity_s32);
					char humidity[10];
                   sprintf(humidity, "%.2f %%", hum);
				   

			if (com_rslt == SUCCESS) {
				
	            ssd1306_display_text(&dev, 0, temperature, 12, false);
				ssd1306_display_text(&dev, 2, pressure, 10, false);
				ssd1306_display_text(&dev, 4, humidity, 10, false);

			} else {
				ESP_LOGE(TAG_BME280, "measure error. code: %d", com_rslt);
			}
		}
	} else {
		ESP_LOGE(TAG_BME280, "init or setting error. code: %d", com_rslt);
	}

	vTaskDelete(NULL);
}


void app_main(void)
{
  xTaskCreate(&bme280_oled_task, "bme280_oled_task",  2048*2, NULL, 6, NULL);

}

How does the Code Works?

Most of the code is the same as in the previous project where we accessed sensor readings of BME280 continuously. This time however, instead of displaying the readings on the ESP-IDF terminal, our aim is to display them on an OLED. So let us look over the parts where we are incorporating the OLED. The rest of the functions are the same to acquire the BME280 temperature, pressure, and humidity readings.

Inside the bme280_oled_task() we will first initialize the I2C interface. The I2C interface will be configured by calling the i2c_master_init() function and passing the address of the SSD1306_t structure, SDA pin, SCL pin and Reset pin as parameters inside it. This will configure the ESP32 master to listen to slaves on the I2C bus.

	SSD1306_t dev;
	i2c_master_init(&dev, CONFIG_SDA_GPIO, CONFIG_SCL_GPIO, CONFIG_RESET_GPIO);

After that, we will initialize the OLED display by calling ssd1306_init() function. This takes in three parameters. The first parameter is the address of the SSD1306_t structure, the second parameter is the width and the third parameter is the height of the display in pixels. We are using a 128×64 display hence the width is 128 and the height is 64.

ssd1306_init(&dev, 128, 64);

Let’s clear the screen of the OLED display by using ssd1306_clear_screen(). This function takes in two parameters. The first is the address of the SSD1306_t structure and the second parameter is invert which is a bool variable. It is set as false. This means we have a dark background and the text will be displayed in white.

ssd1306_clear_screen(&dev, false);

Secondly, the contrast of the screen is set using ssd1306_contrast(). This function takes in two parameters. The first is the address of the SSD1306_t structure and the second parameter is the contrast value which is an int variable. It is set as 0xff.

ssd1306_contrast(&dev, 0xff);

After that the BME280 sensor is configured and initialized. If all the configurations of the sensor are set successfully, then we will start the infinite loop which reads and prints the sensor data on the OLED. To read the uncompensated pressure, temperature and humidity readings we call the function bme280_read_uncomp_pressure_temperature_humidity().

com_rslt = bme280_read_uncomp_pressure_temperature_humidity(&v_uncomp_pressure_s32, &v_uncomp_temperature_s32, &v_uncomp_humidity_s32);

The bme280_compensate_temperature_double() outputs the actual temperature from uncompensated temperature. The bme280_compensate_pressure_double() outputs the actual pressure from the uncompensated pressure. Similarly, the bme280_compensate_humidity_double() outputs the actual humidity from the uncompensated humidity. We will first convert the double data type to an array of characters, round off to two decimal places and add the units for each reading.

double temp = bme280_compensate_temperature_double(v_uncomp_temperature_s32);
char temperature[12];
sprintf(temperature, "%.2f degC", temp);

double press = bme280_compensate_pressure_double(v_uncomp_pressure_s32)/100; // Pa -> hPa
char pressure[10];
sprintf(pressure, "%.2f hPa", press);

double hum = bme280_compensate_humidity_double(v_uncomp_humidity_s32);
char humidity[10];
sprintf(humidity, "%.2f %%", hum);

If the BME280 sensor is measuring correctly, the sensor readings will get displayed on the OLED. Incase, of an error in measuring or initialization of the sensor, a relevant message will be printed on the console.

To display each sensor reading on the OLED screen, we will call the function ssd1306_display_text. It takes in five parameters. The first parameter is the address of the SSD1306_t structure. The second parameter is the page number of the composition data which is set as ‘0’ for the temperature reading, ‘2’ for the pressure reading and ‘4’ for the humidity reading. The third parameter is the text that we want to display. It is the array of characters for each of the sensor reading. The fourth parameter is the length of the text. Lastly, the fifth parameter is invert which is a bool variable. It is set as false. This means we have a dark background and the text will be displayed in white. This function will be called separately for each sensor reading that will get updated after every second.

if (com_rslt == SUCCESS) {
				
	            ssd1306_display_text(&dev, 0, temperature, 12, false);
		    ssd1306_display_text(&dev, 2, pressure, 10, false);
		    ssd1306_display_text(&dev, 4, humidity, 10, false);

			} else {
				ESP_LOGE(TAG_BME280, "measure error. code: %d", com_rslt);
			}
		}
	} else {
		ESP_LOGE(TAG_BME280, "init or setting error. code: %d", com_rslt);
	}

Inside the app_main() function, we will call xTaskCreate() to create the bme280 oled task that will obtain BME280 sensor readings and print them on the OLED after every second.

void app_main(void)
{
  xTaskCreate(&bme280_oled_task, "bme280_oled_task",  2048*2, NULL, 6, NULL);
}

Hardware Setup OLED with ESP32 and BME280

Connect the three devices as shown in the table and schematic diagram below:

OLED DisplayESP32BME280
VCCVCC=3.3VVCC
GNDGNDGND
SCLGPIO22SCL
SDAGPIO21SDA
ESP32 with BME280 sensor and OLED connection diagram

Compiling the Sketch

To flash your chip, type the following command in the serial terminal. Remember to replace the COM port with the one through which your board is connected.

idf.py -p COMX flash monitor

After the code is successfully flashed, you can view in the ESP-IDF terminal that the OLED got configured successfully.

ESP-IDF BME280 Display on OLED Terminal

The OLED screen will start displaying the BME280 sensor readings that include temperature in degree Celsius, pressure in hPa and humidity in percentage.

ESP-IDF BME280 Display on OLED

You may also like to read:

2 thoughts on “BME280 with ESP32 ESP-IDF and Display Readings on OLED”

  1. Hi,
    Great tutorial, everything explained including how to set up the ESP IDF environment, something missing in most other examples given and such a huge range of examples.
    Thank you.
    Sean

    Reply
  2. “This time the command link sent by the master to read data from the slave contains the address ESP_SLAVE_ADDR << 1) | I2C_MASTER_READ which is specified as the second " this not inside code

    Reply

Leave a Comment