Writing Your First Character Device Driver
Creating a character device driver can be a rewarding experience, allowing you to interact with hardware devices on a deeper level. This article will walk you through the process step-by-step, helping you develop a basic but functional character device driver.
Prerequisites
Before we dive into the code, it's essential to understand some prerequisites:
-
Linux Kernel Development Environment: Make sure you have a Linux system set up for kernel development. A recent version of Linux (preferably 5.x or later) is recommended.
-
Development Tools: Install build tools if you haven't already. You can typically do this using your package manager. For Debian/Ubuntu-based systems, you can run:
sudo apt-get install build-essential linux-headers-$(uname -r) -
Basic Understanding of C Programming: Since drivers are written in C, familiarity with the language will be crucial.
-
Kernel Module Knowledge: Have a basic understanding of kernel modules as the driver we’ll create will be a loadable kernel module (LKM).
Step 1: Setup the Project Structure
Create a new directory for your driver project:
mkdir my_char_device_driver
cd my_char_device_driver
Inside this directory, create these three files:
Makefilemy_char_device.c
Here’s a simple Makefile to compile your driver:
obj-m += my_char_device.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
Step 2: Writing the Basic Driver Code
Now, let’s jump into my_char_device.c. Below is the basic structure of a character device driver:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/kernel.h>
#define DEVICE_NAME "my_char_device"
#define BUFFER_SIZE 256
static char message[BUFFER_SIZE];
static int read_index = 0;
static int write_index = 0;
static int major_number;
static int dev_open(struct inode *inodep, struct file *filep) {
printk(KERN_INFO "Device opened\n");
return 0;
}
static ssize_t dev_read(struct file *filep, char *buffer, size_t len, loff_t *offset) {
int error_count = 0;
if (read_index >= write_index) {
return 0; // No more data to read
}
error_count = copy_to_user(buffer, message + read_index, write_index - read_index);
if (error_count == 0) {
read_index = write_index; // Reset index after reading
return write_index - read_index;
} else {
return -EFAULT; // Failed to send the data
}
}
static ssize_t dev_write(struct file *filep, const char *buffer, size_t len, loff_t *offset) {
if (write_index + len >= BUFFER_SIZE) {
len = BUFFER_SIZE - write_index; // Prevent buffer overflow
}
if (copy_from_user(message + write_index, buffer, len) != 0) {
return -EFAULT; // Failed to receive data
}
write_index += len;
message[write_index] = '\0'; // Null terminate the string
printk(KERN_INFO "Received: %s\n", message);
return len; // Return the number of bytes written
}
static int dev_release(struct inode *inodep, struct file *filep) {
printk(KERN_INFO "Device closed\n");
return 0;
}
static struct file_operations fops = {
.open = dev_open,
.read = dev_read,
.write = dev_write,
.release = dev_release,
};
static int __init my_char_device_init(void) {
major_number = register_chrdev(0, DEVICE_NAME, &fops);
if (major_number < 0) {
printk(KERN_ALERT "Failed to register character device\n");
return major_number;
}
printk(KERN_INFO "Character device registered with major number %d\n", major_number);
return 0;
}
static void __exit my_char_device_exit(void) {
unregister_chrdev(major_number, DEVICE_NAME);
printk(KERN_INFO "Character device unregistered\n");
}
module_init(my_char_device_init);
module_exit(my_char_device_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver");
MODULE_VERSION("0.1");
Explanation of the Code
-
Includes and Defines: Begin by including the necessary headers and defining constants like
DEVICE_NAMEandBUFFER_SIZE. -
Global Variables: The
messagearray serves as the buffer, whileread_indexandwrite_indexmanage the read and write positions. -
File Operations: The
file_operationsstructure holds pointers to the functions that handle operations likeopen,read,write, andrelease. -
Driver Initialization and Cleanup:
my_char_device_initregisters the device and allocates a major number.my_char_device_exitunregisters the device.
Step 3: Build and Test the Driver
To build your driver, navigate to the my_char_device_driver directory and run:
make
If the build process completes without errors, load the driver using:
sudo insmod my_char_device.ko
Check the output with:
dmesg | tail
You should see a message confirming the driver registration.
Step 4: Interact with the Device
You can interact with your character device using basic command-line tools:
-
Create a device file in
/dev:sudo mknod /dev/my_char_device c <major_number> 0Replace
<major_number>with the major number outputted during the insertion of the module. -
To write data to the device, use
echo:echo "Hello, Kernel!" > /dev/my_char_device -
To read data back:
cat /dev/my_char_device
To clean up, remove the driver with:
sudo rmmod my_char_device
Conclusion
Congratulations! You've successfully written and tested your first character device driver for Linux. This step-by-step guide introduced you to the process of creating a character device, from setup to interaction. As you grow in your driver development journey, exploring more sophisticated features like interrupt handling or DMA will be exciting.
Remember, the Linux kernel is a vast space where continued learning is essential. Whether you choose to delve deeper into driver development or explore networking and infrastructure, keep coding and experimenting. Happy coding!