Device Tree and Drivers
Goal: Understand how Linux describes hardware without hard-coding board details into drivers, and how kernel drivers expose that hardware to applications.
Related Tutorials
For hands-on practice, see: MCP9808 Driver | BMI160 SPI Driver | BUSE Driver | Enable I2C
Your sensor is wired correctly, the driver is present, but Linux never sees it. Most often the issue is not the driver code; it is the hardware description.
Application works on one board but fails on another despite identical user-space code. The break is usually in driver interface assumptions, not app logic.
1. Why Device Tree Exists
On a microcontroller, you hard-code hardware details — pin numbers, bus addresses, clock speeds — directly into your firmware. This works when you ship one product with one board. Linux, however, runs on thousands of different boards, each with different hardware configurations. If every board's wiring were hard-coded into every driver, the kernel would be an unmaintainable mess of #ifdef blocks.
Device Tree solves this by separating what hardware exists from how to use it. The driver contains the logic for talking to a sensor. The Device Tree tells the driver where that sensor is connected — which bus, which address, which interrupt line. This separation means a single I2C temperature driver works on every board that has an I2C bus, as long as the Device Tree correctly describes the wiring.
In practice, Device Tree provides three things:
- Hardware description outside driver source — the kernel binary stays generic; board-specific details live in a separate data file
- Separation between board wiring and driver logic — changing a sensor's I2C address does not require recompiling the kernel
- Reusable drivers across multiple products — the same MCP9808 driver works on a Raspberry Pi, a BeagleBone, and a custom industrial board
2. What It Contains
A Device Tree is a structured description of the hardware on a specific board. Think of it as a manifest that tells the kernel: "here are all the buses on this board, here are the devices connected to each bus, and here are the addresses and pins each device uses." Without this description, the kernel has no way to discover most embedded hardware — unlike USB or PCIe, I2C and SPI devices cannot identify themselves.
Device Tree describes:
- Buses (I2C, SPI, UART) — which bus controllers exist and how they are configured
- Devices on those buses — what is connected, at which address
- GPIO pins, interrupts, and addresses — the physical wiring that drivers need to communicate with the hardware
Annotated DTS Example (MCP9808 Overlay)
This is the overlay used in the MCP9808 driver tutorial:
/dts-v1/; // DTS format version
/plugin/; // This is an overlay, not a base DT
/ {
compatible = "brcm,bcm2835"; // Target: any Raspberry Pi
fragment@0 {
target = <&i2c1>; // Attach to I2C bus 1 (GPIO 2/3)
__overlay__ {
#address-cells = <1>; // I2C addresses are 1 cell (byte)
#size-cells = <0>; // No memory-mapped region
mcp9808@18 { // Node name @ I2C address
compatible = "mcp9808"; // Must match driver's ID table
reg = <0x18>; // I2C address (from datasheet)
};
};
};
};
Key fields:
- target = <&i2c1> -- which bus to add the device to
- compatible = "mcp9808" -- MUST match the driver code exactly
- reg = <0x18> -- the device's I2C address (verify with i2cdetect -y 1)
3. Matching Flow (How Driver Binding Happens)
The connection between a Device Tree node and a kernel driver happens through a process called binding. The kernel does not magically know that your MCP9808 node should use the MCP9808 driver — it matches them using the compatible string. This string acts as a contract: the Device Tree declares "I have a device of type X," and the kernel searches for a driver that says "I know how to handle type X." If the strings match, the kernel calls the driver's probe() function with the node's parameters.
Understanding this flow explains most Device Tree debugging. If probe() never runs, the match failed. If probe() runs but returns an error, the match succeeded but the parameters (address, pins) are wrong.
- Kernel parses device tree at boot
- Bus node and child device nodes are created in the kernel's device model
- Driver
compatiblestring is matched against each node'scompatibleproperty probe()runs with that node's parameters (bus address, interrupt, GPIOs)
If compatible, bus address, or pin config is wrong, probe never succeeds — and your device silently does not appear.
graph TD
A[Kernel parses Device Tree] --> B[Creates bus nodes]
B --> C[Creates child device nodes]
C --> D{Driver with matching<br>'compatible' string?}
D -->|Yes| E[Call driver probe]
D -->|No| F[Device remains unbound]
E --> G{probe succeeds?}
G -->|Yes| H[Device node in /dev or sysfs]
G -->|No| I[Error in dmesg]
The compatible string is the key: it must match exactly between the Device Tree node and the driver's i2c_device_id[] or of_match_table[].
4. Overlays
The base Device Tree describes the SoC and board as they ship from the manufacturer. But what happens when you plug in a custom sensor board, enable an interface that is disabled by default, or change a pin assignment? Rebuilding the entire Device Tree for each hardware change is impractical — especially during development when you are iterating on the wiring.
Device Tree Overlays solve this by letting you patch the base tree at boot time. An overlay is a small fragment that adds, modifies, or disables nodes in the base tree. On a Raspberry Pi, you add overlays in config.txt; the bootloader merges them with the base tree before handing it to the kernel. This means you can ship a standard base image and customize the hardware configuration per deployment — one unit with an MCP9808 sensor, another with a BMI160 IMU — without different kernel builds.
Overlays allow:
- Adding devices without rebuilding the kernel
- Dynamic configuration of hardware features
5. Common Failure Modes
- wrong I2C address in node
- missing or incorrect
compatible - pinmux conflict with another peripheral
- overlay loaded but not applied to active base DT
What Errors Look Like in Practice
Wrong I2C address:
Error -6 isENXIO (no device at that address). Fix: check reg value matches i2cdetect output.
Missing compatible string:
The kernel never tried to bind because no driver matched thecompatible string.
Pinmux conflict:
Another peripheral is using the same GPIO pins. Check/boot/firmware/config.txt for conflicting overlays.
6. Device Tree Quick Checks
dmesg | grep -i -E "i2c|spi|probe|dt"- verify
/proc/device-treenodes exist - verify device appears on bus scan (
i2cdetect, etc.) - confirm matching driver module is loaded
7. Kernel vs User Space
On a microcontroller, your code accesses hardware directly — you write to a register, and the peripheral responds. On Linux, a strict boundary separates the software that can touch hardware (kernel space) from the software that runs your application (user space). Drivers sit exactly on this boundary: they translate between hardware registers and the clean, stable interfaces that applications use.
This separation exists because Linux runs multiple programs simultaneously. If any application could write to any hardware register, a bug in one program could corrupt a device that another program depends on — or crash the entire system. Drivers provide controlled access: your application asks the kernel (through a system call or file operation), and the kernel's driver performs the hardware operation safely.
- Kernel: drivers, scheduling, hardware access
- User space: applications, services, tools
Drivers act as the boundary between these two worlds.
graph TB
subgraph "User Space"
APP[Application]
LIB["Libraries: glibc, smbus, opencv"]
SYSCALL[System Calls]
end
subgraph "Kernel Space"
VFS["VFS / sysfs / ioctl"]
DEVDRV["Device Driver (e.g. sensor driver)"]
BUSCORE["Bus Subsystem Core: I2C / SPI / GPIO"]
CTRLDRV["Bus Controller Driver"]
end
subgraph "Hardware"
BUS["I2C / SPI Physical Bus"]
DEV["Physical Device: sensor, display, motor"]
end
APP --> LIB
LIB --> SYSCALL
SYSCALL --> VFS
VFS --> DEVDRV
DEVDRV --> BUSCORE
BUSCORE --> CTRLDRV
CTRLDRV --> BUS
BUS --> DEV
Why This Separation Exists
The boundary between user space and kernel space is not just a design decision — it is enforced by hardware.
- The CPU provides privilege levels.
- The MMU enforces memory isolation.
- Kernel memory is marked as privileged.
- Hardware registers are accessible only in kernel mode.
User programs run in unprivileged mode. They cannot:
- Access hardware registers
- Modify kernel memory
- Execute privileged instructions
If they try, the CPU blocks them.
So how does user space access hardware?
Through controlled entry points:
- System calls
/devdevice filessysfsattributesioctl()
When a system call is made, the CPU switches from user mode to kernel mode. The kernel performs the operation safely and returns the result.
This separation provides:
- Security
- Stability
- Process isolation
- Fault containment
One crashing application should not crash the entire system.
8. How "Everything Is a File" Works: VFS
The Virtual File System (VFS) is the abstraction layer that unifies access to:
- Disk files
- Device nodes (
/dev/i2c-1) - Kernel data (
/proc/cpuinfo) - Driver attributes (
/sys/class/...)
All use the same system calls:
The Virtual File System (VFS) is the kernel layer that makes /dev/i2c-1, /proc/cpuinfo, and /home/user/file.txt all accessible through the same open/read/write/close system calls — even though they come from completely different sources (a hardware driver, a kernel data structure, a disk filesystem).
| Concept | What It Is | Analogy |
|---|---|---|
| inode | Metadata about a file (size, permissions, data location) | File's "passport" |
| dentry | Links a filename to its inode in the directory tree | Directory entry — name → passport |
| superblock | Describes a mounted filesystem (block size, root inode) | Volume label / table of contents |
| file object | An open file descriptor — tracks current position, mode | Your current "reading session" |
When you:
VFS routes theread() call to the I2C character driver — not to a disk filesystem.
When you:
VFS routes it to a kernel function that formats CPU information on the fly. This is the power of the abstraction: user-space code does not know (or care) whether it is reading a file, a sensor, or kernel information.This is why drivers in this course implement read/write/open/release callbacks — they are plugging into the VFS framework.
9. Common Interfaces
Once a driver is loaded, applications need a way to communicate with it. Linux provides several interface mechanisms, each suited to different kinds of interaction. Choosing the right interface for your driver is an important design decision — it affects how easy the driver is to use from applications, how portable the interface is across kernel versions, and how discoverable it is for debugging.
Linux drivers typically expose functionality through:
/devdevice nodes — file-based interface for data streams (read bytes from a sensor, write bytes to an actuator)sysfsfor configuration and state — one value per file, human-readable, easy to inspect from the shellioctl()for structured control operations — when you need to pass complex commands that do not fit the read/write model
Think of them as different levels of semantic richness.
| Interface | Best For | Characteristics |
|---|---|---|
/dev + read/write |
Data streams | Simple, byte-oriented |
sysfs |
Configuration/state | One value per file, text-based |
ioctl() |
Complex control | Structured commands |
10. Driver Types
The Linux kernel organizes drivers into subsystems based on the type of hardware they manage. This matters because each subsystem provides a standard interface and infrastructure that your driver plugs into. Writing a character driver for an I2C sensor is very different from writing a network driver for an Ethernet controller — not because the C code is fundamentally different, but because the kernel subsystem handles buffering, queuing, and user-space interfaces differently for each type.
Choosing the correct subsystem is more important than writing clever code. A temperature sensor implemented as a character device when it should use the hwmon (hardware monitoring) subsystem will work, but it will not integrate with standard Linux temperature monitoring tools, and every application will need custom code to read from it.
- Character drivers (e.g., I2C, UART) — byte-stream access via
/dev/ - Input drivers (buttons, touch) — event-based access via
/dev/input/ - Framebuffer/DRM drivers (display) — pixel buffer access
Different hardware maps to different kernel subsystems.
| Type | Interface | Examples | Data Pattern |
|---|---|---|---|
| Character device | /dev/xxx |
I2C sensors, UART | Byte stream |
| Block device | /dev/sdX |
SD, eMMC, NVMe | Fixed-size blocks |
| Network device | eth0, wlan0 |
Ethernet, WiFi | Packets |
| Input device | /dev/input/eventX |
Buttons, touchscreen | Events |
| Framebuffer/DRM | /dev/fbX, /dev/dri |
Displays | Pixel buffers |
Choosing the correct subsystem matters more than writing clever code.
11. Interface Design Trade-offs
The interface you choose for your driver is the contract between kernel space and user space. Changing it later means breaking every application that depends on it. This is why interface design deserves careful thought — the right choice makes the driver easy to use and maintain for years; the wrong choice creates compatibility headaches that compound with every product revision.
Interface choice affects portability, API stability, security, and long-term maintainability.
sysfs
Sysfs is the simplest driver interface: each attribute is a text file containing a single value. You can read a sensor from the shell with cat /sys/class/hwmon/hwmon0/temp1_input and configure a setting with echo. This makes sysfs ideal for configuration, status, and single-value readings. However, sysfs is not designed for streaming data or complex protocols — if you need to transfer a block of sensor samples, sysfs will be awkward and slow.
ioctl
When your driver needs to accept structured commands — "set sampling rate to 200 Hz and enable FIFO mode" — ioctl() provides a way to pass typed data structures between user space and kernel space. The flexibility is powerful, but it comes at a cost: ioctl interfaces are hard to version correctly, hard to document, and invisible to shell tools. A poorly designed ioctl interface becomes a maintenance burden that only the original developer can use.
read/write
The read()/write() interface treats the device like a file: you read bytes from it and write bytes to it. This is the natural fit for stream-oriented devices (serial ports, sensor data streams). The limitation is that there is no structure — the bytes could mean anything, and the application must know the protocol. For a simple sensor that returns a temperature as ASCII text, this works well. For a complex device with multiple channels and configuration modes, you need to layer a protocol on top.
Design based on product architecture — not prototype convenience.
12. User-space vs Kernel-space Responsibility
Default rule:
Put logic in user space unless kernel execution is required.
Move logic into kernel only if you need: - Interrupt-level timing - Shared kernel infrastructure - Arbitration of hardware resources - Standard subsystem integration
Kernel bugs can panic the system but User-space bugs crash only the process. Moving too much into kernel space increases risk and debug cost.
Decision Model: Kernel or User Space?
graph TD
A[Need hardware access?] -->|No| B[User space]
A -->|Yes| C{Need interrupt-level timing?}
C -->|Yes| D[Kernel driver]
C -->|No| E{Existing kernel interface?}
E -->|Yes — sysfs, i2c-dev, spidev| F[User space via existing driver]
E -->|No| G[Write a kernel driver]
Code Comparison: Temperature Reading
Kernel driver approach (via /dev/mcp9808):
User-space approach (via smbus library):
import smbus
bus = smbus.SMBus(1)
raw = bus.read_word_data(0x18, 0x05)
raw = ((raw << 8) & 0xFF00) + (raw >> 8)
temp = (raw & 0x0FFF) / 16.0
print(f"{temp:.4f}")
Both produce the same result. The kernel driver approach is simpler for applications but requires writing kernel code. The user-space approach is faster to develop but puts I2C protocol knowledge in every application.
13. UIO: When a Full Kernel Driver Is Overkill
Sometimes you need direct hardware access (memory-mapped registers, interrupts) but the device doesn't fit any standard kernel subsystem. Writing a full kernel driver is expensive and risky — every kernel bug can crash the entire system.
UIO (Userspace I/O) provides a middle ground:
- A minimal kernel component registers memory regions and IRQ lines
- The user-space program uses
mmap()to access device registers directly — no context-switch per read/write - Interrupts are delivered via a blocking
read()on/dev/uioN - The generic UIO driver (
generic-uio) can be configured entirely via Device Tree — no custom kernel code needed
Trade-offs
| Full Kernel Driver | UIO | Pure User Space (spidev, i2c-dev) | |
|---|---|---|---|
| Hardware access | Direct (in kernel) | mmap (direct from user space) | Via kernel driver (indirect) |
| IRQ handling | In kernel (fast) | Notification to user space | Polling only |
| Development effort | High | Medium | Low |
| Crash risk | Kernel panic possible | User-space crash only | User-space crash only |
| Use case | Standard subsystems (I2C, SPI, FB) | FPGA, custom IP blocks, industrial I/O | Prototyping, simple sensors |
Note
UIO is common for FPGA register access in industrial embedded systems — the FPGA exposes a memory-mapped register block, and UIO lets user-space software access it directly. If your device fits a standard subsystem (I2C, SPI, GPIO), use the subsystem — don't reinvent it with UIO.
14. Driver Quick Checks
- does
/dev/...or sysfs node exist? - are file permissions correct?
- does probe log expected capabilities?
- can a minimal test tool perform one transaction?
Mini Exercise 1: Device Tree
An accelerometer (LIS3DH) is connected to I2C bus 1 at address 0x19. Complete the Device Tree overlay node:
__overlay__ {
#address-cells = <1>;
#size-cells = <0>;
________@____ {
compatible = "________";
reg = <____>;
};
};
Then list two reasons why probe() might still fail even with a correct Device Tree entry.
Mini Exercise 2: Driver Architecture
For a temperature sensor, decide what belongs in: - driver - user-space service - application UI
Justify each split briefly.
Key Takeaways
- Device Tree is the hardware contract for Linux.
- Drivers rely on it to find and configure devices.
- Overlays make changes easier and safer.
- Drivers translate hardware behavior into consistent user-space interfaces.
- sysfs and /dev are the main access points.
- Good driver design simplifies application code.
Hands-On
Try this in practice: Tutorial: MCP9808 Driver — write a Device Tree overlay and bind it to a real driver. Tutorial: BUSE Driver -- write real kernel drivers for I2C and SPI devices.