Un driver IIO: iio_dev, canales y read_raw
Cómo se escribe un driver IIO mínimo: reservar struct iio_dev con devm_iio_device_alloc, describir los canales con struct iio_chan_spec y sus máscaras de información, implementar la devolución de datos con .read_raw y los códigos IIO_VAL, y registrar el dispositivo para que el núcleo exponga los canales por sysfs.
Ya sabes qué promete IIO a userspace: canales, unidades, la fórmula raw por scale. Ahora te toca cumplir esa promesa desde el otro lado. Un driver IIO es sorprendentemente pequeño porque el núcleo hace casi todo: tú describes qué mides —los canales— y aportas una única función que devuelve el valor de cada uno; el núcleo genera los archivos de sysfs, valida las máscaras y formatea los números. Escribir un driver IIO es, en esencia, rellenar tres estructuras y una función. Vamos a rellenarlas con un sensor de temperatura real por I2C.
- Reservar y configurar un
struct iio_devcondevm_iio_device_allocyiio_priv. - Describir los canales con
struct iio_chan_specy sus máscarasinfo_mask_*. - Implementar
.read_rawy devolver el formato correcto con los códigosIIO_VAL_*. - Registrar el dispositivo con
devm_iio_device_registerpara exponerlo por sysfs.
Reservar el iio_dev y colgar el estado privado
Todo driver IIO gira en torno a un struct iio_dev, que representa el dispositivo ante el núcleo. Se reserva con devm_iio_device_alloc, pasándole el tamaño de tu estado privado; el núcleo asigna ambos juntos y te devuelve el estado con iio_priv. El patrón es idéntico al de otras clases del kernel: una estructura genérica visible para el subsistema y un área opaca tuya colgando de ella.
#include <linux/iio/iio.h>
#include <linux/i2c.h>
#include <linux/mod_devicetable.h>
#include <linux/bitops.h>
#define TMP_REG_TEMP 0x00
struct tmp_state {
struct i2c_client *client;
struct mutex lock; /* serializa el acceso al bus */
};
Describir los canales
Un canal es una entrada de un vector de struct iio_chan_spec. Cada entrada declara qué magnitud es y qué atributos expone. Las dos máscaras clave son info_mask_separate —atributos propios de este canal— e info_mask_shared_by_type —atributos comunes a todos los canales del mismo tipo, que aparecen sin índice—:
static const struct iio_chan_spec tmp_channels[] = {
{
.type = IIO_TEMP,
/* expone in_temp_raw (propio) e in_temp_scale (compartido por tipo) */
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
.info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE),
},
};
Esa única declaración le dice al núcleo que cree los archivos in_temp_raw e in_temp_scale en sysfs. Si el sensor tuviera varios canales del mismo tipo —tres ejes de un acelerómetro— se usarían .indexed con .channel, o .modified con .channel2 para las variantes _x, _y, _z. Aquí, con un solo canal de temperatura, basta lo mínimo.
Implementar read_raw
El corazón del driver es .read_raw. El núcleo la llama cada vez que userspace lee un atributo, pasándole el canal y una mask que indica cuál de los atributos quiere. Tú resuelves con un switch: para IIO_CHAN_INFO_RAW lees el hardware y devuelves el entero crudo; para IIO_CHAN_INFO_SCALE devuelves la constante de conversión a la unidad estándar. El valor de retorno no es un errno cualquiera: es un código IIO_VAL_* que le dice al núcleo cómo formatear lo que dejaste en *val y *val2.
static int tmp_read_raw(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int *val, int *val2, long mask)
{
struct tmp_state *st = iio_priv(indio_dev);
int ret;
switch (mask) {
case IIO_CHAN_INFO_RAW:
mutex_lock(&st->lock);
ret = i2c_smbus_read_word_swapped(st->client, TMP_REG_TEMP);
mutex_unlock(&st->lock);
if (ret < 0)
return ret;
/* registro de 16 bits, dato en los 12 altos, con signo */
*val = sign_extend32(ret >> 4, 11);
return IIO_VAL_INT;
case IIO_CHAN_INFO_SCALE:
/* 0.0625 C por cuenta = 62.5 milesimas de grado */
*val = 62;
*val2 = 500000;
return IIO_VAL_INT_PLUS_MICRO;
default:
return -EINVAL;
}
}
Fíjate en la aritmética del valor crudo. i2c_smbus_read_word_swapped lee los dos bytes del registro y corrige el orden; el dato útil son los 12 bits altos, así que desplazas 4 y extiendes el signo con sign_extend32(x, 11) —el 11 es el índice del bit de signo—. Para el scale, la unidad de IIO_TEMP son milésimas de grado, y este sensor da 0.0625 grados por cuenta, o sea 62.5 milésimas: se devuelve como 62 más 500000 millonésimas con el código IIO_VAL_INT_PLUS_MICRO, que el núcleo formatea como 62.500000. Userspace hace entonces raw * 62.5 / 1000 y obtiene grados.
Los códigos IIO_VAL_* cubren cada forma de expresar un número: IIO_VAL_INT para un entero puro, IIO_VAL_INT_PLUS_MICRO y IIO_VAL_INT_PLUS_NANO para parte entera más fracción en millonésimas o milmillonésimas, IIO_VAL_FRACTIONAL para un cociente val/val2, y IIO_VAL_FRACTIONAL_LOG2 para dividir por una potencia de dos. Elegir el correcto es lo que hace que un scale de un sensor de 24 bits se exprese sin perder precisión.
Registrar el dispositivo
Con las piezas listas, el probe las ensambla: reserva el iio_dev, rellena el estado, apunta los canales y la tabla de operaciones, y registra. struct iio_info agrupa las callbacks —aquí solo .read_raw—; indio_dev->modes = INDIO_DIRECT_MODE declara que por ahora solo hay lectura puntual, sin buffer:
static const struct iio_info tmp_info = {
.read_raw = tmp_read_raw,
};
static int tmp_probe(struct i2c_client *client)
{
struct iio_dev *indio_dev;
struct tmp_state *st;
indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*st));
if (!indio_dev)
return -ENOMEM;
st = iio_priv(indio_dev);
st->client = client;
mutex_init(&st->lock);
indio_dev->name = "tmp_sensor";
indio_dev->modes = INDIO_DIRECT_MODE;
indio_dev->info = &tmp_info;
indio_dev->channels = tmp_channels;
indio_dev->num_channels = ARRAY_SIZE(tmp_channels);
return devm_iio_device_register(&client->dev, indio_dev);
}
static const struct of_device_id tmp_of_match[] = {
{ .compatible = "myvendor,tmp-sensor" },
{ }
};
MODULE_DEVICE_TABLE(of, tmp_of_match);
static struct i2c_driver tmp_driver = {
.driver = {
.name = "tmp_sensor",
.of_match_table = tmp_of_match,
},
.probe = tmp_probe,
};
module_i2c_driver(tmp_driver);
Tras devm_iio_device_register, el núcleo crea /sys/bus/iio/devices/iio:deviceN/ con in_temp_raw e in_temp_scale, y un cat in_temp_raw acaba entrando en tu read_raw. No escribiste ni una línea de código de sysfs.
El núcleo IIO no serializa por ti el acceso al hardware. Si dos lecturas concurrentes de sysfs entran a read_raw a la vez, ambas tocarán el bus I2C sin coordinación y corromperán la transacción. Por eso el mutex del estado privado no es decorativo: protege la conversación con el chip. Es la misma disciplina de concurrencia de siempre, aplicada al recurso compartido que es el bus.
Cuenta las líneas de este driver que hablan con el hardware y las que hablan con userspace: las primeras existen, las segundas no. No hay una sola llamada para crear un archivo de sysfs, para parsear lo que escribe el usuario, para formatear un número. Toda esa mecánica la genera el núcleo a partir de una descripción declarativa —el vector de iio_chan_spec— y una función que devuelve valores. Este es el corazón del diseño de subsistemas maduros del kernel: invertir el control. En un programa ingenuo, tú escribes el bucle que atiende a userspace y llamas al hardware; en un driver IIO, el núcleo escribe ese bucle y te llama a ti, y tu trabajo se reduce a declarar la estructura de tus datos y a resolver una consulta puntual. La iio_chan_spec es un pequeño lenguaje de descripción: dice qué magnitud, con qué atributos, en qué unidad, con qué disposición de bits, sin decir cómo mostrarlo. Cuando separas la descripción de lo que un dispositivo es de la mecánica de cómo se expone, ganas dos cosas a la vez: drivers minúsculos y una ABI perfectamente uniforme, porque el mismo generador produce el sysfs de todos. Interioriza esta forma —describe datos, implementa una consulta, deja que el marco haga el resto— porque la reconocerás en el modelo de dispositivos, en los reguladores, en casi todo el kernel moderno.
- Traza el camino completo de un
cat in_temp_raw: qué función del núcleo se invoca y con quémaskllega a turead_raw. - Añade un segundo canal
IIO_CHAN_INFO_OFFSETy explica cómo cambia la fórmula que aplica userspace. - Justifica por qué
sign_extend32(ret >> 4, 11)es correcto para un dato de 12 bits con signo alojado en los bits altos de una palabra de 16. - Cambia el
scalepara devolverlo conIIO_VAL_FRACTIONALen vez deIIO_VAL_INT_PLUS_MICROy razona cuándo cada formato conserva mejor la precisión. - Explica qué corrompería quitar el
mutexsi dos procesos leenin_temp_rawa la vez.