Feedback
Feedback

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

Exercise 1

v2.9.0 – v2.7.0

Connecting an BME280 temperature sensor

In this exercise, we will use a BME280 Sensor hosted on a Waveshare 1523 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 1523 board

The Waveshare 1523 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.

P1 connector on nRF54L15DK 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 whichever version directory you are using (BME280 sensor is used in v2.9.0 – 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=y
Kconfig

Important

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>
C

4. 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>
C

4.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=y
Kconfig

5. 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

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.

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.

&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
&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
&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

&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

Note

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 1523 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 1523 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)
C

7. 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;
}
C

With 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	 0xFA
C

9. 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;
	}
C

12. 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;
		}
C

12.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 colorSensor boardnRF54L15 DK
RedVCCVDDIO by Port P1
BlackGND GND by Port P1
BlueSDA Port P1.12
YellowSCLPort P1.11
Connection for sensor board
Connection for DK

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 colorSensor boardnRF52 DK
RedVCCVDD
BlackGND GND
BlueSDA Port P0.25
YellowSCLPort P0.24

13. Connect the sensor board to your DK using jumper cables.

Wire colorSensor boardnRF52833 DK
RedVCCVDD
BlackGND GND
BlueSDA Port P0.23
YellowSCLPort P0.22

13. Connect the sensor board to your DK using jumper cables.

Wire colorSensor boardnRF52840 DK
RedVCCVDD
BlackGND GND
BlueSDA Port P1.15
YellowSCLPort P1.14

13. Connect the sensor board to your DK using jumper cables.

Wire colorSensor boardnRF5340 DK
RedVCCVDD
BlackGND GND
BlueSDA Port P1.15
YellowSCLPort P1.14

13. Connect the sensor board to your DK using jumper cables.

Wire colorSensor boardnRF9160 DK
RedVCCVDD
BlackGND GND
BlueSDA Port P0.13
YellowSCLPort P0.12

13. Connect the sensor board to your DK using jumper cables.

Wire colorSensor boardnRF7002 DK
RedVCCVDD
BlackGND GND
BlueSDA Port P1.15
YellowSCLPort P1.14

13. Connect the sensor board to your DK using jumper cables.

Wire colorSensor boardnRF91x1 DK
RedVCCVDD
BlackGND GND
BlueSDA Port P0.13
YellowSCLPort 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 v2.8.0-preview1-11645184a54d ***
*** Using Zephyr OS v3.7.99-adcffa835a8e ***
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 F
Terminal

Try 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 of whichever version directory you are using (BME280 sensor is used in v2.9.0 – v2.7.0 and above).

v2.6.2 – v2.0.0

Connecting an STTS751 temperature sensor

For this exercise, we will use the very simple STTS751 temperature sensor, which is hosted on an X-NUCLEO-IKS01A3 expansion board. This will help us illustrate the use of the I2C API with ease.

Important

This exercise is not supported by the nRF7002 DK.

One advantage of the X-NUCLEO-IKS01A3 is that it can easily be attached to any of our development kits (with the exception of the nRF7002 DK) through the Arduino UNO R3 Connector.

X-NUCLEO-IKS01A3 expansion board
UNO R3 connector on the nRF52833 DK

When placing the board on top of the DK in the UNO R3 connector, the sensors on the expansion board connect to the nRF52833 DK through the I2C SDA and SCL lines which are wired to pins P0.26 and P0.27. This is shown in the GPIO pin mapping obtained from the nRF52833 DK schematics.

UNO R3 connector pin mapping on the nRF52833 DK

The STTS751 is a digital temperature sensor that communicates over a two-wire interface that is I2C compatible. The temperature is measured with a user-configurable resolution between 9 and 12 bits. At 9 bits, the smallest step size is 0.5 °C and at 12 bits, it is 0.0625 °C. The sensor supports different conversion rates starting from 0.0625 conversions per second up to 32. We will use the default of 1 conversion/sec.

The STTS751 has several internal registers as shown in the figure below, taken from its datasheet.

SSTS751 register map

There are three registers that are important to us, the configuration register (0x03), the temperature value high byte (0x00), and the temperature value low byte (0x02).

To read the temperature from the SSTS751 sensor, you must set up the sensor by writing the desired settings to the configuration register. Read the temperature value high byte, then read the temperature value low byte. Concatenate the two raw bytes and convert them to a temperature reading in either Celsius or Fahrenheit.

Exercise steps

1. In the GitHub repository for this course, open the base code for this exercise, found in l6/l6_e1 of whichever version directory you are using.

Make sure to mount the X-NUCLEO-IKS01A3 expansion board on your development kit.

2. Let’s enable the I2C driver by adding the following line into the application configuration file prj.conf.

CONFIG_I2C=y
Kconfig

Important

If you are building with TF-M (EX: 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>
C

4. To display the sensor readings on the console, we will use the simple printk().

4.1 Include the header file <sys/printk.h> to use printk().

#include <zephyr/sys/printk.h>
C

4.2 Add the following configuration opinion 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=y
Kconfig

5. Devicetree preparations

5.1 Create a folder called “boards” and place it in this exercise’s directory as shown below.

5.2 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. In the same overlay file, we also specify the sensor’s address.

If you are using any nRF52 Series DK (nRF52 DK , nRF52840 DK, nRF52833 DK), the external sensor is a child of the i2c0 node. If you are using nRF5340 DK , the external sensor is a child of the i2c1 node.

As we explained in the previous topic (I2C Driver), create an overlay file, name it nrf52833dk_nrf52833.overlay , and add the following code in the overlay file. How we got the 0x4a is explained in step 5.3

&i2c0 {  
    mysensor: mysensor@4a{
        compatible = "i2c-device";
        status = "okay";
        reg = < 0x4a >;
    };
};
Devicetree

Note

When adding an overlay file to an application, a pristine build must be run before flashing. In VS Code, a warning will pop-up asking if you would like to run the pristine build now.

Important

Depending on your board, the I2C controller connected to the UNO R3 Connector can be i2c0 , i2c1 i2c2 or i2c3. Check the schematic of your board to know which I2C controller the UNO R3 Connector is connected to, like we did at the beginning of this exercise.

5.3 The 0x4a address was obtained from the datasheet of the shield indirectly, as shown below. The provided address in the datasheet is an 8-bit address 0x94 (1001 0100); therefore, it must be logically shifted to the right by 1 bit to get the 7-bit address. (1001 0100 -> 100 1010), which yields 0x4a.

I2C addresses of the sensors on the X-NUCLEO-IKS01A3 (Provided as 8-bit addresses). Source: UM2559

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 (That is why we logically shifted the provided address in the datasheet to the right by 1). Another way to determine if a vendor is using 8-bit addresses instead of 7-bit addresses, you can also verify the address range. All 7-bit addresses should fall between the range of 0x08 to 0x77 (decimal 8 to 119). The first three bits of the address are fixed, and the remaining four bits can be programmed to any value. If your target address is beyond this range, it is likely that the sensor/shield vendor has indicated an 8-bit address.

6. Get the node identifier of the sensor. This was explained in detail in step 4 of the I2C Driver section.

#define I2C0_NODE DT_NODELABEL(mysensor)
C

7. 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(I2C0_NODE);
if (!device_is_ready(dev_i2c.bus)) {
	printk("I2C bus %s is not ready!\n\r",dev_i2c.bus->name);
	return -1;
}
C

With 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 STTS751_TEMP_HIGH_REG            0x00
#define STTS751_TEMP_LOW_REG             0x02
#define STTS751_CONFIG_REG               0x03
C

9. Now we need to configure the sensor by writing to the configuration register. Recall from the beginning of this exercise that the configuration register has the address 0x03.

SSTS751 configuration register
  • We will disable the EVENT pin.
  • We want the device running in continuous conversion mode.
  • We select the bit combination that will give us the highest temperature resolution to get the most accurate reading from the sensor.

Notice that bits 0, 1, 4 and 5 are not used so they will all be 0.

b7b6b5b4b3b2b1b0
MASK1RUN/STOP0RFUTres1Tres0RFURFU
10001100
Sending 1001100 (0x8C) to configuration register

Using the table below, the bit combination we need to use is 10001100 which is 0x8C if converted to hex.

Note

Since binary literals are not natively supported in the C language, we commonly convert the value to hexadecimal and pass it as a hexadecimal literal by adding 0x as a prefix with the number.

This is the value we need to write to the configuration register, see below

	uint8_t config[2] = {STTS751_CONFIG_REG,0x8C};
	ret = i2c_write_dt(&dev_i2c, config, sizeof(config));
	if(ret != 0){
		printk("Failed to write to I2C device address %x at Reg. %x \n", dev_i2c.addr,config[0]);
		return -1;
	}
C

Notice how we are actually writing two bytes in the above code snippet, STTS751_CONFIG_REG and 0x8C. The reason for this is that we first need to write the address of the register that we wish to write to, i.e the configuration register (0x03). Then we write the value which will be written to the configuration register, i.e 0x8C. This is a very common operation in I2C.

10. Once the sensor is configured, reading the temperature is a straightforward task of reading the two registers temperature value high byte and temperature value low byte.

SSTS751 temperature register

In order to read the registers, we first need to write the address and then issue a read. We will use the i2c_write_read_dt() API to do that in one shot as shown below:

uint8_t temp_reading[2]= {0};
uint8_t sensor_regs[2] ={STTS751_TEMP_LOW_REG,STTS751_TEMP_HIGH_REG};
ret = i2c_write_read_dt(&dev_i2c,&sensor_regs[0],1,&temp_reading[0],1);
if(ret != 0){
	printk("Failed to write/read I2C device address %x at Reg. %x \r\n", dev_i2c.addr,sensor_regs[0]);
}
ret = i2c_write_read_dt(&dev_i2c,&sensor_regs[1],1,&temp_reading[1],1);
if(ret != 0){
	printk("Failed to write/read I2C device address %x at Reg. %x \r\n", dev_i2c.addr,sensor_regs[1]);
}
C

11. Now that we have the temperature reading in raw bytes, the next step to do is to convert them to Celsius and Fahrenheit.

int temp = ((int)temp_reading[1] * 256 + ((int)temp_reading[0] & 0xF0)) / 16;
if(temp > 2047)
{
	temp -= 4096;
}

// Convert to engineering units 
double cTemp = temp * 0.0625;
double fTemp = cTemp * 1.8 + 32;

//Print reading to console  
printk("Temperature in Celsius : %.2f C \n", cTemp);
printk("Temperature in Fahrenheit : %.2f F \n", fTemp);
C

12. Build the application and flash it on your development kit. Using a serial terminal you should see the below output:

*** Booting nRF Connect SDK 2.6.1-3758bcbfa5cd ***
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 F
Terminal

Try 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 of whichever version directory you are using.

v1.9.1 – v1.6.0

Connecting an STTS751 temperature sensor

For this exercise, we will use the very simple STTS751 temperature sensor, which is hosted on an X-NUCLEO-IKS01A3 expansion board. This will help us illustrate the use of the I2C driver API with ease.

One advantage of the X-NUCLEO-IKS01A3 is that it can easily be attached to any of our development kits through the Arduino UNO R3 Connector.

X-NUCLEO-IKS01A3 expansion board
UNO R3 connector on the nRF52833 DK

When placing the board on top of the DK in the UNO R3 connector, the sensors on the expansion board connect to the nRF52833 DK through the I2C SDA and SCL lines which are wired to pins P0.26 and P0.27. This is shown in the GPIO pin mapping obtained from the nRF52833 DK schematics.

UNO R3 connector pin mapping on the nRF52833 DK

The STTS751 is a digital temperature sensor that communicates over a two-wire interface that is I2C compatible. The temperature is measured with a user-configurable resolution between 9 and 12 bits. At 9 bits, the smallest step size is 0.5 °C and at 12 bits, it is 0.0625 °C. The sensor supports different conversion rates starting from 0.0625 conversions per second up to 32. We will use the default of 1 conversion/sec.

The STTS751 has several internal registers as shown in the figure below, taken from its datasheet.

SSTS751 register map

There are three registers that are important to us, the configuration register (0x03), the temperature value high byte (0x00), and the temperature value low byte (0x02).

To read the temperature from the SSTS751 sensor, you must set up the sensor by writing the desired settings to the configuration register. Read the temperature value high byte, then read the temperature value low byte. Concatenate the two raw bytes and convert them to a temperature reading in either Celsius or Fahrenheit.

Exercise steps

1. In the GitHub repository for this course, open the base code for this exercise, found in l6/l6_e1 of whichever version directory you are using.

Make sure to mount the X-NUCLEO-IKS01A3 expansion board on your development kit.

2. Let’s enable the I2C driver by adding the following line into the application configuration file prj.conf.

3. In main.c , include the header file of the I2C API.

#include <drivers/i2c.h>

4. To display the sensor readings on the console, we will use the simple printk().

4.1 Include the header file <sys/printk.h> to use printk().

#include <sys/printk.h>

4.2 Add the following configuration opinion 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.

5. Get the label of the I2C controller connected to your sensor. This was explained in detail in step 3 of the I2C Driver section.

/* The devicetree node identifier for the "i2c0" */
#define I2C0_NODE DT_NODELABEL(i2c0)
#if DT_NODE_HAS_STATUS(I2C0_NODE, okay)
#define I2C0	DT_LABEL(I2C0_NODE)
#else
/* A build error here means your board does not have I2C enabled. */
#error "i2c0 devicetree node is disabled"
#define I2C0	""
#endif

Important

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Euismod in pellentesque massa placerat duis. Consectetur purus ut faucibus pulvinar elementum integer. Tempor nec feugiat nisl pretium fusce id velit. Sed sed risus pretium quam vulputate. Ultrices vitae auctor eu augue ut lectus arcu bibendum.

6. Now that we have the label of the I2C controller devicetree node, we can simply get the binding through the function device_get_binding() as shown below:

const struct device *dev_i2c = device_get_binding(I2C0);
if (dev_i2c == NULL) {
	printk("Could not find  %s!\n\r",I2C0);
	return;
}

With this, we have the pointer to the device structure of the I2C controller, and can start using the I2C driver API to configure the sensor connected to the I2C controller.

7. Define the I2C slave device address (from the shield board datasheet) and 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 STTS751_TEMP_HIGH_REG            0x00
#define STTS751_TEMP_LOW_REG             0x02
#define STTS751_CONFIG_REG               0x03
#define STTS751_I2C_ADDRESS              0x4A

8. Now we need to configure the sensor by writing to the configuration register. Recall from the beginning of this exercise that the configuration register has the address 0x03.

SSTS751 configuration register
  1. We will disable the EVENT pin.
  2. We want the device running in continuous conversion mode.
  3. We select the bit combination that will give us the highest temperature resolution to get the most accurate reading from the sensor.

Notice that bits 0, 1, 4 and 5 are not used so they will all be 0.

b7b6b5b4b3b2b1b0
MASK1RUN/STOP0RFUTres1Tres0RFURFU
10001100
Sending 1001100 (0x8C) to configuration register

Using the table below, the bit combination we need to use is 10001100 which is 0x8C if converted to hex.

Note

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Euismod in pellentesque massa placerat duis. Consectetur purus ut faucibus pulvinar elementum integer. Tempor nec feugiat nisl pretium fusce id velit. Sed sed risus pretium quam vulputate. Ultrices vitae auctor eu augue ut lectus arcu bibendum.

This is the value we need to write to the configuration register, see below

uint8_t config[2] = {STTS751_CONFIG_REG,0x8C};
ret = i2c_write(dev_i2c, config, sizeof(config), STTS751_I2C_ADDRESS);
if(ret != 0){
	printk("Failed to write to I2C device address %x at Reg. %x \n", STTS751_I2C_ADDRESS,config[0]);
}

Notice how we are actually writing two bytes in the above code snippet, STTS751_CONFIG_REG and 0x8C. The reason for this is that we first need to write the address of the register that we wish to write to, i.e the configuration register (0x03). Then we write the value which will be written to the configuration register, i.e 0x8C. This is a very common operation in I2C.

9. Once the sensor is configured, reading the temperature is a straightforward task of reading the two registers temperature value high byte and temperature value high byte.

SSTS751 temperature register

In order to read the registers, we first need to write the address and then issue a read. We will use the i2c_write_read() API to do that in one shot as shown below:


uint8_t temp_reading[2]= {0};
uint8_t sensor_regs[2] ={STTS751_TEMP_LOW_REG,STTS751_TEMP_HIGH_REG};
ret = i2c_write_read(dev_i2c,STTS751_I2C_ADDRESS,&sensor_regs[0],1,&temp_reading[0],1);
if(ret != 0){
	printk("Failed to write/read I2C device address %x at Reg. %x \n", STTS751_I2C_ADDRESS,sensor_regs[0]);
}
ret = i2c_write_read(dev_i2c,STTS751_I2C_ADDRESS,&sensor_regs[1],1,&temp_reading[1],1);
if(ret != 0){
	printk("Failed to write/read I2C device address %x at Reg. %x \n", STTS751_I2C_ADDRESS,sensor_regs[1]);
}

10. Now that we have the temperature reading in raw bytes, the next step to do is to convert them to Celsius and Fahrenheit.

int temp = ((int)temp_reading[1] * 256 + ((int)temp_reading[0] & 0xF0)) / 16;
if(temp > 2047)
{
	temp -= 4096;
}

// Convert to engineering units 
double cTemp = temp * 0.0625;
double fTemp = cTemp * 1.8 + 32;

//Print reading to console  
printk("Temperature in Celsius : %.2f C \n", cTemp);
printk("Temperature in Fahrenheit : %.2f F \n", fTemp);

11. Build the application and flash it on your development kit. Using a serial terminal you should see the below output:

Try 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 of whichever version directory you are using.

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.