Creating Character Devices

Character devices are essential components in the Linux kernel, providing an interface for the user space to interact with hardware devices. In this article, we'll walk through the process of creating a simple character device in the Linux kernel, focusing on the essentials you'll need to know to get started. Let's dive into the step-by-step guide!

Step 1: Understanding the Basics

Before we jump into the code, let’s clarify what a character device is. A character device is a type of device file that allows users to read or write data one character at a time. This includes devices such as keyboards, mice, and serial ports. When creating a character device, it is necessary to implement operations for opening, reading, writing, and closing the device.

Key Concepts:

  • Device Files: Located in /dev, these files represent the character devices.
  • Major and Minor Numbers: The major number identifies the driver associated with the device, while the minor number differentiates between different devices or instances of the same type.

Step 2: Setting Up Your Development Environment

To develop a character device, you’ll need a Linux environment with the necessary kernel headers and development tools. If you haven't done so, install them using the package manager of your choice. For example, in Ubuntu, you can run:

sudo apt-get install build-essential linux-headers-$(uname -r)

Step 3: Writing Your Character Device Code

Now, let’s get to the coding part. Create a new directory for your project and navigate into it. Here’s a step-by-step tutorial to help you create a simple character device.

3.1: Create Files

Create a file named simple_char_device.c. Here’s a basic skeleton of what you’ll be writing:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
// Define device name and major number
#define DEVICE_NAME "simplechar"
#define CLASS_NAME "simple"

// Function prototypes
static int simple_open(struct inode *inodep, struct file *filep);
static int simple_release(struct inode *inodep, struct file *filep);
static ssize_t simple_read(struct file *filep, char *buffer, size_t len, loff_t *offset);
static ssize_t simple_write(struct file *filep, const char *buffer, size_t len, loff_t *offset);

// Creating struct file_operations
static struct file_operations fops = {
    .open = simple_open,
    .read = simple_read,
    .write = simple_write,
    .release = simple_release,
};

// Variables for device
static int majorNumber;
static char message[256] = {0};
static int size_of_message;

static int __init simple_init(void) {
    majorNumber = register_chrdev(0, DEVICE_NAME, &fops);
    if (majorNumber < 0) {
        printk(KERN_ALERT "SimpleChar failed to register a major number\n");
        return majorNumber;
    }
    printk(KERN_INFO "SimpleChar: registered correctly with major number %d\n", majorNumber);
    return 0;
}

static void __exit simple_exit(void) {
    unregister_chrdev(majorNumber, DEVICE_NAME);
    printk(KERN_INFO "SimpleChar: unregistered the device\n");
}

// Implement other functions...
module_init(simple_init);
module_exit(simple_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Linux character driver");

3.2: Implementing Functionality

Now let’s implement the functionalities of the file operations defined earlier.

3.2.1: Opening the Device

The open function is called when a user tries to interact with the device:

static int simple_open(struct inode *inodep, struct file *filep) {
    printk(KERN_INFO "SimpleChar: Device has been opened\n");
    return 0;
}

3.2.2: Reading from the Device

The read function passes data from the kernel to the user space:

static ssize_t simple_read(struct file *filep, char *buffer, size_t len, loff_t *offset) {
    int error_count = 0;
    error_count = copy_to_user(buffer, message, size_of_message);
  
    if (error_count == 0) {
        printk(KERN_INFO "SimpleChar: Sent %d characters to the user\n", size_of_message);
        return (size_of_message = 0); // clear the position to the beginning
    } else {
        printk(KERN_INFO "SimpleChar: Failed to send %d characters to the user\n", error_count);
        return -EFAULT; // Failed -- Return a bad address message (i.e., -14)
    }
}

3.2.3: Writing to the Device

The write function allows the user to send data to the kernel:

static ssize_t simple_write(struct file *filep, const char *buffer, size_t len, loff_t *offset) {
    snprintf(message, sizeof(message), "%s(%zu letters)", buffer, len);
    size_of_message = strlen(message);
    printk(KERN_INFO "SimpleChar: Received %zu characters from the user\n", len);
    return len;
}

3.2.4: Closing the Device

The release function handles cleanup when the device is closed:

static int simple_release(struct inode *inodep, struct file *filep) {
    printk(KERN_INFO "SimpleChar: Device successfully closed\n");
    return 0;
}

Step 4: Compiling Your Module

Next, we need a Makefile to compile the kernel module. Create a file named Makefile in the same directory:

MODULE := simple_char_device
obj-m += $(MODULE).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

Now compile the module by running:

make

Step 5: Loading and Unloading the Module

Once the module is compiled, it’s time to load the module into the kernel. You can do this using the insmod command:

sudo insmod simple_char_device.ko

Check for messages in the kernel log:

dmesg

Create a Device Node

Next, create a device node in /dev so that userspace applications can interact with it:

sudo mknod /dev/simplechar c <major number> 0

Replace <major number> with the number shown in the dmesg output.

Step 6: Testing Your Character Device

To test the character device, you can use echo and cat commands.

To write to the device:

echo "Hello, Kernel!" > /dev/simplechar

To read from the device:

cat /dev/simplechar

Step 7: Unload the Module

When you're done testing, you can unload your module with:

sudo rmmod simple_char_device

Check dmesg for the unload message.

Conclusion

Congratulations! You have successfully created a simple character device in the Linux kernel. This guide covered the essential steps from initialization to testing your character device. While this was a straightforward example, character devices can serve a wide range of more complex applications.

Feel free to expand your device functionality by adding more features, experimenting with different operations, or even building upon this foundation to interface with real hardware. Happy coding!