Skip to main content

Device Driver Development

This comprehensive guide covers Linux device driver development for embedded systems, focusing on the Rock 5B+ platform.

Understanding Device Drivers

Device drivers are kernel modules that provide an interface between hardware devices and the operating system. They act as translators, converting generic OS requests into device-specific commands.

Driver Types

  • Character Drivers: Handle byte streams (keyboards, mice, serial ports)
  • Block Drivers: Handle block-oriented devices (hard drives, SSDs)
  • Network Drivers: Handle network interfaces (Ethernet, WiFi)
  • Platform Drivers: Handle platform-specific devices (GPIO, I2C, SPI)

Driver Architecture

┌──────────────────────────────────────┐
│ User Space │
│ ┌─────────────────────────────────┐ │
│ │ Application │ │
│ └─────────────────────────────────┘ │
├──────────────────────────────────────┤
│ System Calls │
├──────────────────────────────────────┤
│ Kernel Space │
│ ┌─────────────────────────────────┐ │
│ │ Device Driver │ │
│ │ ┌────────────────────────────┐ │ │
│ │ │ File Operations │ │ │
│ │ │ (open, read, write) │ │ │
│ │ └────────────────────────────┘ │ │
│ │ ┌────────────────────────────┐ │ │
│ │ │ Interrupt Handler │ │ │
│ │ └────────────────────────────┘ │ │
│ └─────────────────────────────────┘ │
├──────────────────────────────────────┤
│ Hardware │
└──────────────────────────────────────┘

Development Environment Setup

1. Install Development Tools

# Install kernel development tools
sudo apt install -y build-essential libncurses-dev libssl-dev
sudo apt install -y flex bison libelf-dev bc rsync
sudo apt install -y device-tree-compiler

# Install cross-compilation tools
sudo apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu

# Install kernel headers
sudo apt install -y linux-headers-$(uname -r)

2. Set Up Development Environment

# Create driver development directory
mkdir -p ~/driver-dev
cd ~/driver-dev

# Clone kernel source for reference
git clone https://github.com/radxa/kernel.git -b stable-5.10-rock5
cd kernel

Writing Your First Character Driver

1. Basic Character Driver

// simple_char_driver.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/cdev.h>

#define DEVICE_NAME "simple_char"
#define CLASS_NAME "simple_char_class"

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character driver");
MODULE_VERSION("0.1");

static int major_number;
static struct class* simple_char_class = NULL;
static struct device* simple_char_device = NULL;
static char message[256] = {0};
static short size_of_message;

// Function prototypes
static int device_open(struct inode*, struct file*);
static int device_release(struct inode*, struct file*);
static ssize_t device_read(struct file*, char*, size_t, loff_t*);
static ssize_t device_write(struct file*, const char*, size_t, loff_t*);

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

// Device open function
static int device_open(struct inode *inodep, struct file *filep) {
printk(KERN_INFO "Simple char device opened\n");
return 0;
}

// Device release function
static int device_release(struct inode *inodep, struct file *filep) {
printk(KERN_INFO "Simple char device closed\n");
return 0;
}

// Device read function
static ssize_t device_read(struct file *filep, char *buffer, size_t len, loff_t *offset) {
int error_count = 0;

if (*offset >= size_of_message) {
return 0;
}

error_count = copy_to_user(buffer, message + *offset, len);
if (error_count == 0) {
*offset += len;
return len;
} else {
return -EFAULT;
}
}

// Device write function
static ssize_t device_write(struct file *filep, const char *buffer, size_t len, loff_t *offset) {
sprintf(message, "%s", buffer);
size_of_message = strlen(message);
printk(KERN_INFO "Received %zu characters from the user\n", len);
return len;
}

// Module initialization
static int __init simple_char_init(void) {
printk(KERN_INFO "Simple char driver initialized\n");

// Register character device
major_number = register_chrdev(0, DEVICE_NAME, &fops);
if (major_number < 0) {
printk(KERN_ALERT "Failed to register char device\n");
return major_number;
}

printk(KERN_INFO "Registered with major number %d\n", major_number);

// Create device class
simple_char_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(simple_char_class)) {
unregister_chrdev(major_number, DEVICE_NAME);
printk(KERN_ALERT "Failed to create device class\n");
return PTR_ERR(simple_char_class);
}

// Create device
simple_char_device = device_create(simple_char_class, NULL, MKDEV(major_number, 0), NULL, DEVICE_NAME);
if (IS_ERR(simple_char_device)) {
class_destroy(simple_char_class);
unregister_chrdev(major_number, DEVICE_NAME);
printk(KERN_ALERT "Failed to create device\n");
return PTR_ERR(simple_char_device);
}

return 0;
}

// Module cleanup
static void __exit simple_char_exit(void) {
device_destroy(simple_char_class, MKDEV(major_number, 0));
class_destroy(simple_char_class);
unregister_chrdev(major_number, DEVICE_NAME);
printk(KERN_INFO "Simple char driver removed\n");
}

module_init(simple_char_init);
module_exit(simple_char_exit);

2. Makefile for Driver

# Makefile
obj-m += simple_char_driver.o

all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

3. Build and Test Driver

# Build the driver
make

# Load the driver
sudo insmod simple_char_driver.ko

# Check if device was created
ls -l /dev/simple_char

# Test the driver
echo "Hello World" > /dev/simple_char
cat /dev/simple_char

# Unload the driver
sudo rmmod simple_char_driver

GPIO Driver Development

1. GPIO Driver for Rock 5B+

// gpio_driver.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/gpio.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/cdev.h>

#define DEVICE_NAME "rock5b_gpio"
#define CLASS_NAME "rock5b_gpio_class"
#define GPIO_PIN 18 // GPIO2_A2 on Rock 5B+

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Rock 5B+ GPIO Driver");
MODULE_VERSION("0.1");

static int major_number;
static struct class* gpio_class = NULL;
static struct device* gpio_device = NULL;
static int gpio_pin = GPIO_PIN;

// Function prototypes
static int device_open(struct inode*, struct file*);
static int device_release(struct inode*, struct file*);
static ssize_t device_read(struct file*, char*, size_t, loff_t*);
static ssize_t device_write(struct file*, const char*, size_t, loff_t*);

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

// Device open function
static int device_open(struct inode *inodep, struct file *filep) {
printk(KERN_INFO "GPIO device opened\n");
return 0;
}

// Device release function
static int device_release(struct inode *inodep, struct file *filep) {
printk(KERN_INFO "GPIO device closed\n");
return 0;
}

// Device read function (read GPIO state)
static ssize_t device_read(struct file *filep, char *buffer, size_t len, loff_t *offset) {
int gpio_value;
char message[10];

gpio_value = gpio_get_value(gpio_pin);
sprintf(message, "%d\n", gpio_value);

if (copy_to_user(buffer, message, strlen(message)) != 0) {
return -EFAULT;
}

return strlen(message);
}

// Device write function (set GPIO state)
static ssize_t device_write(struct file *filep, const char *buffer, size_t len, loff_t *offset) {
char message[10];
int value;

if (copy_from_user(message, buffer, len) != 0) {
return -EFAULT;
}

message[len] = '\0';
value = simple_strtol(message, NULL, 10);

if (value == 0 || value == 1) {
gpio_set_value(gpio_pin, value);
printk(KERN_INFO "GPIO %d set to %d\n", gpio_pin, value);
} else {
printk(KERN_ERR "Invalid GPIO value: %d\n", value);
return -EINVAL;
}

return len;
}

// Module initialization
static int __init gpio_driver_init(void) {
printk(KERN_INFO "GPIO driver initialized\n");

// Request GPIO
if (gpio_request(gpio_pin, "rock5b_gpio") < 0) {
printk(KERN_ERR "Failed to request GPIO %d\n", gpio_pin);
return -1;
}

// Set GPIO as output
gpio_direction_output(gpio_pin, 0);

// Register character device
major_number = register_chrdev(0, DEVICE_NAME, &fops);
if (major_number < 0) {
gpio_free(gpio_pin);
printk(KERN_ALERT "Failed to register char device\n");
return major_number;
}

// Create device class
gpio_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(gpio_class)) {
unregister_chrdev(major_number, DEVICE_NAME);
gpio_free(gpio_pin);
printk(KERN_ALERT "Failed to create device class\n");
return PTR_ERR(gpio_class);
}

// Create device
gpio_device = device_create(gpio_class, NULL, MKDEV(major_number, 0), NULL, DEVICE_NAME);
if (IS_ERR(gpio_device)) {
class_destroy(gpio_class);
unregister_chrdev(major_number, DEVICE_NAME);
gpio_free(gpio_pin);
printk(KERN_ALERT "Failed to create device\n");
return PTR_ERR(gpio_device);
}

return 0;
}

// Module cleanup
static void __exit gpio_driver_exit(void) {
device_destroy(gpio_class, MKDEV(major_number, 0));
class_destroy(gpio_class);
unregister_chrdev(major_number, DEVICE_NAME);
gpio_free(gpio_pin);
printk(KERN_INFO "GPIO driver removed\n");
}

module_init(gpio_driver_init);
module_exit(gpio_driver_exit);

2. Test GPIO Driver

# Build and load driver
make
sudo insmod gpio_driver.ko

# Test GPIO control
echo "1" > /dev/rock5b_gpio # Set GPIO high
cat /dev/rock5b_gpio # Read GPIO state
echo "0" > /dev/rock5b_gpio # Set GPIO low

# Unload driver
sudo rmmod gpio_driver

Interrupt-Driven Driver

1. Interrupt Handler Driver

// interrupt_driver.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/wait.h>
#include <linux/sched.h>

#define DEVICE_NAME "interrupt_driver"
#define CLASS_NAME "interrupt_class"
#define GPIO_PIN 18
#define IRQ_NUMBER 49 // GPIO interrupt number

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Interrupt-driven GPIO driver");
MODULE_VERSION("0.1");

static int major_number;
static struct class* interrupt_class = NULL;
static struct device* interrupt_device = NULL;
static int irq_number;
static int irq_count = 0;
static int gpio_pin = GPIO_PIN;

// Wait queue for blocking read
static DECLARE_WAIT_QUEUE_HEAD(wq);
static int data_ready = 0;

// Function prototypes
static int device_open(struct inode*, struct file*);
static int device_release(struct inode*, struct file*);
static ssize_t device_read(struct file*, char*, size_t, loff_t*);
static irqreturn_t gpio_irq_handler(int, void*);

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

// Interrupt handler
static irqreturn_t gpio_irq_handler(int irq, void *dev_id) {
irq_count++;
data_ready = 1;
wake_up_interruptible(&wq);
printk(KERN_INFO "GPIO interrupt occurred! Count: %d\n", irq_count);
return IRQ_HANDLED;
}

// Device open function
static int device_open(struct inode *inodep, struct file *filep) {
printk(KERN_INFO "Interrupt device opened\n");
return 0;
}

// Device release function
static int device_release(struct inode *inodep, struct file *filep) {
printk(KERN_INFO "Interrupt device closed\n");
return 0;
}

// Device read function (blocking read)
static ssize_t device_read(struct file *filep, char *buffer, size_t len, loff_t *offset) {
char message[20];
int error_count;

// Wait for interrupt
if (wait_event_interruptible(wq, data_ready)) {
return -ERESTARTSYS;
}

sprintf(message, "Interrupt count: %d\n", irq_count);
data_ready = 0;

error_count = copy_to_user(buffer, message, strlen(message));
if (error_count == 0) {
return strlen(message);
} else {
return -EFAULT;
}
}

// Module initialization
static int __init interrupt_driver_init(void) {
printk(KERN_INFO "Interrupt driver initialized\n");

// Request GPIO
if (gpio_request(gpio_pin, "interrupt_gpio") < 0) {
printk(KERN_ERR "Failed to request GPIO %d\n", gpio_pin);
return -1;
}

// Set GPIO as input
gpio_direction_input(gpio_pin);

// Get IRQ number
irq_number = gpio_to_irq(gpio_pin);
if (irq_number < 0) {
gpio_free(gpio_pin);
printk(KERN_ERR "Failed to get IRQ number\n");
return -1;
}

// Request interrupt
if (request_irq(irq_number, gpio_irq_handler, IRQF_TRIGGER_RISING, "gpio_interrupt", NULL)) {
gpio_free(gpio_pin);
printk(KERN_ERR "Failed to request IRQ %d\n", irq_number);
return -1;
}

// Register character device
major_number = register_chrdev(0, DEVICE_NAME, &fops);
if (major_number < 0) {
free_irq(irq_number, NULL);
gpio_free(gpio_pin);
printk(KERN_ALERT "Failed to register char device\n");
return major_number;
}

// Create device class
interrupt_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(interrupt_class)) {
unregister_chrdev(major_number, DEVICE_NAME);
free_irq(irq_number, NULL);
gpio_free(gpio_pin);
printk(KERN_ALERT "Failed to create device class\n");
return PTR_ERR(interrupt_class);
}

// Create device
interrupt_device = device_create(interrupt_class, NULL, MKDEV(major_number, 0), NULL, DEVICE_NAME);
if (IS_ERR(interrupt_device)) {
class_destroy(interrupt_class);
unregister_chrdev(major_number, DEVICE_NAME);
free_irq(irq_number, NULL);
gpio_free(gpio_pin);
printk(KERN_ALERT "Failed to create device\n");
return PTR_ERR(interrupt_device);
}

return 0;
}

// Module cleanup
static void __exit interrupt_driver_exit(void) {
device_destroy(interrupt_class, MKDEV(major_number, 0));
class_destroy(interrupt_class);
unregister_chrdev(major_number, DEVICE_NAME);
free_irq(irq_number, NULL);
gpio_free(gpio_pin);
printk(KERN_INFO "Interrupt driver removed\n");
}

module_init(interrupt_driver_init);
module_exit(interrupt_driver_exit);

I2C Driver Development

1. I2C Client Driver

// i2c_driver.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/i2c.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/cdev.h>

#define DEVICE_NAME "i2c_sensor"
#define CLASS_NAME "i2c_sensor_class"
#define I2C_ADAPTER 0
#define I2C_ADDRESS 0x48

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("I2C sensor driver");
MODULE_VERSION("0.1");

static int major_number;
static struct class* i2c_class = NULL;
static struct device* i2c_device = NULL;
static struct i2c_client *client = NULL;

// Function prototypes
static int device_open(struct inode*, struct file*);
static int device_release(struct inode*, struct file*);
static ssize_t device_read(struct file*, char*, size_t, loff_t*);
static ssize_t device_write(struct file*, const char*, size_t, loff_t*);

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

// Device open function
static int device_open(struct inode *inodep, struct file *filep) {
printk(KERN_INFO "I2C device opened\n");
return 0;
}

// Device release function
static int device_release(struct inode *inodep, struct file *filep) {
printk(KERN_INFO "I2C device closed\n");
return 0;
}

// Device read function
static ssize_t device_read(struct file *filep, char *buffer, size_t len, loff_t *offset) {
char data[2];
char message[20];
int error_count;

// Read from I2C device
if (i2c_master_recv(client, data, 2) < 0) {
printk(KERN_ERR "Failed to read from I2C device\n");
return -EIO;
}

sprintf(message, "0x%02x%02x\n", data[0], data[1]);

error_count = copy_to_user(buffer, message, strlen(message));
if (error_count == 0) {
return strlen(message);
} else {
return -EFAULT;
}
}

// Device write function
static ssize_t device_write(struct file *filep, const char *buffer, size_t len, loff_t *offset) {
char data[2];

if (copy_from_user(data, buffer, len) != 0) {
return -EFAULT;
}

// Write to I2C device
if (i2c_master_send(client, data, len) < 0) {
printk(KERN_ERR "Failed to write to I2C device\n");
return -EIO;
}

printk(KERN_INFO "Wrote %zu bytes to I2C device\n", len);
return len;
}

// I2C probe function
static int i2c_probe(struct i2c_client *client, const struct i2c_device_id *id) {
printk(KERN_INFO "I2C device probed\n");
return 0;
}

// I2C remove function
static int i2c_remove(struct i2c_client *client) {
printk(KERN_INFO "I2C device removed\n");
return 0;
}

// I2C device ID table
static const struct i2c_device_id i2c_id[] = {
{ "i2c_sensor", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, i2c_id);

// I2C driver structure
static struct i2c_driver i2c_driver = {
.driver = {
.name = "i2c_sensor",
},
.probe = i2c_probe,
.remove = i2c_remove,
.id_table = i2c_id,
};

// Module initialization
static int __init i2c_driver_init(void) {
struct i2c_adapter *adapter;
struct i2c_board_info board_info;

printk(KERN_INFO "I2C driver initialized\n");

// Get I2C adapter
adapter = i2c_get_adapter(I2C_ADAPTER);
if (!adapter) {
printk(KERN_ERR "Failed to get I2C adapter\n");
return -ENODEV;
}

// Create I2C client
memset(&board_info, 0, sizeof(board_info));
strcpy(board_info.type, "i2c_sensor");
board_info.addr = I2C_ADDRESS;

client = i2c_new_client_device(adapter, &board_info);
if (!client) {
i2c_put_adapter(adapter);
printk(KERN_ERR "Failed to create I2C client\n");
return -ENODEV;
}

// Register character device
major_number = register_chrdev(0, DEVICE_NAME, &fops);
if (major_number < 0) {
i2c_unregister_device(client);
i2c_put_adapter(adapter);
printk(KERN_ALERT "Failed to register char device\n");
return major_number;
}

// Create device class
i2c_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(i2c_class)) {
unregister_chrdev(major_number, DEVICE_NAME);
i2c_unregister_device(client);
i2c_put_adapter(adapter);
printk(KERN_ALERT "Failed to create device class\n");
return PTR_ERR(i2c_class);
}

// Create device
i2c_device = device_create(i2c_class, NULL, MKDEV(major_number, 0), NULL, DEVICE_NAME);
if (IS_ERR(i2c_device)) {
class_destroy(i2c_class);
unregister_chrdev(major_number, DEVICE_NAME);
i2c_unregister_device(client);
i2c_put_adapter(adapter);
printk(KERN_ALERT "Failed to create device\n");
return PTR_ERR(i2c_device);
}

// Register I2C driver
if (i2c_add_driver(&i2c_driver)) {
device_destroy(i2c_class, MKDEV(major_number, 0));
class_destroy(i2c_class);
unregister_chrdev(major_number, DEVICE_NAME);
i2c_unregister_device(client);
i2c_put_adapter(adapter);
printk(KERN_ERR "Failed to add I2C driver\n");
return -1;
}

return 0;
}

// Module cleanup
static void __exit i2c_driver_exit(void) {
device_destroy(i2c_class, MKDEV(major_number, 0));
class_destroy(i2c_class);
unregister_chrdev(major_number, DEVICE_NAME);
i2c_del_driver(&i2c_driver);
if (client) {
i2c_unregister_device(client);
}
printk(KERN_INFO "I2C driver removed\n");
}

module_init(i2c_driver_init);
module_exit(i2c_driver_exit);

Debugging Device Drivers

1. Using printk for Debugging

// Debug levels
printk(KERN_EMERG "Emergency message\n");
printk(KERN_ALERT "Alert message\n");
printk(KERN_CRIT "Critical message\n");
printk(KERN_ERR "Error message\n");
printk(KERN_WARNING "Warning message\n");
printk(KERN_NOTICE "Notice message\n");
printk(KERN_INFO "Info message\n");
printk(KERN_DEBUG "Debug message\n");

2. Using GDB for Kernel Debugging

# Install debugging tools
sudo apt install -y gdb-multiarch

# Start kernel with debugging
qemu-system-aarch64 -M virt -cpu cortex-a57 \
-kernel arch/arm64/boot/Image \
-append "console=ttyAMA0" \
-nographic -s -S

# Connect GDB
gdb-multiarch vmlinux
(gdb) target remote :1234
(gdb) b your_function
(gdb) c

3. Using ftrace for Performance Analysis

# Enable ftrace
echo 1 > /sys/kernel/debug/tracing/tracing_on

# Set trace function
echo function > /sys/kernel/debug/tracing/current_tracer
echo your_function > /sys/kernel/debug/tracing/set_ftrace_filter

# View trace
cat /sys/kernel/debug/tracing/trace

Best Practices

1. Error Handling

// Always check return values
if (register_chrdev(major_number, DEVICE_NAME, &fops) < 0) {
printk(KERN_ALERT "Failed to register char device\n");
return -1;
}

// Clean up on error
if (error_condition) {
unregister_chrdev(major_number, DEVICE_NAME);
return -1;
}

2. Memory Management

// Use appropriate memory allocation
void *ptr = kmalloc(size, GFP_KERNEL);
if (!ptr) {
return -ENOMEM;
}

// Free memory
kfree(ptr);

3. Concurrency Control

#include <linux/spinlock.h>

static DEFINE_SPINLOCK(my_lock);

// Critical section
spin_lock(&my_lock);
// Critical code here
spin_unlock(&my_lock);

Testing Device Drivers

1. Unit Testing

// Test function
static int test_driver_function(void) {
int result;

// Test case 1
result = driver_function(valid_input);
if (result != expected_output) {
printk(KERN_ERR "Test case 1 failed\n");
return -1;
}

// Test case 2
result = driver_function(invalid_input);
if (result != -EINVAL) {
printk(KERN_ERR "Test case 2 failed\n");
return -1;
}

printk(KERN_INFO "All tests passed\n");
return 0;
}

2. Integration Testing

# Test driver with hardware
sudo insmod your_driver.ko
echo "test" > /dev/your_device
cat /dev/your_device
sudo rmmod your_driver

Next Steps

Resources