zephyr-comunicacion-i2c

I2C Communication in Zephyr: Connecting Sensors

  • 4 min

The I2C is a synchronous serial bus that connects multiple peripherals using two signals: data (SDA) and clock (SCL).

The go-to protocol for connecting peripherals over short distances is I2C (Inter-Integrated Circuit). It only needs two wires (SDA and SCL) and allows connecting dozens of devices to the same bus.

On Arduino, you may have used Wire.begin() and Wire.read(). In Zephyr, we start a bit earlier by describing the hardware.

Defining the Peripheral in the Overlay

In Zephyr, we do not scan the bus looking for “what’s there” (well, we can do it for debugging, but not for production). We tell the operating system: “Hey, on I2C bus 0, at address 0x42, there is a device connected.”

To do this, we use our .overlay file.

Let’s assume we have a generic sensor at address 0x42, connected to the I2C controller i2c0.

/* app.overlay */

&i2c0 {
    status = "okay"; /* Enable the bus (sometimes it comes disabled) */
    
    /* Define our device "hanging" off the bus */
    my_sensor: sensor@42 {
        compatible = "my-company,my-sensor"; /* Example compatible */
        reg = <0x42>;              /* I2C address (7-bit) */
    };
};
Copied!

Note on Drivers: If your sensor is well-known (e.g., a BME280), Zephyr likely already has a native driver. In that case, you would set compatible = "bosch,bme280" and use the Sensor API (covered later).

Today we’ll use the Low-level I2C API, suitable when no driver exists. The compatible in the example is illustrative: for your own device, you should create its binding if you plan to develop a driver or validate specific properties.

The I2C API in C

To communicate with the device, we use the i2c_dt_spec structure. This structure stores the pointer to the bus (i2c0) and the slave address (0x42), so we don’t have to write them in every call.

Initialization

#include <zephyr/kernel.h>
#include <zephyr/drivers/i2c.h>
#include <errno.h>

/* Get the node from the Devicetree */
#define I2C_NODE DT_NODELABEL(my_sensor)

/* Create the device specification */
static const struct i2c_dt_spec dev_i2c = I2C_DT_SPEC_GET(I2C_NODE);

int main(void) {
    /* Check that the BUS is ready (not the sensor, but the controller) */
    if (!i2c_is_ready_dt(&dev_i2c)) {
        printk("I2C bus is not ready.\n");
        return -ENODEV;
    }
    
    /* ... continue ... */

    return 0;
}
Copied!

Basic Operations: Write and Read

The most common operation in I2C is: “Write the register address we want to read, then read the data”.

This involves:

START

Send I2C Address + Write

Send Register Address (e.g., 0x0F)

RESTART (Important to avoid losing the bus)

Send I2C Address + Read

Read Data

STOP

Zephyr has a wonderful function that does all of this at once: i2c_write_read_dt.

/* Suppose we want to read the WHO_AM_I register (0x0F) */
uint8_t config[1] = {0x0F}; // The register address
uint8_t data[1] = {0};      // Where we store the read value

/* i2c_write_read_dt(spec, write_buf, num_write, read_buf, num_read) */
int ret = i2c_write_read_dt(&dev_i2c, config, 1, data, 1);

if (ret != 0) {
    printk("I2C read failed\n");
} else {
    printk("Register value: 0x%02X\n", data[0]);
}
Copied!

Writing to a Register

To configure a sensor (write a value to a register), we simply send a buffer with two bytes: [Register_Address, Value].

/* Write value 0x01 to register 0x20 */
uint8_t write_buf[2] = {0x20, 0x01};

/* Use i2c_write_dt */
ret = i2c_write_dt(&dev_i2c, write_buf, sizeof(write_buf));
Copied!

Debugging with the I2C Shell

Sometimes you connect the wires backwards (SDA to SCL) or the I2C address isn’t what the datasheet says. Zephyr has an incredible console tool to save the day.

If you enable this in prj.conf:

CONFIG_I2C=y
CONFIG_SHELL=y
CONFIG_I2C_SHELL=y
Copied!

You can connect via serial console and use live commands:

  • i2c scan i2c0: Scans the bus and indicates which addresses respond. This is very useful for verifying wiring and the sensor’s address.
  • i2c read i2c0 42 0F 1: Reads 1 byte from register 0F on device 42.
  • i2c write i2c0 42 20 01: Writes to the device.

Using the I2C Shell is the fastest way to verify your hardware before writing a single line of C code.