IIO Channel Type Modifiers Guide- Free Linux Device Drivers Tutorial

IIO Channel Type Modifiers Guide

Free Linux Kernel Development Course — IIO Framework, Lecture 4

free linux kernel development course
free linux device drivers course
free embedded systems course
free embedded linux course
iio channel type modifier

In this free linux device drivers course lecture, you will learn how the Linux IIO (Industrial I/O) subsystem
decides the iio channel type and modifier for every sysfs attribute it exposes. If you have
already gone through our earlier lessons on the iio_dev structure and IIO buffer setup, this lecture
fills in the remaining pieces of struct iio_chan_spec — the fields that decide what a channel
measures, how it is numbered, and how the kernel builds its sysfs file name automatically.

What You Will Learn

  • What the type, channel, channel2, indexed, and
    modified fields do inside struct iio_chan_spec
  • How the IIO core builds a sysfs attribute name from those fields
  • The difference between indexing a channel and applying a modifier to a channel
  • How to write a multi-channel ADC-style driver using indexed channels
  • How to write a 3-axis accelerometer-style driver using modifier channels
  • What changed in modern kernels around info_mask_*_available and scan_type

Prerequisites

  • Completion of Lecture 1 (IIO device/channel model) and Lecture 3 (buffer setup, iio_chan_spec
    basics) of this chapter
  • Comfortable writing and loading a basic Linux kernel module
  • A Linux machine or VM running kernel 6.x with kernel headers installed

Quick Recap: Where We Left Off

In the previous lecture we introduced struct iio_chan_spec and looked at scan_index
and the info_mask_separate family of bitmasks. Those fields controlled which values a channel
exports and where it sits inside a buffer. This lecture covers the fields that control how many
channels of the same physical type you can have on one device, and what name each one gets in sysfs.

The type Field: What Is Being Measured

Every channel starts with a type field of enum iio_chan_type. This single field tells
the IIO core what kind of physical quantity the channel represents — voltage, light, acceleration, and so on.
The complete, up to date list lives in the kernel header
include/uapi/linux/iio/types.h, and it keeps growing as new sensor classes are added to the kernel.
A few common values you will use constantly:

Enum Value Measures sysfs Type Name
IIO_VOLTAGE Voltage voltage
IIO_ACCEL Acceleration accel
IIO_LIGHT Ambient light illuminance
IIO_TEMP Temperature temp
IIO_PRESSURE Pressure pressure

Think of type as answering “what physical thing does this channel read”. Everything else in
iio_chan_spec only exists to disambiguate which channel of that same type you are talking
about, and what name it gets on disk.

channel and channel2: Two Numbers, Two Purposes

A single IIO device rarely has just one channel. An 8-channel ADC has eight voltage lines; a 3-axis
accelerometer has three acceleration lines. The kernel needs a way to tell them apart, and that is exactly what
channel and channel2 are for:

Two Ways To Tell Channels Apart

indexed = 1
Use .channel to number similar lines
Example: in_voltage0_raw, in_voltage1_raw
modified = 1
Use .channel2 to name an axis or component
Example: in_accel_x_raw, in_accel_y_raw

channel holds a plain index, used when .indexed is set to 1. channel2
holds a modifier value from enum iio_modifier, used when .modified is set to 1. A
differential channel (rare, but supported) also borrows channel2 to record the second line of the
pair. The important rule to remember: a modifier only changes the sysfs name of the attribute, never the
value the channel reports.

How the IIO Core Builds the sysfs Attribute Name

This is the part students usually find confusing the first time, so let’s slow down. Every channel attribute
name the IIO core generates follows one fixed pattern:

sysfs Attribute Name Pattern

direction
_
type
_
index
_
modifier
_
info_mask

Each piece comes from a different field of iio_chan_spec, and each piece is optional except
type and info_mask:

Name Piece Comes From Present When
direction .output Always: “in” or “out”
type .type Always
index .channel .indexed = 1
modifier .channel2 .modified = 1
info_mask bit set in an info_mask_* field Always: e.g. “raw”, “scale”

The direction table, the type name table, and the modifier name table are all plain C arrays inside the IIO
core (for example the type names are looked up from an internal iio_chan_type_name_spec style array,
and modifiers such as X/Y/Z axes come from a similar internal names array). You never touch these arrays yourself
— you only set the numeric fields, and the core does the string building for you at registration time.

Example 1: Indexed Channels — A Multi-Channel ADC Driver

Let’s build an original driver, ep_iio_multichan_demo, that simulates a small 3-channel ADC. Each
channel is the same IIO_VOLTAGE type, so we use .indexed = 1 with a different
.channel value for each one:

// ep_iio_multichan_demo.c
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/iio/iio.h>

struct ep_multichan {
    int raw_value[3];
};

static int ep_multichan_read_raw(struct iio_dev *indio_dev,
                                  struct iio_chan_spec const *chan,
                                  int *val, int *val2, long mask)
{
    struct ep_multichan *priv = iio_priv(indio_dev);

    switch (mask) {
    case IIO_CHAN_INFO_RAW:
        /* chan->channel tells us WHICH of the 3 lines was read */
        *val = priv->raw_value[chan->channel];
        return IIO_VAL_INT;
    default:
        return -EINVAL;
    }
}

static const struct iio_info ep_multichan_info = {
    .read_raw = ep_multichan_read_raw,
};

#define EP_ADC_CHANNEL(idx) {                        \
    .type = IIO_VOLTAGE,                              \
    .indexed = 1,                                     \
    .channel = idx,                                   \
    .info_mask_separate = BIT(IIO_CHAN_INFO_RAW),     \
}

static const struct iio_chan_spec ep_multichan_channels[] = {
    EP_ADC_CHANNEL(0),
    EP_ADC_CHANNEL(1),
    EP_ADC_CHANNEL(2),
};

static int ep_multichan_probe(struct platform_device *pdev)
{
    struct iio_dev *indio_dev;
    struct ep_multichan *priv;

    indio_dev = devm_iio_device_alloc(&pdev->dev, sizeof(*priv));
    if (!indio_dev)
        return -ENOMEM;

    priv = iio_priv(indio_dev);
    priv->raw_value[0] = 100;
    priv->raw_value[1] = 250;
    priv->raw_value[2] = 400;

    indio_dev->name = "ep_multichan_demo";
    indio_dev->info = &ep_multichan_info;
    indio_dev->modes = INDIO_DIRECT_MODE;
    indio_dev->channels = ep_multichan_channels;
    indio_dev->num_channels = ARRAY_SIZE(ep_multichan_channels);

    return devm_iio_device_register(&pdev->dev, indio_dev);
}

static struct platform_driver ep_multichan_driver = {
    .driver = { .name = "ep_multichan_demo" },
    .probe  = ep_multichan_probe,
};
module_platform_driver(ep_multichan_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Indexed IIO channel demo driver");

Build and load it:

$ make
$ sudo insmod ep_iio_multichan_demo.ko
$ dmesg | tail -3
[  102.441123] ep_multichan_demo ep_multichan_demo.0: iio device registered

$ ls /sys/bus/iio/devices/iio:device0/
in_voltage0_raw  in_voltage1_raw  in_voltage2_raw  name

$ cat /sys/bus/iio/devices/iio:device0/in_voltage1_raw
250

Notice the pattern in action: in (direction) + voltage (type) + 1
(index, from .channel) + raw (info_mask). No modifier segment appears because
.modified was never set for this channel.

Example 2: Modified Channels — A 3-Axis Accelerometer Driver

Now let’s handle the other case. An accelerometer reports the same physical quantity along three
axes, so instead of indexing, we apply a modifier with .channel2 set to IIO_MOD_X,
IIO_MOD_Y, or IIO_MOD_Z:

// ep_iio_accel_demo.c
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/iio/iio.h>

struct ep_accel {
    int axis_value[3]; /* 0=x, 1=y, 2=z */
};

static int ep_accel_axis_index(struct iio_chan_spec const *chan)
{
    switch (chan->channel2) {
    case IIO_MOD_X: return 0;
    case IIO_MOD_Y: return 1;
    case IIO_MOD_Z: return 2;
    default:        return -1;
    }
}

static int ep_accel_read_raw(struct iio_dev *indio_dev,
                              struct iio_chan_spec const *chan,
                              int *val, int *val2, long mask)
{
    struct ep_accel *priv = iio_priv(indio_dev);
    int idx = ep_accel_axis_index(chan);

    if (idx < 0)
        return -EINVAL;

    switch (mask) {
    case IIO_CHAN_INFO_RAW:
        *val = priv->axis_value[idx];
        return IIO_VAL_INT;
    default:
        return -EINVAL;
    }
}

static const struct iio_info ep_accel_info = {
    .read_raw = ep_accel_read_raw,
};

#define EP_ACCEL_CHANNEL(mod) {                       \
    .type = IIO_ACCEL,                                 \
    .modified = 1,                                     \
    .channel2 = mod,                                   \
    .info_mask_separate = BIT(IIO_CHAN_INFO_RAW),      \
}

static const struct iio_chan_spec ep_accel_channels[] = {
    EP_ACCEL_CHANNEL(IIO_MOD_X),
    EP_ACCEL_CHANNEL(IIO_MOD_Y),
    EP_ACCEL_CHANNEL(IIO_MOD_Z),
};

static int ep_accel_probe(struct platform_device *pdev)
{
    struct iio_dev *indio_dev;
    struct ep_accel *priv;

    indio_dev = devm_iio_device_alloc(&pdev->dev, sizeof(*priv));
    if (!indio_dev)
        return -ENOMEM;

    priv = iio_priv(indio_dev);
    priv->axis_value[0] = 12;
    priv->axis_value[1] = -5;
    priv->axis_value[2] = 998;

    indio_dev->name = "ep_accel_demo";
    indio_dev->info = &ep_accel_info;
    indio_dev->modes = INDIO_DIRECT_MODE;
    indio_dev->channels = ep_accel_channels;
    indio_dev->num_channels = ARRAY_SIZE(ep_accel_channels);

    return devm_iio_device_register(&pdev->dev, indio_dev);
}

static struct platform_driver ep_accel_driver = {
    .driver = { .name = "ep_accel_demo" },
    .probe  = ep_accel_probe,
};
module_platform_driver(ep_accel_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Modified IIO channel demo driver");

Expected output:

$ sudo insmod ep_iio_accel_demo.ko

$ ls /sys/bus/iio/devices/iio:device1/
in_accel_x_raw  in_accel_y_raw  in_accel_z_raw  name

$ cat /sys/bus/iio/devices/iio:device1/in_accel_z_raw
998

Here the attribute name is in + accel + z (from
.channel2 = IIO_MOD_Z) + raw. There is no numeric index segment this time, because
.indexed was left at 0 — the modifier alone is enough to tell the three channels apart.

Modern Kernel Update: info_mask_*_available and scan_type

Two things changed in struct iio_chan_spec since older reference books were written, and it is
worth knowing about them so your driver code matches current mainline kernel headers:

  • Every info_mask_* field now has a matching info_mask_*_available field
    (for example info_mask_separate_available). Setting a bit here exposes a companion
    _available sysfs file listing the valid range or set of values for that attribute, which is
    very useful for things like sampling_frequency_available.
  • The scan_type field is now wrapped inside a union alongside an ext_scan_type
    pointer and a has_ext_scan_type flag, added to support devices whose data format changes at
    runtime (for example a channel whose resolution depends on a configurable gain setting). For a fixed-format
    channel like the ones in this lecture’s examples, you still just fill in .scan_type exactly as
    before.

Indexed vs Modified: Side-by-Side Comparison

Aspect Indexed (.indexed = 1) Modified (.modified = 1)
Use case Several identical, unrelated lines (ADC pins) Several components of one measurement (X/Y/Z axis)
Field used .channel .channel2
sysfs name segment Plain number, e.g. voltage2 Named modifier, e.g. accel_x
Value meaning changed by field? No No — modifiers only change the name

Common Mistakes and Troubleshooting

  • Forgetting to set .indexed = 1 while still assigning different
    .channel values — the index will not appear in the attribute name and every channel collapses
    onto the same sysfs file.
  • Mixing up channel and channel2 — remember, indexing uses
    channel, modifiers use channel2.
  • Reusing the same .channel value on two channels of the same type — this
    produces a duplicate sysfs attribute name and driver registration will fail.
  • Assuming a modifier changes the returned value — it does not. Your
    read_raw() callback is still responsible for returning the correct axis value based on
    chan->channel2, as shown in the accelerometer example.

Best Practices for Designing IIO Channels

  • Use indexing for physically separate, same-type lines (multiplexed ADC inputs).
  • Use modifiers for components of a single physical measurement (axes, color channels).
  • Keep a small helper function (like ep_accel_axis_index() above) to translate a modifier back
    into an array index — it keeps read_raw() readable as channel count grows.
  • Define channels with a macro (like EP_ADC_CHANNEL) once your channel count grows past two or
    three, to avoid repetitive, error-prone struct literals.

Performance and Security Considerations

Channel definitions themselves cost nothing at runtime beyond a small constant array in kernel memory; the
real cost lives in your read_raw() implementation, which should avoid unnecessary bus transactions
per axis when a burst read from the device is available. From a security standpoint, always validate that
chan->channel or chan->channel2 resolves to an index inside your private array
bounds before using it, exactly as the accelerometer example checks for a negative index — a missing check
here becomes an out-of-bounds read reachable from user space through sysfs.

Real-World Use Cases

Indexed channels appear in almost every general-purpose ADC driver in the kernel tree, where a single die
exposes 4, 8, or 16 identical input pins. Modified channels appear throughout motion and environmental sensor
drivers — accelerometers, gyroscopes, and magnetometers all use the X/Y/Z modifier pattern, while some gas
and color sensors use modifiers like IIO_MOD_CO2 or IIO_MOD_LIGHT_RED for their
sub-measurements.

Summary / Key Takeaways

  • type says what physical quantity a channel measures.
  • indexed + channel numbers multiple same-type lines.
  • modified + channel2 names components of one measurement.
  • The sysfs attribute name is always built as direction_type_index_modifier_info_mask.
  • Modern kernels add info_mask_*_available fields and wrap scan_type in a union
    with ext_scan_type for variable-format channels.

Conclusion

With type, indexing, and modifiers covered, you now understand every field the IIO core uses to
build a channel’s identity and its sysfs name. Combined with the buffer and info_mask knowledge from
the previous lecture, you have everything needed to describe a real multi-channel sensor correctly on modern
kernel 6.x. The next lecture in this free linux kernel development course moves on to IIO events —
threshold and roc notifications delivered outside the regular read/write sysfs path.

FAQ

What happens if I set both .indexed and .modified on the same channel?

Both segments appear in the attribute name, in the pattern order shown earlier: type, then index, then
modifier. This is valid and used, for example, in devices with several instances of a multi-axis sensor.

Can I use the same channel type for two completely different sysfs names?

Only by combining indexed or modified uniquely for each channel — the type
name segment itself is fixed by the enum value.

Do I need to write the modifier string tables myself?

No. The IIO core already contains the full name tables for directions, types, and modifiers; you only set the
numeric enum fields in your channel spec.

Is scan_index related to the index used in indexed channels?

No, they are unrelated. .channel affects the sysfs attribute name; scan_index only
affects the ordering of raw samples inside a triggered buffer capture.

What is the difference between channel and address fields?

channel is used purely for naming and disambiguation in sysfs; address is a
driver-private value (often a register offset) that you can use however you like inside your own read/write
callbacks.

Why did my multi-channel driver register only one sysfs attribute?

This almost always means .indexed or .modified was not set, so every channel of the
same type produced the same attribute name and only the first one stuck.

Are info_mask_*_available fields mandatory?

No, they are optional. Set them only when you want to expose a companion “_available” sysfs file describing
the valid range or set for that attribute, such as available sampling frequencies.

Continue Your Free Linux Kernel Development Course

 

Leave a Reply

Your email address will not be published. Required fields are marked *