zephyr-comunicacion-i2c

Comunicación I2C en Zephyr: conectar sensores

  • 4 min

El I2C es un bus serie síncrono que conecta varios periféricos mediante dos señales: datos (SDA) y reloj (SCL).

El protocolo rey para conectar periféricos a distancias cortas es el I2C (Inter-Integrated Circuit). Solo necesita dos cables (SDA y SCL) y permite conectar decenas de dispositivos al mismo bus.

En Arduino quizá hayas utilizado Wire.begin() y Wire.read(). En Zephyr empezamos un poco antes, describiendo el hardware.

Definir el periférico en el overlay

En Zephyr, no escaneamos el bus buscando “a ver qué hay” (bueno, podemos hacerlo para depurar, pero no para producción). Nosotros le decimos al sistema operativo: “Oye, en el bus I2C 0, en la dirección 0x42, hay un dispositivo conectado”.

Para ello, usamos nuestro archivo .overlay.

Supongamos que tenemos un sensor genérico en la dirección 0x42, conectado al controlador I2C i2c0.

/* app.overlay */

&i2c0 {
    status = "okay"; /* Activamos el bus (a veces viene apagado) */
    
    /* Definimos nuestro dispositivo "colgando" del bus */
    my_sensor: sensor@42 {
        compatible = "mi-empresa,mi-sensor"; /* Compatible de ejemplo */
        reg = <0x42>;              /* La dirección I2C (7 bits) */
    };
};
Copied!

Nota sobre Drivers: Si tu sensor es conocido (ej. un BME280), Zephyr probablemente ya tenga un driver nativo. En ese caso, pondríais compatible = "bosch,bme280" y usaríais la API de Sensores (que veremos más adelante).

Hoy usaremos la API I2C de bajo nivel, adecuada cuando no existe un driver. El compatible del ejemplo es ilustrativo: para un dispositivo propio conviene crear su binding si vamos a desarrollar un driver o validar propiedades específicas.

La API de I2C en C

Para hablar con el dispositivo, usamos la estructura i2c_dt_spec. Esta estructura guarda el puntero al bus (i2c0) y la dirección del esclavo (0x42), para que no tengamos que escribirlos en cada llamada.

Inicialización

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

/* Obtenemos el nodo desde el Devicetree */
#define I2C_NODE DT_NODELABEL(my_sensor)

/* Creamos la especificación del dispositivo */
static const struct i2c_dt_spec dev_i2c = I2C_DT_SPEC_GET(I2C_NODE);

int main(void) {
    /* Verificamos que el BUS esté listo (no el sensor, sino el controlador) */
    if (!i2c_is_ready_dt(&dev_i2c)) {
        printk("El bus I2C no está listo.\n");
        return -ENODEV;
    }
    
    /* ... continuamos ... */

    return 0;
}
Copied!

Operaciones básicas: escribir y leer

La operación más común en I2C es: “Escribir la dirección del registro que queremos leer, y luego leer el dato”.

Esto implica:

START

Enviar Dirección I2C + Write

Enviar Dirección de Registro (ej. 0x0F)

RESTART (Importante para no perder el bus)

Enviar Dirección I2C + Read

Leer Dato

STOP

Zephyr tiene una función maravillosa que hace todo esto de una vez: i2c_write_read_dt.

/* Supongamos que queremos leer el registro WHO_AM_I (0x0F) */
uint8_t config[1] = {0x0F}; // La dirección del registro
uint8_t data[1] = {0};      // Donde guardaremos el valor leído

/* 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("Fallo al leer I2C\n");
} else {
    printk("Valor del registro: 0x%02X\n", data[0]);
}
Copied!

Escribir en un registro

Para configurar un sensor (escribir un valor en un registro), simplemente enviamos un buffer con dos bytes: [Dirección_Registro, Valor].

/* Escribir el valor 0x01 en el registro 0x20 */
uint8_t write_buf[2] = {0x20, 0x01};

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

Depurar con la shell de I2C

A veces conectamos los cables al revés (SDA en SCL) o la dirección I2C no es la que dice el datasheet. Zephyr tiene una herramienta de consola increíble para salvarnos la vida.

Si activas esto en prj.conf:

CONFIG_I2C=y
CONFIG_SHELL=y
CONFIG_I2C_SHELL=y
Copied!

Puedes conectarte por consola serie y usar comandos en vivo:

  • i2c scan i2c0: Escanea el bus e indica qué direcciones responden. Es muy útil para comprobar el cableado y la dirección del sensor.
  • i2c read i2c0 42 0F 1: Lee 1 byte del registro 0F en el dispositivo 42.
  • i2c write i2c0 42 20 01: Escribe en el dispositivo.

Usar la I2C Shell es la forma más rápida de verificar tu hardware antes de escribir una sola línea de código C.