wandres.dev
GPIO E IIO · pines, sensores, IRQ

Triggers y buffers IIO: muestreo continuo con marca de tiempo

El camino de alta frecuencia de IIO: triggers que marcan el ritmo del muestreo, buffers que acumulan escaneos y el char device que los entrega a userspace. Cómo se declara la disposición de bits con scan_type, cómo el manejador con hilo empuja datos con iio_push_to_buffers_with_timestamp, y por qué la separación trigger/buffer/consumidor es tan potente.

⏱ 17 min

Leer un sensor a mano por sysfs sirve para preguntarle la temperatura de vez en cuando. No sirve para capturar un acelerómetro a mil muestras por segundo mientras el sistema hace otras cosas: cada cat es una syscall, cada muestra llega sin hora fiable y el ritmo lo pone el que lee, no el reloj. Para eso IIO tiene su segundo camino, el de verdad industrial: un trigger que dicta cuándo muestrear, un buffer que acumula los escaneos y un char device que los vuelca en bloque a userspace, cada muestra sellada con su marca de tiempo. Esta lección construye ese camino.

🎯 Al terminar esta lección sabrás
  • Entender el papel de los triggers, los buffers y el char device /dev/iio:deviceN.
  • Declarar la disposición de un escaneo con .scan_index y .scan_type, incluyendo el timestamp.
  • Escribir un manejador de trigger que empuje datos con iio_push_to_buffers_with_timestamp.
  • Conectar todo con devm_iio_triggered_buffer_setup y activar la captura desde sysfs.

Trigger, buffer y char device

El muestreo con buffer separa tres responsabilidades. El trigger es la fuente del “¡ahora!”: puede ser la interrupción de “datos listos” del propio sensor, un temporizador de alta resolución (iio-trig-hrtimer) o incluso un disparo manual desde sysfs. El buffer es una cola tipo kfifo donde se acumulan los escaneos —cada escaneo es el conjunto de canales muestreados en un instante—. Y el char device /dev/iio:deviceN es por donde userspace lee esos escaneos en bloque, sin una syscall por muestra.

flowchart LR
Trig[Trigger: hrtimer o dato listo] -->|dispara| PF[Manejador con hilo]
PF -->|lee los canales del sensor| Scan[Escaneo con marca de tiempo]
Scan -->|iio_push_to_buffers| Buf[Buffer kfifo]
Buf -->|dev iio:deviceN| User[Userspace lee en bloque]

La potencia del diseño está en que los tres son piezas independientes y combinables: cualquier trigger puede mover cualquier dispositivo con buffer. Un mismo acelerómetro se puede muestrear con su propia señal de datos listos hoy y con un temporizador software mañana, sin tocar el driver.

La disposición del escaneo: scan_type

Para meter muestras en un buffer binario, el núcleo necesita saber exactamente cómo son los bits de cada canal: cuántos bits reales, en cuántos de almacenamiento, con qué signo y en qué orden de bytes. Eso se declara en .scan_type, y la posición de cada canal en el escaneo en .scan_index. El último canal es siempre el timestamp, con el macro IIO_CHAN_SOFT_TIMESTAMP:

#include <linux/iio/iio.h>
#include <linux/iio/buffer.h>
#include <linux/iio/trigger_consumer.h>
#include <linux/iio/triggered_buffer.h>

#define ACCEL_SCAN_X   0
#define ACCEL_SCAN_Y   1
#define ACCEL_SCAN_Z   2
#define ACCEL_SCAN_TS  3

#define ACCEL_CHAN(axis, idx) {                                      \
        .type = IIO_ACCEL,                                           \
        .modified = 1,                                              \
        .channel2 = IIO_MOD_##axis,                                 \
        .info_mask_separate = BIT(IIO_CHAN_INFO_RAW),              \
        .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE),     \
        .scan_index = (idx),                                        \
        .scan_type = {                                             \
                .sign = 's',                                       \
                .realbits = 16,                                    \
                .storagebits = 16,                                 \
                .endianness = IIO_LE,                              \
        },                                                         \
}

static const struct iio_chan_spec accel_channels[] = {
        ACCEL_CHAN(X, ACCEL_SCAN_X),
        ACCEL_CHAN(Y, ACCEL_SCAN_Y),
        ACCEL_CHAN(Z, ACCEL_SCAN_Z),
        IIO_CHAN_SOFT_TIMESTAMP(ACCEL_SCAN_TS),
};

Cada eje aporta 16 bits reales alojados en 16 de almacenamiento, con signo y en little-endian: así el núcleo sabe empaquetar el escaneo y userspace sabe desempaquetarlo. Userspace puede activar solo los canales que le interesen; el núcleo compacta el buffer para incluir únicamente esos.

El manejador de trigger

Cuando el trigger dispara, el núcleo llama a tu manejador —el “bottom half” del muestreo—, que corre en un hilo y por tanto puede dormir sobre I2C o SPI. Su trabajo es leer las muestras del sensor a un buffer local y empujarlas con iio_push_to_buffers_with_timestamp. El detalle sutil es la alineación: el timestamp de 64 bits debe quedar alineado a 8 bytes dentro del buffer del escaneo, o el empujado corrompe datos.

struct accel_state {
        struct regmap *regmap;
        struct mutex   lock;
        /* buffer del escaneo: 3 ejes de 16 bits + timestamp de 64 bits.
         * El timestamp debe quedar alineado a 8 bytes. */
        struct {
                s16          channels[3];
                aligned_s64  timestamp;
        } scan __aligned(IIO_DMA_MINALIGN);
};

static irqreturn_t accel_trigger_handler(int irq, void *p)
{
        struct iio_poll_func *pf = p;
        struct iio_dev *indio_dev = pf->indio_dev;
        struct accel_state *st = iio_priv(indio_dev);
        int ret;

        mutex_lock(&st->lock);
        /* lee los 3 registros de salida de golpe (regmap de 16 bits) */
        ret = regmap_bulk_read(st->regmap, ACCEL_REG_XOUT,
                               st->scan.channels,
                               ARRAY_SIZE(st->scan.channels));
        mutex_unlock(&st->lock);
        if (ret)
                goto done;

        iio_push_to_buffers_with_timestamp(indio_dev, &st->scan,
                                           iio_get_time_ns(indio_dev));
done:
        /* imprescindible: avisa al nucleo de que este disparo termino */
        iio_trigger_notify_done(indio_dev->trig);
        return IRQ_HANDLED;
}

Dos llamadas no son opcionales. iio_push_to_buffers_with_timestamp copia el escaneo al buffer con su marca de tiempo en nanosegundos, y como el timestamp es el último canal, el núcleo lo coloca por ti si el usuario lo activó. iio_trigger_notify_done cierra el ciclo: le dice al núcleo que este disparo terminó y que puede aceptar el siguiente. Olvidarla congela el muestreo.

Ensamblar y activar

El probe conecta el buffer al trigger con una sola llamada. devm_iio_triggered_buffer_setup recibe dos manejadores: uno “de arriba” que se ejecuta al instante del disparo —iio_pollfunc_store_time, que graba la marca de tiempo lo antes posible— y el tuyo “de abajo”, que lee el hardware:

        indio_dev->modes = INDIO_DIRECT_MODE | INDIO_BUFFER_TRIGGERED;

        ret = devm_iio_triggered_buffer_setup(&client->dev, indio_dev,
                                              iio_pollfunc_store_time,
                                              accel_trigger_handler, NULL);
        if (ret)
                return dev_err_probe(&client->dev, ret, "sin buffer\n");

        return devm_iio_device_register(&client->dev, indio_dev);

Con eso, el núcleo crea los archivos de control del buffer, y userspace arranca la captura eligiendo canales, tamaño de cola y trigger:

# activar los ejes X e Y y el timestamp
echo 1 > /sys/bus/iio/devices/iio:device0/buffer0/in_accel_x_en
echo 1 > /sys/bus/iio/devices/iio:device0/buffer0/in_accel_y_en
echo 1 > /sys/bus/iio/devices/iio:device0/buffer0/in_timestamp_en

echo 512 > /sys/bus/iio/devices/iio:device0/buffer0/length
# enganchar un trigger hrtimer creado aparte
echo "mi-hrtimer" > /sys/bus/iio/devices/iio:device0/trigger/current_trigger
echo 1 > /sys/bus/iio/devices/iio:device0/buffer0/enable

# leer el flujo binario: cada escaneo son X, Y (16 bits c/u) + timestamp de 64 bits
cat /dev/iio:device0 | hexdump -C
ℹ️
El escaneo se compacta a lo que actives

El buffer no incluye siempre todos los canales: solo los que userspace habilitó con los archivos _en. Si activas X y timestamp pero no Y ni Z, cada escaneo son 2 bytes de X más el relleno de alineación más 8 bytes de timestamp. Por eso el driver declara scan_type de cada canal por separado: el núcleo calcula la disposición exacta según la máscara activa y se la comunica a userspace en los atributos del buffer.

Separar cuándo, qué y quién es lo que hace componible al kernel

Mira la arquitectura que acabas de montar y verás tres preguntas deliberadamente desacopladas. Cuándo muestrear lo responde el trigger. Qué muestrear y con qué forma lo responden los canales y el buffer. Quién consume el resultado lo responde el char device o un consumidor interno. Ninguna de las tres sabe de las otras: el trigger no conoce el dispositivo que mueve, el driver del sensor no sabe si lo dispara un temporizador o una interrupción de hardware, y el lector no sabe de dónde salen los bytes. Esa indiferencia mutua es exactamente lo que hace el sistema componible: puedes cruzar cualquier trigger con cualquier dispositivo con buffer y con cualquier consumidor, y todo encaja porque cada pieza habla solo con la frontera del núcleo, nunca con sus vecinas. Es el mismo principio que separa mecanismo de política, productor de consumidor, reloj de trabajo. La marca de tiempo es la prueba de que el diseño va en serio: al sellar cada escaneo en el instante del disparo y no en el de la lectura, IIO conserva la hora física de la medida aunque el hilo que la procesa se retrase, y eso es lo que distingue una adquisición de datos de un muestreo de juguete. Cuando un sistema te deja recombinar sus piezas sin que ninguna conozca a las demás, no estás ante una colección de funciones: estás ante una arquitectura, y esa recombinabilidad es la marca de que el kernel fue diseñado por gente que pensaba a largo plazo.

⚔️ Muestrea a alta frecuencia
  1. Explica por qué iio_trigger_notify_done es imprescindible y qué le pasa al muestreo si la omites.
  2. Justifica la alineación a 8 bytes del timestamp en la estructura scan y qué corrompería sin ella.
  3. Razona la diferencia entre el manejador de arriba iio_pollfunc_store_time y el de abajo accel_trigger_handler, y por qué el sello de tiempo se toma en el primero.
  4. Describe qué bytes exactos saldrían por /dev/iio:device0 si activas solo X y el timestamp, con su relleno.
  5. Compara el coste de capturar mil muestras por segundo por sysfs frente al camino con trigger y buffer, y explica qué se pierde además del rendimiento.