Connecting a BME280 temperature sensor
In this exercise, we will use a BME280 Sensor hosted on a Waveshare 15231 board to provide temperature readings. We chose a breakout board that supports I2C and SPI communication protocols to use the sensor in multiple lessons. The image below shows the BME280 module and the breakout board.

Waveshare 15231 board
Note
This exercise is not supported on the Thingy. If you are using the Thingy prototyping platform (Thingy:53, Thingy:91, Thingy:91 X), please see Exercise 2 of this lesson.
The Waveshare 15231 board can easily be attached to any of our development kits through one of the available pin header connectors. The sensor board should be connected to the DK board (nRF54L15 in this case) using the P1 connector. Pin P1.11 will be configured as SCL, and P1.12 as SDA. VDDIO and GND should be connected as VCC and GND to the sensor board.

The BME280 sensor offers three sensor modes: sleep mode, forced mode, and normal mode. After powering up the sensor, it will go into sleep mode by default. We should configure the sensor to go to normal or forced mode after powering up. That is to say, we need to configure the sensor’s mode bits before reading the values.
The entire communication with the BME280 is performed by reading and writing to registers that are 8-bit wide. The memory map of BME280 is shown below:

BME280 register memory map
Some registers are reserved and should not be changed. The calibration data is read-only, as these values are fixed at the time of production. Control registers are read-write capable and are used to control the settings. Data registers are the values returned or stored by the sensor after performing the sensing operation and, hence, are read-only. Status and chip-id are also read-only, while reset is write-only.
Data registers provide a 20-bit pressure value, a 20-bit temperature value, and a 16-bit humidity value. As all registers are 8-bit wide, we will perform a multiple-byte read operation and then construct the value by putting bits/bytes at their proper locations. To read out the data values after conversion, it is recommended to use a burst mode and not address every register individually. Data readout is done by starting a burst read from 0xF7 to 0xFC (for temperature and pressure) or from 0xF7 to 0xFE (for temperature, pressure, and humidity).
The data is read out in an unsigned 20-bit format both for temperature and pressure and an unsigned 16-bit format for humidity. These readout values represent the uncompensated measurements. The actual temperature, pressure, and humidity values are then calculated using the compensation parameters. These compensation parameters are stored in the non-volatile memory of the device at the time of production. The register address, the content, and the datatype for these compensation parameters are shown in the table below.

BME280 compensation parameters’ register addresses
Exercise steps
1. In the GitHub repository for this course, open the base code for this exercise, found in l6/l6_e1. Note that the BME280 sensor is used in v2.7.0 and above.
2. Let’s enable the I2C driver by adding the following line to the application configuration file prj.conf.
CONFIG_I2C=yKconfigImportant
If you are building with TF-M (e.g.: nrf5340dk/nrf5340/ns , nrf9160dk/nrf9160/ns , etc ), you will need to disable logging for TF-M. The UART peripheral used for TF-M shares the same base address as the TWIM peripheral used in this exercise, and it’s enabled by default in nRF Connect SDK 2.6.0 and above. To disable it, simply add these two Kconfig symbols in prj.conf.
CONFIG_TFM_SECURE_UART=n
CONFIG_TFM_LOG_LEVEL_SILENCE=y
3. In main.c , include the header file of the I2C API.
#include <zephyr/drivers/i2c.h>C4. To display the sensor readings on the console, we will use a simple printk().
4.1 Include the header file <sys/printk.h> to use printk().
#include <zephyr/sys/printk.h>C4.2 Add the following configuration option to prj.conf to enable support for floating-point format specifiers, so we can print the temperature readings as floats. The reason why this option is not enabled by default is to save memory space, as enabling this option would increase code size by at least 1Kbytes.
CONFIG_CBPRINTF_FP_SUPPORT=yKconfig5. Devicetree preparations
5.1 Create a folder called “boards” and place it in this exercise’s directory as shown below.

5.2 Add an overlay file for the board
Note
If you are using a development kit other than the nRF54L15DK, please use the appropriate overlay content from the tabs below and name the overlay file according to your specific board target.
Since the sensor is external to the board, we must add an overlay file to specify this external sensor is a child to which i2c node and specify the sensor’s address. We also need to enable the i2c node and configure the pins.
As we explained in the previous topic (I2C Driver), create an overlay file, name in [board]_[soc].overlay form (For the nRF54L15 DK, it will be nrf54l15dk_nrf54l15_cpuapp_ns.overlay) and add the following code in the overlay file. How we got it 0x77 is explained in step 5.3.
&i2c21 {
status = "okay";
pinctrl-0 = <&i2c21_default>;
pinctrl-1 = <&i2c21_sleep>;
pinctrl-names = "default", "sleep";
mysensor: mysensor@77{
compatible = "i2c-device";
status = "okay";
reg = < 0x77 >;
};
};
&pinctrl {
/omit-if-no-ref/ i2c21_default: i2c21_default {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 1, 11)>,
<NRF_PSEL(TWIM_SDA, 1, 12)>;
};
};
/omit-if-no-ref/ i2c21_sleep: i2c21_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 1, 11)>,
<NRF_PSEL(TWIM_SDA, 1, 12)>;
};
};
};Devicetree&i2c22 {
status = "okay";
pinctrl-0 = <&i2c22_default>;
pinctrl-1 = <&i2c22_sleep>;
pinctrl-names = "default", "sleep";
mysensor: mysensor@77{
compatible = "i2c-device";
status = "okay";
reg = < 0x77 >;
};
};
&pinctrl {
/omit-if-no-ref/ i2c22_default: i2c22_default {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 1, 11)>,
<NRF_PSEL(TWIM_SDA, 1, 12)>;
};
};
/omit-if-no-ref/ i2c22_sleep: i2c22_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 1, 11)>,
<NRF_PSEL(TWIM_SDA, 1, 12)>;
low-power-enable;
};
};
};
Devicetree&i2c0 {
status = "okay";
pinctrl-0 = <&i2c0_default>;
pinctrl-1 = <&i2c0_sleep>;
pinctrl-names = "default", "sleep";
mysensor: mysensor@77{
compatible = "i2c-device";
status = "okay";
reg = < 0x77 >;
};
};
&pinctrl {
/omit-if-no-ref/ i2c0_default: i2c0_default {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 0, 24)>,
<NRF_PSEL(TWIM_SDA, 0, 25)>;
};
};
/omit-if-no-ref/ i2c0_sleep: i2c0_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 0, 24)>,
<NRF_PSEL(TWIM_SDA, 0, 25)>;
low-power-enable;
};
};
};Devicetree&i2c0 {
status = "okay";
pinctrl-0 = <&i2c0_default>;
pinctrl-1 = <&i2c0_sleep>;
pinctrl-names = "default", "sleep";
mysensor: mysensor@77{
compatible = "i2c-device";
status = "okay";
reg = < 0x77 >;
};
};
&pinctrl {
/omit-if-no-ref/ i2c0_default: i2c0_default {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 0, 22)>,
<NRF_PSEL(TWIM_SDA, 0, 23)>;
};
};
/omit-if-no-ref/ i2c0_sleep: i2c0_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 0, 22)>,
<NRF_PSEL(TWIM_SDA, 0, 23)>;
low-power-enable;
};
};
};
Devicetree&i2c0 {
status = "okay";
pinctrl-0 = <&i2c0_default>;
pinctrl-1 = <&i2c0_sleep>;
pinctrl-names = "default", "sleep";
mysensor: mysensor@77{
compatible = "i2c-device";
status = "okay";
reg = < 0x77 >;
};
};
&pinctrl {
/omit-if-no-ref/ i2c0_default: i2c0_default {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 1, 14)>,
<NRF_PSEL(TWIM_SDA, 1, 15)>;
};
};
/omit-if-no-ref/ i2c0_sleep: i2c0_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 1, 14)>,
<NRF_PSEL(TWIM_SDA, 1, 15)>;
low-power-enable;
};
};
};
Devicetree&i2c1 {
status = "okay";
pinctrl-0 = <&i2c1_default>;
pinctrl-1 = <&i2c1_sleep>;
pinctrl-names = "default", "sleep";
mysensor: mysensor@77{
compatible = "i2c-device";
status = "okay";
reg = < 0x77 >;
};
};
&pinctrl {
/omit-if-no-ref/ i2c1_default: i2c1_default {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 1, 14)>,
<NRF_PSEL(TWIM_SDA, 1, 15)>;
};
};
/omit-if-no-ref/ i2c1_sleep: i2c1_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 1, 14)>,
<NRF_PSEL(TWIM_SDA, 1, 15)>;
low-power-enable;
};
};
};Devicetree&i2c2 {
status = "okay";
pinctrl-0 = <&i2c2_default>;
pinctrl-1 = <&i2c2_sleep>;
pinctrl-names = "default", "sleep";
mysensor: mysensor@77{
compatible = "i2c-device";
status = "okay";
reg = < 0x77 >;
};
};
&pinctrl {
/omit-if-no-ref/ i2c2_default: i2c2_default {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 0, 12)>,
<NRF_PSEL(TWIM_SDA, 0, 13)>;
};
};
/omit-if-no-ref/ i2c2_sleep: i2c2_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 0, 12)>,
<NRF_PSEL(TWIM_SDA, 0, 13)>;
low-power-enable;
};
};
};
Devicetree&i2c1 {
status = "okay";
pinctrl-0 = <&i2c1_default>;
pinctrl-1 = <&i2c1_sleep>;
pinctrl-names = "default", "sleep";
mysensor: mysensor@77{
compatible = "i2c-device";
status = "okay";
reg = < 0x77 >;
};
};
&pinctrl {
/omit-if-no-ref/ i2c1_default: i2c1_default {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 1, 14)>,
<NRF_PSEL(TWIM_SDA, 1, 15)>;
};
};
/omit-if-no-ref/ i2c1_sleep: i2c1_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 1, 14)>,
<NRF_PSEL(TWIM_SDA, 1, 15)>;
low-power-enable;
};
};
};&i2c2 {
status = "okay";
pinctrl-0 = <&i2c2_default>;
pinctrl-1 = <&i2c2_sleep>;
pinctrl-names = "default", "sleep";
mysensor: mysensor@77{
compatible = "i2c-device";
status = "okay";
reg = < 0x77 >;
};
};
&pinctrl {
/omit-if-no-ref/ i2c2_default: i2c2_default {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 0, 12)>,
<NRF_PSEL(TWIM_SDA, 0, 13)>;
};
};
/omit-if-no-ref/ i2c2_sleep: i2c2_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 0, 12)>,
<NRF_PSEL(TWIM_SDA, 0, 13)>;
low-power-enable;
};
};
};
DevicetreeNote
When adding an overlay file to an application, a pristine build must be run before flashing. A warning in VS Code will pop up asking if you want to run the pristine build now.
5.3 The 7-bit device address is 111011x. The 6 MSB bits are fixed. The last bit is changeable by SDO value (see bme280 datasheet ) and can be changed during operation. Connecting SDO to GND results in slave address 1110110 (0x76); connection it to VDD/VDDIO results in slave address 1110111 (0x77), which is the same as BMP280’s I²C address.
The SDO Pin is represented by ADDR/MISO on the Waveshare 15231 board. This pin is pulled up, so if the connection is unchanged, the SDO in the BME280 will remain high, giving a 0x77 address

Waveshare 15231 board ADDR/MISO pin.
Some sensor vendors offer 8-bit addresses that include the read/write bit. To identify this, they usually provide separate addresses for writing and reading. In such cases, only the top seven bits of the address should be used.
6. Get the sensor’s node identifier. This was explained in detail in step 4 of the I2C Driver section.
#define I2C_NODE DT_NODELABEL(mysensor)C7. Retrieve the API-specific device structure and make sure that the device is ready to use:
static const struct i2c_dt_spec dev_i2c = I2C_DT_SPEC_GET(I2C_NODE);
if (!device_is_ready(dev_i2c.bus)) {
printk("I2C bus %s is not ready!\n\r",dev_i2c.bus->name);
return -1;
}CWith this, we have the pointer to the device structure of the I2C controller and the sensor address (target device address) and can start using the I2C driver API to configure the sensor connected to the I2C controller.
8. Define the addresses of the relevant registers (from the sensor datasheet). Typically this information goes into a separate header file (.h). However, for the sake of keeping this demonstration simple, we will add them in main.c.
#define CTRLMEAS 0xF4
#define CALIB00 0x88
#define ID 0xD0
#define TEMPMSB 0xFAC9. We verify that we have properly connected the sensor by reading the chip ID. When the value is correct (see bme280 datasheet), we know that the sensor is working properly, and we can take the next step – Read Calibration registers
uint8_t id = 0;
uint8_t regs[] = {ID};
int ret = i2c_write_read_dt(&dev_i2c, regs, 1, &id, 1);
if (ret != 0) {
printk("Failed to read register %x \n", regs[0]);
return -1;
}
if (id != CHIP_ID) {
printk("Invalid chip id! %x \n", id);
return -1;
}10. Read Calibration Registers. Most of the compensation parameters are 2-byte values. In this case, we read temperature compensation parameter registers (2B each) in burst mode.
Temperature compensation parameters begin from 0x88 address and contain 3 following registers.

Temperature calibration registers
uint8_t values[6];
int ret = i2c_burst_read_dt(spec, CALIB00, values, 6);
if (ret != 0) {
printk("Failed to read register %x \n", CALIB00);
return;
}
After reading from register locations, we will put bits/bytes in their proper locations to form compensation parameter values.
sensor_data_ptr->dig_t1 = ((uint16_t)values[1]) << 8 | values[0];
sensor_data_ptr->dig_t2 = ((uint16_t)values[3]) << 8 | values[2];
sensor_data_ptr->dig_t3 = ((uint16_t)values[5]) << 8 | values[4];These compensation parameters will be used by the compensation routines along with uncompensated environmental readings to make the correct / compensated outputs. Compensation formulas and routines are provided in the datasheet. Refer to the datasheet for more information. We use those routines as is with small modifications (in the prototype and variable datatypes).
11. Now we need to configure the sensor by writing to the configuration register. As per the datasheet, we have to put register into active/normal mode by setting MODE bits to b’11. And to set the oversampling parameters to b’100 (i.e. 8x oversampling), we have to 0x93 to the CTRLMEAS register. These registers are defined at the top of the main.c.

BME280 Configuration register
uint8_t sensor_config[] = {CTRLMEAS, SENSOR_CONFIG_VALUE};
ret = i2c_write_dt(&dev_i2c, sensor_config, 2);
if (ret != 0) {
printk("Failed to write register %x \n", sensor_config[0]);
return -1;
}C12. In order to get temperature sensor data, we need to read 3 following registers starting from TEMPMSB.
uint8_t temp_val[3] = {0};
int ret = i2c_burst_read_dt(&dev_i2c, TEMPMSB, temp_val, 3);
if (ret != 0) {
printk("Failed to read register %x \n", TEMPMSB);
k_msleep(SLEEP_TIME_MS);
continue;
}C12.1. We need to put data from registers into valid order and combine them into 20B temperature signed value (uncompensated)
int32_t adc_temp =
(temp_val[0] << 12) | (temp_val[1] << 4) | ((temp_val[2] >> 4) & 0x0F);12.2 Next, we can use parameters we previously stored to compensate temperature value
int32_t comp_temp = bme280_compensate_temp(&bmedata, adc_temp);12.3 The next step to do is to convert them to Celsius and Fahrenheit.
float temperature = (float)comp_temp / 100.0f;
double fTemp = (double)temperature * 1.8 + 32;
// Print reading to console
printk("Temperature in Celsius : %8.2f C\n", (double)temperature);
printk("Temperature in Fahrenheit : %.2f F\n", fTemp);13. Connect the sensor board to your DK using jumper cables.
| Wire color | Sensor board | nRF54L15 DK |
| Red | VCC | VDDIO by Port P1 |
| Black | GND | GND by Port P1 |
| Blue | SDA | Port P1.12 |
| Yellow | SCL | Port P1.11 |


13.1 Configure the board to output the correct voltage.
In nRF Connect for Desktop, install and launch the Board Configurator. The Board Configurator is a desktop app that lets you adjust the settings of the “board controller” on Nordic Development Kits (DKs).
The board controller is firmware running on the DK’s Interface MCU, which controls the DK’s operation. Using the Board Configurator, you can easily change the DK’s configuration.
In the upper left-hand corner, click Select Device, and a drown-down menu will appear with all connected devices. Select the nRF54L15 DK.
Under VDD (nPM VOUT1), make sure 3.3V is selected. Then select Write config, to write the current configuration to the board. Now, the board will give the correct output voltage to power the shield.

13. Connect the sensor board to your DK using jumper cables.
| Wire color | Sensor board | nRF52 DK |
| Red | VCC | VDD |
| Black | GND | GND |
| Blue | SDA | Port P0.25 |
| Yellow | SCL | Port P0.24 |
13. Connect the sensor board to your DK using jumper cables.
| Wire color | Sensor board | nRF52833 DK |
| Red | VCC | VDD |
| Black | GND | GND |
| Blue | SDA | Port P0.23 |
| Yellow | SCL | Port P0.22 |
13. Connect the sensor board to your DK using jumper cables.
| Wire color | Sensor board | nRF52840 DK |
| Red | VCC | VDD |
| Black | GND | GND |
| Blue | SDA | Port P1.15 |
| Yellow | SCL | Port P1.14 |
13. Connect the sensor board to your DK using jumper cables.
| Wire color | Sensor board | nRF5340 DK |
| Red | VCC | VDD |
| Black | GND | GND |
| Blue | SDA | Port P1.15 |
| Yellow | SCL | Port P1.14 |
13. Connect the sensor board to your DK using jumper cables.
| Wire color | Sensor board | nRF9160 DK |
| Red | VCC | VDD |
| Black | GND | GND |
| Blue | SDA | Port P0.13 |
| Yellow | SCL | Port P0.12 |
13. Connect the sensor board to your DK using jumper cables.
| Wire color | Sensor board | nRF7002 DK |
| Red | VCC | VDD |
| Black | GND | GND |
| Blue | SDA | Port P1.15 |
| Yellow | SCL | Port P1.14 |
13. Connect the sensor board to your DK using jumper cables.
| Wire color | Sensor board | nRF91x1 DK |
| Red | VCC | VDD |
| Black | GND | GND |
| Blue | SDA | Port P0.13 |
| Yellow | SCL | Port P0.12 |
14. Build the application and flash it on your development kit.
Using a serial terminal, you should see the below output:
*** Booting nRF Connect SDK ***
Temperature in Celsius : 26.37 C
Temperature in Fahrenheit : 79.47 F
Temperature in Celsius : 26.37 C
Temperature in Fahrenheit : 79.47 F
Temperature in Celsius : 26.37 C
Temperature in Fahrenheit : 79.47 F
Temperature in Celsius : 26.37 C
Temperature in Fahrenheit : 79.36 F
Temperature in Celsius : 26.37 C
Temperature in Fahrenheit : 79.36 FTerminalTry blowing on the sensor, and notice an immediate change in the readings.
The solution for this exercise can be found in the GitHub repository, l6/l6_e1_sol.