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()andiio_device_register() - Define IIO channels with
struct iio_chan_spec(type, index, scan format) - Implement
read_rawto 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:
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
- The file compiles with
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules - 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
- All four channels read valid values:
cat in_voltage{0,1,2,3}_raw - Scale is ~0.806 (3300/4096)
- 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
hexdumpshows continuous data with changing values- Each sample contains 4 × 16-bit values + 64-bit timestamp
- 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) |