Skip to content

Writing a Custom IIO Driver

Time estimate: ~90 minutes Prerequisites: MCP9808 Kernel Driver, IIO Buffered Capture

Learning Objectives

By the end of this tutorial you will be able to:

  • Allocate and register an IIO device with iio_device_alloc() and iio_device_register()
  • Define IIO channels with struct iio_chan_spec (type, index, scan format)
  • Implement read_raw to serve polled sysfs reads via SPI transactions
  • Set up triggered buffered capture with iio_triggered_buffer_setup()
  • Compare the effort of writing a custom IIO driver vs a custom chardev
When You Need a Custom IIO Driver

In the IIO Buffered Capture tutorial, you used the mainline BMI160 IIO driver — zero kernel code. But what if the kernel does not have an IIO driver for your hardware?

This happens with: - Custom FPGA-based ADCs and DACs - Prototype sensor ASICs - Obscure or very new sensors without upstream support - Sensors with non-standard register protocols

In these cases, you write your own IIO driver. You get the IIO framework's benefits — sysfs channels, buffered capture, triggers, timestamps, libiio support — while providing the hardware-specific SPI/I2C access code.

See also: IIO Subsystem reference | Device Tree and Drivers reference

Course Source Repository

This tutorial references source files from the course repository. If you haven't cloned it yet on your Pi:

cd ~
git clone https://github.com/OE-KVK-H2IoT/embedded-linux.git

Source files for this tutorial are in ~/embedded-linux/drivers/fpga_adc_iio.c and ~/embedded-linux/overlays/fpga-adc-spi.dts.


Introduction

In this tutorial you will write an IIO driver for a 4-channel, 12-bit ADC connected over SPI. The driver exposes standard IIO voltage channels — the same interface that tools like iio_readdev and libiio expect.

Hardware Options

The protocol we implement matches the MCP3008 (8-channel, 10-bit) and MCP3208 (8-channel, 12-bit) ADC chips. These are inexpensive, widely available SPI ADCs.

The mainline kernel already has an MCP320x IIO driver (drivers/iio/adc/mcp320x.c), but we will write our own to learn the IIO API — the same approach as writing a custom BMI160 driver when a mainline one exists.

No hardware? You can test the driver with SPI loopback (MOSI connected to MISO) or a mock device that returns fixed values. The driver structure is the same.


1. Driver Skeleton

Create the driver source file at src/embedded-linux/drivers/fpga_adc_iio.c:

#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/iio/iio.h>
#include <linux/iio/sysfs.h>
#include <linux/iio/buffer.h>
#include <linux/iio/trigger_consumer.h>
#include <linux/iio/triggered_buffer.h>

#define FPGA_ADC_NUM_CHANNELS  4
#define FPGA_ADC_RESOLUTION    12   /* 12-bit ADC */
#define FPGA_ADC_VREF_MV       3300 /* 3.3V reference */

struct fpga_adc_state {
    struct spi_device *spi;
    /* DMA-safe buffer for SPI transfers (must not be on stack) */
    u8 tx_buf[3] __aligned(IIO_DMA_MINALIGN);
    u8 rx_buf[3] __aligned(IIO_DMA_MINALIGN);
};

Module Boilerplate

static const struct of_device_id fpga_adc_of_match[] = {
    { .compatible = "custom,fpga-adc" },
    { }
};
MODULE_DEVICE_TABLE(of, fpga_adc_of_match);

static const struct spi_device_id fpga_adc_id[] = {
    { "fpga-adc", 0 },
    { }
};
MODULE_DEVICE_TABLE(spi, fpga_adc_id);

static struct spi_driver fpga_adc_driver = {
    .driver = {
        .name = "fpga-adc",
        .of_match_table = fpga_adc_of_match,
    },
    .probe = fpga_adc_probe,
    .remove = fpga_adc_remove,
    .id_table = fpga_adc_id,
};
module_spi_driver(fpga_adc_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux Course");
MODULE_DESCRIPTION("IIO driver for FPGA ADC (MCP3008-compatible)");
Checkpoint
  1. The file compiles with make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
  2. No errors — just missing function definitions (we add those next)

2. Define Channels

IIO channels describe what data the sensor provides. Each channel becomes a sysfs attribute:

#define FPGA_ADC_CHANNEL(idx) {                             \
    .type = IIO_VOLTAGE,                                    \
    .indexed = 1,                                           \
    .channel = (idx),                                       \
    .info_mask_separate = BIT(IIO_CHAN_INFO_RAW),            \
    .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE),    \
    .scan_index = (idx),                                    \
    .scan_type = {                                          \
        .sign = 'u',                                        \
        .realbits = 12,                                     \
        .storagebits = 16,                                  \
        .endianness = IIO_CPU,                              \
    },                                                      \
}

static const struct iio_chan_spec fpga_adc_channels[] = {
    FPGA_ADC_CHANNEL(0),
    FPGA_ADC_CHANNEL(1),
    FPGA_ADC_CHANNEL(2),
    FPGA_ADC_CHANNEL(3),
    IIO_CHAN_SOFT_TIMESTAMP(FPGA_ADC_NUM_CHANNELS),
};

This creates:

sysfs file Channel Description
in_voltage0_raw Channel 0 Raw 12-bit ADC value
in_voltage1_raw Channel 1 Raw 12-bit ADC value
in_voltage2_raw Channel 2 Raw 12-bit ADC value
in_voltage3_raw Channel 3 Raw 12-bit ADC value
in_voltage_scale Shared mV per LSB (3300/4096 ≈ 0.806)

Channel Spec Fields Explained

Field Value Meaning
.type IIO_VOLTAGE sysfs prefix: in_voltage
.indexed 1 Append channel number: in_voltage0_raw
.info_mask_separate BIT(IIO_CHAN_INFO_RAW) Each channel has its own _raw
.info_mask_shared_by_type BIT(IIO_CHAN_INFO_SCALE) All voltage channels share _scale
.scan_index 0..3 Position in buffered data stream
.scan_type u12/16 Unsigned 12-bit, stored in 16 bits

3. Implement read_raw

The read_raw callback is called when userspace reads a sysfs attribute (e.g., cat in_voltage0_raw):

static int fpga_adc_read_channel(struct fpga_adc_state *st, int channel)
{
    struct spi_transfer t = {
        .tx_buf = st->tx_buf,
        .rx_buf = st->rx_buf,
        .len = 3,
    };

    /*
     * MCP3008-compatible SPI protocol:
     * TX: [0x01] [channel << 4 | 0x80] [0x00]
     * RX: [xxxx] [0000 MSB3..0] [LSB7..0]
     * Result: 10-bit (MCP3008) or 12-bit (MCP3208) value
     */
    st->tx_buf[0] = 0x01;                      /* start bit */
    st->tx_buf[1] = (0x80 | (channel << 4));    /* single-ended, channel select */
    st->tx_buf[2] = 0x00;

    int ret = spi_sync_transfer(st->spi, &t, 1);
    if (ret)
        return ret;

    /* Extract 12-bit result from bytes 1-2 */
    return ((st->rx_buf[1] & 0x0F) << 8) | st->rx_buf[2];
}

static int fpga_adc_read_raw(struct iio_dev *indio_dev,
                             struct iio_chan_spec const *chan,
                             int *val, int *val2, long mask)
{
    struct fpga_adc_state *st = iio_priv(indio_dev);

    switch (mask) {
    case IIO_CHAN_INFO_RAW:
    {
        int ret;

        ret = iio_device_claim_direct_mode(indio_dev);
        if (ret)
            return ret;

        ret = fpga_adc_read_channel(st, chan->channel);
        iio_device_release_direct_mode(indio_dev);

        if (ret < 0)
            return ret;

        *val = ret;
        return IIO_VAL_INT;
    }

    case IIO_CHAN_INFO_SCALE:
        /* Scale: Vref_mV / 2^resolution = 3300 / 4096 */
        *val = FPGA_ADC_VREF_MV;
        *val2 = FPGA_ADC_RESOLUTION;
        return IIO_VAL_FRACTIONAL_LOG2;

    default:
        return -EINVAL;
    }
}

static const struct iio_info fpga_adc_info = {
    .read_raw = fpga_adc_read_raw,
};

Test Polled Mode

# Load the driver
sudo insmod fpga_adc_iio.ko

# Verify IIO device appeared
cat /sys/bus/iio/devices/iio:device0/name
# → fpga-adc

# Read channel 0
cat /sys/bus/iio/devices/iio:device0/in_voltage0_raw
# → 2048  (mid-scale for a 12-bit ADC at ~1.65V)

# Read scale
cat /sys/bus/iio/devices/iio:device0/in_voltage_scale
# → 0.805664  (3300 / 4096)

# Physical voltage = raw × scale = 2048 × 0.806 = 1651 mV
Checkpoint
  1. All four channels read valid values: cat in_voltage{0,1,2,3}_raw
  2. Scale is ~0.806 (3300/4096)
  3. Values change when you vary the input voltage (or connect a potentiometer)

4. Add Triggered Buffer

With polled mode working, add buffered capture — this gives you high-rate streaming through /dev/iio:device0:

static irqreturn_t fpga_adc_trigger_handler(int irq, void *p)
{
    struct iio_poll_func *pf = p;
    struct iio_dev *indio_dev = pf->indio_dev;
    struct fpga_adc_state *st = iio_priv(indio_dev);

    /*
     * Buffer layout: up to 4 × u16 channels + u64 timestamp
     * Use a stack buffer large enough for all channels + timestamp.
     */
    struct {
        u16 channels[FPGA_ADC_NUM_CHANNELS];
        s64 timestamp;
    } __aligned(8) scan;

    int i, bit, ret;

    memset(&scan, 0, sizeof(scan));

    i = 0;
    for_each_set_bit(bit, indio_dev->active_scan_mask,
                     indio_dev->masklength) {
        ret = fpga_adc_read_channel(st, bit);
        if (ret < 0)
            goto done;
        scan.channels[i++] = ret;
    }

    iio_push_to_buffers_with_timestamp(indio_dev, &scan,
                                       iio_get_time_ns(indio_dev));

done:
    iio_trigger_notify_done(indio_dev->trig);
    return IRQ_HANDLED;
}

Register the triggered buffer in probe():

ret = iio_triggered_buffer_setup(indio_dev, NULL,
                                 fpga_adc_trigger_handler, NULL);
if (ret) {
    dev_err(&spi->dev, "Failed to setup triggered buffer\n");
    return ret;
}

Test Buffered Mode

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

# Use hrtimer trigger (no hardware IRQ needed)
sudo modprobe iio-trig-hrtimer
mkdir -p /sys/bus/iio/devices/iio_hrtimer_trigger/trigger0

# Configure
echo 1 > $IIO/scan_elements/in_voltage0_en
echo 1 > $IIO/scan_elements/in_voltage1_en
echo 1 > $IIO/scan_elements/in_voltage2_en
echo 1 > $IIO/scan_elements/in_voltage3_en
echo 1 > $IIO/scan_elements/in_timestamp_en
echo iio_hrtimer_trigger0 > $IIO/trigger/current_trigger
echo 100 > /sys/bus/iio/devices/trigger0/sampling_frequency
echo 256 > $IIO/buffer/length
echo 1 > $IIO/buffer/enable

# Read data
cat /dev/iio:device0 | hexdump -C | head -20

# Stop
echo 0 > $IIO/buffer/enable
Checkpoint
  1. hexdump shows continuous data with changing values
  2. Each sample contains 4 × 16-bit values + 64-bit timestamp
  3. Timestamps increment consistently (~10 ms at 100 Hz)

5. (Optional) Hardware Trigger

If your ADC has a data-ready output pin (or if you add an external signal), register a hardware IIO trigger:

static irqreturn_t fpga_adc_irq_handler(int irq, void *p)
{
    struct iio_trigger *trig = p;
    iio_trigger_poll(trig);
    return IRQ_HANDLED;
}

/* In probe(): */
st->trig = iio_trigger_alloc(indio_dev->dev.parent,
                             "fpga-adc-dev%d", indio_dev->id);
/* ... configure trigger ... */

devm_request_irq(&spi->dev, gpio_to_irq(drdy_gpio),
                 fpga_adc_irq_handler, IRQF_TRIGGER_RISING,
                 "fpga-adc-drdy", st->trig);

iio_trigger_register(st->trig);
indio_dev->trig = iio_trigger_get(st->trig);

The Device Tree specifies the interrupt pin:

fpga_adc@1 {
    compatible = "custom,fpga-adc";
    reg = <1>;
    spi-max-frequency = <1000000>;
    interrupt-parent = <&gpio>;
    interrupts = <24 1>;  /* GPIO 24, rising edge */
};

Flow: GPIO IRQ → iio_trigger_poll() → IIO core calls fpga_adc_trigger_handler() → reads all enabled channels → pushes to kfifo → userspace reads from /dev/iio:device0.


6. Complete probe() and remove()

static int fpga_adc_probe(struct spi_device *spi)
{
    struct iio_dev *indio_dev;
    struct fpga_adc_state *st;
    int ret;

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

    st = iio_priv(indio_dev);
    st->spi = spi;
    spi_set_drvdata(spi, indio_dev);

    indio_dev->name = "fpga-adc";
    indio_dev->info = &fpga_adc_info;
    indio_dev->modes = INDIO_DIRECT_MODE;
    indio_dev->channels = fpga_adc_channels;
    indio_dev->num_channels = ARRAY_SIZE(fpga_adc_channels);

    ret = iio_triggered_buffer_setup(indio_dev, NULL,
                                     fpga_adc_trigger_handler, NULL);
    if (ret) {
        dev_err(&spi->dev, "Failed to setup triggered buffer\n");
        return ret;
    }

    ret = iio_device_register(indio_dev);
    if (ret) {
        dev_err(&spi->dev, "Failed to register IIO device\n");
        iio_triggered_buffer_cleanup(indio_dev);
        return ret;
    }

    dev_info(&spi->dev, "FPGA ADC IIO driver probed (%d channels, %d-bit)\n",
             FPGA_ADC_NUM_CHANNELS, FPGA_ADC_RESOLUTION);
    return 0;
}

static void fpga_adc_remove(struct spi_device *spi)
{
    struct iio_dev *indio_dev = spi_get_drvdata(spi);

    iio_device_unregister(indio_dev);
    iio_triggered_buffer_cleanup(indio_dev);
}
devm_ Variants

In newer kernels (6.x+), you can simplify by using devm_iio_triggered_buffer_setup() and devm_iio_device_register(), which automatically clean up in reverse order when the device is removed. The explicit version shown here works on all kernel versions.


7. Complete Driver Summary

The complete driver is ~200 lines of C — compare to the custom BMI160 chardev at ~530 lines:

Feature Custom chardev (BMI160) IIO driver (FPGA ADC)
Lines of code ~530 ~200
sysfs attributes Manual (DEVICE_ATTR) Automatic from iio_chan_spec
Buffered capture Not implemented iio_triggered_buffer_setup()
Timestamps Manual iio_push_to_buffers_with_timestamp()
Tool support Custom scripts iio_readdev, libiio, iio_info
Trigger support Not implemented IIO trigger framework

What You Wrote vs What IIO Gives You

You implemented IIO provided for free
read_raw() — SPI transaction for each channel sysfs file creation and naming
iio_chan_spec[] — channel definitions Buffer management (kfifo)
trigger_handler() — read channels on trigger Trigger framework (hrtimer, IRQ)
probe() / remove() — setup and teardown /dev/iio:deviceN character device
Timestamps
iio_readdev, iio_info, libiio support
Scan element format export

What Just Happened?

You wrote a custom IIO driver for an SPI ADC — the same approach you would use for any unsupported sensor:

Step What you did Lines
1. Skeleton Module boilerplate, spi_driver struct ~30
2. Channels iio_chan_spec array with type, index, scan format ~25
3. read_raw SPI transaction + scale calculation ~50
4. Buffer Triggered buffer setup + handler ~40
5. Probe IIO device allocation and registration ~40

Total: ~200 lines. The IIO framework handles everything else — sysfs, buffers, triggers, timestamps, character device, tool compatibility.


Challenges

Challenge 1: Differential Channels

The MCP3008 supports differential mode (measure voltage between two pins). Add differential channels: .differential = 1, .channel = pos, .channel2 = neg. Test with cat in_voltage0-voltage1_raw.

Challenge 2: Configurable Vref

Read the reference voltage from the Device Tree (vref-supply property) instead of hard-coding 3300 mV. Use devm_regulator_get() and regulator_get_voltage().

Challenge 3: High-Speed Capture with libiio

Install libiio-dev and write a C program using the libiio API to capture from your custom IIO driver. Compare with the raw /dev/iio:device0 approach from the IIO Buffered Capture tutorial.


Deliverable

Item Description
Compiled module fpga_adc_iio.ko that loads and probes without errors
Device tree overlay fpga-adc-spi.dts compiled to .dtbo
Polled read screenshot cat in_voltage{0..3}_raw showing valid ADC values
Buffered capture hexdump or iio_generic_buffer output with timestamps
Code comparison Brief notes on effort vs custom chardev (BMI160 tutorial)

Course Overview | Previous: ← IIO Buffered Capture