IIO Buffered Capture
Time estimate: ~60 minutes Prerequisites: BMI160 SPI Driver (for context), SPI DMA Optimization (for DMA background)
Learning Objectives
By the end of this tutorial you will be able to:
- Load the mainline BMI160 IIO driver and read sensor data through the IIO sysfs interface
- Configure IIO buffered mode: scan elements, triggers, and buffer parameters
- Write a C program that reads high-rate sensor data from
/dev/iio:deviceN - Compare CPU load and throughput between polled sysfs, custom chardev, and IIO buffered capture
- Verify DMA activation for SPI bulk transfers
IIO: The Kernel's Standard Sensor Framework
The Industrial I/O (IIO) subsystem is the kernel's standard framework for sensors — accelerometers, gyroscopes, ADCs, pressure sensors, and more. Instead of writing a custom character device with hand-crafted sysfs attributes (as we did in the BMI160 SPI Driver tutorial), IIO provides a standard interface: sysfs channels for polled reads, a ring buffer for high-rate capture, trigger support, automatic timestamps, and a userspace toolchain (iio_readdev, libiio).
The mainline kernel already includes a BMI160 IIO driver (drivers/iio/imu/bmi160/). In this tutorial you will use it — writing zero kernel code — and compare the results to your custom driver.
See also: IIO Subsystem reference | DMA Fundamentals 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/apps/iio-buffered-capture/ and ~/embedded-linux/overlays/bmi160-iio.dts.
Introduction
In the BMI160 SPI Driver tutorial, you wrote ~530 lines of kernel code to expose the BMI160 as a custom character device. That gave you full control — but also full responsibility for sysfs attributes, data formatting, and any buffering.
The kernel's mainline BMI160 IIO driver gives you the same raw sensor data through a standardized interface — with buffered capture, triggers, timestamps, and DMA support included. In this tutorial you will:
- Switch from your custom driver to the mainline IIO driver
- Read sensor data via IIO sysfs (polled mode)
- Enable buffered capture at 200 Hz
- Write a C program to read buffered data from
/dev/iio:device0 - Compare CPU load across all three approaches
1. Switch to the Mainline IIO Driver
The BMI160 IIO Module Is Not in the Stock Raspberry Pi Kernel
The stock Raspberry Pi OS kernel does not include the BMI160 IIO driver (CONFIG_BMI160 is not set). You need to build it yourself. Choose one of these approaches:
Option A: Build from kernel source (recommended for learning)
# Install kernel headers (package name varies by Pi OS version)
sudo apt install linux-headers-$(uname -r)
# If not found, try: sudo apt install linux-headers-rpi-v8
# Create a build directory
mkdir -p ~/bmi160-iio && cd ~/bmi160-iio
# Download the BMI160 IIO driver source from the RPi kernel repo
KVER=$(uname -r)
# Extract the RPi kernel branch tag (e.g., "rpi-6.12.y" from "6.12.47+rpt-rpi-v8")
KMAJMIN=$(echo $KVER | grep -oP '^\d+\.\d+')
KBRANCH="rpi-${KMAJMIN}.y"
BASE="https://raw.githubusercontent.com/raspberrypi/linux/${KBRANCH}/drivers/iio/imu/bmi160"
for f in bmi160_core.c bmi160_spi.c bmi160_i2c.c bmi160.h; do
wget -q "$BASE/$f" && echo "Downloaded $f" || echo "Failed: $f"
done
# Create Makefile
cat > Makefile << 'EOF'
obj-m += bmi160_core.o bmi160_spi.o bmi160_i2c.o
KDIR ?= /lib/modules/$(shell uname -r)/build
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
EOF
make
# ── Fix: SPI timing after soft reset ──
# The upstream driver waits only 1 ms after soft reset, but the BMI160
# needs up to 80 ms to complete startup (datasheet §2.11.1). With 1 ms
# the first SPI read returns 0xFF and the accelerometer stays in suspend.
# Increase the delay and add a pause after the SPI-mode dummy read:
sed -i 's/#define BMI160_SOFTRESET_USLEEP.*/#define BMI160_SOFTRESET_USLEEP 100000/' bmi160_core.c
sed -i '/ret = regmap_read(data->regmap, BMI160_REG_DUMMY, \&val);/{n;s/if (ret)/usleep_range(1000, 2000);\n\t\tif (ret)/}' bmi160_core.c
make clean && make
# Test: load directly from the build directory
# IIO framework modules first, then core, then bus binding:
sudo modprobe industrialio
sudo modprobe industrialio-triggered-buffer
sudo insmod bmi160_core.ko
sudo modprobe regmap-spi
sudo insmod bmi160_spi.ko
# Once verified, install permanently:
sudo mkdir -p /lib/modules/$KVER/extra
sudo cp bmi160_core.ko bmi160_spi.ko /lib/modules/$KVER/extra/
sudo depmod -a
# Now modprobe handles load order automatically:
# sudo modprobe bmi160_spi
# Load on every boot — add all modules to /etc/modules:
sudo tee -a /etc/modules << 'MODS'
industrialio
industrialio-triggered-buffer
regmap-spi
bmi160_core
bmi160_spi
MODS
Option B: Use a Buildroot custom image
If you completed the Buildroot SDL2 Image tutorial, enable CONFIG_BMI160=m, CONFIG_BMI160_SPI=m, and CONFIG_IIO=y in your kernel config. The modules will be included in the image automatically.
Option C: Use rpi-update to switch to a kernel that includes it
Some rpi-testing kernels include CONFIG_BMI160=m. Check with: find /lib/modules/$(uname -r) -name '*bmi160*'
If you have the custom bmi160_spi module from the BMI160 SPI Driver tutorial loaded, unload it first:
# Unload the custom driver
sudo rmmod bmi160_spi
# Load the mainline IIO driver (IIO framework first, then core, then bus binding)
sudo modprobe industrialio # IIO framework (iio_device_register etc.)
sudo modprobe industrialio-triggered-buffer # triggered buffer support
sudo modprobe regmap-spi # SPI regmap (__devm_regmap_init_spi)
sudo insmod ~/bmi160-iio/bmi160_core.ko
sudo insmod ~/bmi160-iio/bmi160_spi.ko
# Or, if you installed the modules with depmod (see Option A above):
# sudo modprobe bmi160_spi
Load Order Matters
Each module depends on symbols from the previous one. With insmod, load in this exact order:
industrialio— IIO framework (iio_device_register,iio_push_to_buffers, etc.)industrialio-triggered-buffer— triggered buffer supportregmap-spi— SPI regmap (__devm_regmap_init_spi)bmi160_core.ko— BMI160 channel definitions, register accessbmi160_spi.ko— SPI bus binding
If any step gives Unknown symbol in module, check dmesg | tail — it shows which symbol is missing and which module provides it. Using modprobe (after depmod -a) handles the dependency chain automatically.
Device Tree: compatible String Matters
Your custom driver used compatible = "bmi160" (no vendor prefix). The mainline driver expects compatible = "bosch,bmi160" (with vendor prefix, as required by upstream naming conventions).
Use the IIO-specific overlay:
# Copy fomr the source folder
cp src/embedded-linux/overlays/bmi160-iio.dts ~/bmi160-iio/
# Compile the IIO overlay
dtc -@ -I dts -O dtb -o bmi160-iio.dtbo bmi160-iio.dts
sudo cp bmi160-iio.dtbo /boot/firmware/overlays/
Then in /boot/firmware/config.txt, replace dtoverlay=bmi160-spi with dtoverlay=bmi160-iio and reboot.
The overlay source is in src/embedded-linux/overlays/bmi160-iio.dts.
Verify the IIO device appeared:
Checkpoint
cat /sys/bus/iio/devices/iio:device0/nameshowsbmi160ls /sys/bus/iio/devices/iio:device0/in_accel_*showsin_accel_x_raw,in_accel_y_raw,in_accel_z_raw,in_accel_scalels /sys/bus/iio/devices/iio:device0/in_anglvel_*shows gyroscope channels
Stuck?
- "No iio:device found" — check
dmesg | grep bmi160for probe errors. Verify the Device Tree overlay usescompatible = "bosch,bmi160". - "Chip id not found: ff" and accelerometer reads 0 — the soft reset delay is too short. The BMI160 defaults to I2C mode after reset; a dummy SPI read switches it to SPI mode, but both the reset and the mode switch need enough delay. Apply the timing fix from the build step above (increase
BMI160_SOFTRESET_USLEEPto 100 ms, add 1 ms delay after the dummy read), rebuild, and reinstall. - "Device or resource busy" — another driver has claimed the SPI device. Run
lsmod | grep bmi160andrmmodany custom module first. - "modprobe: FATAL: Module not found" — the stock Raspberry Pi OS kernel does not include the BMI160 IIO module. See the warning box above for how to build it.
2. Polled Reads via Sysfs
Read individual channels — each cat triggers one SPI transaction:
IIO=/sys/bus/iio/devices/iio:device0
# Read raw accelerometer values
cat $IIO/in_accel_x_raw
cat $IIO/in_accel_y_raw
cat $IIO/in_accel_z_raw
# Read scale factor
cat $IIO/in_accel_scale
# → 0.000598 (for ±2g range)
# Convert to physical units: physical = raw × scale
# If in_accel_z_raw = 16384: 16384 × 0.000598 = 9.8 m/s² (1g)
Compare with your custom driver output:
# Custom driver (if still available):
cat /sys/class/bmi160/bmi160/accel_x
# vs IIO:
cat /sys/bus/iio/devices/iio:device0/in_accel_x_raw
The values should be identical (same sensor, same register read). The interface is different — IIO uses standardized naming.
Measure Polled Read Speed
# Time 100 sequential sysfs reads
time for i in $(seq 100); do cat $IIO/in_accel_x_raw > /dev/null; done
Each read opens the sysfs file, triggers a kernel call, executes an SPI transaction, and returns. At higher rates, this per-read overhead becomes significant.
Checkpoint
- Raw values are consistent between IIO and custom driver (for the same sensor orientation)
- The timing test shows measurable per-read overhead (typically 0.5–2 ms per read)
3. Enable Buffered Mode
Why buffered mode? Polled vs buffered at the hardware level
In polled mode (Section 2), every cat in_accel_x_raw triggers this sequence:
Userspace Kernel (IIO driver) SPI bus BMI160
│ │ │ │
├─ open() ──────────────►│ │ │
├─ read() ──────────────►│ │ │
│ ├─ spi_sync() ──────────►│ │
│ │ ├─ CLK + MOSI ───►│
│ │ │◄── MISO ────────┤
│ │◄── 2 bytes ────────────┤ │
│◄── "16384\n" ──────────┤ │ │
├─ close() ─────────────►│ │ │
Each read is a full round-trip: userspace → kernel → SPI transaction → kernel → userspace. At 100 Hz that's fine. At 1000 Hz, the overhead of 1000 open()/read()/close() syscalls per second dominates — you spend more time in system calls than reading the sensor.
Buffered mode inverts the control flow. Instead of userspace asking for data, a trigger tells the kernel when to sample. The driver reads the sensor and pushes data into a ring buffer (kfifo). Userspace reads batches when ready:
Trigger (IRQ/hrtimer) Kernel (IIO driver) SPI bus BMI160
│ │ │ │
├─ IRQ fires ──────────────►│ │ │
│ ├─ spi_sync() ──────────►│ │
│ │ ├─ CLK+MOSI ───►│
│ │ │◄── MISO ──────┤
│ ├─ push to kfifo ────────┤ │
│ │ (+ timestamp) │ │
├─ IRQ fires ──────────────►│ │ │
│ ├─ spi_sync() ... push │ │
│ : (repeats at trigger rate) │
│ │ │ │
Userspace │ │ │
│ │ │ │
├─ read(/dev/iio:device0) ────────►│ │
│◄── batch of N samples ───────────┤ │
Key difference: zero syscalls per sample. Userspace makes one read() and gets hundreds of samples with nanosecond timestamps. The kernel does all the work in interrupt context.
Why a trigger? Why the interrupt pin?
The kernel needs to know when to read the sensor. Three options:
| Trigger | How it works | Pros | Cons |
|---|---|---|---|
| Data-ready IRQ | BMI160 pulls INT1 pin low when new data is ready. Kernel GPIO interrupt fires → driver reads sensor immediately. | Lowest latency, no missed samples, hardware-paced | Requires wiring INT1 pin to a GPIO |
| hrtimer | Kernel high-resolution timer fires at a configured rate (e.g., 200 Hz). | No extra wiring needed | Software-paced — can miss samples if kernel is busy, slight jitter |
| sysfs | Manual echo 1 > trigger_now. |
Good for testing | Not practical for continuous capture |
Without any trigger, echo 1 > buffer/enable fails with Invalid argument — the kernel refuses to start buffered capture because it has no way to know when to sample.
The data-ready interrupt in detail
The BMI160 has a configurable INT1 output pin. When the internal ADC finishes a conversion (at the configured sampling_frequency), the chip asserts INT1. This creates a GPIO interrupt on the Raspberry Pi:
- BMI160 finishes conversion → pulls INT1 low
- Pi's GPIO controller detects the edge → raises an IRQ to the CPU
- Kernel runs the IIO trigger handler → calls
bmi160_trigger_handler() - Handler reads all enabled channels via SPI → pushes data + timestamp to kfifo
- BMI160 deasserts INT1 → ready for the next sample
This is declared in the device tree overlay:
Without this declaration, the BMI160 IIO driver does not register a trigger — ls /sys/bus/iio/devices/trigger*/ is empty.
If the INT1 pin is not wired
You can still use buffered mode with the hrtimer software trigger:
sudo modprobe iio-trig-hrtimer
mkdir -p /sys/kernel/config/iio/triggers/hrtimer/my_trigger
echo 200 > /sys/bus/iio/devices/trigger0/sampling_frequency
echo my_trigger > /sys/bus/iio/devices/iio:device0/trigger/current_trigger
This uses a kernel timer instead of the hardware interrupt — slightly more jitter, but no wiring needed.
For polled use cases (like the Doom IMU controller), buffered mode is not needed — direct sysfs reads at 100 Hz are sufficient.
Reference
For the full IIO architecture — channels, triggers, buffers, DMA, and when to use IIO vs a custom driver — see IIO Subsystem reference, particularly §3 Sysfs Interface and §4 Buffered Mode and Triggers.
Select Channels
IIO=/sys/bus/iio/devices/iio:device0
# Enable accelerometer channels
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
# Enable gyroscope channels
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
# Enable timestamp
echo 1 > $IIO/scan_elements/in_timestamp_en
Check Scan Element Format
cat $IIO/scan_elements/in_accel_x_type
# → le:s16/16>>0
# Meaning: little-endian, signed 16-bit, stored in 16 bits, no shift
cat $IIO/scan_elements/in_timestamp_type
# → le:s64/64>>0
# Meaning: little-endian, signed 64-bit (nanoseconds)
Set Trigger and Buffer
# List available triggers
cat /sys/bus/iio/devices/trigger*/name
# Set trigger (data-ready IRQ if available, or hrtimer)
# Option A: BMI160 data-ready trigger
echo bmi160-dev0 > $IIO/trigger/current_trigger
# Option B: hrtimer trigger (if no hardware IRQ)
# sudo modprobe iio-trig-hrtimer
# echo 200 > /sys/bus/iio/devices/trigger0/sampling_frequency
# Set sample rate (if using data-ready trigger)
echo 200 > $IIO/sampling_frequency
# Set buffer length (number of sample sets)
echo 256 > $IIO/buffer/length
# Enable buffer
echo 1 > $IIO/buffer/enable
Quick Test
# Read buffered data (binary)
cat /dev/iio:device0 | hexdump -C | head -20
# Or use iio_generic_buffer (from kernel tools)
iio_generic_buffer -n bmi160 -t bmi160-dev0 -c 10
Stop the buffer when done:
Checkpoint
hexdumpshows continuous binary data with changing values- Data stops when you disable the buffer
dmesgshows no errors during capture
Stuck?
- "No trigger available" — load the hrtimer trigger module:
sudo modprobe iio-trig-hrtimer - "Permission denied" on /dev/iio:device0 — run with
sudoor add a udev rule - "Device or resource busy" when enabling buffer — disable the buffer first (
echo 0 > buffer/enable), then reconfigure scan elements
4. C Program: High-Rate Capture
Write a C program that reads buffered IIO data, parses the binary format, and prints timestamped samples.
The source code is in src/embedded-linux/apps/iio-buffered-capture/iio_capture.c:
/* iio_capture.c — Read BMI160 IIO buffered data
*
* Usage: sudo ./iio_capture [device_num] [num_samples]
*
* Reads scan element format from sysfs, opens /dev/iio:deviceN,
* and prints timestamped accelerometer + gyroscope data.
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <time.h>
#define IIO_SYSFS "/sys/bus/iio/devices/iio:device%d"
#define IIO_DEV "/dev/iio:device%d"
/* One sample set: 6 × int16 channels + padding + int64 timestamp */
struct iio_sample {
int16_t accel_x;
int16_t accel_y;
int16_t accel_z;
int16_t anglvel_x;
int16_t anglvel_y;
int16_t anglvel_z;
int16_t _pad; /* alignment padding to 8-byte boundary */
int64_t timestamp;
} __attribute__((packed));
static float read_float_attr(int dev_num, const char *attr)
{
char path[256];
snprintf(path, sizeof(path), IIO_SYSFS "/%s", dev_num, attr);
FILE *f = fopen(path, "r");
if (!f) { perror(path); exit(1); }
float val;
if (fscanf(f, "%f", &val) != 1) { fprintf(stderr, "Bad value: %s\n", path); exit(1); }
fclose(f);
return val;
}
int main(int argc, char *argv[])
{
int dev_num = argc > 1 ? atoi(argv[1]) : 0;
int num_samples = argc > 2 ? atoi(argv[2]) : 100;
/* Read scale factors */
float accel_scale = read_float_attr(dev_num, "in_accel_scale");
float gyro_scale = read_float_attr(dev_num, "in_anglvel_scale");
printf("Accel scale: %f, Gyro scale: %f\n", accel_scale, gyro_scale);
printf("Sample size: %zu bytes\n", sizeof(struct iio_sample));
/* Open the IIO character device */
char dev_path[64];
snprintf(dev_path, sizeof(dev_path), IIO_DEV, dev_num);
int fd = open(dev_path, O_RDONLY);
if (fd < 0) { perror(dev_path); return 1; }
printf("Reading %d samples from %s...\n\n", num_samples, dev_path);
printf("%12s %8s %8s %8s %8s %8s %8s\n",
"timestamp_ns", "ax", "ay", "az", "gx", "gy", "gz");
struct iio_sample sample;
int count = 0;
while (count < num_samples) {
ssize_t n = read(fd, &sample, sizeof(sample));
if (n < 0) {
if (errno == EAGAIN) continue;
perror("read");
break;
}
if (n != sizeof(sample)) {
fprintf(stderr, "Short read: %zd bytes\n", n);
continue;
}
printf("%12ld %8.3f %8.3f %8.3f %8.3f %8.3f %8.3f\n",
(long)sample.timestamp,
sample.accel_x * accel_scale,
sample.accel_y * accel_scale,
sample.accel_z * accel_scale,
sample.anglvel_x * gyro_scale,
sample.anglvel_y * gyro_scale,
sample.anglvel_z * gyro_scale);
count++;
}
close(fd);
printf("\nCaptured %d samples.\n", count);
return 0;
}
Build and Run
cd ~/embedded-linux/apps/iio-buffered-capture
gcc -Wall -O2 -o iio_capture iio_capture.c
sudo ./iio_capture 0 100
Buffer Must Be Enabled First
The C program reads from /dev/iio:device0, which only produces data when buffered mode is active. Enable it with the shell commands from Section 3 before running iio_capture.
Checkpoint
- The program prints 100 timestamped samples
- Accelerometer Z shows ~9.8 m/s² when flat (gravity)
- Timestamps increment by ~5 ms (at 200 Hz sampling)
- Gyroscope values are near zero when stationary
5. DMA Verification
Check whether SPI DMA is active during buffered capture:
# Check for SPI DMA channel allocation
dmesg | grep -i dma
# Look for: "spi-bcm2835 ... DMA channel ... allocated"
When DMA Activates
At 200 Hz with 6 channels × 2 bytes = 12 bytes per sample, each individual SPI read is 12 bytes — well below the DMA threshold (~96 bytes). PIO is used for each trigger event.
To trigger DMA, you need bulk reads — for example, reading the BMI160's hardware FIFO in one burst:
- FIFO watermark set to 50 samples: 50 × 12 = 600 bytes per SPI transaction → DMA
- Without FIFO: each trigger reads 12 bytes → PIO
# Monitor CPU load during capture
mpstat 1 10
# Compare:
# 1. IIO buffered at 200 Hz (individual reads, PIO) → moderate %sys
# 2. If FIFO burst reads were enabled → lower %sys (DMA)
DMA Threshold
The BCM2835 SPI driver uses DMA for transfers ≥ 96 bytes. Individual sensor reads (2–12 bytes) always use PIO. DMA kicks in when reading the sensor's hardware FIFO in bulk. For the full picture, see DMA Fundamentals.
6. Comparison
Fill in the table with your measurements:
| Metric | Custom chardev (/dev/bmi160) |
IIO polled (sysfs) | IIO buffered (/dev/iio:device0) |
|---|---|---|---|
| CPU load at 200 Hz | ___% | ___% | ___% |
| Max samples/sec | ___ | ___ | ___ |
| Timestamp accuracy | Manual ktime_get() |
None (user adds) | Kernel iio_get_time_ns() |
| Driver code lines | ~530 | 0 (mainline) | 0 (mainline) |
| App complexity | Simple read() |
sysfs string parse | Binary buffer parse |
| DMA potential | Manual | No | Automatic (with FIFO) |
Key Observations
- IIO polled has the highest per-read overhead (sysfs open/read/close for each value)
- Custom chardev reads all axes in one
read()call — efficient for polled use - IIO buffered has the lowest CPU load at high rates — data accumulates in a kernel ring buffer
- Timestamps: IIO buffered provides kernel-quality nanosecond timestamps automatically
For a discussion of when each approach is best: Custom Driver vs Mainline IIO.
What Just Happened?
You used the mainline BMI160 IIO driver — zero kernel code written — to achieve high-rate sensor capture with buffered mode:
| Step | What you did | What IIO provided |
|---|---|---|
| Switch driver | Changed compatible string, loaded mainline module |
Standard sysfs channels |
| Polled reads | cat in_accel_x_raw |
One SPI transaction per read |
| Buffered mode | Configured scan elements, trigger, buffer | kfifo ring buffer, timestamps |
| C program | Parsed binary stream from /dev/iio:device0 |
Consistent binary format |
| DMA check | Monitored dmesg and mpstat |
Automatic DMA for bulk transfers |
The IIO framework handles buffering, triggers, timestamps, and DMA — the same features you would need to implement manually in a custom driver.
Challenges
Challenge 1: hrtimer Trigger
If your setup has no data-ready IRQ, use the hrtimer trigger instead. Load iio-trig-hrtimer, create a trigger, and configure the sampling frequency. Compare timestamp jitter between hrtimer and data-ready triggers.
Challenge 2: Log to CSV
Modify iio_capture.c to write samples directly to a CSV file. Capture 10 seconds of movement data and plot with matplotlib or gnuplot. Compare the plot to the custom chardev log from the BMI160 tutorial Challenge 3.
Challenge 3: libiio Capture
Install libiio-dev and rewrite the capture program using the libiio API (iio_create_local_context, iio_device_create_buffer, iio_buffer_refill). This approach handles scan element parsing automatically.
Deliverable
| Item | Description |
|---|---|
| IIO device screenshot | cat /sys/bus/iio/devices/iio:device0/name and channel listing |
| Buffered capture output | Terminal output of iio_capture showing timestamped samples |
| CPU load comparison | mpstat output during polled vs buffered capture |
| Filled comparison table | Section 6 table with your measurements |
| DMA verification | dmesg output showing SPI DMA channel allocation |
Course Overview | Previous: ← SPI DMA Optimization | Next: Custom IIO Driver →