The Embedded New Testament

The "Holy Bible" for embedded engineers


Project maintained by theEmbeddedGeorge Hosted on GitHub Pages — Theme by mattgraham

Device Drivers

Bridging Hardware and Software in Linux
Understanding how device drivers create the interface between hardware devices and the operating system


📋 Table of Contents


🏗️ Driver Fundamentals

What are Device Drivers?

Device drivers are specialized software components that act as translators between hardware devices and the Linux kernel. They provide a standardized interface that allows the kernel to interact with diverse hardware without needing to understand the specific details of each device.

The Driver’s Role in the System:

Driver Architecture Philosophy

Linux device drivers follow the layered abstraction principle—they create multiple levels of abstraction that separate hardware-specific details from the kernel’s core functionality.

┌─────────────────────────────────────┐
│         User Applications           │ ← User space
├─────────────────────────────────────┤
│         System Call Interface      │ ← Boundary
├─────────────────────────────────────┤
│         Virtual File System        │ ← Kernel space
│         (VFS)                      │
├─────────────────────────────────────┤
│         Driver Interface Layer     │ ← Driver framework
│         (file_operations, etc.)    │
├─────────────────────────────────────┤
│         Device Driver              │ ← Hardware-specific code
│         (Hardware interface)       │
├─────────────────────────────────────┤
│         Hardware Device            │ ← Physical hardware
│         (Actual device)            │
└─────────────────────────────────────┘

Driver Types and Characteristics

Character Drivers:

Block Drivers:

Network Drivers:


🔌 Character Device Drivers

Simple Interfaces for Simple Devices

Character drivers provide the most straightforward interface for devices that don’t require complex data organization or high-performance optimization.

Character Driver Design Philosophy

Character drivers follow the simplicity principle—they provide the simplest possible interface that meets the device’s requirements.

Design Goals:

Character Driver Implementation

Character drivers implement the file_operations structure, which defines how the kernel handles various operations on the device file:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/slab.h>

#define DEVICE_NAME "my_device"
#define CLASS_NAME "my_class"

// Device-specific data structure
struct device_data {
    char buffer[256];           // Internal data buffer
    size_t buffer_size;         // Current buffer size
    struct mutex lock;          // Synchronization lock
    struct cdev *cdev;          // Character device structure
    dev_t dev_num;              // Device number
    struct class *class;        // Device class
    struct device *device;      // Device instance
};

static struct device_data *dev_data = NULL;

// File operations implementation
static int device_open(struct inode *inode, struct file *file)
{
    file->private_data = dev_data;
    printk(KERN_INFO "Device opened by process %d\n", current->pid);
    return 0;
}

static int device_release(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Device closed by process %d\n", current->pid);
    return 0;
}

static ssize_t device_read(struct file *file, char __user *buffer, 
                          size_t count, loff_t *offset)
{
    struct device_data *data = (struct device_data *)file->private_data;
    ssize_t bytes_read = 0;
    
    if (mutex_lock_interruptible(&data->lock))
        return -ERESTARTSYS;
    
    if (*offset >= data->buffer_size) {
        bytes_read = 0;  // End of file
    } else {
        bytes_read = min(count, data->buffer_size - *offset);
        
        if (copy_to_user(buffer, data->buffer + *offset, bytes_read)) {
            bytes_read = -EFAULT;
        } else {
            *offset += bytes_read;
        }
    }
    
    mutex_unlock(&data->lock);
    return bytes_read;
}

static ssize_t device_write(struct file *file, const char __user *buffer, 
                           size_t count, loff_t *offset)
{
    struct device_data *data = (struct device_data *)file->private_data;
    ssize_t bytes_written = 0;
    
    if (mutex_lock_interruptible(&data->lock))
        return -ERESTARTSYS;
    
    if (count > sizeof(data->buffer)) {
        bytes_written = -EINVAL;
    } else {
        if (copy_from_user(data->buffer, buffer, count)) {
            bytes_written = -EFAULT;
        } else {
            data->buffer_size = count;
            bytes_written = count;
        }
    }
    
    mutex_unlock(&data->lock);
    return bytes_written;
}

// File operations structure
static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = device_open,
    .release = device_release,
    .read = device_read,
    .write = device_write,
};

Key Concepts in Character Driver Implementation:


💾 Block Device Drivers

Efficient Storage Device Interfaces

Block drivers provide sophisticated interfaces for devices that store data in fixed-size blocks. They must handle complex issues such as request queuing, caching, and data buffering.

Block Driver Design Philosophy

Block drivers follow the performance principle—they must provide the highest possible I/O performance while maintaining data integrity and system stability.

Design Goals:

Block Driver Implementation

Block drivers implement the block_device_operations structure and handle request queuing:

#include <linux/module.h>
#include <linux/blkdev.h>
#include <linux/genhd.h>
#include <linux/fs.h>
#include <linux/slab.h>

#define DEVICE_NAME "my_block_device"
#define DEVICE_SIZE (16 * 1024 * 1024) // 16 MB
#define SECTOR_SIZE 512

static dev_t dev_num;
static struct gendisk *device_disk = NULL;
static struct request_queue *device_queue = NULL;

// Device data structure
struct block_device_data {
    void *data;                 // Device data storage
    spinlock_t lock;            // Synchronization lock
    sector_t capacity;          // Device capacity in sectors
};

static struct block_device_data *device_data = NULL;

// Request handling function
static void device_request_handler(struct request_queue *q)
{
    struct request *req;
    struct block_device_data *data = device_data;
    unsigned long flags;
    
    while ((req = blk_fetch_request(q)) != NULL) {
        if (req->cmd_type != REQ_TYPE_FS) {
            blk_end_request_all(req, -EIO);
            continue;
        }
        
        spin_lock_irqsave(&data->lock, flags);
        
        // Handle read/write operations
        if (rq_data_dir(req) == READ) {
            if (copy_to_bio(req->bio, data->data + (blk_rq_pos(req) << 9), 
                           blk_rq_cur_bytes(req))) {
                blk_end_request_all(req, -EIO);
            } else {
                blk_end_request_all(req, 0);
            }
        } else {
            if (copy_from_bio(req->bio, data->data + (blk_rq_pos(req) << 9), 
                              blk_rq_cur_bytes(req))) {
                blk_end_request_all(req, -EIO);
            } else {
                blk_end_request_all(req, 0);
            }
        }
        
        spin_unlock_irqrestore(&data->lock, flags);
    }
}

// Block device operations
static int device_open(struct block_device *bdev, fmode_t mode)
{
    return 0;
}

static void device_release(struct gendisk *disk, fmode_t mode)
{
}

static struct block_device_operations device_ops = {
    .owner = THIS_MODULE,
    .open = device_open,
    .release = device_release,
};

Block Driver Key Concepts:


🌐 Network Device Drivers

Communication Interface Management

Network drivers provide the interface between network hardware and the kernel’s networking stack. They’re the most complex type of driver due to the need to handle packet queuing, interrupt processing, and various network protocols.

Network Driver Design Philosophy

Network drivers follow the throughput principle—they must handle high-bandwidth packet processing while maintaining low latency and high reliability.

Design Goals:

Network Driver Implementation

Network drivers implement the net_device_ops structure and handle packet processing:

#include <linux/module.h>
#include <linux/netdevice.h>
#include <linux/skbuff.h>
#include <linux/interrupt.h>
#include <linux/etherdevice.h>

#define DEVICE_NAME "my_net_device"
#define DEVICE_MTU 1500

// Network device data structure
struct net_device_data {
    struct net_device *ndev;    // Network device structure
    spinlock_t lock;            // Synchronization lock
    struct sk_buff_head tx_queue; // Transmission queue
    struct sk_buff_head rx_queue; // Reception queue
    unsigned int irq_number;    // Interrupt number
    void __iomem *io_base;     // I/O base address
};

static struct net_device_data *net_data = NULL;

// Network device operations
static int netdev_open(struct net_device *dev)
{
    struct net_device_data *data = netdev_priv(dev);
    
    netif_start_queue(dev);
    enable_irq(data->irq_number);
    
    printk(KERN_INFO "Network device opened\n");
    return 0;
}

static int netdev_close(struct net_device *dev)
{
    struct net_device_data *data = netdev_priv(dev);
    
    netif_stop_queue(dev);
    disable_irq(data->irq_number);
    
    printk(KERN_INFO "Network device closed\n");
    return 0;
}

static netdev_tx_t netdev_xmit(struct sk_buff *skb, struct net_device *dev)
{
    struct net_device_data *data = netdev_priv(dev);
    unsigned long flags;
    
    spin_lock_irqsave(&data->lock, flags);
    
    __skb_queue_tail(&data->tx_queue, skb);
    
    dev->stats.tx_packets++;
    dev->stats.tx_bytes += skb->len;
    
    spin_unlock_irqrestore(&data->lock, flags);
    
    schedule_work(&tx_work);
    
    return NETDEV_TX_OK;
}

static struct net_device_ops netdev_ops = {
    .ndo_open = netdev_open,
    .ndo_stop = netdev_close,
    .ndo_start_xmit = netdev_xmit,
};

Network Driver Key Concepts:


🔄 Driver Lifecycle Management

Managing Driver State and Resources

Driver initialization and lifecycle management involves setting up the driver, managing its runtime operation, and cleaning up resources when the driver is unloaded.

Driver Lifecycle Philosophy

Driver lifecycle management follows the resource management principle—ensure that all resources are properly allocated during initialization, managed during runtime, and cleaned up during shutdown.

Lifecycle Goals:

Driver Initialization Flow

Driver Module Load
        │
        ▼
   Module Init Function
        │
        ▼
   Allocate Resources
        │
        ▼
   Initialize Hardware
        │
        ▼
   Register with Kernel
        │
        ▼
   Driver Ready
        │
        ▼
   Runtime Operation
        │
        ▼
   Module Exit Function
        │
        ▼
   Unregister from Kernel
        │
        ▼
   Clean Up Hardware
        │
        ▼
   Free Resources
        │
        ▼
   Driver Unloaded

Driver Cleanup Implementation

Proper cleanup is essential to prevent resource leaks and system instability:

static void __exit device_exit(void)
{
    // Remove device file
    if (dev_data->device) {
        device_destroy(dev_data->class, dev_data->dev_num);
    }
    
    // Remove device class
    if (dev_data->class) {
        class_destroy(dev_data->class);
    }
    
    // Remove character device
    if (dev_data->cdev) {
        cdev_del(dev_data->cdev);
    }
    
    // Free device numbers
    if (dev_data->dev_num) {
        unregister_chrdev_region(dev_data->dev_num, 1);
    }
    
    // Free device data
    if (dev_data) {
        kfree(dev_data);
    }
    
    printk(KERN_INFO "Device driver unloaded\n");
}

module_init(device_init);
module_exit(device_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A sample device driver");

Cleanup Best Practices:


🎯 Conclusion

Device driver development in Linux provides a powerful and flexible framework for interfacing with hardware devices. The layered architecture separates hardware-specific details from the kernel’s core functionality, enabling drivers to be developed independently and loaded dynamically.

Key Takeaways:

The Path Forward:

As embedded systems become more complex and require more sophisticated hardware interfaces, the importance of understanding device driver development will only increase. Linux continues to evolve its driver model, providing new features and optimizations that enable more powerful and efficient embedded systems.

Remember: Device driver development is not just about writing code—it’s about understanding how hardware and software interact, how to manage system resources efficiently, and how to build reliable interfaces between the physical and digital worlds. The skills you develop here will serve you throughout your embedded systems career.