Skip to content

Industrial I/O (IIO) Subsystem

Goal: Understand the kernel's standard framework for sensors — accelerometers, gyroscopes, ADCs, pressure, light, humidity — so you can use mainline drivers, configure buffered capture, and decide when to write your own IIO driver vs a custom chardev.

Related Tutorials

For hands-on practice, see: BMI160 SPI Driver | IIO Buffered Capture | Custom IIO Driver | SPI DMA Optimization


You have an IMU, an ADC, or a pressure sensor on your embedded board. You could write a custom character device driver for each one — defining your own sysfs attributes, your own buffer management, your own ioctl interface. Or you could plug into IIO, the kernel's standard framework that gives you all of this for free: sysfs channels, buffered capture, triggers, timestamps, DMA integration, and a userspace toolchain that works across hundreds of devices.

The IIO subsystem lives in drivers/iio/ and supports hundreds of sensors out of the box. For any sensor with a mainline IIO driver, you get a consistent interface without writing a single line of kernel code.


1. What Is IIO?

The Industrial I/O subsystem is the kernel's standard framework for sensors and data acquisition devices. It covers:

  • Accelerometers and gyroscopes (BMI160, LSM6DSO, MPU6050, ...)
  • ADCs and DACs (MCP3008, ADS1115, INA219, ...)
  • Pressure sensors (BMP280, MS5611, ...)
  • Light and proximity sensors (TSL2561, VCNL4000, ...)
  • Humidity and temperature sensors (SHT31, HDC1080, ...)
  • Magnetometers (HMC5883L, AK8963, ...)

IIO replaces the need to write custom sysfs, chardev, or hwmon interfaces for every sensor. A single framework handles:

Concern Without IIO (custom driver) With IIO
Sysfs attributes You define each one manually Standard in_accel_x_raw, in_voltage0_raw, etc.
Buffered capture You implement ring buffer + read() kfifo + scan elements + trigger — automatic
Timestamps You call ktime_get() yourself iio_get_time_ns() added automatically
Userspace tools Custom scripts only iio_readdev, iio_info, libiio
Sensor swap Rewrite driver Change compatible string in Device Tree

2. Architecture Overview

graph LR
    HW[Sensor<br>Hardware] -->|SPI / I2C| BUS[Bus<br>Subsystem]
    BUS --> CORE[IIO Core]
    CORE --> SYSFS[sysfs<br>/sys/bus/iio/]
    CORE --> CHARDEV[chardev<br>/dev/iio:deviceN]
    CORE --> BUF[Buffer<br>kfifo]
    BUF --> CHARDEV

Key Kernel Structures

struct iio_dev — represents one IIO device (one sensor chip):

struct iio_dev *indio_dev = iio_device_alloc(sizeof(struct my_state));
indio_dev->name = "bmi160";
indio_dev->info = &my_iio_info;        /* callbacks */
indio_dev->channels = my_channels;      /* channel definitions */
indio_dev->num_channels = ARRAY_SIZE(my_channels);
indio_dev->modes = INDIO_DIRECT_MODE;   /* polled reads allowed */

struct iio_chan_spec — defines one data channel (e.g., X-axis acceleration):

static const struct iio_chan_spec my_channels[] = {
    {
        .type = IIO_ACCEL,
        .modified = 1,
        .channel2 = IIO_MOD_X,
        .info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
        .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE),
        .scan_index = 0,
        .scan_type = {
            .sign = 's',
            .realbits = 16,
            .storagebits = 16,
            .endianness = IIO_LE,
        },
    },
    /* ... more channels ... */
};

struct iio_info — driver callbacks:

static const struct iio_info my_info = {
    .read_raw = my_read_raw,       /* polled channel read */
    .write_raw = my_write_raw,     /* set config (ODR, scale) */
};

Channel Types

Type constant sysfs prefix Example sensors
IIO_ACCEL in_accel_ BMI160, LSM6DSO, MPU6050
IIO_ANGL_VEL in_anglvel_ BMI160, LSM6DSO (gyroscope)
IIO_TEMP in_temp_ BMI160 (on-die), BMP280
IIO_VOLTAGE in_voltage MCP3008, ADS1115, INA219
IIO_PRESSURE in_pressure_ BMP280, MS5611
IIO_LIGHT in_illuminance_ TSL2561, BH1750
IIO_HUMIDITYRELATIVE in_humidityrelative_ SHT31, HDC1080
IIO_MAGN in_magn_ HMC5883L, AK8963

Channel Attributes

Every channel can expose standard attributes:

  • _raw — raw ADC/register value from the sensor
  • _scale — multiplier to convert raw to physical units
  • _offset — additive offset (physical = (raw + offset) × scale)
  • _sampling_frequency — current sample rate in Hz

3. Sysfs Interface (Polled Mode)

When an IIO driver loads, it creates a device under /sys/bus/iio/devices/iio:deviceN/:

/sys/bus/iio/devices/iio:device0/
├── name                          # "bmi160"
├── in_accel_x_raw                # raw X acceleration
├── in_accel_y_raw
├── in_accel_z_raw
├── in_accel_scale                # shared scale factor
├── in_anglvel_x_raw              # raw X gyroscope
├── in_anglvel_y_raw
├── in_anglvel_z_raw
├── in_anglvel_scale
├── in_temp_raw
├── in_temp_scale
├── in_temp_offset
├── sampling_frequency
├── sampling_frequency_available
├── scan_elements/                # for buffered mode
├── buffer/                       # buffer control
└── trigger/                      # trigger selection

Reading a Channel

Each cat of a _raw file triggers one SPI or I2C transaction:

# Read raw accelerometer X value
cat /sys/bus/iio/devices/iio:device0/in_accel_x_raw
# → 1234

# Read scale factor
cat /sys/bus/iio/devices/iio:device0/in_accel_scale
# → 0.000598

# Convert to physical units (m/s²):
# physical = raw × scale = 1234 × 0.000598 = 0.738 m/s²

Comparison: IIO sysfs vs Custom sysfs vs hwmon

Aspect IIO sysfs Custom sysfs hwmon
Path /sys/bus/iio/devices/ /sys/class/<your_class>/ /sys/class/hwmon/
Naming Standardized (in_accel_x_raw) Whatever you choose Standardized (temp1_input)
Scale/offset Standard attributes Manual Standard (millidegrees)
Buffer support Built-in Write your own None
Tool ecosystem iio_readdev, libiio Custom scripts lm-sensors, fancontrol
Best for Sensors, ADCs, IMUs Unique interfaces Temperature, voltage, fan monitoring

4. Buffered Mode and Triggers

Polled sysfs reads work for low-rate sampling (a few Hz), but at higher rates the per-read overhead becomes significant. Buffered mode solves this: a trigger fires, the driver reads the sensor, and data accumulates in a kernel ring buffer. Userspace reads batches from /dev/iio:deviceN.

Triggers

A trigger tells IIO when to sample:

Trigger type Source Use case
Data-ready IRQ Sensor's DRDY/INT pin Highest accuracy, hardware-paced
hrtimer Kernel high-res timer No IRQ pin needed, software-paced
sysfs Manual echo 1 > trigger_now Testing, one-shot captures

Configuring Buffered Mode

IIO=/sys/bus/iio/devices/iio:device0

# 1. Select which channels to capture
echo 1 > $IIO/scan_elements/in_accel_x_en
echo 1 > $IIO/scan_elements/in_accel_y_en
echo 1 > $IIO/scan_elements/in_accel_z_en
echo 1 > $IIO/scan_elements/in_anglvel_x_en
echo 1 > $IIO/scan_elements/in_anglvel_y_en
echo 1 > $IIO/scan_elements/in_anglvel_z_en

# 2. Set trigger (data-ready IRQ or hrtimer)
echo bmi160-dev0 > $IIO/trigger/current_trigger

# 3. Set sample rate
echo 200 > $IIO/sampling_frequency

# 4. Set buffer length (number of sample sets to buffer)
echo 256 > $IIO/buffer/length

# 5. Enable buffered capture
echo 1 > $IIO/buffer/enable

# 6. Read data from the character device
cat /dev/iio:device0 | hexdump -C

Data Flow

sequenceDiagram
    participant T as Trigger (IRQ/hrtimer)
    participant D as IIO Driver
    participant B as kfifo Buffer
    participant U as Userspace

    T->>D: trigger fires
    D->>D: read sensor via SPI/I2C
    D->>B: iio_push_to_buffers_with_timestamp()
    Note over B: samples accumulate
    U->>B: read(/dev/iio:device0)
    B->>U: batch of samples + timestamps

Timestamps

IIO automatically adds a nanosecond timestamp to each sample using iio_get_time_ns(). The timestamp channel appears as the last element in each sample set. This gives you precise timing without any driver-side code — you just enable it:

echo 1 > $IIO/scan_elements/in_timestamp_en

Scan Element Format

Each channel's binary format is described in sysfs:

cat $IIO/scan_elements/in_accel_x_type
# → le:s16/16>>0
# Meaning: little-endian, signed 16-bit, stored in 16 bits, no shift

Userspace code parses this to decode the binary stream from /dev/iio:deviceN.


5. DMA Integration

When buffered transfers are large enough, the underlying SPI or I2C subsystem automatically uses DMA — you don't need to configure anything in the IIO layer.

How It Works

  1. IIO trigger fires → driver calls spi_sync() to read sensor data
  2. SPI subsystem checks transfer size against DMA threshold (~96 bytes on BCM2835)
  3. If above threshold → SPI uses DMA channel; if below → PIO (programmed I/O)
  4. Driver pushes data to IIO kfifo buffer
  5. Userspace reads batch from /dev/iio:deviceN

At low sample rates with small reads (e.g., 12 bytes per sample at 10 Hz), each individual SPI transfer is below the DMA threshold — PIO is used. But with hardware FIFO burst reads (many samples at once), the transfer can exceed the threshold and trigger DMA automatically.

Hardware FIFO Integration

Many sensors (BMI160, LSM6DSO, ADXL345) have an on-chip FIFO. The IIO driver can:

  1. Configure a watermark — "interrupt me when FIFO has N samples"
  2. Sensor fills its FIFO autonomously at the configured ODR
  3. Watermark IRQ fires → driver reads the entire FIFO in one burst
  4. Burst read (e.g., 200 samples × 12 bytes = 2400 bytes) easily exceeds DMA threshold
  5. DMA moves the data → CPU is free during the transfer

CPU Load Comparison

Mode CPU involvement per sample Practical impact
Polled sysfs Full: sysfs read → context switch → SPI PIO → copy_to_user High at >50 Hz
Buffered (no FIFO) Per-trigger: IRQ → SPI PIO → kfifo push Moderate at >200 Hz
Buffered + FIFO + DMA Per-watermark: IRQ → one DMA burst → kfifo push Minimal even at 1600 Hz

For detailed measurement techniques, see DMA Fundamentals.


6. IIO Tools

iio_info

List all IIO devices, channels, and attributes:

iio_info
# Shows: device name, channels, trigger, buffer status, attributes

iio_readdev

Stream buffered data to stdout:

# Stream accel X, Y, Z at the device's configured rate
iio_readdev iio:device0 in_accel_x in_accel_y in_accel_z

iio_generic_buffer

Kernel sample application for buffered capture (from tools/iio/ in the kernel source):

# Capture 100 samples from device 0
iio_generic_buffer -n bmi160 -t bmi160-dev0 -c 100

libiio

Cross-platform C library for IIO access. Works locally or over the network (with iiod daemon):

#include <iio.h>

struct iio_context *ctx = iio_create_local_context();
struct iio_device *dev = iio_context_find_device(ctx, "bmi160");
struct iio_channel *ch = iio_device_find_channel(dev, "accel_x", false);
// Read channel value, configure buffer, etc.

Install on Raspberry Pi:

sudo apt install libiio-dev libiio-utils

7. When to Use IIO vs a Custom Driver

Aspect Custom chardev Mainline IIO
App-facing API Exactly what you design Generic channels + scale
Buffering You implement it kfifo + triggers + DMA free
Timestamps You add them Automatic iio_get_time_ns()
Tool support Custom scripts only iio_readdev, libiio, desktop rotation
Maintenance You maintain across kernel versions Upstream maintains
Sensor swap Rewrite driver Change compatible string
Custom features Full control Limited to IIO model

Use mainline IIO when:

  • You need standard raw sensor data (accel, gyro, ADC, temperature)
  • You want buffered capture + DMA without writing buffer code
  • Portability matters — swap BMI160 for BMI270 or LSM6DSO with a compatible string change
  • You want ecosystem tools (iio_readdev, libiio, GNOME screen rotation)
  • Long-term maintenance is a concern — upstream handles API changes and fixes

Use a custom driver when:

  • You need a specific ABI for one product (not portability)
  • You need fused outputs in-kernel (gravity vector, orientation, gestures)
  • You need strict real-time guarantees (deterministic buffering, lock-free ring buffers)
  • You need advanced FIFO/batching beyond IIO triggers (custom framing, multi-sensor sync)
  • You need proprietary pipeline integration (calibration store, manufacturing test, attestation)

For a detailed comparison using the BMI160 as a case study, see the Custom Driver vs Mainline IIO section in the BMI160 SPI Driver tutorial.


Course Overview | Reference Index