Lesson 4: Kernel Drivers & Device Tree
Óbuda University — Linux in Embedded Systems
"Three programs need this sensor simultaneously — now what?"
Today's Map
- Block 1 (45 min): Drivers and interfaces: kernel vs user space, VFS abstraction, sysfs/ioctl/read-write, driver types. Case studies: MCP9808 (I2C character device), SSD1306 OLED (I2C framebuffer), BUSE LED matrix (SPI framebuffer with real-time refresh).
- Block 2 (45 min): IIO & DMA: the kernel's sensor framework, buffered capture, triggers, DMA transfers.
Block 1 — Drivers & Interfaces
"Three programs need this sensor simultaneously -- now what?"
Kernel vs User Space
On a microcontroller, your code touches hardware registers directly. On Linux, a strict boundary separates hardware access (kernel) from applications (user space).
+----------------------------------------------------+
| User Space |
| +-------------+ +----------+ +--------------+ |
| | Application | | Library | | System Calls | |
| +------+------+ +----+-----+ +------+-------+ |
+---------|--------------|---------------|-----------+
| | |
==========|==============|===============|=== CPU ====
| | | boundary
+---------|--------------|---------------|-----------+
| +------v--------------v---------------v-------+ |
| | VFS / sysfs / ioctl | |
| +---------------------+-----------------------+ |
| | Device Driver | Bus Subsystem Core | |
| | (e.g. MCP9808) | (I2C / SPI / GPIO) | |
| +----------+----------+-----------+-----------+ |
| Kernel Space |
+-------------|----------------------|---------------+
v v
+----------------------------------------------------+
| Hardware: I2C bus, physical sensor |
+----------------------------------------------------+
Why This Separation Exists
The CPU enforces this boundary:
- Privilege levels — kernel runs in privileged mode
- MMU — enforces memory isolation
- Hardware registers — accessible only in kernel mode
User programs cannot access hardware directly. If they try, the CPU blocks them.
So how does user space reach hardware? Through controlled entry points:
| Entry Point | Example |
|---|---|
| System calls | open(), read(), write(), close() |
/dev files |
/dev/i2c-1, /dev/mcp9808 |
sysfs |
/sys/class/hwmon/hwmon0/temp1_input |
ioctl() |
Structured control commands |
VFS — "Everything Is a File"
The Virtual File System makes /dev/i2c-1, /proc/cpuinfo, and /home/user/file.txt all accessible through the same system calls:
+------------------+------------------+------------------+
| /dev/i2c-1 | /proc/cpuinfo | /home/user/f.txt |
+--------+---------+--------+---------+--------+---------+
v v v
+--------------------------------------------------------+
| VFS (Virtual File System) |
| Routes calls to the correct handler: |
| - I2C char driver | - procfs | - ext4 filesystem |
+--------------------------------------------------------+
User-space code does not know (or care) whether it is reading a sensor, kernel info, or a disk file.
VFS Key Concepts
| Concept | What It Is | Analogy |
|---|---|---|
| inode | Metadata: size, permissions, data location | File's passport |
| dentry | Links filename to inode | Directory entry |
| superblock | Describes a mounted filesystem | Volume label |
| file object | Open file descriptor, tracks position | Your reading session |
This is why drivers implement read/write/open/release callbacks -- they plug into the VFS framework.
The sysfs Interface

sysfs exposes kernel objects as a directory tree under /sys/. Each attribute is a file — read for status, write for configuration.
Common Interfaces
| Interface | Best For | Characteristics |
|---|---|---|
/dev + read/write |
Data streams | Byte-oriented, simple |
sysfs |
Config and state | One value per file, text |
ioctl() |
Structured control | Typed commands, complex |
sysfs example:
ioctl example: "set sampling rate to 200 Hz and enable FIFO mode" -- one structured call instead of writing multiple files.
Device Driver Architecture

Driver architecture: user-space applications access hardware through VFS, which routes calls to the appropriate kernel driver and bus subsystem.
Driver Types

Linux driver categories: character devices (byte streams), block devices (fixed-size blocks), and network devices (packets).
Driver Types in Detail
| Type | Interface | Examples | Data Pattern |
|---|---|---|---|
| Character device | /dev/xxx |
I2C sensors, UART | Byte stream |
| Block device | /dev/sdX |
SD, eMMC, NVMe | Fixed blocks |
| Network device | eth0, wlan0 |
Ethernet, WiFi | Packets |
| Input device | /dev/input/eventX |
Buttons, touch | Events |
| Framebuffer/DRM | /dev/fbX |
Displays | Pixel buffers |
Choosing the correct subsystem matters more than writing clever code.
A temperature sensor as a character device works -- but it will not integrate with standard Linux monitoring tools (hwmon).
Interface Design Trade-offs
The interface you choose is the contract between kernel and user space. Changing it later breaks every application.
sysfs — simple, one value per file, shell-friendly. Limitation: not designed for streaming data.
ioctl — flexible, structured commands, typed data. Limitation: hard to version, invisible to shell tools.
read/write — byte stream, natural for serial/sensor data. Limitation: no structure -- application must know the protocol.
Design based on product architecture -- not prototype convenience.
Live: Read MCP9808 from Python
User-space approach (via smbus library, no kernel driver needed):
import smbus
bus = smbus.SMBus(1) # I2C bus 1
raw = bus.read_word_data(0x18, 0x05) # Addr 0x18, temp register
raw = ((raw << 8) & 0xFF00) + (raw >> 8) # MCP9808 returns MSB first — swap bytes before interpreting
temp = (raw & 0x0FFF) / 16.0 # Convert to Celsius
print(f"{temp:.4f}") # e.g. 26.7500
Kernel driver approach:
Both produce the same result. The trade-off is where the I2C protocol knowledge lives.
Try It Now: Read Sensor via sysfs (5 min)
Read the MCP9808 temperature through the kernel's sysfs interface — no Python library needed:
# Find the hwmon device
ls /sys/class/hwmon/
# Read temperature (value in millidegrees Celsius)
cat /sys/class/hwmon/hwmon0/temp1_input
# Example: 26750 means 26.75°C
# Compare with direct I2C bus scan
i2cdetect -y 1
Which approach — sysfs or smbus — is better for a multi-process system?
Tutorial: MCP9808 Driver — Read Temperature Theory: Section 3: Common Interfaces
Discussion: Three Programs, One Sensor
Scenario: A data logger, a display daemon, and an alarm service all need to read the MCP9808 simultaneously.
User-space only (smbus): Each program opens I2C bus 1 independently. Bus contention. Race conditions. Corrupted reads.
Kernel driver:
The driver arbitrates access. Each program reads /dev/mcp9808 or sysfs. The kernel serializes bus transactions.
This is why kernel drivers exist -- not just for abstraction, but for resource arbitration when multiple consumers share hardware.
Decision Model: Kernel or User Space?
+------------------------------+
| Need hardware access? |
+------+-----------------------+
|
No --+--> User space (done)
|
Yes -+--> Need interrupt-level timing?
|
Yes -+--> Kernel driver
|
No --+--> Existing kernel interface?
(sysfs, i2c-dev, spidev)
|
Yes -+--> User space via existing driver
|
No --+--> Write a kernel driver
Default rule: put logic in user space unless kernel execution is required.
Kernel bugs can panic the system. User-space bugs crash only the process.
Case Study: SSD1306 OLED as Framebuffer
The same I2C bus carries the MCP9808 temperature and the SSD1306 OLED display. But the OLED is not a sensor — it is a display. Different subsystem, different driver type.
I2C Bus 1
├── 0x18: MCP9808 → hwmon subsystem → /sys/class/hwmon/
└── 0x3C: SSD1306 → fbdev subsystem → /dev/fb1
User-space approach: Python library sends raw I2C commands to draw pixels. Works, but no integration with Linux graphics.
Kernel driver approach: ssd1306 fbdev driver makes the OLED appear as /dev/fb1. Any program that writes to a framebuffer works — cat, fbset, or even Pong.
SSD1306: Device Tree Overlay
/dts-v1/;
/plugin/;
/ {
fragment@0 {
target = <&i2c1>;
__overlay__ {
ssd1306: oled@3c {
compatible = "solomon,ssd1306fb-i2c";
reg = <0x3c>;
solomon,width = <128>;
solomon,height = <64>;
solomon,page-offset = <0>;
solomon,com-invdir;
};
};
};
};
Same bus, same overlay mechanism as MCP9808. The compatible string determines which driver probes: ssd1306fb-i2c → framebuffer driver, not a sensor driver.
SSD1306: From Overlay to Pixels
┌──────────────────────────────┐
│ Device Tree Overlay │ "SSD1306 at 0x3C on I2C-1"
├──────────────────────────────┤
│ compatible = "solomon, │
│ ssd1306fb-i2c" │
├──────────────────────────────┤
│ ssd1306 fbdev driver │ Registers /dev/fb1
│ probe(): init display, │ 128×64, 1bpp
│ allocate framebuffer │
├──────────────────────────────┤
│ Kernel refresh thread │ Copies framebuffer → I2C
│ (~20 fps at I2C 400 kHz) │ at regular intervals
├──────────────────────────────┤
│ Application writes to │ mmap() or write() to /dev/fb1
│ /dev/fb1 │ Standard fbdev API
└──────────────────────────────┘
Key insight: the same /dev/fbN API that drives an 800×480 HDMI display also drives a 128×64 OLED. Write pixels to memory, kernel handles the transport.
Pong on the OLED Framebuffer
Once the OLED is /dev/fb1, you can write a Pong game that draws directly to it:
int fb = open("/dev/fb1", O_RDWR);
char *buf = mmap(NULL, 128 * 64 / 8, PROT_WRITE, MAP_SHARED, fb, 0);
// Clear screen
memset(buf, 0, 128 * 64 / 8);
// Draw a pixel at (x, y) in 1bpp layout
buf[y / 8 * 128 + x] |= (1 << (y % 8));
Same framebuffer pattern as writing to /dev/fb0 (HDMI), just smaller and 1-bit-per-pixel.
Tutorial: Pong on Framebuffer — write a complete game using only open, mmap, memset.
Case Study: BUSE LED Matrix — Framebuffer on SPI
The OLED uses I2C. The BUSE LED matrix uses SPI — different bus, same fbdev subsystem, new challenges.
BUSE LED matrix: 128×19 monochrome LEDs driven by shift registers via SPI. No built-in frame memory — the driver must actively refresh at a fixed rate.
┌──────────────────────────────┐
│ /dev/fb2 │ Standard framebuffer interface
├──────────────────────────────┤
│ BUSE fbdev driver │ Allocates pixel buffer
│ hrtimer → workqueue → SPI │ Periodic refresh (~60 Hz)
├──────────────────────────────┤
│ SPI bus + GPIO CS │ Manual CS for brightness control
│ Shift registers → LEDs │ CS hold time = LED intensity
└──────────────────────────────┘
Key difference from OLED: the SSD1306 has built-in GDDRAM (display memory). The BUSE matrix has none — if the driver stops refreshing, the display goes dark.
BUSE: Real-Time Constraints in a Display Driver
The BUSE driver must solve two timing problems:
1. Refresh rate: shift register displays need continuous SPI updates (~60 Hz minimum)
2. CS hold time: brightness is controlled by how long CS is held LOW — this requires microsecond precision
hrtimer fires (µs precision)
│
▼
workqueue: capture framebuffer (spinlock)
│
▼
Convert pixel data to SPI frame format
│
▼
SPI transfer (shift register data)
│
▼
GPIO CS LOW → hold for brightness → CS HIGH
Why not hardware CS? The SPI controller's automatic CS cannot hold for a precise duration. The driver uses gpio_set_value() with hrtimer for manual timing.
| Mechanism | Purpose |
|---|---|
hrtimer |
Microsecond-precision timing for CS and refresh |
workqueue |
Deferred SPI transfer (cannot do SPI in interrupt context) |
spinlock |
Protect framebuffer during capture |
BUSE vs OLED: Same Subsystem, Different Constraints
Both are fbdev drivers. Both register /dev/fbN. But the implementation differs:
| Aspect | SSD1306 OLED | BUSE LED Matrix |
|---|---|---|
| Bus | I2C (400 kHz) | SPI (MHz) |
| Display memory | Built-in GDDRAM | None — driver must refresh |
| Refresh | Deferred I/O (on write) | Continuous (hrtimer) |
| CS control | N/A (I2C uses addressing) | Manual GPIO (brightness) |
| Resolution | 128×64, 1bpp | 128×19, 1bpp |
| Timing | Relaxed | Real-time constraint |
Same user-space API: both work with cat /dev/urandom > /dev/fbN and the Pong game.
Tutorial: LDD BUSE — build a framebuffer driver for the SPI LED matrix with hrtimer refresh and manual CS timing.
Two Framebuffer Drivers, Two Buses, One API
I2C Bus 1 SPI Bus 0
│ │
┌────v────┐ ┌────v────┐
│ SSD1306 │ │ BUSE │
│ 0x3C │ │ LED │
│ (OLED) │ │ Matrix │
└────┬────┘ └────┬────┘
┌────v────┐ ┌────v────┐
│ fbdev │ │ fbdev │
│ deferred│ │ hrtimer │
│ I/O │ │ refresh │
└────┬────┘ └────┬────┘
/dev/fb1 /dev/fb2
└───────────┬───────────┘
Same user-space API:
mmap(), write(), ioctl()
Pong runs on both!
The fbdev subsystem abstracts away the bus, timing, and refresh mechanism. Applications just write pixels.
UIO — When a Full Driver Is Overkill
Sometimes you need direct hardware access but the device does not fit any standard subsystem. A full kernel driver is expensive and risky.
UIO (Userspace I/O) provides a middle ground:
- Minimal kernel component registers memory regions and IRQ lines
- User-space program uses
mmap()for direct register access - Interrupts delivered via blocking
read()on/dev/uioN - Can be configured entirely via Device Tree (no custom kernel code)

UIO Trade-offs
| Full Kernel Driver | UIO | User Space (i2c-dev) | |
|---|---|---|---|
| HW access | Direct (in kernel) | mmap (direct) | Via kernel (indirect) |
| IRQ handling | In kernel (fast) | Notification | Polling only |
| Dev effort | High | Medium | Low |
| Crash risk | Kernel panic | Process crash | Process crash |
| Use case | Standard subsystems | FPGA, custom IP | Prototyping |
If your device fits a standard subsystem (I2C, SPI, GPIO), use the subsystem. Do not reinvent it with UIO.
Quick Checks — Drivers
# 1. Does the device node exist?
ls -la /dev/mcp9808
ls /sys/class/hwmon/
# 2. Are file permissions correct?
stat /dev/mcp9808
# 3. Does probe log expected capabilities?
dmesg | grep mcp9808
# 4. Can a minimal test read one value?
cat /dev/mcp9808
# or
cat /sys/class/hwmon/hwmon0/temp1_input
If the node does not exist, go back to Block 1 (Device Tree). If it exists but reads fail, the driver has a bug.
The Full Path — DTS to Application
+---------------------+
| Device Tree (.dts) | "MCP9808 on I2C bus 1 at 0x18"
+---------+-----------+
v
+---------------------+
| Kernel parses DT | compatible = "mcp9808"
+---------+-----------+
v
+---------------------+
| Driver probe() | Registers /dev/mcp9808 + sysfs
+---------+-----------+
v
+---------------------+
| VFS routes calls | open/read/write/close
+---------+-----------+
v
+---------------------+
| Application reads | cat /dev/mcp9808 --> "26.7500"
+---------------------+
Every step must succeed. A break at any point means silence.
Mini Exercise — Where Does Each Belong?
For a temperature sensor, decide what belongs in each layer:
| Layer | Responsibility |
|---|---|
| Kernel driver | ? |
| User-space service | ? |
| Application UI | ? |
Justify each split briefly. Think about: who arbitrates the bus? Who formats the data? Who draws the graph?
Driver Lifecycle: From insmod to probe
insmod my_driver.ko
▼
module_init()
▼
platform_driver_register()
▼
Kernel scans Device Tree
▼
┌──────────────────────────┐
│ compatible string match? │
│ │──── No ──► Driver registered but idle
│ │ (waits for matching device)
└────────────┬─────────────┘
│ Yes
▼
.probe() called with device info
▼
Device appears in /dev or /sys
The kernel does not call probe() when you load the module. It calls probe() when it finds a matching Device Tree node. No match = no probe = no error = silence.
Try It Now: Module Info (5 min)
Inspect loaded kernel modules and their Device Tree bindings:
# List all loaded modules
lsmod
# Filter for sensor-related modules
lsmod | grep -E "mcp|i2c|spi"
# Get detailed info about a module
modinfo mcp9808
# Check if a driver bound to a device
ls /sys/bus/i2c/drivers/mcp9808/
Find the compatible string in the modinfo output. Does it match your Device Tree overlay?
Tutorial: MCP9808 Driver — Install and Load Theory: Section 5: Driver Lifecycle
Inside .probe(): What Can Go Wrong
probe() acquires resources in order. Each step can fail:
devm_kzalloc() → Memory for driver state
│
devm_ioremap_resource() → Map hardware registers
│
devm_request_irq() → Register interrupt handler
│
devm_gpiod_get() → Claim GPIO pins
│
Register with subsystem → /dev node or sysfs entry
devm_* (device-managed) variants automatically release resources when the device is removed or probe fails. Without devm_, you must manually free everything in .remove() and on every error path.
| Without devm | With devm |
|---|---|
kzalloc + kfree in .remove() |
devm_kzalloc — auto-freed |
request_irq + free_irq |
devm_request_irq — auto-freed |
| 5 error labels in probe | Clean return on failure |
Always use devm_* in new drivers — it eliminates an entire class of resource leak bugs.
Deferred Probing
"Driver loaded but no device appeared."
Sometimes probe() needs a resource (clock, regulator, GPIO) that is not ready yet — its provider driver has not probed yet.
Driver A .probe()
│
Needs clock from Driver B
│
Driver B not loaded yet
▼
Return -EPROBE_DEFER
▼
Kernel queues Driver A for retry
│
(later: Driver B loads, provides clock)
▼
Kernel retries Driver A .probe() → success
Common source of confusion: dmesg shows the probe attempt, but the device appears seconds later after dependencies resolve.
Interrupts in Drivers
Instead of polling, let the hardware notify you:
devm_request_threaded_irq(dev, irq,
top_half, /* Fast: runs with IRQs disabled, must not sleep */
bottom_half, /* Threaded: can sleep, do I2C/SPI reads here */
IRQF_ONESHOT,
"my_sensor", data);
Top-half vs bottom-half:
| Top Half | Bottom Half (threaded) | |
|---|---|---|
| Context | Interrupt (atomic) | Kernel thread (can sleep) |
| Duration | Microseconds | Milliseconds OK |
| Can do | Set flag, wake thread | I2C read, process data |
| Cannot do | Sleep, allocate, I2C/SPI | — |
Device tree specifies the interrupt line:
sensor@18 {
interrupt-parent = <&gpio>;
interrupts = <17 IRQ_TYPE_EDGE_FALLING>; /* GPIO 17, falling edge */
};
Edge trigger: fires once per transition. Level trigger: fires as long as the line is active.
DMA: Let the Hardware Do the Work
CPU-driven transfer:
CPU reads byte from SPI → stores in RAM → reads next byte → stores → ...
CPU is 100% busy during the entire transfer.
DMA-driven transfer:
CPU configures DMA: source=SPI, dest=RAM, length=4096
│
DMA engine transfers bytes autonomously
│ ← CPU is FREE
DMA fires interrupt: "transfer complete"
│
CPU processes the buffer
| CPU-Driven | DMA | |
|---|---|---|
| CPU usage | 100% during transfer | Near 0% during transfer |
| Throughput | Limited by CPU speed | Limited by bus speed |
| Latency | Low (immediate start) | Higher (setup overhead) |
| Use when | Small transfers (<64 bytes) | Large transfers (>256 bytes) |
At SPI speeds of 10+ MHz, a 4 KB transfer takes ~3 ms of CPU time without DMA. With DMA, that CPU time is available for your control loop.
DMA in Device Tree
DMA channels are described in the SoC's base device tree — you inherit them, not write them:
/* In the SoC base .dtsi (already provided by vendor): */
spi0: spi@7e204000 {
dma-names = "tx", "rx";
dmas = <&dma 6>, <&dma 7>; /* DMA channels 6 (TX) and 7 (RX) */
};
| Property | Meaning |
|---|---|
dmas |
References to DMA controller + channel number |
dma-names |
Logical names the driver uses to request channels |
The SPI controller's DMA channels are described in the SoC's base device tree — you do not write this, you inherit it.
Your driver requests DMA by name:
If the DMA channel is not described in Device Tree, the driver falls back to CPU-driven (PIO) mode — slower but functional.
Block 2 Summary
- Kernel/user space boundary is enforced by CPU hardware
- VFS makes everything a file -- drivers plug into this framework
- Three interfaces: sysfs (simple), read/write (streams), ioctl (structured)
- Driver types map to kernel subsystems -- choose the right one
- Default: put logic in user space; move to kernel only when required
- UIO bridges the gap for custom hardware that needs direct access
Block 2 — IIO & DMA
"The kernel already wrote your sensor driver"
The Problem with Custom Drivers
You just learned to write a custom character device driver. For every sensor you would need to:
- Define sysfs attributes manually
- Implement your own buffering for high-rate data
- Add timestamps yourself
- Write custom userspace tools
- Maintain it across kernel versions
What if the kernel had a standard framework for sensors?
It does: IIO (Industrial I/O).
What Is IIO?
The kernel's standard framework for sensors — accelerometers, gyroscopes, ADCs, pressure, light, humidity.
drivers/iio/
├── accel/ ← Accelerometers (ADXL345, MMA8452, ...)
├── adc/ ← ADCs (MCP3008, ADS1115, INA219, ...)
├── gyro/ ← Gyroscopes (ITG3200, ...)
├── imu/ ← IMUs (BMI160, LSM6DSO, MPU6050, ...)
├── pressure/ ← Pressure (BMP280, MS5611, ...)
├── light/ ← Light (TSL2561, BH1750, ...)
└── humidity/ ← Humidity (SHT31, HDC1080, ...)
Hundreds of sensors supported out of the box. No custom driver needed.
IIO Architecture
+-------------------+
| Sensor Hardware | (BMI160, MCP3008, BMP280, ...)
+--------+----------+
| SPI / I2C
+--------v----------+
| IIO Core | struct iio_dev, iio_chan_spec
+--+-------+--------+
| | |
v v v
sysfs chardev buffer
/sys/ /dev/ kfifo
bus/iio iio:devN + trigger
One framework gives you: sysfs channels, buffered capture, triggers, timestamps, DMA, userspace tools.
IIO Sysfs — Polled Mode
Each sensor channel becomes a standard sysfs file:
/sys/bus/iio/devices/iio:device0/
├── name # "bmi160"
├── in_accel_x_raw # Raw value
├── in_accel_scale # Multiplier
├── in_anglvel_x_raw # Gyroscope
├── sampling_frequency # Sample rate
└── ...
# Read one axis (triggers one SPI transaction):
cat /sys/bus/iio/devices/iio:device0/in_accel_x_raw
# → 16384
# Convert: physical = raw × scale
# 16384 × 0.000598 = 9.8 m/s² (1g)
IIO vs Custom Driver — The Trade-off
| Aspect | Custom chardev | Mainline IIO |
|---|---|---|
| App API | You design it | Standard channels |
| Buffering | You write it | kfifo + triggers |
| Timestamps | You add them | Automatic |
| Tool support | Custom scripts | iio_readdev, libiio |
| Maintenance | You vs kernel updates | Upstream maintains |
| Sensor swap | Rewrite driver | Change compatible string |
| Custom features | Full control | IIO model only |
Use IIO when standard sensor data is enough. Use custom when you need fused outputs, strict RT, or proprietary features.
Why Buffered Mode Exists
The problem: Reading one sensor sample through sysfs takes ~0.5 ms (system call, SPI transaction, string formatting). At 200 Hz, that's 100 ms of CPU per second just reading one axis. For 6 axes, the CPU is 60% busy doing nothing but sensor reads.
Polled sysfs at 200 Hz:
App: read()→ Kernel: SPI tx/rx → format → copy_to_user → App: parse
└── 0.5 ms per read ──────────────────────────────┘
× 6 axes × 200 Hz = 600 reads/s × 0.5 ms = 300 ms/s = 30% CPU
(plus scheduling overhead → closer to 60%)
The solution: Don't read one sample at a time. Let the kernel collect samples into a buffer, then read them all at once.
This is exactly what IIO Buffered Mode does.
IIO Buffered Mode — The Concept
Instead of poll-per-sample, a trigger fires at the desired rate. The driver reads the sensor, pushes data into a kernel FIFO (kfifo), and userspace reads batches:
Polled (BAD at high rates): Buffered (GOOD):
User ──► Kernel ──► SPI Trigger fires ──► Driver reads SPI
User ──► Kernel ──► SPI │
User ──► Kernel ──► SPI ▼
User ──► Kernel ──► SPI Data → kfifo (kernel ring buffer)
...every sample │
▼
200 context switches/s User reads 64 samples at once
3 context switches/s
Key components:
| Component | What it is | Analogy |
|---|---|---|
| Trigger | Event that causes a sample read | An alarm clock |
| kfifo | Kernel ring buffer holding samples | A mailbox |
| Scan elements | Which channels to capture | The order form |
/dev/iio:device0 |
Binary data stream to userspace | The pickup window |
IIO Buffered Mode — Step by Step
# 1. Choose which channels to capture
echo 1 > scan_elements/in_accel_x_en
echo 1 > scan_elements/in_accel_y_en
echo 1 > scan_elements/in_accel_z_en
echo 1 > scan_elements/in_timestamp_en
Each enabled channel adds bytes to each sample. The scan elements also tell you the binary format — type, bit width, endianness, sign:
Trigger types:
| Trigger | Source | When to use |
|---|---|---|
bmi160-dev0 |
Sensor data-ready IRQ pin | Best: sample exactly when data is ready |
hrtimer-0 |
Kernel high-res timer | When sensor has no IRQ or you want a custom rate |
IIO Buffered Mode — Start Streaming
# 3. Configure rate and buffer size
echo 200 > sampling_frequency # 200 Hz ODR (sensor's internal rate)
echo 256 > buffer/length # kernel holds up to 256 samples
Buffer length is how many samples the kernel can hold before the oldest is overwritten. If userspace doesn't read fast enough → data loss. 256 at 200 Hz = 1.28 seconds of slack.
Now the trigger fires at 200 Hz. Each fire: driver reads 3 axes + timestamp via SPI, pushes ~14 bytes to kfifo.
Each read returns one or more complete samples. The data is binary — not human-readable strings like sysfs. Your application must parse the bytes according to the scan element types.
IIO Buffered Mode — Data Flow
┌─────────────────────────────────────────────┐
│ KERNEL │
│ │
Trigger fires ──► │ Driver: spi_sync_transfer(...) │
(IRQ or timer) │ ↓ │
│ Raw bytes from sensor (6 bytes accel) │
│ ↓ │
│ iio_push_to_buffers_with_timestamp() │
│ ↓ │
│ ┌─────────────────────────────────┐ │
│ │ kfifo (ring buffer in kernel) │ │
│ │ [sample][sample][sample][...] │ │
│ │ 14 bytes each (3×s16 + s64 ts) │ │
│ └────────────────┬────────────────┘ │
│ │ │
└───────────────────┼─────────────────────────┘
│
▼
┌───────────────────────────────────────────┐
│ USERSPACE │
│ read(/dev/iio:device0, buf, 14*64) │
│ → 64 samples in one syscall │
│ → parse: x = *(int16_t*)(buf + 0) │
│ y = *(int16_t*)(buf + 2) │
│ z = *(int16_t*)(buf + 4) │
│ ts = *(int64_t*)(buf + 8) │
└───────────────────────────────────────────┘
Why timestamps matter: Each sample has a kernel timestamp (nanoseconds since boot). This lets you calculate the exact sample interval — critical for sensor fusion (complementary filter, Kalman) and jitter measurement.
DMA: Let Hardware Move the Data
Now consider what happens inside that SPI transaction. Without DMA, the CPU handles every byte:
PIO (Programmed I/O) — CPU does all the work:
CPU SPI Controller Memory
│ │ │
├── write TX byte ────────►│ │
│ ├── shift out ──► │
│ ◄── shift in ─── │
◄── read RX byte ─────────┤ │
├── store to RAM ──────────────────────────────►│
│ │
repeat for EVERY byte...
For 2400 bytes: CPU executes ~4800 instructions, 100% busy for ~0.5ms
With DMA (Direct Memory Access):
DMA — hardware moves data while CPU is free:
CPU DMA Controller SPI Controller Memory
│ │ │ │
├── "move 2400 bytes" ────►│ │ │
│ (one instruction) │ │ │
│ ├── drive SPI ────►│ │
│ CPU is FREE │ ├── shift ──► │
│ (runs other code) │◄── data ────────┤ │
│ ├── store ───────────────────────►│
│ │ ... 2400 times ... │
│◄── IRQ: "done!" ────────┤ │
│ │
CPU cost: 2 instructions (start + acknowledge IRQ)
DMA — When It Matters
| Transfer | Size | PIO time | DMA time | CPU freed |
|---|---|---|---|---|
| Read 1 register | 2 bytes | 2 µs | N/A (overhead not worth it) | — |
| Read 6-axis accel | 12 bytes | 12 µs | N/A | — |
| Read sensor FIFO | 2400 bytes | 500 µs | ~50 µs setup + hardware | ~450 µs |
| Update SPI display | 153,600 bytes | 30 ms | ~1 ms setup + hardware | ~29 ms |
The threshold on the Pi's SPI controller is ~96 bytes. Below that, PIO is faster (DMA setup overhead dominates). Above that, DMA wins dramatically.
The SPI subsystem makes this decision automatically — your driver code doesn't change:
struct spi_transfer t = {
.rx_buf = buf,
.len = 2400, /* Above 96 bytes → SPI core uses DMA */
};
spi_sync_transfer(spi, &t, 1); /* Same API for PIO and DMA */
You don't ask for DMA. You don't configure DMA. The SPI core sees len >= threshold and does it for you.
Combining IIO + DMA — The Full Path
The highest-performance sensor path combines all three techniques:
┌─────────────┐
│ BMI160 IMU │ 1. Sensor samples internally at ODR (200 Hz)
│ │ Accumulates in hardware FIFO (1024 bytes)
│ ┌───────┐ │
│ │ FIFO │ │ 2. When FIFO has 200 samples → watermark IRQ fires
│ │ 1024B │──┼──── INT pin → GPIO → kernel IRQ
│ └───────┘ │
└─────────────┘
│
▼
IIO trigger handler fires
│
▼
Driver: spi_sync() reads 2400 bytes from FIFO
(200 samples × 12 bytes each)
│
▼
len=2400 > 96 → SPI core uses DMA automatically
CPU is free during transfer
│
▼
Driver: iio_push_to_buffers_with_timestamp() × 200
Data flows into kfifo
│
▼
Userspace: read(/dev/iio:device0) gets 200 samples at once
One syscall. One context switch. 200 samples.
Performance Comparison
| Approach | How it works | CPU per sample | 200 Hz, 6-axis |
|---|---|---|---|
| Polled sysfs | User reads /sys/... per axis per sample |
~0.5 ms (full syscall + SPI) | 60% CPU |
| IIO buffered + PIO | Trigger → driver PIO SPI → kfifo → batch read | ~12 µs (IRQ + SPI PIO) | 2.4% CPU |
| IIO + FIFO + DMA | Watermark IRQ → burst DMA → kfifo → batch read | ~0.5 µs (IRQ + DMA amortized) | 0.1% CPU |
The difference between polled sysfs and IIO+DMA is 600×. That's the difference between "CPU too busy for display rendering" and "sensor capture is invisible to the system."
This is why IIO exists. It's not just a nicer API — it's a fundamentally different data path that makes high-rate sensing practical on embedded Linux.
FAQ — Buffered Mode and DMA
Q: What happens if userspace doesn't read fast enough?
The kfifo overwrites the oldest samples. You lose data but the system doesn't crash or block. Monitor /sys/bus/iio/devices/iio:device0/buffer/data_available to check for overruns.
Q: Can I use buffered mode without a hardware IRQ?
Yes — use hrtimer trigger: echo iio_hrtimer_trigger > /sys/bus/iio/devices/trigger0/name. The kernel timer fires at the configured rate. Less precise than hardware IRQ but works with any sensor.
Q: How do I know the binary format of buffered data?
Read scan_elements/in_accel_x_type — it tells you le:s16/16>>0 (little-endian, signed 16-bit, 16 bits storage, 0 shift). Parse accordingly in your C/Python code.
Q: Does DMA work with I2C too? Yes, but the benefit is smaller. I2C is already slow (100-400 kHz) and transfers are typically small (2-4 bytes). DMA helps most with SPI bulk transfers (displays, sensor FIFOs).
Q: What's ODR? Output Data Rate — the sensor's internal sample rate. For BMI160: configurable from 12.5 Hz to 1600 Hz. The sensor samples at ODR regardless of how fast you read — if you poll slower than ODR, you miss samples. Buffered mode with FIFO solves this.
Writing a Custom IIO Driver
When the kernel has no IIO driver for your hardware:
// 1. Define channels
static const struct iio_chan_spec my_channels[] = {
{ .type = IIO_VOLTAGE, .channel = 0,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW), ... },
};
// 2. Implement read_raw
static int my_read_raw(struct iio_dev *dev, ...) {
// SPI/I2C transaction to read sensor
}
// 3. Register
indio_dev = iio_device_alloc(...);
indio_dev->channels = my_channels;
iio_device_register(indio_dev);
~200 lines vs ~530 for a custom chardev. You get buffers, triggers, timestamps, tools for free.
Block 3 Summary
- IIO is the kernel's standard sensor framework — use it before writing custom drivers
- Polled mode:
cat in_accel_x_raw— one SPI transaction per read - Buffered mode: trigger + kfifo +
/dev/iio:deviceN— high-rate streaming - DMA: automatic for SPI transfers above ~96 bytes — zero driver code
- Custom IIO driver: ~200 lines gives you the full IIO toolchain
- Decision: IIO for standard data, custom chardev for specific ABI/RT/fusion needs
Key Takeaways — All Blocks
- Device Tree is the hardware contract for Linux -- without it, drivers cannot find devices
- The
compatiblestring is the single most important field -- one typo and the device is invisible - Overlays let you customize hardware config without rebuilding the kernel
- Drivers translate hardware into consistent user-space interfaces (
/dev, sysfs) - Design interfaces deliberately -- sysfs for config, read/write for streams, ioctl for control
- Put logic in user space unless kernel execution is truly required
- IIO replaces custom sensor drivers for standard use cases — buffers, timestamps, DMA included
- DMA frees the CPU for large transfers — the SPI subsystem enables it automatically
Hands-On Next — Lab 4
Today's lab walks through all three driver types from the lecture:
Tutorial: MCP9808 Driver (60 min)
Build a character device driver step by step — from skeleton module to working /dev/mcp9808. Write a Device Tree overlay, compile it, load the driver, read temperature.
Tutorial: OLED Framebuffer Driver (45 min)
Turn the SSD1306 OLED into /dev/fb1. Same I2C bus as MCP9808, different kernel subsystem (fbdev instead of cdev).
Tutorial: Pong on Framebuffer (30 min)
Write a simple game that draws directly to /dev/fb1 using mmap().
Extension: LDD BUSE (60 min) Build a user-space block device driver — create a virtual disk, format it, mount it. See the block I/O subsystem from the inside.
Extension: IIO Buffered Capture + Custom IIO Driver — use the mainline BMI160 driver for high-rate capture, or write your own IIO driver.