Skip to content

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:

open()   read()   write()   close()
+------------------+------------------+------------------+
|   /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

center

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:

cat /sys/class/hwmon/hwmon0/temp1_input
# 26750  (millidegrees Celsius)

ioctl example: "set sampling rate to 200 Hz and enable FIFO mode" -- one structured call instead of writing multiple files.


Device Driver Architecture

center

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


Driver Types

center

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:

cat /dev/mcp9808
# 26.7500

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.

# After loading the driver:
cat /dev/urandom > /dev/fb1   # Random noise on the OLED

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)

center


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.

# See deferred probes:
cat /sys/kernel/debug/devices_deferred

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:

dma_chan = dma_request_chan(dev, "rx");  /* Kernel looks up "rx" in DT */

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:

cat scan_elements/in_accel_x_type
# → le:s16/16>>0    (little-endian signed 16-bit, no shift)
# 2. Select a trigger — what causes each sample read
echo bmi160-dev0 > trigger/current_trigger

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.

# 4. Start streaming
echo 1 > buffer/enable

Now the trigger fires at 200 Hz. Each fire: driver reads 3 axes + timestamp via SPI, pushes ~14 bytes to kfifo.

# 5. Read binary data (raw bytes — not text!)
cat /dev/iio:device0 | hexdump -C

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 compatible string 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.