Device drivers are the critical interface between hardware and the operating system. They translate generic OS requests into hardware-specific operations, enabling applications to interact with devices without needing to understand their low-level details. This comprehensive guide covers everything from basic driver concepts to advanced techniques for writing Linux device drivers in C.
Understanding Device Drivers
A device driver is a software component that manages a specific hardware device. It provides a uniform interface to the kernel while handling device-specific operations.
┌─────────────────────────────────────────────────────────────┐ │ User Space Applications │ ├─────────────────────────────────────────────────────────────┤ │ System Call Interface │ ├─────────────────────────────────────────────────────────────┤ │ Virtual File System (VFS) │ ├─────────────────────────────────────────────────────────────┤ │ Device Driver Framework │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ │ │ Character │ │ Block │ │ Network │ │ │ │ Drivers │ │ Drivers │ │ Drivers │ │ │ └─────────────┘ └─────────────┘ └─────────────────────┘ │ ├─────────────────────────────────────────────────────────────┤ │ Hardware Layer │ └─────────────────────────────────────────────────────────────┘
Types of Device Drivers
| Type | Description | Examples |
|---|---|---|
| Character | Stream of bytes, no buffering | Serial ports, keyboards |
| Block | Fixed-size blocks, buffered | Hard drives, SSDs |
| Network | Network interfaces | Ethernet, Wi-Fi |
| Misc | Simple character devices | LEDs, GPIOs |
Driver Development Environment Setup
# Install kernel headers and build tools sudo apt-get install linux-headers-$(uname -r) sudo apt-get install build-essential # Verify kernel version uname -r # Set up kernel build directory cd /lib/modules/$(uname -r)/build
Basic Driver Structure
// simplest_driver.c - Minimal character driver
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character driver");
MODULE_VERSION("1.0");
// Driver information
static int major_number;
static struct class *driver_class;
static struct device *driver_device;
// Device data
static char device_buffer[256];
static int device_open_count = 0;
// Called when device is opened
static int driver_open(struct inode *inode, struct file *file) {
device_open_count++;
pr_info("Driver: Device opened %d times\n", device_open_count);
return 0;
}
// Called when device is closed
static int driver_release(struct inode *inode, struct file *file) {
device_open_count--;
pr_info("Driver: Device closed\n");
return 0;
}
// Called when reading from device
static ssize_t driver_read(struct file *file, char __user *user_buffer,
size_t len, loff_t *offset) {
size_t bytes_to_read;
pr_info("Driver: Read request for %zu bytes\n", len);
// Check if we've reached the end
if (*offset >= sizeof(device_buffer)) {
return 0;
}
// Calculate bytes to read
bytes_to_read = min(len, sizeof(device_buffer) - *offset);
// Copy data from kernel space to user space
if (copy_to_user(user_buffer, device_buffer + *offset, bytes_to_read)) {
return -EFAULT;
}
// Update offset
*offset += bytes_to_read;
return bytes_to_read;
}
// Called when writing to device
static ssize_t driver_write(struct file *file, const char __user *user_buffer,
size_t len, loff_t *offset) {
size_t bytes_to_write;
pr_info("Driver: Write request for %zu bytes\n", len);
// Check if we've reached the end
if (*offset >= sizeof(device_buffer)) {
return -ENOSPC;
}
// Calculate bytes to write
bytes_to_write = min(len, sizeof(device_buffer) - *offset);
// Copy data from user space to kernel space
if (copy_from_user(device_buffer + *offset, user_buffer, bytes_to_write)) {
return -EFAULT;
}
// Update offset
*offset += bytes_to_write;
return bytes_to_write;
}
// File operations structure
static struct file_operations driver_fops = {
.owner = THIS_MODULE,
.open = driver_open,
.release = driver_release,
.read = driver_read,
.write = driver_write,
};
// Module initialization
static int __init driver_init(void) {
pr_info("Driver: Initializing\n");
// Allocate major number dynamically
major_number = register_chrdev(0, "simpledriver", &driver_fops);
if (major_number < 0) {
pr_err("Driver: Failed to register device\n");
return major_number;
}
// Create device class
driver_class = class_create(THIS_MODULE, "simpledriver_class");
if (IS_ERR(driver_class)) {
unregister_chrdev(major_number, "simpledriver");
pr_err("Driver: Failed to create class\n");
return PTR_ERR(driver_class);
}
// Create device
driver_device = device_create(driver_class, NULL,
MKDEV(major_number, 0),
NULL, "simpledriver");
if (IS_ERR(driver_device)) {
class_destroy(driver_class);
unregister_chrdev(major_number, "simpledriver");
pr_err("Driver: Failed to create device\n");
return PTR_ERR(driver_device);
}
// Initialize device buffer
memset(device_buffer, 0, sizeof(device_buffer));
snprintf(device_buffer, sizeof(device_buffer), "Hello from driver!\n");
pr_info("Driver: Initialized successfully (major=%d)\n", major_number);
return 0;
}
// Module cleanup
static void __exit driver_exit(void) {
pr_info("Driver: Exiting\n");
device_destroy(driver_class, MKDEV(major_number, 0));
class_destroy(driver_class);
unregister_chrdev(major_number, "simpledriver");
pr_info("Driver: Cleaned up\n");
}
module_init(driver_init);
module_exit(driver_exit);
Makefile for Kernel Module
# Makefile obj-m += simpledriver.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean install: sudo insmod simpledriver.ko remove: sudo rmmod simpledriver info: modinfo simpledriver.ko test: @echo "Testing driver..." sudo dmesg | tail -20 .PHONY: all clean install remove info test
Build and test:
# Build the driver
make
# Install the driver
sudo insmod simpledriver.ko
# Check if loaded
lsmod | grep simpledriver
# Check kernel messages
dmesg | tail
# Create device node (if not created automatically)
sudo mknod /dev/simpledriver c $(cat /proc/devices | grep simpledriver | awk '{print $1}') 0
# Test with cat and echo
echo "Hello Driver" > /dev/simpledriver
cat /dev/simpledriver
# Remove driver
sudo rmmod simpledriver
Advanced Character Driver
// advanced_char_driver.c - With ioctl and mmap support
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/mm.h>
#include <linux/ioctl.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Advanced character driver");
#define DEVICE_NAME "advdriver"
#define BUFFER_SIZE 4096
// IOCTL commands
#define IOCTL_MAGIC 'a'
#define IOCTL_GET_SIZE _IOR(IOCTL_MAGIC, 1, int)
#define IOCTL_SET_SIZE _IOW(IOCTL_MAGIC, 2, int)
#define IOCTL_CLEAR _IO(IOCTL_MAGIC, 3)
#define IOCTL_GET_FLAGS _IOR(IOCTL_MAGIC, 4, int)
#define IOCTL_SET_FLAGS _IOW(IOCTL_MAGIC, 5, int)
// Flags
#define FLAG_READ_ONLY 0x01
#define FLAG_WRITE_ONLY 0x02
#define FLAG_APPEND 0x04
// Device structure
struct adv_device {
struct cdev cdev;
char *buffer;
size_t buffer_size;
size_t data_size;
int flags;
struct mutex lock;
wait_queue_head_t read_queue;
wait_queue_head_t write_queue;
};
static struct adv_device *device;
static dev_t dev_num;
static struct class *device_class;
// Called when device is opened
static int adv_open(struct inode *inode, struct file *file) {
struct adv_device *dev = container_of(inode->i_cdev, struct adv_device, cdev);
file->private_data = dev;
pr_info("Advanced driver: Device opened\n");
return 0;
}
// Called when device is closed
static int adv_release(struct inode *inode, struct file *file) {
pr_info("Advanced driver: Device closed\n");
return 0;
}
// Read from device
static ssize_t adv_read(struct file *file, char __user *user_buffer,
size_t len, loff_t *offset) {
struct adv_device *dev = file->private_data;
size_t bytes_to_read;
ssize_t ret;
mutex_lock(&dev->lock);
// Wait for data if none available (non-blocking check)
if (dev->data_size == 0) {
if (file->f_flags & O_NONBLOCK) {
ret = -EAGAIN;
goto out_unlock;
}
mutex_unlock(&dev->lock);
if (wait_event_interruptible(dev->read_queue, dev->data_size > 0)) {
return -ERESTARTSYS;
}
mutex_lock(&dev->lock);
}
// Calculate bytes to read
if (*offset >= dev->data_size) {
ret = 0;
goto out_unlock;
}
bytes_to_read = min(len, dev->data_size - *offset);
// Copy to user space
if (copy_to_user(user_buffer, dev->buffer + *offset, bytes_to_read)) {
ret = -EFAULT;
goto out_unlock;
}
*offset += bytes_to_read;
ret = bytes_to_read;
// Wake up writers if space available
if (dev->data_size < dev->buffer_size) {
wake_up_interruptible(&dev->write_queue);
}
out_unlock:
mutex_unlock(&dev->lock);
return ret;
}
// Write to device
static ssize_t adv_write(struct file *file, const char __user *user_buffer,
size_t len, loff_t *offset) {
struct adv_device *dev = file->private_data;
size_t bytes_to_write;
size_t write_pos;
ssize_t ret;
// Check if device is read-only
if (dev->flags & FLAG_READ_ONLY) {
return -EPERM;
}
mutex_lock(&dev->lock);
// Determine write position
if (dev->flags & FLAG_APPEND) {
write_pos = dev->data_size;
} else {
write_pos = *offset;
}
// Check if we need to expand buffer
if (write_pos + len > dev->buffer_size) {
char *new_buffer;
size_t new_size = max(dev->buffer_size * 2, write_pos + len);
new_buffer = kmalloc(new_size, GFP_KERNEL);
if (!new_buffer) {
ret = -ENOMEM;
goto out_unlock;
}
memcpy(new_buffer, dev->buffer, dev->buffer_size);
kfree(dev->buffer);
dev->buffer = new_buffer;
dev->buffer_size = new_size;
}
// Calculate bytes to write
bytes_to_write = min(len, dev->buffer_size - write_pos);
// Copy from user space
if (copy_from_user(dev->buffer + write_pos, user_buffer, bytes_to_write)) {
ret = -EFAULT;
goto out_unlock;
}
// Update data size if writing beyond current end
if (write_pos + bytes_to_write > dev->data_size) {
dev->data_size = write_pos + bytes_to_write;
}
// Update offset
if (!(dev->flags & FLAG_APPEND)) {
*offset = write_pos + bytes_to_write;
}
ret = bytes_to_write;
// Wake up readers
wake_up_interruptible(&dev->read_queue);
out_unlock:
mutex_unlock(&dev->lock);
return ret;
}
// mmap support
static int adv_mmap(struct file *file, struct vm_area_struct *vma) {
struct adv_device *dev = file->private_data;
unsigned long size = vma->vm_end - vma->vm_start;
int ret;
if (size > dev->buffer_size) {
return -EINVAL;
}
// Set permissions
vma->vm_flags |= VM_DONTCOPY;
// Map kernel buffer to user space
ret = remap_pfn_range(vma, vma->vm_start,
virt_to_phys(dev->buffer) >> PAGE_SHIFT,
size, vma->vm_page_prot);
if (ret) {
pr_err("Advanced driver: mmap failed\n");
return ret;
}
pr_info("Advanced driver: mmap successful\n");
return 0;
}
// IOCTL handling
static long adv_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {
struct adv_device *dev = file->private_data;
int ret = 0;
int value;
// Verify command
if (_IOC_TYPE(cmd) != IOCTL_MAGIC) {
return -ENOTTY;
}
switch (cmd) {
case IOCTL_GET_SIZE:
ret = put_user(dev->data_size, (int __user *)arg);
break;
case IOCTL_SET_SIZE:
ret = get_user(value, (int __user *)arg);
if (ret) break;
mutex_lock(&dev->lock);
if (value > dev->buffer_size) {
ret = -ENOSPC;
} else {
dev->data_size = value;
}
mutex_unlock(&dev->lock);
break;
case IOCTL_CLEAR:
mutex_lock(&dev->lock);
memset(dev->buffer, 0, dev->buffer_size);
dev->data_size = 0;
mutex_unlock(&dev->lock);
break;
case IOCTL_GET_FLAGS:
ret = put_user(dev->flags, (int __user *)arg);
break;
case IOCTL_SET_FLAGS:
ret = get_user(dev->flags, (int __user *)arg);
break;
default:
return -ENOTTY;
}
return ret;
}
// File operations structure
static struct file_operations adv_fops = {
.owner = THIS_MODULE,
.open = adv_open,
.release = adv_release,
.read = adv_read,
.write = adv_write,
.unlocked_ioctl = adv_ioctl,
.mmap = adv_mmap,
};
// Module initialization
static int __init adv_init(void) {
int ret;
pr_info("Advanced driver: Initializing\n");
// Allocate device structure
device = kmalloc(sizeof(struct adv_device), GFP_KERNEL);
if (!device) {
pr_err("Advanced driver: Failed to allocate device structure\n");
return -ENOMEM;
}
// Allocate device buffer
device->buffer = kmalloc(BUFFER_SIZE, GFP_KERNEL);
if (!device->buffer) {
kfree(device);
pr_err("Advanced driver: Failed to allocate buffer\n");
return -ENOMEM;
}
// Initialize device
device->buffer_size = BUFFER_SIZE;
device->data_size = 0;
device->flags = 0;
mutex_init(&device->lock);
init_waitqueue_head(&device->read_queue);
init_waitqueue_head(&device->write_queue);
// Allocate device numbers
ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
if (ret < 0) {
pr_err("Advanced driver: Failed to allocate device numbers\n");
goto err_free_buffer;
}
// Initialize cdev
cdev_init(&device->cdev, &adv_fops);
device->cdev.owner = THIS_MODULE;
// Add cdev to kernel
ret = cdev_add(&device->cdev, dev_num, 1);
if (ret < 0) {
pr_err("Advanced driver: Failed to add cdev\n");
goto err_unregister_region;
}
// Create device class
device_class = class_create(THIS_MODULE, "advdriver_class");
if (IS_ERR(device_class)) {
pr_err("Advanced driver: Failed to create class\n");
ret = PTR_ERR(device_class);
goto err_cdev_del;
}
// Create device
device_create(device_class, NULL, dev_num, NULL, DEVICE_NAME);
pr_info("Advanced driver: Initialized (major=%d, minor=%d)\n",
MAJOR(dev_num), MINOR(dev_num));
return 0;
err_cdev_del:
cdev_del(&device->cdev);
err_unregister_region:
unregister_chrdev_region(dev_num, 1);
err_free_buffer:
kfree(device->buffer);
kfree(device);
return ret;
}
// Module cleanup
static void __exit adv_exit(void) {
pr_info("Advanced driver: Exiting\n");
device_destroy(device_class, dev_num);
class_destroy(device_class);
cdev_del(&device->cdev);
unregister_chrdev_region(dev_num, 1);
kfree(device->buffer);
kfree(device);
pr_info("Advanced driver: Cleaned up\n");
}
module_init(adv_init);
module_exit(adv_exit);
User-Space Test Program
// test_adv_driver.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#define IOCTL_MAGIC 'a'
#define IOCTL_GET_SIZE _IOR(IOCTL_MAGIC, 1, int)
#define IOCTL_SET_SIZE _IOW(IOCTL_MAGIC, 2, int)
#define IOCTL_CLEAR _IO(IOCTL_MAGIC, 3)
#define IOCTL_GET_FLAGS _IOR(IOCTL_MAGIC, 4, int)
#define IOCTL_SET_FLAGS _IOW(IOCTL_MAGIC, 5, int)
int main() {
int fd;
char buffer[256];
int size, flags;
// Open device
fd = open("/dev/advdriver", O_RDWR);
if (fd < 0) {
perror("Failed to open device");
return 1;
}
printf("Device opened successfully\n");
// Test write
strcpy(buffer, "Hello from user space!");
write(fd, buffer, strlen(buffer));
printf("Wrote: %s\n", buffer);
// Test read
memset(buffer, 0, sizeof(buffer));
lseek(fd, 0, SEEK_SET);
read(fd, buffer, sizeof(buffer));
printf("Read: %s\n", buffer);
// Test append mode
flags = 0x04; // FLAG_APPEND
ioctl(fd, IOCTL_SET_FLAGS, &flags);
write(fd, "Appended text", 13);
lseek(fd, 0, SEEK_SET);
read(fd, buffer, sizeof(buffer));
printf("After append: %s\n", buffer);
// Test IOCTL
ioctl(fd, IOCTL_GET_SIZE, &size);
printf("Data size: %d bytes\n", size);
// Test clear
ioctl(fd, IOCTL_CLEAR, 0);
ioctl(fd, IOCTL_GET_SIZE, &size);
printf("After clear, size: %d bytes\n", size);
close(fd);
return 0;
}
Platform Device Driver
// platform_driver.c - GPIO LED driver for embedded systems
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/platform_device.h>
#include <linux/gpio.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("GPIO LED Platform Driver");
struct led_data {
int gpio;
int active_low;
struct device *dev;
};
static int led_probe(struct platform_device *pdev) {
struct device *dev = &pdev->dev;
struct led_data *led;
struct device_node *np = dev->of_node;
int ret;
led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL);
if (!led) {
return -ENOMEM;
}
led->dev = dev;
// Get GPIO from device tree
led->gpio = of_get_gpio(np, 0);
if (!gpio_is_valid(led->gpio)) {
dev_err(dev, "Invalid GPIO\n");
return -EINVAL;
}
// Get active-low property
led->active_low = of_property_read_bool(np, "active-low");
// Request GPIO
ret = devm_gpio_request(dev, led->gpio, "led_gpio");
if (ret) {
dev_err(dev, "Failed to request GPIO\n");
return ret;
}
// Set GPIO direction
ret = gpio_direction_output(led->gpio, led->active_low ? 1 : 0);
if (ret) {
dev_err(dev, "Failed to set GPIO direction\n");
return ret;
}
platform_set_drvdata(pdev, led);
dev_info(dev, "LED driver probed (GPIO=%d, active_low=%d)\n",
led->gpio, led->active_low);
return 0;
}
static int led_remove(struct platform_device *pdev) {
struct led_data *led = platform_get_drvdata(pdev);
gpio_set_value(led->gpio, led->active_low ? 1 : 0);
dev_info(&pdev->dev, "LED driver removed\n");
return 0;
}
static const struct of_device_id led_of_match[] = {
{ .compatible = "example,led-gpio" },
{ }
};
MODULE_DEVICE_TABLE(of, led_of_match);
static struct platform_driver led_driver = {
.driver = {
.name = "led_driver",
.of_match_table = led_of_match,
},
.probe = led_probe,
.remove = led_remove,
};
module_platform_driver(led_driver);
Device Tree Binding
// led_device.dts - Device tree entry
/dts-v1/;
/ {
model = "My Embedded Board";
compatible = "example,board";
leds {
compatible = "gpio-leds";
led0 {
label = "status-led";
gpios = <&gpio1 18 GPIO_ACTIVE_HIGH>;
default-state = "off";
linux,default-trigger = "heartbeat";
};
};
myled {
compatible = "example,led-gpio";
gpios = <&gpio1 19 GPIO_ACTIVE_LOW>;
active-low;
};
};
Interrupt Handling
// interrupt_driver.c - Driver with interrupt support
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#define DEVICE_NAME "intr_driver"
static int irq_number;
static int irq_counter = 0;
static wait_queue_head_t wq;
static int event_occurred = 0;
// Interrupt handler
static irqreturn_t irq_handler(int irq, void *dev_id) {
irq_counter++;
event_occurred = 1;
// Wake up any waiting processes
wake_up_interruptible(&wq);
pr_info("Interrupt occurred! Count: %d\n", irq_counter);
return IRQ_HANDLED;
}
static int intr_open(struct inode *inode, struct file *file) {
pr_info("Interrupt driver: Opened\n");
return 0;
}
static int intr_release(struct inode *inode, struct file *file) {
pr_info("Interrupt driver: Closed\n");
return 0;
}
static ssize_t intr_read(struct file *file, char __user *user_buffer,
size_t len, loff_t *offset) {
char buffer[32];
size_t bytes;
// Wait for interrupt (non-blocking check)
if (file->f_flags & O_NONBLOCK) {
if (!event_occurred) {
return -EAGAIN;
}
} else {
wait_event_interruptible(wq, event_occurred);
if (signal_pending(current)) {
return -ERESTARTSYS;
}
}
// Clear event flag
event_occurred = 0;
// Format interrupt count
bytes = snprintf(buffer, sizeof(buffer), "%d\n", irq_counter);
// Copy to user space
if (copy_to_user(user_buffer, buffer, bytes)) {
return -EFAULT;
}
return bytes;
}
static struct file_operations intr_fops = {
.owner = THIS_MODULE,
.open = intr_open,
.release = intr_release,
.read = intr_read,
};
static int __init intr_init(void) {
int ret;
dev_t dev_num;
pr_info("Interrupt driver: Initializing\n");
// Initialize wait queue
init_waitqueue_head(&wq);
// Allocate device number
ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
if (ret) {
pr_err("Failed to allocate device number\n");
return ret;
}
// Register interrupt (example: GPIO interrupt)
irq_number = gpio_to_irq(18); // GPIO 18
if (irq_number < 0) {
pr_err("Failed to get IRQ number\n");
unregister_chrdev_region(dev_num, 1);
return irq_number;
}
ret = request_irq(irq_number, irq_handler,
IRQF_TRIGGER_RISING, "intr_driver", NULL);
if (ret) {
pr_err("Failed to register IRQ handler\n");
unregister_chrdev_region(dev_num, 1);
return ret;
}
pr_info("Interrupt driver: Initialized (IRQ=%d)\n", irq_number);
return 0;
}
static void __exit intr_exit(void) {
pr_info("Interrupt driver: Exiting\n");
free_irq(irq_number, NULL);
unregister_chrdev_region(dev_num, 1);
}
module_init(intr_init);
module_exit(intr_exit);
DMA Support
// dma_driver.c - Driver with DMA support
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/dma-mapping.h>
#include <linux/slab.h>
#define DMA_BUFFER_SIZE (1024 * 1024) // 1 MB
struct dma_device {
struct device *dev;
void *cpu_addr;
dma_addr_t dma_addr;
size_t size;
};
static struct dma_device *dma_dev;
static int __init dma_init(void) {
dma_dev = kmalloc(sizeof(*dma_dev), GFP_KERNEL);
if (!dma_dev) {
return -ENOMEM;
}
dma_dev->dev = NULL; // Set to actual device
dma_dev->size = DMA_BUFFER_SIZE;
// Allocate DMA buffer
dma_dev->cpu_addr = dma_alloc_coherent(dma_dev->dev, dma_dev->size,
&dma_dev->dma_addr, GFP_KERNEL);
if (!dma_dev->cpu_addr) {
pr_err("Failed to allocate DMA buffer\n");
kfree(dma_dev);
return -ENOMEM;
}
// Initialize buffer
memset(dma_dev->cpu_addr, 0, dma_dev->size);
pr_info("DMA driver: Allocated buffer at virt=%p, phys=%pad\n",
dma_dev->cpu_addr, &dma_dev->dma_addr);
// Setup DMA transfer (simplified)
// dma_async_memcpy_buf_to_buf(...);
return 0;
}
static void __exit dma_exit(void) {
if (dma_dev) {
dma_free_coherent(dma_dev->dev, dma_dev->size,
dma_dev->cpu_addr, dma_dev->dma_addr);
kfree(dma_dev);
pr_info("DMA driver: Cleaned up\n");
}
}
module_init(dma_init);
module_exit(dma_exit);
Debugging Techniques
// debug_helper.h
#ifndef DEBUG_HELPER_H
#define DEBUG_HELPER_H
// Debug levels
#define DEBUG_INFO BIT(0)
#define DEBUG_WARN BIT(1)
#define DEBUG_ERROR BIT(2)
#define DEBUG_IO BIT(3)
#define DEBUG_DMA BIT(4)
static int debug_mask = DEBUG_INFO | DEBUG_ERROR;
#define dbg_print(level, fmt, ...) \
do { \
if (debug_mask & level) { \
pr_info("[" #level "] " fmt, ##__VA_ARGS__); \
} \
} while(0)
// Procfs debug interface
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
static int debug_proc_show(struct seq_file *m, void *v) {
seq_printf(m, "Debug mask: 0x%x\n", debug_mask);
seq_printf(m, "Device status: ...\n");
return 0;
}
static ssize_t debug_proc_write(struct file *file, const char __user *buffer,
size_t count, loff_t *pos) {
char buf[32];
int mask;
if (count > sizeof(buf) - 1) {
return -EINVAL;
}
if (copy_from_user(buf, buffer, count)) {
return -EFAULT;
}
buf[count] = '\0';
if (kstrtoint(buf, 0, &mask) == 0) {
debug_mask = mask;
pr_info("Debug mask set to 0x%x\n", debug_mask);
}
return count;
}
static int debug_proc_open(struct inode *inode, struct file *file) {
return single_open(file, debug_proc_show, NULL);
}
static const struct file_operations debug_proc_fops = {
.owner = THIS_MODULE,
.open = debug_proc_open,
.read = seq_read,
.write = debug_proc_write,
.llseek = seq_lseek,
.release = single_release,
};
static void create_debug_interface(void) {
proc_create("driver_debug", 0644, NULL, &debug_proc_fops);
}
static void remove_debug_interface(void) {
remove_proc_entry("driver_debug", NULL);
}
#endif
Best Practices
- Error Handling: Always check return values and clean up on failure
- Memory Management: Use kernel allocators (kmalloc, kzalloc) appropriately
- Synchronization: Use mutexes, spinlocks, and atomic operations correctly
- Interrupt Context: Be aware of what can be called in interrupt handlers
- User Space Copy: Always use copy_to_user/copy_from_user
- Device Tree: Use device tree for hardware description
- Power Management: Implement suspend/resume callbacks
- Testing: Test on multiple kernel versions and hardware
- Documentation: Document device tree bindings and driver interfaces
- License Compliance: Ensure proper license headers
Building and Testing
# Build module make # Install module sudo insmod simpledriver.ko # Check module lsmod | grep simpledriver cat /proc/devices | grep simpledriver # Test with user program gcc -o test test_adv_driver.c ./test # Check kernel log dmesg | tail -50 # Remove module sudo rmmod simpledriver
Conclusion
Writing device drivers in C requires deep understanding of kernel internals, hardware interfaces, and system programming concepts. This guide covered:
- Basic character driver structure
- Advanced features (ioctl, mmap)
- Platform drivers and device tree
- Interrupt handling
- DMA support
- Debugging techniques
Driver development is a complex but rewarding field that bridges hardware and software. With practice and attention to kernel conventions, you can create reliable, efficient drivers that power everything from embedded systems to high-performance computing platforms.