How Embedded Linux Drivers Connect Hardware to Applications
Kernel modules provide a mechanism for adding code to a running Linux kernel. In embedded Linux systems, this mechanism is used primarily to support hardware through drivers.
While kernel modules describe how code is delivered into the kernel, drivers define why that code exists. A driver is the kernel’s representation of a hardware device and the boundary through which all interaction with that device occurs. Understanding how drivers are loaded, matched to hardware, and exposed to user space is essential for bringing up and maintaining embedded Linux systems.
This
article focuses on how drivers fit into the kernel architecture, how
they are activated by hardware description, and how they ultimately
become visible to user space.
2. Drivers as Kernel Modules
In embedded Linux systems, most drivers are implemented as kernel modules. This allows hardware support to be included only when required and omitted when not needed.
When a driver is built as a module, it follows the kernel module lifecycle. Its code is inserted into the kernel, initialized, and made available for hardware binding. Once loaded, the driver executes with the same privileges as built-in kernel code and is subject to the same constraints.
Loading a driver module does not mean that hardware is present or usable. It only means that the kernel now has code capable of managing a specific type of hardware if such hardware is discovered.
Fig.1 Driver code delivered into the kernel through the module mechanism
3. How Drivers Are Matched to Hardware
Linux does not probe embedded hardware arbitrarily. Instead, hardware is described explicitly and drivers are matched based on identifiers.
In embedded systems, this matching is typically driven by the Device Tree. Each hardware node describes a device and includes a compatible string that identifies the hardware type. Drivers declare which compatible strings they support.
During kernel initialization or when a driver module is loaded, the kernel compares the Device Tree entries against available drivers. When a match is found, the driver’s probe function is invoked.
This mechanism ensures that drivers are only activated for hardware that is expected to exist and avoids unsafe probing of buses or registers.
Fig.2 Driver matching using Device Tree compatible strings
4.
From Driver Probe to an Initialized Device
When a driver probes a device, it attempts to take ownership of the hardware described in the Device Tree. This typically involves mapping registers, requesting interrupts, enabling clocks, and performing basic configuration.
A successful probe indicates that the kernel believes the hardware is present and accessible. It does not guarantee that the hardware is fully functional or correctly wired. Incorrect pin configuration, missing clocks, or invalid memory regions can still cause subtle failures.
At this stage, the driver becomes part of the kernel’s active device set. Whether user space can interact with the device depends on the type of interface the driver exposes.
Kernel log messages are often the primary source of visibility during this phase.
Fig.3 Driver probe sequence and resource acquisition
5.
How Drivers Become Visible to User Space
Drivers do not expose hardware directly to applications. Instead, they register interfaces with the kernel, which are then made visible to user space.
The most common of these interfaces appears under the /dev directory. Entries under /dev are device nodes that act as entry points into kernel drivers. They are not ordinary files stored on disk. A device node represents a binding between a user space file descriptor and a set of operations implemented inside the kernel.
This binding is defined by the driver through a table of file operations. Each operation corresponds to a system call that user space may invoke. The driver decides how these calls translate into hardware access.
The following simplified example illustrates a character driver controlling a memory-mapped GPIO peripheral. The driver maps GPIO registers into the kernel address space, configures pin direction using an ioctl, and drives output values using a write operation.
#include <linux/module.h> #include <linux/fs.h> #include <linux/io.h> #include <linux/uaccess.h>
#define GPIO_SIZE 0x1000 #define GPIO_DATA_REG 0x00 #define GPIO_DIR_REG 0x04 #define GPIO_SET_DIR _IOW('G', 0x01, u32)
static int gpio_open(struct inode *inode, struct file *file) { return 0; } static ssize_t gpio_write(struct file *file, const char __user *buf, size_t len, loff_t *offset) { u32 value; if (len < sizeof(value)) return -EINVAL; if (copy_from_user(&value, buf, sizeof(value))) return -EFAULT; /* Drive GPIO output register directly */ writel(value, gpio_base + GPIO_DATA_REG); return sizeof(value); } static long gpio_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { u32 dir; switch (cmd) { case GPIO_SET_DIR: if (copy_from_user(&dir, (void __user *)arg, sizeof(dir))) return -EFAULT; /* Configure GPIO direction */ writel(dir, gpio_base + GPIO_DIR_REG); return 0; default: return -EINVAL; } } static const struct file_operations gpio_fops = { .owner = THIS_MODULE, .open = gpio_open, .write = gpio_write, .unlocked_ioctl = gpio_ioctl, };
{ gpio_base = ioremap(GPIO_BASE_ADDR, GPIO_SIZE); if (!gpio_base) return -ENOMEM; /* Character device registration and /dev creation omitted */ return 0; } static void __exit gpio_exit(void) { if (gpio_base) iounmap(gpio_base); module_init(gpio_init); module_exit(gpio_exit); MODULE_LICENSE("GPL"); |
Once this driver registers a character device, the kernel creates a corresponding entry under /dev. Any access to that device node is routed through the file operations table shown above.
From user space, interaction with the GPIO looks like ordinary file access:
#include <fcntl.h> #include <unistd.h> #include <sys/ioctl.h> #include <stdint.h>
int main(void) { int fd; uint32_t dir = 1; /* 1 = output */ uint32_t value = 1; /* drive GPIO high */ fd = open("/dev/gpio0", O_WRONLY); ioctl(fd, GPIO_SET_DIR, &dir); write(fd, &value, sizeof(value)); close(fd); return 0; } |
Each system call crosses the user–kernel boundary and invokes the corresponding driver operation. The kernel performs permission checks and context switching, but the meaning of each operation is defined entirely by the driver.
In this example, ioctl is used to configure hardware behavior that does not fit a stream model, while write directly updates a memory-mapped register to drive the GPIO output. This pattern is common in embedded Linux drivers.
The presence of a /dev entry indicates that a driver has successfully registered an interface. It does not guarantee that the hardware is present, wired correctly, or functioning. /dev reflects driver registration and kernel state, not hardware truth.
For simplicity, this example omits Device Tree integration and uses a fixed physical address. Production drivers should obtain resources from the Device Tree during probe.
Fig.4 Path from user-space syscalls to driver file operations
6.
Character Drivers and Block Drivers
Linux
drivers are commonly categorized by the type of interface they expose
to user space. The two most important categories in embedded systems
are character drivers and block drivers.
Character drivers provide sequential access to a device. Data is transferred as a stream of bytes, and the driver defines how read, write, and control operations behave. Serial ports, GPIO controllers, sensors, and many custom peripherals are exposed as character devices.
Block drivers provide access to devices that store data in fixed-size blocks and support random access. Storage devices such as eMMC, SD cards, and other mass storage devices are handled through block drivers. The kernel layers buffering, caching, and scheduling on top of these drivers to support filesystems.
This
distinction is architectural. It determines how the kernel interacts
with the device and how user space is expected to use it. The driver
type reflects the nature of the hardware, not developer preference.
7. Conclusion
Kernel modules provide the mechanism for delivering driver code into the Linux kernel. Drivers use that mechanism to bind hardware into the kernel’s device model and expose controlled interfaces to user space.
In embedded Linux systems, understanding how drivers are matched, initialized, and surfaced through interfaces such as /dev is critical. Many bring-up failures occur not because of bugs, but because these boundaries are misunderstood.
A clear mental model of this flow allows engineers to reason effectively about hardware integration, system startup behavior, and driver-related failures in real embedded Linux products.
