The "Holy Bible" for embedded engineers
Bridging Hardware and Software in Linux
Understanding how device drivers create the interface between hardware devices and the operating system
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:
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) │
└─────────────────────────────────────┘
Character Drivers:
Block Drivers:
Network Drivers:
Character drivers provide the most straightforward interface for devices that don’t require complex data organization or high-performance optimization.
Character drivers follow the simplicity principle—they provide the simplest possible interface that meets the device’s requirements.
Design Goals:
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:
copy_to_user(): Safely copies data from kernel to user spacecopy_from_user(): Safely copies data from user to kernel spacemutex_lock_interruptible(): Provides synchronization with interrupt handlingfile->private_data: Stores driver-specific data for each file handleBlock 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 drivers follow the performance principle—they must provide the highest possible I/O performance while maintaining data integrity and system stability.
Design Goals:
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 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 drivers follow the throughput principle—they must handle high-bandwidth packet processing while maintaining low latency and high reliability.
Design Goals:
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 initialization and lifecycle management involves setting up the driver, managing its runtime operation, and cleaning up resources when the driver is unloaded.
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 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
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:
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.