I2C Driver

v2.x.x

To learn how to set up the I2C driver in nRF Connect SDK, we will focus on the I2C master driver.

Enabling driver

1. Enable the I2C driver by adding the following line into the application configuration file prj.conf.

2. Include the header file of the I2C API in your source code file.

#include <drivers/i2c.h>

Initializing the device

Just like the GPIO driver covered in lesson 2, the generic I2C driver in Zephyr has an API-specific struct i2c_dt_spec, with the following signature:

This structure contains the device pointer for the I2C bus const struct deivce *bus and the slave address uint16_t addr.

To retrieve this structure, we need to use the API-specific function I2C_DT_SPEC_GET(), which has the following signature:

3. Specify which I2C controller your device (sensor) is connected to and its I2C address.

If the sensor is not already defined in the board’s devicetree, you need to manually add your sensor as a child devicetree node to the i2c controller, using a devicetree overlay file.

Important: Thingy:53 & Thingy:91

This step depends on whether the I2C slave device is already defined in your board’s files or not. In the case of the Thingy:53 and the Thingy:91, the sensors are already defined as child nodes of their I2C controller and you can skip this step (step 3.1 and step 3.2).

See the devicetree file of the Thingy:91, available in <install_path>\nrf\boards\arm\thingy91_nrf9160thingy91_nrf9160_common.dts.

Notice that the BME680 and BH1749 sensors are already added as child nodes in the i2c2-controller they are connected to.

3.1 Create overlay files.
As we learned in Lesson 3, from the details panel, expand Input files and create an overlay file.

This will create an empty overlay file in your application root directory.

3.2 In the overlay file, specify the I2C controller that your sensor is connected to and its address.

Note

Depending on the Nordic chip used, there can be more than one I2C controller, so make sure to select the controller that is connected to your sensor.

This can be confirmed by checking the schematic of your board or development kit to locate the pins connected to SDA and SCL. We will examine this in the exercise section of this lesson. You can use the DeviceTree viewer in VS Code to display the devicetree nodes for the available I2C controllers, which will tell you which pins are used by the controller

In the below illustration we are assuming that the sensor is connected to the i2c0 controller and it has the slave address 0x4a (from the sensor datasheet), and we are labeling it mysensor.

At a minimum, you need to specify the compatible, the address of the i2c slave device, and its label.

Note, for the compatible member you could specify the driver for the sensor if one exists in the SDK. However, the focus of this lesson is on raw I2C transactions.

4. Define the node identifier

The line below uses the devicetree macro DT_NODELABEL() to get the node identifier symbol I2C0_NODE, which will represent the I2C hardware controller i2c0.

#define I2C0_NODE DT_NODELABEL(mysensor)

I2C0_NODE contains information about the pins used for SDA and SCL, a memory map of the I2C controller, the default I2C frequency, and the address of the slave device.

5. Retrieve the API-specific device structure.

The macro call I2C_DT_SPEC_GET() returns the structure i2c_dt_spec, which contains the device pointer for the I2C bus, as well as the slave address.

static const struct i2c_dt_spec dev_i2c = I2C_DT_SPEC_GET(I2C0_NODE);

6. Use device_is_ready() to verify that the device is ready to use.

if (!device_is_ready(dev_i2c.bus)) {
	printk("I2C bus %s is not ready!\n\r",dev_i2c.bus->name);
	return;
}

We now have a device struct i2c_dt_spec *dev_i2c that we can pass to the I2C generic API interface to perform read/write operations.

I2C Write

The simplest way to write to a slave device is through the function i2c_write_dt(), which has the following signature:

For example, the following code snippet writes 2 bytes to an I2C slave device.

uint8_t config[2] = {0x03,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]);
}

I2C Read

  1. The simplest way to read from an I2C slave device is through the function i2c_read_dt(), which has the following signature:

For example, the following code snippet reads 1 byte from an I2C slave device.

uint8_t data;
ret = i2c_read_dt(&dev_i2c, &data, sizeof(data));
if(ret != 0){
	printk("Failed to read from I2C device address %x at Reg. %x n", dev_i2c->addr,config[0]);
}

2. Another way to read from an I2C device is by using the i2c_burst_read_dt()function. This function reads data from multiple registers sequentially. It has the following signature:

For example, the following code snippet reads data from 3 registers/6 bytes starting from the 1st byte of the register pointed to by the address “BH1749_RED_DATA_LSB”.

uint8_t rgb_value[6]= {0};
//Do a burst read of 6 bytes as each color channel is 2 bytes
ret = i2c_burst_read_dt(&dev_i2c, BH1749_RED_DATA_LSB,rgb_value,sizeof(rgb_value));

This function is used and explained in more detail in exercise 2 of this lesson.

I2C Write/Read

With I2C devices, it is very common to perform a write and a read data back to back by using the function i2c_write_read_dt(), which has the following signature:

A common scenario for this is to first write the address of an internal register to be read, then directly follow with a read to get the content of that register. We will demonstrate this in more detail in the exercise section of this lesson.

For example, the following code snippet writes the value sensor_regs[0] = 0x02 to the I2C device at the address 0x4A and then reads 1 byte from that same device and saves it in the variable temp_reading[0].

uint8_t sensor_regs[2] ={0x02,0x00};
uint8_t temp_reading[2]= {0};	
int 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 n", dev_i2c->addr,sensor_regs[0]);
}
v1.6.0 – v1.9.1

To learn how to set up the I2C driver in nRF Connect SDK, we will focus on the I2C master driver. In order to use the I2C master driver we need to perform the following steps by order.

Enabling driver

1. Enable the I2C driver by adding the following line into the application configuration file prj.conf.

2. Include the header file of the I2C API in your source code file.

#include <drivers/i2c.h>

3. Get the label property of the I2C controller devicetree node and make sure that the controller is enabled. The label is an important parameter that we will pass to generate the binding of the device driver through the device_get_binding() function.

Note

Depending on the Nordic chip used, there can be more than one I2C controller, so make sure to select the controller that is connected to your sensor.

This can be confirmed by checking the schematic of your board or development kit to locate the pins connected to SDA and SCL. We will examine this in the exercise section of this lesson. You can use the DeviceTree viewer in VS Code to display the devicetree nodes for the available I2C controllers, which will tell you which pins are used by the controller as shown in the screenshot below.

DeviceTree View in nRF Connect for VS Code
SDA and SCL pins for i2c0 node

3.1 Get a node identifier for the I2C controller devicetree node. This is done using the macro DT_NODELABEL().

#define I2C0_NODE DT_NODELABEL(i2c0)

The line above creates the node identifier symbol I2C0_NODE from the devicetree node i2c0 . The I2C0_NODE is now representing the I2C hardware controller. I2C0_NODE contains information about the pins used for SDA and SCL, a memory map of the controller, the default I2C frequency, and the label property associated with the node.

Note

The reason for extracting this information in a #define instead of passing it directly to the device_get_binding() function like we did in Lesson 5 is so we can check that the node is enabled, like we do in the next step.

This is good practice when using a peripheral that is not always enabled by default.

3.2 Make sure that the status of the node is set to okay, meaning that the device is enabled. This is done through the macro DT_NODE_HAS_STATUS().

DT_NODE_HAS_STATUS(I2C0_NODE, okay)

3.3 Get the label property of the node. The label is an important parameter that we will pass to the device_get_binding() function. This is done through the micro DT_LABEL().

#define I2C0	DT_LABEL(I2C0_NODE)

It is recommended to place the code that checks for the status of the node inside a conditional compilation, as shown below:

/* 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

An error will be generated (i2c0 devicetree node is disabled) and the code will not compile if i2c0 node’s status is not set to "okay" .

4. Now that we have the label property of the I2C controller devicetree node, we can simply get the binding through the function device_get_binding():

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

We now have a pointer dev_i2c of type struct device that we can pass to the I2C generic API interface to perform read/write operations.

I2C Write

The simplest way to write to a slave device is through the function i2c_write(), which has the following signature:

For example, the following code snippet writes 2 bytes to an I2C slave device at the address 0x4A.

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

I2C Read

The simplest way to read from an I2C slave device is through the function i2c_read(), which has the following signature:

For example, the following code snippet reads 1 byte from an I2C slave device at the address 0x4A.

uint8_t data;
ret = i2c_read(dev_i2c, &data, sizeof(data), 0x4A);
if(ret != 0){
	printk("Failed to read from I2C device address %x at Reg. %x \n", 0x4A,config[0]);
}

I2C Write/Read

With I2C devices, it is very common to perform a write and a read data back to back by using the function i2c_write_read(), which has the following signature:

A common scenario for this is to first write the address of an internal register to be read, then directly follow with a read to get the content of that register. We will demonstrate this in more detail in the exercise section of this lesson.

For example, the following code snippet writes the value sensor_regs[0] = 0x02 to the I2C device at the address 0x4A and then reads 1 byte from that same device and saves it in the variable temp_reading[0].

uint8_t sensor_regs[2] ={0x02,0x00};
uint8_t temp_reading[2]= {0};	
int ret = i2c_write_read(dev_i2c,0x4A,&sensor_regs[0],1,&temp_reading[0],1);
if(ret != 0){
	printk("Failed to write/read I2C device address %x at Reg. %x \n", 0x4A,sensor_regs[0]);
}
Register an account
Already have an account? Log in
(All fields are required unless specified optional)

Forgot your password?
Enter your email address, and we will send a link to reset your password.