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:
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
- IIO trigger fires → driver calls
spi_sync()to read sensor data - SPI subsystem checks transfer size against DMA threshold (~96 bytes on BCM2835)
- If above threshold → SPI uses DMA channel; if below → PIO (programmed I/O)
- Driver pushes data to IIO kfifo buffer
- 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:
- Configure a watermark — "interrupt me when FIFO has N samples"
- Sensor fills its FIFO autonomously at the configured ODR
- Watermark IRQ fires → driver reads the entire FIFO in one burst
- Burst read (e.g., 200 samples × 12 bytes = 2400 bytes) easily exceeds DMA threshold
- 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_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):
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:
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.