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:

  1. 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.

  2. 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)
    
  3. Basic Understanding of C Programming: Since drivers are written in C, familiarity with the language will be crucial.

  4. 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:

  • Makefile
  • my_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

  1. Includes and Defines: Begin by including the necessary headers and defining constants like DEVICE_NAME and BUFFER_SIZE.

  2. Global Variables: The message array serves as the buffer, while read_index and write_index manage the read and write positions.

  3. File Operations: The file_operations structure holds pointers to the functions that handle operations like open, read, write, and release.

  4. Driver Initialization and Cleanup:

    • my_char_device_init registers the device and allocates a major number.
    • my_char_device_exit unregisters 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:

  1. Create a device file in /dev:

    sudo mknod /dev/my_char_device c <major_number> 0
    

    Replace <major_number> with the major number outputted during the insertion of the module.

  2. To write data to the device, use echo:

    echo "Hello, Kernel!" > /dev/my_char_device
    
  3. 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!