Back to Technology

Embedded Systems Series Part 7: Linux Device Drivers

January 25, 2026 Wasil Zafar 70 min read

Master Linux device driver development—character drivers, platform drivers, device model, and kernel modules for embedded systems.

Table of Contents

  1. Introduction to Device Drivers
  2. Kernel Modules Basics
  3. Character Device Drivers
  4. Linux Device Model
  5. Platform Drivers
  6. Interrupt Handling
  7. Memory Management in Drivers
  8. DMA & Data Transfer
  9. Debugging Device Drivers
  10. Conclusion & Next Steps

Introduction to Device Drivers

Series Navigation: This is Part 7 of the 12-part Embedded Systems Series. Review Part 6: U-Boot Bootloader first.

A device driver is kernel code that interfaces between hardware and userspace applications. Drivers abstract hardware complexity—applications use standard system calls (open, read, write) while drivers handle register-level communication.

Driver Types

Linux Driver Categories

Character Block Network
  • Character Drivers: Sequential byte streams (serial ports, GPIO, sensors)
  • Block Drivers: Random access blocks (disks, flash, SD cards)
  • Network Drivers: Packet-based (Ethernet, WiFi, CAN)

Kernel Modules Basics

Kernel modules can be loaded/unloaded at runtime without rebooting—essential for driver development and debugging.

// hello_module.c - Minimal kernel module
#include 
#include 
#include 

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Wasil Zafar");
MODULE_DESCRIPTION("Hello World Kernel Module");
MODULE_VERSION("1.0");

static int __init hello_init(void)
{
    pr_info("Hello, Kernel World!\n");
    return 0;  // 0 = success, negative = error
}

static void __exit hello_exit(void)
{
    pr_info("Goodbye, Kernel World!\n");
}

module_init(hello_init);
module_exit(hello_exit);
# Makefile for kernel module
obj-m += hello_module.o

KERNEL_DIR ?= /lib/modules/$(shell uname -r)/build

all:
	make -C $(KERNEL_DIR) M=$(PWD) modules

clean:
	make -C $(KERNEL_DIR) M=$(PWD) clean

# Build and test
make
sudo insmod hello_module.ko   # Load module
lsmod | grep hello            # Verify loaded
dmesg | tail -5               # Check kernel log
sudo rmmod hello_module       # Unload module

Character Device Drivers

Character drivers create device nodes in /dev/ that applications can open, read, and write.

// simple_char.c - Character device driver
#include 
#include 
#include 
#include 
#include 
#include 

#define DEVICE_NAME "simple_char"
#define CLASS_NAME  "simple"
#define BUFFER_SIZE 256

static int major_number;
static struct class *device_class;
static struct device *device;
static char kernel_buffer[BUFFER_SIZE];
static int buffer_size = 0;

// File operations
static int dev_open(struct inode *inodep, struct file *filep)
{
    pr_info("Device opened\n");
    return 0;
}

static int dev_release(struct inode *inodep, struct file *filep)
{
    pr_info("Device closed\n");
    return 0;
}

static ssize_t dev_read(struct file *filep, char __user *buffer,
                        size_t len, loff_t *offset)
{
    int bytes_to_read = min(len, (size_t)(buffer_size - *offset));
    
    if (bytes_to_read <= 0)
        return 0;  // EOF
    
    if (copy_to_user(buffer, kernel_buffer + *offset, bytes_to_read))
        return -EFAULT;
    
    *offset += bytes_to_read;
    pr_info("Read %d bytes\n", bytes_to_read);
    return bytes_to_read;
}

static ssize_t dev_write(struct file *filep, const char __user *buffer,
                         size_t len, loff_t *offset)
{
    int bytes_to_write = min(len, (size_t)BUFFER_SIZE);
    
    if (copy_from_user(kernel_buffer, buffer, bytes_to_write))
        return -EFAULT;
    
    buffer_size = bytes_to_write;
    pr_info("Wrote %d bytes\n", bytes_to_write);
    return bytes_to_write;
}

static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = dev_open,
    .release = dev_release,
    .read = dev_read,
    .write = dev_write,
};

static int __init char_init(void)
{
    // Register character device
    major_number = register_chrdev(0, DEVICE_NAME, &fops);
    if (major_number < 0) {
        pr_err("Failed to register major number\n");
        return major_number;
    }
    
    // Create device class
    device_class = class_create(CLASS_NAME);
    if (IS_ERR(device_class)) {
        unregister_chrdev(major_number, DEVICE_NAME);
        return PTR_ERR(device_class);
    }
    
    // Create device node /dev/simple_char
    device = device_create(device_class, NULL, MKDEV(major_number, 0),
                           NULL, DEVICE_NAME);
    if (IS_ERR(device)) {
        class_destroy(device_class);
        unregister_chrdev(major_number, DEVICE_NAME);
        return PTR_ERR(device);
    }
    
    pr_info("Device registered: /dev/%s (major %d)\n",
            DEVICE_NAME, major_number);
    return 0;
}

static void __exit char_exit(void)
{
    device_destroy(device_class, MKDEV(major_number, 0));
    class_destroy(device_class);
    unregister_chrdev(major_number, DEVICE_NAME);
    pr_info("Device unregistered\n");
}

module_init(char_init);
module_exit(char_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Wasil Zafar");
MODULE_DESCRIPTION("Simple Character Device Driver");
# Test the driver
sudo insmod simple_char.ko
ls -l /dev/simple_char
echo "Hello Driver" | sudo tee /dev/simple_char
sudo cat /dev/simple_char

Linux Device Model

The Linux device model organizes hardware into buses, devices, and drivers—enabling automatic binding and power management.

Device Model Concepts:
  • Bus: Communication protocol (PCI, USB, I2C, SPI, platform)
  • Device: Hardware described by device tree or enumeration
  • Driver: Code that controls a specific device type
  • Binding: Kernel matches devices to compatible drivers

Platform Drivers

Platform drivers are for non-discoverable devices (GPIOs, on-chip peripherals) described in device tree.

// platform_gpio.c - Platform driver example
#include 
#include 
#include 
#include 
#include 

struct gpio_led_data {
    struct gpio_desc *gpio;
};

static int gpio_led_probe(struct platform_device *pdev)
{
    struct gpio_led_data *data;
    
    data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;
    
    // Get GPIO from device tree
    data->gpio = devm_gpiod_get(&pdev->dev, "led", GPIOD_OUT_LOW);
    if (IS_ERR(data->gpio)) {
        dev_err(&pdev->dev, "Failed to get GPIO\n");
        return PTR_ERR(data->gpio);
    }
    
    platform_set_drvdata(pdev, data);
    
    // Turn on LED
    gpiod_set_value(data->gpio, 1);
    dev_info(&pdev->dev, "LED GPIO initialized\n");
    
    return 0;
}

static int gpio_led_remove(struct platform_device *pdev)
{
    struct gpio_led_data *data = platform_get_drvdata(pdev);
    gpiod_set_value(data->gpio, 0);
    dev_info(&pdev->dev, "LED GPIO removed\n");
    return 0;
}

// Device tree matching
static const struct of_device_id gpio_led_of_match[] = {
    { .compatible = "mycompany,gpio-led" },
    { }
};
MODULE_DEVICE_TABLE(of, gpio_led_of_match);

static struct platform_driver gpio_led_driver = {
    .probe = gpio_led_probe,
    .remove = gpio_led_remove,
    .driver = {
        .name = "gpio-led",
        .of_match_table = gpio_led_of_match,
    },
};

module_platform_driver(gpio_led_driver);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("GPIO LED Platform Driver");
// Corresponding device tree entry
my_led: led@0 {
    compatible = "mycompany,gpio-led";
    led-gpios = <&gpio1 17 GPIO_ACTIVE_HIGH>;
    status = "okay";
};

Interrupt Handling

// Interrupt handling in drivers
#include 

static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    
    // Check if this interrupt is for us
    if (!check_interrupt_pending(dev))
        return IRQ_NONE;
    
    // Clear interrupt flag
    clear_interrupt(dev);
    
    // Schedule bottom half for heavy processing
    tasklet_schedule(&dev->tasklet);
    
    return IRQ_HANDLED;
}

// In probe function
static int my_probe(struct platform_device *pdev)
{
    int irq, ret;
    
    // Get IRQ from device tree
    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;
    
    // Request IRQ
    ret = devm_request_irq(&pdev->dev, irq, my_irq_handler,
                           IRQF_SHARED, "my_device", dev);
    if (ret) {
        dev_err(&pdev->dev, "Failed to request IRQ\n");
        return ret;
    }
    
    return 0;
}

Memory Management in Drivers

// Kernel memory allocation
#include 

// kmalloc - small, contiguous allocations
void *ptr = kmalloc(size, GFP_KERNEL);
kfree(ptr);

// kzalloc - zeroed memory
void *ptr = kzalloc(size, GFP_KERNEL);

// devm_* - device-managed (auto-freed on driver removal)
void *ptr = devm_kzalloc(&pdev->dev, size, GFP_KERNEL);

// vmalloc - large, virtual contiguous
void *ptr = vmalloc(large_size);
vfree(ptr);

// I/O memory mapping
void __iomem *base = devm_ioremap_resource(&pdev->dev, res);
u32 val = readl(base + OFFSET);
writel(new_val, base + OFFSET);

DMA & Data Transfer

// DMA memory allocation
#include 

// Allocate coherent DMA buffer (no cache issues)
dma_addr_t dma_handle;
void *cpu_addr = dma_alloc_coherent(&pdev->dev, size,
                                    &dma_handle, GFP_KERNEL);

// Use dma_handle for hardware, cpu_addr for CPU
// ...

dma_free_coherent(&pdev->dev, size, cpu_addr, dma_handle);

// Streaming DMA (for existing buffers)
dma_addr_t dma_addr = dma_map_single(&pdev->dev, buffer, size,
                                      DMA_TO_DEVICE);
// Configure hardware with dma_addr
// ...
dma_unmap_single(&pdev->dev, dma_addr, size, DMA_TO_DEVICE);

Debugging Device Drivers

# Kernel logging
dmesg -w              # Follow kernel log
pr_info("...");       # Info level
pr_err("...");        # Error level
pr_debug("...");      # Debug (needs CONFIG_DYNAMIC_DEBUG)

# Dynamic debug
echo "file my_driver.c +p" > /sys/kernel/debug/dynamic_debug/control

# Module info
modinfo my_driver.ko

# sysfs inspection
ls /sys/bus/platform/drivers/my_driver/
ls /sys/class/my_class/

# ftrace (function tracing)
echo function > /sys/kernel/debug/tracing/current_tracer
echo my_probe > /sys/kernel/debug/tracing/set_ftrace_filter
cat /sys/kernel/debug/tracing/trace

Conclusion & What's Next

You've learned the foundations of Linux device driver development—kernel modules, character drivers, platform drivers, interrupts, and memory management. Driver development requires careful attention to kernel APIs and memory safety.

Key Takeaways:
  • Modules can be loaded/unloaded dynamically
  • Character drivers use file_operations for I/O
  • Platform drivers match device tree entries
  • Use devm_* functions for automatic cleanup
  • DMA requires careful memory management

In Part 8, we'll explore Linux Kernel Customization—configuring, building, and modifying the kernel for embedded systems.

Next Steps

Technology