IIO Channel Type Modifiers Guide
Free Linux Kernel Development Course — IIO Framework, Lecture 4
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
modifiedfields do insidestruct 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_*_availableandscan_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 linesExample: in_voltage0_raw, in_voltage1_raw |
modified = 1 Use .channel2 to name an axis or componentExample: 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
_
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 matchinginfo_mask_*_availablefield
(for exampleinfo_mask_separate_available). Setting a bit here exposes a companion
_availablesysfs file listing the valid range or set of values for that attribute, which is
very useful for things likesampling_frequency_available. - The
scan_typefield is now wrapped inside a union alongside anext_scan_type
pointer and ahas_ext_scan_typeflag, 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_typeexactly 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 = 1while still assigning different
.channelvalues — the index will not appear in the attribute name and every channel collapses
onto the same sysfs file. - Mixing up
channelandchannel2— remember, indexing uses
channel, modifiers usechannel2. - Reusing the same
.channelvalue 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 keepsread_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
typesays what physical quantity a channel measures.indexed+channelnumbers multiple same-type lines.modified+channel2names components of one measurement.- The sysfs attribute name is always built as direction_type_index_modifier_info_mask.
- Modern kernels add
info_mask_*_availablefields and wrapscan_typein a union
withext_scan_typefor 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
