If you are having issues with the exercises, please create a ticket on DevZone:
Click or drag files to this area to upload. You can upload up to 2 files.

Exercise 1 – Interfacing with a sensor over SPI

In this exercise, we will use a BME280 Sensor to provide temperature, pressure, and humidity (T, P, H) readings. The sensor supports both I2C and SPI communication. We choose a breakout board that supports SPI communication. The figure below shows the BME280 module and the breakout board.

BME280 sensor
Sensor breakout board

To use the sensor, please follow the specifications given in the datasheet.

The sensor (breakout board) provides six pins for hardware connection as below:

                VCC        GND       SCL (SCK)        SDA (SDI)             CSB        SDO

Where SDA is the serial data in (SDI) and SDO is the serial data out pin connection. The SPI interface pins should not be at a logical high level when VDD is not connected or switched off as such a configuration can permanently damage the device. As mentioned earlier, when configuring a slave on an SPI bus, make sure that pins are connected correctly, connections are solid, and the device is functional and properly powered.

The BME280 sensor offers three sensor modes: sleep mode, forced mode, and normal mode. Sleep mode is the one selected after the powering up of the sensor. So, we should configure the mode of the sensor to go to normal or forced mode after powering up. That is to say that we need to configure the mode bits of the sensor before going to read 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

Most of the compensation parameters are 2-byte values. After reading from register locations, we will put bits/bytes in their proper locations to form compensation parameter values. For example, first, we read the 1-byte value from 0x88 and store it in variable a, and then we read the 1-byte value from 0x89 and store it in variable b. In the exercise, we read both bytes in one go by specifying how many bytes we want to read from a register. Now we put these bytes (a and b) at their proper location to get the value of dig_T1 (which is the temperature compensation parameter) as below:

These compensation parameter values are then to 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).

The BME280 SPI interface is compatible with the SPI mode 0 (CPOL=0, CPHA=0) and SPI mode 3 (CPOL=1, CPHA=1).

Here are some of the important things to consider while working on this exercise:

  1. Make sure you have your sensor datasheet and consult it whenever you feel the need.
  2. If purchasing a BME280 sensor board, make sure you get the one with the correct sensor. If you have received a BMP280 instead, you should consult the datasheet and make the necessary changes. For example, some register addresses might be different. Also, BMP280 does not support humidity measurements.
  3. Another important thing is to ensure that your sensor board is correctly connected to the DK (pins are correct as per the overlay) and that connections are solid.
  4. Make sure you understand the overlay, and there are no conflicts.

Exercise steps

Open the code base of the exercise by navigating to Create a new application in the nRF Connect for VS Code extension, select Copy a sample, and search for Lesson 5 – Exercise 1.

1. Enable the SPI and GPIO driver

First, we need to enable the SPI and GPIO driver.

1.1 Enable Kconfigs for SPI and GPIO.

Add the following configs in the prj.conf file that is present in the project folder.

1.2 Include the header files for SPI, GPIO, and the devicetree.

Add SPI, GPIO and other device-related header files by including the following lines at the top of main.c.

2. Create an overlay file for your board

In the boards directory of the application, inter_less5_exer1/boards, rename the overlay file to the name of the build target for the board you are using, for example nrf52840dk_nrf52840.overlay.


If using a DK with a multi-core chip (nRF7002 DK, nRF9160 DK, nRF9160 DK, and nRF5340 DK), make sure to build with Trusted Firmware-M (TF-M).

2.1 Add the SPI slave device to &spi1.

Firstly, we are disabling the peripherals that are not being used, so intentionally disabling i2c0, spi0, and i2c1 controllers and enabling the spi1 controller. When using an nRF peripheral, consult the product specification of the SoC to see if there is more than one peripheral with the same ID and ensure that only one of those is active.

Define the compatible property of &spi1 to "nordic, nrf-spim" (with the exception of nRF52 DK, due to Errata 58), status to “okay“, set the pin control states and names, and configure the chip-select bar, cs-gpio.

Then we add our sensor as a child node on the spi1 controller, name it bme280, and set the compatible property to “bosch,bme280“, which is defined in bosch,bme280-spi.yaml to indicate what kind of device it is. Since the bme280 node describes hardware on an SPI bus, then the bus type is considered when matching the node to a binding. We have also set the max frequency to 125KHz conservatively.

nRF52 DK
nRF52833 DK
nRF52840 DK
nRF5340 DK
nRF7002 DK
nRF9160 DK
nRF9161 DK

2.2 Change the pin configuration.

Then, change the pin configuration of the pin control states we defined in the previous step spi1_default and spi1_sleep.

We want to set SCLK, MOSI, and MISO through SPIM_SCK, SPIM_MOSI and SPIM_MISO.

Add the following contents to the overlay file

nRF52 DK
nRF52833 DK
nRF52840 DK
nRF5340 DK
nRF7002 DK
nRF9160 DK
nRF9161 DK

We will be using the last four pins of the Arduino Header as SPI pins on different DKs. These pins are located near the RESET button on all Nordic Boards.

P0.28 to P0.31 are used as SPI pins for nRF52840 DK overlay.
P0.05, P0.06, P0.25 & P0.26 are used as SPI pins for nRF7002 DK overlay.

3. Retrieve the API-specific device structure.

Use SPI_DT_SPEC_GET() to get the device structure for the BME280 node.

Add the following lines in main.c

Here, we are getting the spi_dt_spec from the devicetree as generated through the overlay and board files. We will use spispec to use the SPI-specific functions for communication with the sensor. For the spispec, SPI_DT_SPEC_GET() will be using the SPIOP (which is defined as SPI_WORD_SET(8) | SPI_TRANSFER_MSB) to fill in the spi_config structure. SPIOP represents operation flags, which here is to say that the spi-word-size is 8-bit and the MSB should be transferred first (that is how the data over SPI lines should be interpreted).

After initialization, spispec.config will look like this:

4. Complete the code for the function bme_read_reg() that reads data from a register

The bme_read_reg() function takes in a register address reg, a data pointer *data and a number size, and reads size number of bytes from that register location and stores the values at the location pointed by data pointer.

This is done in two easy steps:

  1. Declare and set the transmit and receive buffers
  2. Communicate with sensor using spi_transceive_dt function and utilizing spispec

4.1 Declare and set the transmit and receive buffers

Add the following code in bme_read_reg()

First, tx_spi_buf is pointing to the register address, reg, and its length is 1 byte

Next, tx_spi_buf_set is pointing to tx_spi_buf, and we are using 1 such buffer (.count = 1)

Similarly, for the reception, rx_spi_bufs is pointing to the location pointed by data, and the length of this buffer is size. Lastly, rx_spi_buf_set is pointing to rx_spi_bufs and we have 1 such buffer.

4.2 Call the transceive function

To write the register address and read the size bytes (as set by the buffers in 4.1), we will use the spi_transceive_dt() function from the SPI API with our spispec.

Add the following code

An important point note is that spi_transceive_dt() will first write and then read from the register into data pointer. Therefore, the first byte read will be the dummy byte, as the data will be sampled but not coming from the actual register. Therefore, to read N bytes, we need to send size = N+1, and ignore the first byte, i.e. data[0].

5. Complete the function bme_write_reg() that writes the data byte to the sensor register

In this step, we will complete the function bme_write_reg(), which takes in a register number, reg, and a data-byte value, and writes the data-byte to the sensor register.

5.1 Set the transmit buffer to point to the register address and the value we want to write.

MSB for write command is 0, so setting it using AND operation. Declare and set the transmit buffers as previously.

Add the following

5.2 Write the data byte to the sensor register using spi_write_dt().

Add the following code snippet in the bme_write_reg() function

6. Go through bme_calibrationdata() and see how we are using the bme_read_reg() function to read different amounts of data from register locations.

The rest of the application’s functions utilize the ones we just completed. In this step, go through bme_calibrationdata() and see how we are using our defined bme_read_reg() function to read different amounts of data from register locations. As explained earlier, using the transceive function we need to set size to N+1 to read N bytes and ignore the first byte.

We are starting from 0x88, which is our calibration register number 0, and we are reading 2 bytes from that location (we have set size=3) and then we are putting the bytes in correct order to construct the dig_T1 value (that is a compensation parameter). Similarly, all of the other compensation parameter values are calculated. We store these values in a bme280_data struct variable (bmedata) which is a global data structure. Lastly, we print all of the compensation parameters. For most of the registers we need to read 2 bytes (size=3) and for few of them we need to read only 1 byte (size=2).

7. Go through bme_print_registers() and see that it uses bme_read_reg() to read and print different registers (1-byte each)

bme_print_registers() reads and prints different BME280 registers. It sets the register addresses as per the data sheet and calls bme_read_reg() to read registers one by one and print the contents to the console. We start with the device ID register, and then read more calibration registers, and then other registers, reading 1 byte from each (size=2), and printing on the screen. As we are not reading in the burst more, but rather one by one, we intentionally put a small delay of between consecutive reads.

8. Go through the compensation functions and note that they use the compensation parameters from the bme280_data structure and then store back the compensated value

The compensation code, bme280_compensate_temp(), bme280_compensate_press(), and bme280_compensate_humidity(), is taken from bme280 datasheet. These functions take in the bme280_data structure and the uncompensated value of respective parameter. These functions use the compensation parameters from the bme280_data structure to calculate the compensated value and store that back in the bme280_data structure, in the comp_temp, comp_press and comp_humidity field respectively. We will use these functions to compensate the sample values obtained from sensor.

9. Complete the function bme_read_sample() to get data samples

Now is the time to do the burst read, compensate and print the values for the temperature, pressure and humidity samples.

9.1 Store register addresses to do burst read.

As per the datasheet, to read sample values we have to read all registers from 0xF7 to 0xFE to do the burst read (to read all the registers using one go).

Set the register addresses with the following code snippet.

This will be used to read 8 data bytes as the last register address is for dummy transmission, and we will ignore the first byte as explained earlier.

9.2 Set the transmit and receive buffers.

As we did in previous steps, set the transmit and receive buffers with the following code snippet

9.3 Use spi_transceive_dt() to transmit and receive at the same time by adding the following code

The rest of the function manipulates the data received, calls the compensation routines and lastly prints the data to the console. Before printing, suitable conversions are done as per the data sheet (as below):

  • Compensated temperature value is divided by 100, i.e: 2462 equals 24.62 degrees C
  • Compensated pressure value is divided by 256, i.e: 25634778 equals 100,135 Pa (or 1001 hPa)
  • Compensated humidity value is divided by 1024, i.e: 2023 equals 1.97%RH

10. Complete the main function

10.1 In the main function, check if the SPI device is ready.

Add the following code

10.2 Call bme_calibrationdata() to read calibration data.

Call bme_calibrationdata() to fill in the bme280_data structure by reading the calibration parameters from the sensor. Add following line under step 10.2:

10.3 Write sampling parameters and read and print the registers.

Here, we will use the bme_write_reg to write to the CTRLHUM and CTRLMEAS registers. 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 write 0x04 and 0x93 to the CTRLHUM and CTRLMEAS register respectively. These registers are defined at the top of the main.c. Add the following code to set sampling parameters and then print bme280 registers:

10.4 Continuously read sensor samples and toggle LED.

Now, we can use the while loop to continuously get and display the samples from our sensor. In the loop, we will call bme_read_sample() to get and print sample (T,P,H) values and toggle the LED0 using gpio_pin_toggle_dt() a visual clue that a new sample from the sensor has been read. We will put the device to sleep for DELAY_VALUES ms and then continue to sample.


11. Connect the sensor to the DK.

To test the application, we need to connect our sensor to the DK. We are performing this test with the nRF52840 DK. From the respective overlay, recall that we have used P0.28, P0.29, P0.30 and P0.31 for the SPI SCLK, MOSI, CS and MISO pins, respectively. Therefore, we have to connect the pins of the sensor breakout board to the respective pins on the DK.

In the images below, we have made the necessary pin connections. On the sensor side, we have the VCC and GND pins required to power the sensor.

  • SCL (orange) – P0.28
  • MOSI (yellow) – P0.29
  • CSB (green) – P0.30
  • MISO (blue) – P0.31
Connections on the sensor board side
Connections on the DK side: We are using P0.28 to P0.31 and VDD/GND

The consideration here is that the device should not be faulty and is functional. Also make sure that the connections to the device are solid. Moreover, the connections should be as per defined in the overlay.

12. Build and flash the application to your board.

After successfully connecting the sensor to the DK, build and flash the code to your device.

You should see a similar output on your terminal (sensor values and parameters will be different, and timestamps have been omitted for clarity)

We can first see all the compensation parameters printed at the top, then 1-byte data from several registers, and then continuous data printing, with a delay between the successive readings.

You should get different readings based on environmental conditions. You may also like to check and verify the current environmental reading at your place.

Below, you can see the signal diagram using a logic analyzer on the SPI pins, which is optional but useful from a hardware perspective and also a good debugging tool. We have highlighted a single write and read transaction where we are sending the value of ID register (0xD0) on the MOSI line and then reading data from the sensor from the MISO line, that is 0x60, which is the chip-id of the sensor.

The solution for this exercise can be found in the GitHub repository, lesson5/inter_less5_exer1_solution.

Register an account
Already have an account? Log in
(All fields are required unless specified optional)

  • 8 or more characters
  • Upper and lower case letters
  • At least one number or special character

Forgot your password?
Enter the email associated with your account, and we will send you a link to reset your password.