To learn how to set up I2C in nRF Connect SDK, we will focus on the I2C controller API.
1. Enable the I2C driver by adding the following line into the application configuration file prj.conf
.
CONFIG_I2C=y
Kconfig2. Include the header file of the I2C API in your source code file.
#include <zephyr/drivers/i2c.h>
CJust 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 device
*bus
and the target 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.
This step depends on whether the I2C target device is already defined in your board’s files or not. In the case of 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.
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 target 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 target device, and its label.
&i2c0 {
mysensor: mysensor@4a{
compatible = "i2c-device";
reg = < 0x4a >;
label = "MYSENSOR";
};
};
DevicetreeNote, 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)
CI2C0_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 target 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 target address.
static const struct i2c_dt_spec dev_i2c = I2C_DT_SPEC_GET(I2C0_NODE);
C6. 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;
}
CWe 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.
The simplest way to write to a target 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 target 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\r", dev_i2c.addr,config[0]);
}
Ci2c_read_dt()
, which has the following signature:For example, the following code snippet reads 1 byte from an I2C target 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\r", dev_i2c.addr,config[0]);
}
C2. 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));
CThis function is used and explained in more detail in exercise 2 of this lesson.
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\r", dev_i2c.addr,sensor_regs[0]);
}
CTo learn how to set up I2C in nRF Connect SDK, we will focus on the I2C controller API.
1. Enable the I2C driver by adding the following line into the application configuration file prj.conf
.
CONFIG_I2C=y
Kconfig2. Include the header file of the I2C API in your source code file.
#include <zephyr/drivers/i2c.h>
CJust 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 device
*bus
and the target 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.
This step depends on whether the I2C target device is already defined in your board’s files or not. In the case of 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.
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 target 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 target device, and its label.
&i2c0 {
mysensor: mysensor@4a{
compatible = "i2c-device";
reg = < 0x4a >;
label = "MYSENSOR";
};
};
DevicetreeNote, 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)
CI2C0_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 target 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 target address.
static const struct i2c_dt_spec dev_i2c = I2C_DT_SPEC_GET(I2C0_NODE);
C6. 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;
}
CWe 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.
The simplest way to write to a target 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 target 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\r", dev_i2c.addr,config[0]);
}
Ci2c_read_dt()
, which has the following signature:For example, the following code snippet reads 1 byte from an I2C target 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\r", dev_i2c.addr,config[0]);
}
C2. 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));
CThis function is used and explained in more detail in exercise 2 of this lesson.
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\r", dev_i2c.addr,sensor_regs[0]);
}
CTo learn how to set up I2C in nRF Connect SDK, we will focus on the I2C controller API. In order to use the I2C API we need to perform the following steps in order.
1. Enable the I2C driver by adding the following line into the application configuration file prj.conf
.
CONFIG_I2C=y
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.
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.
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.
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.
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.
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]);
}
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]);
}
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]);
}