Introduction to Linux Character Device Driver Framework

This article discusses Linux device drivers. In Linux, I/O devices can be divided into two categories: block devices and character devices. These two types of devices do not have any hardware differences; they are mainly classified based on different functionalities, and their main difference lies in whether they can randomly access and manipulate data on hardware.

CharacterDevice: Provides a continuous stream of data that applications can read sequentially and typically does not support random access. In contrast, such devices support reading and writing data by bytes/characters. For example, a modem is a typical character device.

BlockDevice: Applications can randomly access device data, and the program can determine where to read data. Hard disks are typical block devices; applications can address any location on the disk to read data. Additionally, data read and write can only be performed in multiples of blocks (usually 512 bytes). Unlike character devices, block devices do not support character-based addressing.

There is no strict distinction between the two types of devices; the main difference lies in the access interfaces provided by character device and block device drivers (file I/O API). This article mainly compares the data interface, access interface, and device registration methods for the two types of devices.

First, let’s understand the framework of character device drivers.

For upper-level application developers, there is no need to understand how specific hardware is organized and works. For example, in Linux, all devices are files. Therefore, if an application developer needs to print a string of text on the screen, it may seem that this is achieved simply by using the printf function. However, in reality, it is using the int fprintf(FILE *fp, const char* format[, argument,…]) encapsulated result, and in fact, the fprintf function operates on a FILE, which corresponds to the standard output file, that is, our screen.

So, what does the simplest character device driver framework look like?

Introduction to Linux Character Device Driver Framework

Structure of application program and underlying calls

As shown in the figure above, user-space application developers only need to interact with the kernel space through the C library, while the kernel space calls various hardware device drivers through system calls and the VFS (virtual file system). If you have experience with microcontrollers, you will know that operating hardware essentially means manipulating the contents of the corresponding address registers. The hardware driver actually interacts with these registers to directly control the hardware device.

From the above figure, we can see that the driver program is also part of the kernel. Of course, the code can be compiled directly into the kernel. However, for many development boards, the kernel has already been compiled and is running on the board. Is it necessary to recompile and flash the entire kernel?

Switching to a PC, this is clearly not the case. If we buy a keyboard, we shouldn’t have to reinstall the corresponding operating system for the keyboard, as that would be too inconvenient, and our usage experience is not like that.

In the kernel compilation process mentioned earlier, some modules can be compiled as modules and loaded at runtime without needing to recompile the entire kernel each time.

Therefore, this aspect is very important for driver program development and is one of the most commonly used methods in our daily work.

Now let’s review how we usually operate a device file at the application layer. We typically use functions like read, open, write, etc. (Refer to my previous article: Linux File Programming). When using these functions, some header files are included, such as: sys/types.h, sys/stat.h, and fcntl.h, which are part of the C library. The user program only needs to care about how to use the C library, while the C library actually completes the calls to some system calls, similar to sys_open, sys_read, etc., to invoke kernel space.

This article is not intended to analyze the specific implementation principles of the framework; we will gradually elaborate on that in the future. Here, the main goal is to discuss how to quickly use these frameworks to write character device driver programs.

In fact, writing a character driver is not complicated. We first establish the framework, which I think can be roughly divided into the following two steps (details will be elaborated later):

  1. Write the entry and exit functions of the driver, which will be called when the driver module is loaded and unloaded.
  2. Write specific read, write, open, etc. functions, which can be called when the user program uses the corresponding functions (not mandatory).

Let’s first look at a simple driver program framework:

#include <linux/init.h>   // Defines module_init
#include <linux/module.h> // The most basic header file, which defines macros like MODULE_LICENSE
#include <linux/fs.h>    // The file_operations structure is defined in this header file

static const char* dev_name = "first_driver";  // Define device name
static unsigned int major = 55;               // Define device number

// Define open function
static int first_drv_open (struct inode *inode, struct file *file)
{
    printk("open\n");
    return 0;
}

// Define write function
static ssize_t first_drv_write (struct file *file, const char __user *buf, size_t size, loff_t * ppos)
{
    printk("write\n");
    return 0;
}

// Register open and write functions in file_operations
static struct file_operations first_drv_fo =
{
    .owner = THIS_MODULE,

    // Associate the corresponding functions in the file_operations structure
    .open = first_drv_open,      
    .write = first_drv_write,
};

// Init driver entry function
static int first_drv_init(void)
{      
    // Register device, actually putting the file_operations structure into a specific array in the kernel for management
    // Specify major as the main device number in register_chrdev
    register_chrdev(major, dev_name , &first_drv_fo);
    printk("init\n");
    if(dev_id < 0) 
        printk("error\n");
    return 0;
}

// Driver exit function
static void first_drv_exit(void)
{
    printk("exit\n");
    unregister_chrdev(major, dev_name);  // Unload device, actually removing the file_operations structure from the kernel-maintained array using the main device number as index
}

// The kernel will use this macro to know the entry and exit functions of this driver
module_init(first_drv_init);  
module_exit(first_drv_exit);

MODULE_AUTHOR("Ethan Lee xxxxxxxxx");
MODULE_LICENSE("GPL");  // Specify protocol

The above code basically outlines the framework structure of a character device driver. It can be seen that the above code is simply a simple driver program framework. In fact, if there are no first_drv_open and first_dev_write functions, it is still possible to correctly install the driver on the hardware. When installing the driver, the function registered in module_init will be called, and when unloading the program, the function registered in module_exit will be called. However, the file_operations structure still needs to be defined. But actual driver programs that need to manipulate actual hardware generally have functions like open, read, write. This is just to illustrate the minimum framework of the driver.

Now that the driver program is written, let’s use a test program to call it. Here is the test program:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

int main(int argc, char **argv)
{
    int fd;      // Declare device descriptor
    int val = 1; // Arbitrary variable to pass in
    fd = open("/dev/xxx", O_RDWR); // Open device according to device descriptor
    if(fd < 0)   // Open failed
            printf("can't open\n");  
    write(fd, &val, 4); // Call write according to file descriptor

    return 0;
}
Makefile:
# The driver program is actually part of the kernel, so when compiling, you need to use the already compiled kernel to compile the driver program, which is very important.
KERN_DIR=/code/LinuxDev/Lab/KernelOfLinux/linux-2.6.22.6        # Kernel directory

all:
    make -C $(KERN_DIR) M=`pwd` modules     # M=`pwd` means the generated target is placed in the directory of the pwd command   # -C means use the Makefile in the directory for compilation

clean:
    make -C $(KERN_DIR) M=`pwd` modules clean
    rm -f modules.order

obj-m += first.o # Load into the module compilation chain, the kernel will compile and generate the ko file as a module

Introduction to Linux Character Device Driver Framework

Compile driver program using Makefile

Introduction to Linux Character Device Driver Framework

Compile test program

After compiling the test program and driver program, the next step is to install the driver program on the development board and use the lsmod command on the board to check the installed modules.

PS: My development board uses an NFS system, which is provided by a Linux server, so after compiling on the Linux server, I directly switched to the development board to operate. If your development board is not using an NFS system, you will need to copy the compiled executable file and .ko module file to the file system of the development board to perform subsequent operations.

Introduction to Linux Character Device Driver Framework

Use lsmod to check the installed modules in the system

Currently, there are no modules added to the system.
Use insmod module name to load the driver program we just wrote. The added driver program module is the .ko file:

Introduction to Linux Character Device Driver Framework

Load the first.ko module into the system

Now, after using lsmod, we can see that the driver program has been installed, and during insmod, the printk(“init\n”) function in the entry program we wrote in the driver program is called.

Therefore, we know that when loading the driver program, the corresponding entry function of the driver program will be called.

At this point, let’s eagerly test the test program to see if we can correctly open and read it.

Introduction to Linux Character Device Driver Framework

Test situation

Since the driver program has been installed, why is it unable to open correctly when the test program is executed? Looking back at our previous code, no errors were found.
If we check the /proc/devices file, we will find a character device with a major device number of 55.

Introduction to Linux Character Device Driver Framework

At this point, if we check the /dev/ directory, we will find that the device we wrote has not been added there. So we need to add a device file for the development board.

Introduction to Linux Character Device Driver Framework

Add a device file and then execute the test program

After adding the device file, executing the test program found that it opened correctly and called the write function, printing correctly.

mknod command, the first parameter is the name of the device file, which must match the name opened in the test program.

The second parameter c indicates a character device.
55 represents the major device number.

0 represents the minor device number.

The driver program test passed. When we no longer need the driver program, we should unload it using rmmod driver name.

Introduction to Linux Character Device Driver Framework

Writing in the driver program

When we unload the driver program, we can see that the exit function of the driver program is called, and “exit” is printed. At this point, checking /proc/devices shows that there are no devices. The device nodes in the /dev/ directory need to be manually deleted.

This is a simple framework of a character device driver program. The driver program calls the entry function when insmod is executed and calls the exit function when rmmod is executed. When we call write or open, the corresponding write and open functions registered in the file_operations structure of the driver program will be invoked.

If we observe the execution process just now, we will find several issues:

  1. After loading the driver program, the device is allocated a device number in /proc/devices, but the device number is hardcoded in the driver program. If the device number is occupied, the loading will definitely fail;
  2. After loading the driver program, it cannot directly open the corresponding device file with the test program because the device file has not been automatically created. We need to manually create the device node before we can use the test program to operate the hardware corresponding to the driver program.

These issues can certainly be resolved; otherwise, it would be too inconvenient to have to perform these operations every time. So let’s improve our code to achieve automatic allocation of device numbers and creation of device files.

First, the solution to the first issue is simple. When registering the driver program, if the major number passed is 0, the system will automatically assign a major device number to this driver program, and this program will also return the assigned major device number.

The second issue is also not too difficult to solve. Linux provides a mechanism called udev, which can be used to automatically create devices. In embedded Linux file systems, such as busybox, there is also a simplified mechanism called mdev. When configuring the file system, corresponding configurations will be made. After writing the article about the file system, I will post the link.

If we use mdev to create this device file when calling the entry function of the driver program, that would suffice. When using mdev, two structures are needed: one is class and the other is class_device. Both structures are defined in %kernel%/include/linux/device.h header file.

For the sake of brevity in this driver program, we will not define the related open, write, etc. functions, so there is no need to define related test functions either.

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/device.h>    // Location of class and class_device structure definitions

static const char* dev_name = "third_driver";
static struct class *third_class;   // Define class structure
static struct class_device *third_class_dev;   // Define class_device structure

static struct file_operations third_fos= 
{
    .owner = THIS_MODULE,   // Only specify owner in file_operations structure
};

static int dev_id = 0;   // Initialize device number to 0
static int __init third_init(void)
{
    dev_id = register_chrdev(dev_id, dev_name, &third_fos);  // Specify the major device number as 0 and use the device number to receive the return value of the function, which will automatically create the major device number

    third_class = class_create(THIS_MODULE, "third_drv");  // Initialize class structure, specify device file name

    third_class_dev = class_device_create(third_class, NULL, MKDEV(dev_id, 0), NULL, "third_drv"); // Initialize class_device based on class, will create the corresponding device file
       
    printk("init\n");

    return 0;
}

static void __exit third_exit(void)
{
    unregister_chrdev(dev_id, dev_name);  
    class_device_unregister(third_class_dev); // Unload the last created first
    class_destroy(third_class);
    printk("exit\n");
}

module_init(third_init);
module_exit(third_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Ethan Lee xxxxxxxx");

Introduction to Linux Character Device Driver Framework

Execution result

From the above execution results, we can see that the system automatically created a device with a major device number of 252 and the name third_driver, while the actual code did not specify the corresponding major device number, indicating that this device number was automatically created by the system.
At the same time, if we check the /dev/ directory, we will find that a device file with a major device number of 252 has been created in that directory. Therefore, to operate, we only need to operate that device file to manipulate the corresponding hardware device.

Introduction to Linux Character Device Driver Framework

Unload driver program

After unloading the driver program, the exit function will naturally be called. In the exit function, we wrote code to unload the device file, and we found that the previously automatically created device file was also automatically unloaded, thus solving the two issues we raised earlier.

End-

From Information Technology Person Information Technology Person WeChat Official Account

Leave a Comment