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.
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:
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.
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:
dig_T1 = (b<<8) | a;
CThese 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:
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. Make sure to use the version directory that matches the SDK version you are using
Alternatively, in the GitHub repository for this course, go to the base code for this exercise, found in l5/l5_e1
of whichever version directory you are using.
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.
CONFIG_GPIO=y
CONFIG_SPI=y
KconfigIf you are building for a build target that includes TF-M (nrfxxxxdk_nrfxxxx_ns), you will need to disable logging for TF-M. Depending on your SoC/SiP, the UART peripheral used for TF-M could share the same base address as the SPIM 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
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
.
#include <zephyr/device.h>
#include <zephyr/devicetree.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/spi.h>
C2. Create an overlay file for your board
In the boards directory of the application, l5_e1/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 (nRF5340 DK, nRF54L15 DK, nRF7002 DK, nRF9160 DK, and nRF91x1 DK), make sure to build with Trusted Firmware-M (TF-M).
2.1 Add the SPI slave device to &spi1
On the nRF54L15, the peripherals have two digits. In this exercise we will be using the &spi21
instance.
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 1MHz for simplicity.
&i2c0 { status = "disabled";};
&spi0 { status = "disabled";};
&i2c1 { status = "disabled";};
&spi1 {
compatible = "nordic,nrf-spi"; //using SPI as per ERRATA 58
status = "okay";
pinctrl-0 = <&spi1_default>;
pinctrl-1 = <&spi1_sleep>;
pinctrl-names = "default", "sleep";
cs-gpios = <&gpio0 30 GPIO_ACTIVE_LOW>;
bme280: bme280@0 {
compatible = "bosch,bme280";
reg = <0>;
spi-max-frequency = <1000000>;
};
};
Devicetree&i2c0 { status = "disabled";};
&spi0 { status = "disabled";};
&i2c1 { status = "disabled";};
&spi1 {
compatible = "nordic,nrf-spim";
status = "okay";
pinctrl-0 = <&spi1_default>;
pinctrl-1 = <&spi1_sleep>;
pinctrl-names = "default", "sleep";
cs-gpios = <&gpio0 30 GPIO_ACTIVE_LOW>;
bme280: bme280@0 {
compatible = "bosch,bme280";
reg = <0>;
spi-max-frequency = <1000000>;
};
};
Devicetree&i2c0 { status = "disabled";};
&spi0 { status = "disabled";};
&i2c1 { status = "disabled";};
&spi1 {
compatible = "nordic,nrf-spim";
status = "okay";
pinctrl-0 = <&spi1_default>;
pinctrl-1 = <&spi1_sleep>;
pinctrl-names = "default", "sleep";
cs-gpios = <&gpio0 30 GPIO_ACTIVE_LOW>;
bme280: bme280@0 {
compatible = "bosch,bme280";
reg = <0>;
spi-max-frequency = <1000000>;
};
};
Devicetree&i2c0 { status = "disabled";};
&spi0 { status = "disabled";};
&i2c1 { status = "disabled";};
&spi1 {
compatible = "nordic,nrf-spim";
status = "okay";
pinctrl-0 = <&spi1_default>;
pinctrl-1 = <&spi1_sleep>;
pinctrl-names = "default", "sleep";
cs-gpios = <&gpio0 25 GPIO_ACTIVE_LOW>;
bme280: bme280@0 {
compatible = "bosch,bme280";
reg = <0>;
spi-max-frequency = <1000000>;
};
};
Devicetree/* uart20 is being used for serial communication, so use spi21*/
&spi21 {
compatible = "nordic,nrf-spim";
status = "okay";
pinctrl-0 = <&spi21_default>;
pinctrl-1 = <&spi21_sleep>;
pinctrl-names = "default", "sleep";
cs-gpios = <&gpio1 8 GPIO_ACTIVE_LOW>;
bme280: bme280@0 {
compatible = "bosch,bme280";
reg = <0>;
spi-max-frequency = <1000000>;
};
};
Devicetree&i2c0 { status = "disabled";};
&spi0 { status = "disabled";};
&i2c1 { status = "disabled";};
&spi1 {
compatible = "nordic,nrf-spim";
status = "okay";
pinctrl-0 = <&spi1_default>;
pinctrl-1 = <&spi1_sleep>;
pinctrl-names = "default", "sleep";
cs-gpios = <&gpio0 25 GPIO_ACTIVE_LOW>;
bme280: bme280@0 {
compatible = "bosch,bme280";
reg = <0>;
spi-max-frequency = <1000000>;
};
};
Devicetree&i2c0 { status = "disabled";};
&spi0 { status = "disabled";};
&i2c1 { status = "disabled";};
&uart1 { status = "disabled";};
&spi1 {
compatible = "nordic,nrf-spim";
status = "okay";
pinctrl-0 = <&spi1_default>;
pinctrl-1 = <&spi1_sleep>;
pinctrl-names = "default", "sleep";
cs-gpios = <&gpio0 18 GPIO_ACTIVE_LOW>;
bme280: bme280@0 {
compatible = "bosch,bme280";
reg = <0>;
spi-max-frequency = <1000000>;
};
};
Devicetree&i2c0 { status = "disabled";};
&spi0 { status = "disabled";};
&i2c1 { status = "disabled";};
&uart1 { status = "disabled";};
&spi1 {
compatible = "nordic,nrf-spim";
status = "okay";
pinctrl-0 = <&spi1_default>;
pinctrl-1 = <&spi1_sleep>;
pinctrl-names = "default", "sleep";
cs-gpios = <&gpio0 18 GPIO_ACTIVE_LOW>;
bme280: bme280@0 {
compatible = "bosch,bme280";
reg = <0>;
spi-max-frequency = <1000000>;
};
};
Devicetree2.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
&pinctrl {
spi1_default: spi1_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 28)>,
<NRF_PSEL(SPIM_MOSI, 0, 29)>,
<NRF_PSEL(SPIM_MISO, 0, 31)>;
};
};
spi1_sleep: spi1_sleep {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 28)>,
<NRF_PSEL(SPIM_MOSI, 0, 29)>,
<NRF_PSEL(SPIM_MISO, 0, 31)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
spi1_default: spi1_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 28)>,
<NRF_PSEL(SPIM_MOSI, 0, 29)>,
<NRF_PSEL(SPIM_MISO, 0, 31)>;
};
};
spi1_sleep: spi1_sleep {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 28)>,
<NRF_PSEL(SPIM_MOSI, 0, 29)>,
<NRF_PSEL(SPIM_MISO, 0, 31)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
spi1_default: spi1_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 28)>,
<NRF_PSEL(SPIM_MOSI, 0, 29)>,
<NRF_PSEL(SPIM_MISO, 0, 31)>;
};
};
spi1_sleep: spi1_sleep {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 28)>,
<NRF_PSEL(SPIM_MOSI, 0, 29)>,
<NRF_PSEL(SPIM_MISO, 0, 31)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
spi1_default: spi1_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 6)>,
<NRF_PSEL(SPIM_MOSI, 0, 7)>,
<NRF_PSEL(SPIM_MISO, 0, 26)>;
};
};
spi1_sleep: spi1_sleep {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 6)>,
<NRF_PSEL(SPIM_MOSI, 0, 7)>,
<NRF_PSEL(SPIM_MISO, 0, 26)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
spi21_default: spi21_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 1, 11)>,
<NRF_PSEL(SPIM_MOSI, 1, 13)>,
<NRF_PSEL(SPIM_MISO, 1, 14)>;
};
};
spi21_sleep: spi21_sleep {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 1, 11)>,
<NRF_PSEL(SPIM_MOSI, 1, 13)>,
<NRF_PSEL(SPIM_MISO, 1, 14)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
spi1_default: spi1_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 6)>,
<NRF_PSEL(SPIM_MOSI, 0, 7)>,
<NRF_PSEL(SPIM_MISO, 0, 26)>;
};
};
spi1_sleep: spi1_sleep {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 6)>,
<NRF_PSEL(SPIM_MOSI, 0, 7)>,
<NRF_PSEL(SPIM_MISO, 0, 26)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
spi1_default: spi1_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 16)>,
<NRF_PSEL(SPIM_MOSI, 0, 17)>,
<NRF_PSEL(SPIM_MISO, 0, 19)>;
};
};
spi1_sleep: spi1_sleep {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 16)>,
<NRF_PSEL(SPIM_MOSI, 0, 17)>,
<NRF_PSEL(SPIM_MISO, 0, 19)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
spi1_default: spi1_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 16)>,
<NRF_PSEL(SPIM_MOSI, 0, 17)>,
<NRF_PSEL(SPIM_MISO, 0, 19)>;
};
};
spi1_sleep: spi1_sleep {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 16)>,
<NRF_PSEL(SPIM_MOSI, 0, 17)>,
<NRF_PSEL(SPIM_MISO, 0, 19)>;
low-power-enable;
};
};
};
DevicetreeWe 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 DKs, except the nRF54L15 DK which has a different layout and there are some restrictions to pin usage.
nRF54L15 DK pin restrictions: The SPI21 instance must use pins on port P1. Only select pins on P1 can be used for clock pins (P1.03, P1.04, P1.08, P1.11, and P1.12). Pins P1.00 – P1.07 are used by default on the nRF54L15 DK, so this means that pins P1.08-P1.14 will be eligible for this exercise.
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
#define SPIOP SPI_WORD_SET(8) | SPI_TRANSFER_MSB
struct spi_dt_spec spispec = SPI_DT_SPEC_GET(DT_NODELABEL(bme280), SPIOP, 0);
CHere, 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:
{
.frequency = 125000U,
.operation = SPI_WORD_SET(8) | SPI_TRANSFER_MSB,
.slave = 0,
};
C4. 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:
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()
uint8_t tx_buffer = reg;
struct spi_buf tx_spi_buf = {.buf = (void *)&tx_buffer, .len = 1};
struct spi_buf_set tx_spi_buf_set = {.buffers = &tx_spi_buf, .count = 1};
struct spi_buf rx_spi_bufs = {.buf = data, .len = size};
struct spi_buf_set rx_spi_buf_set = {.buffers = &rx_spi_bufs, .count = 1};
CFirst, 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:
err = spi_transceive_dt(&spispec, &tx_spi_buf_set, &rx_spi_buf_set);
if (err < 0) {
LOG_ERR("spi_transceive_dt() failed, err: %d", err);
return err;
}
CAn 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:
uint8_t tx_buf[] = {(reg & 0x7F), value};
struct spi_buf tx_spi_buf = {.buf = tx_buf, .len = sizeof(tx_buf)};
struct spi_buf_set tx_spi_buf_set = {.buffers = &tx_spi_buf, .count = 1};
C5.2 Write the data byte to the sensor register using spi_write_dt()
.
Add the following code snippet in the bme_write_reg()
function:
err = spi_write_dt(&spispec, &tx_spi_buf_set);
if (err < 0) {
LOG_ERR("spi_write_dt() failed, err %d", err);
return err;
}
C6. 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.
uint8_t regs[] = {PRESSMSB, PRESSLSB, PRESSXLSB, \
TEMPMSB, TEMPLSB, TEMPXLSB, \
HUMMSB, HUMLSB, DUMMY}; //0xFF is dummy reg
uint8_t readbuf[sizeof(regs)];
CThis 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:
struct spi_buf tx_spi_buf = {.buf = (void *)®s, .len = sizeof(regs)};
struct spi_buf_set tx_spi_buf_set = {.buffers = &tx_spi_buf, .count = 1};
struct spi_buf rx_spi_bufs = {.buf = readbuf, .len = sizeof(regs)};
struct spi_buf_set rx_spi_buffer_set = {.buffers = &rx_spi_bufs, .count = 1};
C9.3 Use spi_transceive_dt()
to transmit and receive at the same time by adding the following code
err = spi_transceive_dt(&spispec, &tx_spi_buf_set, &rx_spi_buffer_set);
if (err < 0) {
LOG_ERR("spi_transceive_dt() failed, err: %d", err);
return err;
}
CThe 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):
10. Complete the main function
10.1 In the main function, check if the SPI device is ready.
Add the following code:
err = spi_is_ready_dt(&spispec);
if (!err) {
LOG_ERR("Error: SPI device is not ready, err: %d", err);
return 0;
}
C10.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:
bme_calibrationdata();
C10.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:
bme_write_reg(CTRLHUM, 0x04);
bme_write_reg(CTRLMEAS, 0x93);
bme_print_registers();
C10.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.
bme_read_sample();
gpio_pin_toggle_dt(&ledspec);
k_msleep(DELAY_VALUES);
CTesting
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.
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)
*** Booting nRF Connect SDK 2.6.1-3758bcbfa5cd ***
<inf> Lesson5_Exercise1: -------------------------------------------------------------
<inf> Lesson5_Exercise1: bme_read_calibrationdata: Reading from calibration registers:
<inf> Lesson5_Exercise1: Reg[0x88] 2 Bytes read: Param T1 = 28784
<inf> Lesson5_Exercise1: Reg[0x8a] 2 Bytes read: Param Param T2 = 26968
<inf> Lesson5_Exercise1: Reg[0x8c] 2 Bytes read: Param T3 = 50
<inf> Lesson5_Exercise1: Reg[0x8e] 2 Bytes read: Param P1 = 35748
<inf> Lesson5_Exercise1: Reg[0x90] 2 Bytes read: Param P2 = -10450
<inf> Lesson5_Exercise1: Reg[0x92] 2 Bytes read: Param P3 = 3024
<inf> Lesson5_Exercise1: Reg[0x94] 2 Bytes read: Param P4 = 7700
<inf> Lesson5_Exercise1: Reg[0x96] 2 Bytes read: Param P5 = -124
<inf> Lesson5_Exercise1: Reg[0x98] 2 Bytes read: Param P6 = -7
<inf> Lesson5_Exercise1: Reg[0x9a] 2 Bytes read: Param P7 = 12300
<inf> Lesson5_Exercise1: Reg[0x9c] 2 Bytes read: Param P8 = -12000
<inf> Lesson5_Exercise1: Reg[0x9e] 2 Bytes read: Param P9 = 5000
<inf> Lesson5_Exercise1: Reg[0xa1] 1 Bytes read: Param H1 = 75
<inf> Lesson5_Exercise1: Reg[0xe1] 2 Bytes read: Param H2 = 325
<inf> Lesson5_Exercise1: Reg[0xe3] 1 Bytes read: Param H3 = 0
<inf> Lesson5_Exercise1: Reg[0xe4] 2 Bytes read: Param H4 = 422
<inf> Lesson5_Exercise1: Reg[0xe5] 2 Bytes read: Param H5 = 50
<inf> Lesson5_Exercise1: Reg[0xe7] 1 Bytes read: Param H6 = 30
<inf> Lesson5_Exercise1: -------------------------------------------------------------
<inf> Lesson5_Exercise1: bme_print_registers: Reading all BME280 registers (one by one)
<inf> Lesson5_Exercise1: Reg[0xd0] = 0x60
<inf> Lesson5_Exercise1: Reg[0xe1] = 0x45
<inf> Lesson5_Exercise1: Reg[0xe2] = 0x01
<inf> Lesson5_Exercise1: Reg[0xe3] = 0x00
<inf> Lesson5_Exercise1: Reg[0xe4] = 0x1a
<inf> Lesson5_Exercise1: Reg[0xe5] = 0x26
<inf> Lesson5_Exercise1: Reg[0xe6] = 0x03
<inf> Lesson5_Exercise1: Reg[0xe7] = 0x1e
<inf> Lesson5_Exercise1: Reg[0xe8] = 0x36
<inf> Lesson5_Exercise1: Reg[0xe9] = 0x41
<inf> Lesson5_Exercise1: Reg[0xea] = 0xff
<inf> Lesson5_Exercise1: Reg[0xeb] = 0xff
<inf> Lesson5_Exercise1: Reg[0xec] = 0xff
<inf> Lesson5_Exercise1: Reg[0xed] = 0xff
<inf> Lesson5_Exercise1: Reg[0xee] = 0xff
<inf> Lesson5_Exercise1: Reg[0xef] = 0xff
<inf> Lesson5_Exercise1: Reg[0xf0] = 0xff
<inf> Lesson5_Exercise1: Reg[0xf2] = 0x04
<inf> Lesson5_Exercise1: Reg[0xf3] = 0x0c
<inf> Lesson5_Exercise1: Reg[0xf4] = 0x93
<inf> Lesson5_Exercise1: Reg[0xf5] = 0x00
<inf> Lesson5_Exercise1: Reg[0xf6] = 0x00
<inf> Lesson5_Exercise1: Reg[0xf7] = 0x59
<inf> Lesson5_Exercise1: Reg[0xf8] = 0x6d
<inf> Lesson5_Exercise1: Reg[0xf9] = 0x60
<inf> Lesson5_Exercise1: Reg[0xfa] = 0x82
<inf> Lesson5_Exercise1: Reg[0xfb] = 0x8c
<inf> Lesson5_Exercise1: Reg[0xfc] = 0x80
<inf> Lesson5_Exercise1: Reg[0xfd] = 0x7c
<inf> Lesson5_Exercise1: -------------------------------------------------------------
<inf> Lesson5_Exercise1: Continuously read sensor samples, compensate, and display
<inf> Lesson5_Exercise1: Temperature: uncomp = 534718 C comp = 23.85 C
<inf> Lesson5_Exercise1: Pressure: uncomp = 366286 Pa comp = 93709.79 Pa
<inf> Lesson5_Exercise1: Humidity: uncomp = 31890 RH comp = 23.92 %RH
<inf> Lesson5_Exercise1: Temperature: uncomp = 534730 C comp = 23.85 C
<inf> Lesson5_Exercise1: Pressure: uncomp = 366314 Pa comp = 97481.14 Pa
<inf> Lesson5_Exercise1: Humidity: uncomp = 31866 RH comp = 23.80 %RH
<inf> Lesson5_Exercise1: Temperature: uncomp = 534712 C comp = 23.85 C
<inf> Lesson5_Exercise1: Pressure: uncomp = 366310 Pa comp = 97482.66 Pa
<inf> Lesson5_Exercise1: Humidity: uncomp = 31863 RH comp = 23.78 %RH
TerminalWe 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, l5/l5_e1_sol
.
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.
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:
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.
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:
dig_T1 = (b<<8) | a;
CThese 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:
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. Make sure to use the version directory that matches the SDK version you are using
Alternatively, in the GitHub repository for this course, go to the base code for this exercise, found in l5/l5_e1
of whichever version directory you are using.
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.
CONFIG_GPIO=y
CONFIG_SPI=y
KconfigIf you are building for a build target that includes TF-M (nrfxxxxdk_nrfxxxx_ns), you will need to disable logging for TF-M. Depending on your SoC/SiP, the UART peripheral used for TF-M could share the same base address as the SPIM 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
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
.
#include <zephyr/device.h>
#include <zephyr/devicetree.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/spi.h>
C2. Create an overlay file for your board
In the boards directory of the application, l5_e1/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 (nRF5340 DK, nRF7002 DK, nRF9160 DK, and nRF91x1 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 1MHz for simplicity.
&i2c0 { status = "disabled";};
&spi0 { status = "disabled";};
&i2c1 { status = "disabled";};
&spi1 {
compatible = "nordic,nrf-spi"; //using SPI as per ERRATA 58
status = "okay";
pinctrl-0 = <&spi1_default>;
pinctrl-1 = <&spi1_sleep>;
pinctrl-names = "default", "sleep";
cs-gpios = <&gpio0 30 GPIO_ACTIVE_LOW>;
bme280: bme280@0 {
compatible = "bosch,bme280";
reg = <0>;
spi-max-frequency = <1000000>;
};
};
Devicetree&i2c0 { status = "disabled";};
&spi0 { status = "disabled";};
&i2c1 { status = "disabled";};
&spi1 {
compatible = "nordic,nrf-spim";
status = "okay";
pinctrl-0 = <&spi1_default>;
pinctrl-1 = <&spi1_sleep>;
pinctrl-names = "default", "sleep";
cs-gpios = <&gpio0 30 GPIO_ACTIVE_LOW>;
bme280: bme280@0 {
compatible = "bosch,bme280";
reg = <0>;
spi-max-frequency = <1000000>;
};
};
Devicetree&i2c0 { status = "disabled";};
&spi0 { status = "disabled";};
&i2c1 { status = "disabled";};
&spi1 {
compatible = "nordic,nrf-spim";
status = "okay";
pinctrl-0 = <&spi1_default>;
pinctrl-1 = <&spi1_sleep>;
pinctrl-names = "default", "sleep";
cs-gpios = <&gpio0 30 GPIO_ACTIVE_LOW>;
bme280: bme280@0 {
compatible = "bosch,bme280";
reg = <0>;
spi-max-frequency = <1000000>;
};
};
Devicetree&i2c0 { status = "disabled";};
&spi0 { status = "disabled";};
&i2c1 { status = "disabled";};
&spi1 {
compatible = "nordic,nrf-spim";
status = "okay";
pinctrl-0 = <&spi1_default>;
pinctrl-1 = <&spi1_sleep>;
pinctrl-names = "default", "sleep";
cs-gpios = <&gpio0 25 GPIO_ACTIVE_LOW>;
bme280: bme280@0 {
compatible = "bosch,bme280";
reg = <0>;
spi-max-frequency = <1000000>;
};
};
Devicetree&i2c0 { status = "disabled";};
&spi0 { status = "disabled";};
&i2c1 { status = "disabled";};
&spi1 {
compatible = "nordic,nrf-spim";
status = "okay";
pinctrl-0 = <&spi1_default>;
pinctrl-1 = <&spi1_sleep>;
pinctrl-names = "default", "sleep";
cs-gpios = <&gpio0 25 GPIO_ACTIVE_LOW>;
bme280: bme280@0 {
compatible = "bosch,bme280";
reg = <0>;
spi-max-frequency = <1000000>;
};
};
Devicetree&i2c0 { status = "disabled";};
&spi0 { status = "disabled";};
&i2c1 { status = "disabled";};
&uart1 { status = "disabled";};
&spi1 {
compatible = "nordic,nrf-spim";
status = "okay";
pinctrl-0 = <&spi1_default>;
pinctrl-1 = <&spi1_sleep>;
pinctrl-names = "default", "sleep";
cs-gpios = <&gpio0 18 GPIO_ACTIVE_LOW>;
bme280: bme280@0 {
compatible = "bosch,bme280";
reg = <0>;
spi-max-frequency = <1000000>;
};
};
Devicetree&i2c0 { status = "disabled";};
&spi0 { status = "disabled";};
&i2c1 { status = "disabled";};
&uart1 { status = "disabled";};
&spi1 {
compatible = "nordic,nrf-spim";
status = "okay";
pinctrl-0 = <&spi1_default>;
pinctrl-1 = <&spi1_sleep>;
pinctrl-names = "default", "sleep";
cs-gpios = <&gpio0 18 GPIO_ACTIVE_LOW>;
bme280: bme280@0 {
compatible = "bosch,bme280";
reg = <0>;
spi-max-frequency = <1000000>;
};
};
Devicetree2.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
&pinctrl {
spi1_default: spi1_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 28)>,
<NRF_PSEL(SPIM_MOSI, 0, 29)>,
<NRF_PSEL(SPIM_MISO, 0, 31)>;
};
};
spi1_sleep: spi1_sleep {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 28)>,
<NRF_PSEL(SPIM_MOSI, 0, 29)>,
<NRF_PSEL(SPIM_MISO, 0, 31)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
spi1_default: spi1_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 28)>,
<NRF_PSEL(SPIM_MOSI, 0, 29)>,
<NRF_PSEL(SPIM_MISO, 0, 31)>;
};
};
spi1_sleep: spi1_sleep {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 28)>,
<NRF_PSEL(SPIM_MOSI, 0, 29)>,
<NRF_PSEL(SPIM_MISO, 0, 31)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
spi1_default: spi1_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 28)>,
<NRF_PSEL(SPIM_MOSI, 0, 29)>,
<NRF_PSEL(SPIM_MISO, 0, 31)>;
};
};
spi1_sleep: spi1_sleep {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 28)>,
<NRF_PSEL(SPIM_MOSI, 0, 29)>,
<NRF_PSEL(SPIM_MISO, 0, 31)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
spi1_default: spi1_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 6)>,
<NRF_PSEL(SPIM_MOSI, 0, 7)>,
<NRF_PSEL(SPIM_MISO, 0, 26)>;
};
};
spi1_sleep: spi1_sleep {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 6)>,
<NRF_PSEL(SPIM_MOSI, 0, 7)>,
<NRF_PSEL(SPIM_MISO, 0, 26)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
spi1_default: spi1_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 6)>,
<NRF_PSEL(SPIM_MOSI, 0, 7)>,
<NRF_PSEL(SPIM_MISO, 0, 26)>;
};
};
spi1_sleep: spi1_sleep {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 6)>,
<NRF_PSEL(SPIM_MOSI, 0, 7)>,
<NRF_PSEL(SPIM_MISO, 0, 26)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
spi1_default: spi1_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 16)>,
<NRF_PSEL(SPIM_MOSI, 0, 17)>,
<NRF_PSEL(SPIM_MISO, 0, 19)>;
};
};
spi1_sleep: spi1_sleep {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 16)>,
<NRF_PSEL(SPIM_MOSI, 0, 17)>,
<NRF_PSEL(SPIM_MISO, 0, 19)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
spi1_default: spi1_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 16)>,
<NRF_PSEL(SPIM_MOSI, 0, 17)>,
<NRF_PSEL(SPIM_MISO, 0, 19)>;
};
};
spi1_sleep: spi1_sleep {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 16)>,
<NRF_PSEL(SPIM_MOSI, 0, 17)>,
<NRF_PSEL(SPIM_MISO, 0, 19)>;
low-power-enable;
};
};
};
DevicetreeWe 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 DKs.
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
#define SPIOP SPI_WORD_SET(8) | SPI_TRANSFER_MSB
struct spi_dt_spec spispec = SPI_DT_SPEC_GET(DT_NODELABEL(bme280), SPIOP, 0);
CHere, 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:
{
.frequency = 125000U,
.operation = SPI_WORD_SET(8) | SPI_TRANSFER_MSB,
.slave = 0,
};
C4. 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:
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()
uint8_t tx_buffer = reg;
struct spi_buf tx_spi_buf = {.buf = (void *)&tx_buffer, .len = 1};
struct spi_buf_set tx_spi_buf_set = {.buffers = &tx_spi_buf, .count = 1};
struct spi_buf rx_spi_bufs = {.buf = data, .len = size};
struct spi_buf_set rx_spi_buf_set = {.buffers = &rx_spi_bufs, .count = 1};
CFirst, 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:
err = spi_transceive_dt(&spispec, &tx_spi_buf_set, &rx_spi_buf_set);
if (err < 0) {
LOG_ERR("spi_transceive_dt() failed, err: %d", err);
return err;
}
CAn 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:
uint8_t tx_buf[] = {(reg & 0x7F), value};
struct spi_buf tx_spi_buf = {.buf = tx_buf, .len = sizeof(tx_buf)};
struct spi_buf_set tx_spi_buf_set = {.buffers = &tx_spi_buf, .count = 1};
C5.2 Write the data byte to the sensor register using spi_write_dt()
.
Add the following code snippet in the bme_write_reg()
function:
err = spi_write_dt(&spispec, &tx_spi_buf_set);
if (err < 0) {
LOG_ERR("spi_write_dt() failed, err %d", err);
return err;
}
C6. 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.
uint8_t regs[] = {PRESSMSB, PRESSLSB, PRESSXLSB, \
TEMPMSB, TEMPLSB, TEMPXLSB, \
HUMMSB, HUMLSB, DUMMY}; //0xFF is dummy reg
uint8_t readbuf[sizeof(regs)];
CThis 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:
struct spi_buf tx_spi_buf = {.buf = (void *)®s, .len = sizeof(regs)};
struct spi_buf_set tx_spi_buf_set = {.buffers = &tx_spi_buf, .count = 1};
struct spi_buf rx_spi_bufs = {.buf = readbuf, .len = sizeof(regs)};
struct spi_buf_set rx_spi_buffer_set = {.buffers = &rx_spi_bufs, .count = 1};
C9.3 Use spi_transceive_dt()
to transmit and receive at the same time by adding the following code
err = spi_transceive_dt(&spispec, &tx_spi_buf_set, &rx_spi_buffer_set);
if (err < 0) {
LOG_ERR("spi_transceive_dt() failed, err: %d", err);
return err;
}
CThe 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):
10. Complete the main function
10.1 In the main function, check if the SPI device is ready.
Add the following code:
err = spi_is_ready_dt(&spispec);
if (!err) {
LOG_ERR("Error: SPI device is not ready, err: %d", err);
return 0;
}
C10.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:
bme_calibrationdata();
C10.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:
bme_write_reg(CTRLHUM, 0x04);
bme_write_reg(CTRLMEAS, 0x93);
bme_print_registers();
C10.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.
bme_read_sample();
gpio_pin_toggle_dt(&ledspec);
k_msleep(DELAY_VALUES);
CTesting
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.
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)
*** Booting nRF Connect SDK 2.6.1-3758bcbfa5cd ***
<inf> Lesson5_Exercise1: -------------------------------------------------------------
<inf> Lesson5_Exercise1: bme_read_calibrationdata: Reading from calibration registers:
<inf> Lesson5_Exercise1: Reg[0x88] 2 Bytes read: Param T1 = 28784
<inf> Lesson5_Exercise1: Reg[0x8a] 2 Bytes read: Param Param T2 = 26968
<inf> Lesson5_Exercise1: Reg[0x8c] 2 Bytes read: Param T3 = 50
<inf> Lesson5_Exercise1: Reg[0x8e] 2 Bytes read: Param P1 = 35748
<inf> Lesson5_Exercise1: Reg[0x90] 2 Bytes read: Param P2 = -10450
<inf> Lesson5_Exercise1: Reg[0x92] 2 Bytes read: Param P3 = 3024
<inf> Lesson5_Exercise1: Reg[0x94] 2 Bytes read: Param P4 = 7700
<inf> Lesson5_Exercise1: Reg[0x96] 2 Bytes read: Param P5 = -124
<inf> Lesson5_Exercise1: Reg[0x98] 2 Bytes read: Param P6 = -7
<inf> Lesson5_Exercise1: Reg[0x9a] 2 Bytes read: Param P7 = 12300
<inf> Lesson5_Exercise1: Reg[0x9c] 2 Bytes read: Param P8 = -12000
<inf> Lesson5_Exercise1: Reg[0x9e] 2 Bytes read: Param P9 = 5000
<inf> Lesson5_Exercise1: Reg[0xa1] 1 Bytes read: Param H1 = 75
<inf> Lesson5_Exercise1: Reg[0xe1] 2 Bytes read: Param H2 = 325
<inf> Lesson5_Exercise1: Reg[0xe3] 1 Bytes read: Param H3 = 0
<inf> Lesson5_Exercise1: Reg[0xe4] 2 Bytes read: Param H4 = 422
<inf> Lesson5_Exercise1: Reg[0xe5] 2 Bytes read: Param H5 = 50
<inf> Lesson5_Exercise1: Reg[0xe7] 1 Bytes read: Param H6 = 30
<inf> Lesson5_Exercise1: -------------------------------------------------------------
<inf> Lesson5_Exercise1: bme_print_registers: Reading all BME280 registers (one by one)
<inf> Lesson5_Exercise1: Reg[0xd0] = 0x60
<inf> Lesson5_Exercise1: Reg[0xe1] = 0x45
<inf> Lesson5_Exercise1: Reg[0xe2] = 0x01
<inf> Lesson5_Exercise1: Reg[0xe3] = 0x00
<inf> Lesson5_Exercise1: Reg[0xe4] = 0x1a
<inf> Lesson5_Exercise1: Reg[0xe5] = 0x26
<inf> Lesson5_Exercise1: Reg[0xe6] = 0x03
<inf> Lesson5_Exercise1: Reg[0xe7] = 0x1e
<inf> Lesson5_Exercise1: Reg[0xe8] = 0x36
<inf> Lesson5_Exercise1: Reg[0xe9] = 0x41
<inf> Lesson5_Exercise1: Reg[0xea] = 0xff
<inf> Lesson5_Exercise1: Reg[0xeb] = 0xff
<inf> Lesson5_Exercise1: Reg[0xec] = 0xff
<inf> Lesson5_Exercise1: Reg[0xed] = 0xff
<inf> Lesson5_Exercise1: Reg[0xee] = 0xff
<inf> Lesson5_Exercise1: Reg[0xef] = 0xff
<inf> Lesson5_Exercise1: Reg[0xf0] = 0xff
<inf> Lesson5_Exercise1: Reg[0xf2] = 0x04
<inf> Lesson5_Exercise1: Reg[0xf3] = 0x0c
<inf> Lesson5_Exercise1: Reg[0xf4] = 0x93
<inf> Lesson5_Exercise1: Reg[0xf5] = 0x00
<inf> Lesson5_Exercise1: Reg[0xf6] = 0x00
<inf> Lesson5_Exercise1: Reg[0xf7] = 0x59
<inf> Lesson5_Exercise1: Reg[0xf8] = 0x6d
<inf> Lesson5_Exercise1: Reg[0xf9] = 0x60
<inf> Lesson5_Exercise1: Reg[0xfa] = 0x82
<inf> Lesson5_Exercise1: Reg[0xfb] = 0x8c
<inf> Lesson5_Exercise1: Reg[0xfc] = 0x80
<inf> Lesson5_Exercise1: Reg[0xfd] = 0x7c
<inf> Lesson5_Exercise1: -------------------------------------------------------------
<inf> Lesson5_Exercise1: Continuously read sensor samples, compensate, and display
<inf> Lesson5_Exercise1: Temperature: uncomp = 534718 C comp = 23.85 C
<inf> Lesson5_Exercise1: Pressure: uncomp = 366286 Pa comp = 93709.79 Pa
<inf> Lesson5_Exercise1: Humidity: uncomp = 31890 RH comp = 23.92 %RH
<inf> Lesson5_Exercise1: Temperature: uncomp = 534730 C comp = 23.85 C
<inf> Lesson5_Exercise1: Pressure: uncomp = 366314 Pa comp = 97481.14 Pa
<inf> Lesson5_Exercise1: Humidity: uncomp = 31866 RH comp = 23.80 %RH
<inf> Lesson5_Exercise1: Temperature: uncomp = 534712 C comp = 23.85 C
<inf> Lesson5_Exercise1: Pressure: uncomp = 366310 Pa comp = 97482.66 Pa
<inf> Lesson5_Exercise1: Humidity: uncomp = 31863 RH comp = 23.78 %RH
TerminalWe 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, l5/l5_e1_sol
.