Next, we will implement a character device ourselves for a hands-on demonstration.
1. Basic Structure of Character Device Driver
A driver program mainly includes the following key parts:
- Register device number and cdev
- Implement file_operations structure (including read/write and other operations)
- Create device class and device node
- Resource release and module unload
1. Register Device Number and cdev
Purpose: Each character device needs a unique "device number" (major number + minor number) for the kernel to distinguish between different devices. cdev is the "object" that truly represents your device in the kernel.
Steps and API description:
- Use
alloc_chrdev_region()orregister_chrdev_region()to apply for device number. - Define and initialize an object of type
struct cdevto describe your device. - "Bind" the
cdevobject with the device number and register it with the system.
2. Implement file_operations Structure
Purpose: Define which "file operations" the driver supports—such as read, write, open, release, etc. When an application accesses a device node (such as /dev/mydev), the kernel will automatically call these functions you filled in to implement specific data reading/writing and device management.
Structure template:
static struct file_operations mydev_fops = {
.owner = THIS_MODULE, // Module owner (must fill)
.open = mydev_open, // Open operation
.release = mydev_release, // Close/release
.read = mydev_read, // Read operation
.write = mydev_write, // Write operation
// Can implement ioctl, poll, etc. as needed
};2
3
4
5
6
7
8
Description:
- You only need to implement your own
mydev_open,mydev_read, etc. functions (function names can be customized). - Operations not implemented can be left blank; related functions will not be available.
3. Create Device Class and Device Node
Purpose: Allow your device to be accessed from user space (such as /dev/mydev file). Usually use class_create() and device_create() to automatically generate device nodes. Most modern Linux with udev can take effect automatically.
Steps and API description:
- Create a class for sysfs classification and udev recognition.
- Generate corresponding device node (/dev/xxx).
Description:
- During unload, use
device_destroy,class_destroyfor cleanup.
4. Resource Release and Module Unload
Purpose: When the driver exits, it must "clean up"—revoke all aforementioned registrations and resource allocations to avoid system resource leaks and residual nodes.
Steps and API description:
- Delete device node and class.
- Delete cdev object and release device number.
- Release other resources such as memory.
2. Directory and File Organization
Create a new project directory 05_char_device/ and create the main code files:
mychardev.cMakefile
3. Driver Source Code Explained
1. Driver Source Code Writing
Implement /dev/mychardev device file, support application reading and writing, exchange data through an internal memory buffer in the driver.
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/device.h>
// Device name and class name macro definitions
#define DEV_NAME "device_test" // Device node name
#define CLASS_NAME "class_test" // Device class name
static dev_t dev_num; // Save device number
static struct cdev cdev_test; // Character device structure
static struct class *class_test; // Device class pointer
// Function called when opening device
// inode: pointer to inode structure of the file
// file: file structure pointer
static int chrdev_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "mychardev: chrdev_open called\n"); // Print open info to kernel log
return 0; // Return 0 for success
}
// Function called when reading device
// file: file structure pointer
// buf: user space buffer pointer
// size: expected number of bytes to read
// off: offset pointer
static ssize_t chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
printk(KERN_INFO "mychardev: chrdev_read called\n"); // Print read operation info
return 0; // Return 0 for no data to read
}
// Function called when writing device
// file: file structure pointer
// buf: user space buffer pointer
// size: number of bytes to write
// off: offset pointer
static ssize_t chrdev_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
printk(KERN_INFO "mychardev: chrdev_write called\n"); // Print write operation info
return size; // Return bytes written to indicate success
}
// Function called when closing device
// inode: pointer to inode structure of the file
// file: file structure pointer
static int chrdev_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "mychardev: chrdev_release called\n"); // Print close info
return 0; // Return 0 for success
}
// file_operations structure, specifying operations supported by this device
static struct file_operations cdev_fops_test = {
.owner = THIS_MODULE, // Owner, generally THIS_MODULE
.open = chrdev_open, // open operation
.read = chrdev_read, // read operation
.write = chrdev_write, // write operation
.release = chrdev_release, // release operation
};
// Initialization function automatically called when module is loaded
static int __init chrdev_fops_init(void)
{
int ret;
int major, minor;
// 1. Automatically apply for device number, major and minor numbers allocated by kernel
ret = alloc_chrdev_region(&dev_num, 0, 1, DEV_NAME);
if (ret < 0) {
printk(KERN_ERR "mychardev: alloc_chrdev_region failed\n"); // Apply failed
return ret;
}
major = MAJOR(dev_num); // Get major number
minor = MINOR(dev_num); // Get minor number
printk(KERN_INFO "mychardev: alloc_chrdev_region ok: major=%d, minor=%d\n", major, minor);
// 2. Initialize cdev structure and add to kernel
cdev_init(&cdev_test, &cdev_fops_test); // Initialize cdev
ret = cdev_add(&cdev_test, dev_num, 1); // Register cdev to kernel
if (ret < 0) {
printk(KERN_ERR "mychardev: cdev_add failed\n");
unregister_chrdev_region(dev_num,1); // Release device number on failure
return ret;
}
// 3. Create device class for convenient auto-creation of device node
class_test = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(class_test)) {
printk(KERN_ERR "mychardev: class_create failed\n");
cdev_del(&cdev_test);
unregister_chrdev_region(dev_num,1);
return PTR_ERR(class_test);
}
// 4. Create device node /dev/device_test
if (device_create(class_test, NULL, dev_num, NULL, DEV_NAME) == NULL) {
printk(KERN_ERR "mychardev: device_create failed\n");
class_destroy(class_test);
cdev_del(&cdev_test);
unregister_chrdev_region(dev_num,1);
return -ENOMEM;
}
printk(KERN_INFO "mychardev: chrdev driver loaded successfully\n"); // Driver loaded successfully
return 0;
}
// Cleanup function automatically called when module is unloaded
static void __exit chrdev_fops_exit(void)
{
device_destroy(class_test, dev_num); // Delete device node
class_destroy(class_test); // Delete device class
cdev_del(&cdev_test); // Unregister cdev
unregister_chrdev_region(dev_num, 1); // Release device number
printk(KERN_INFO "mychardev: chrdev driver unloaded\n"); // Unload info
}
// Specify module's initialization and exit functions
module_init(chrdev_fops_init); // Called when loading module
module_exit(chrdev_fops_exit); // Called when unloading module
MODULE_LICENSE("GPL"); // Module license declaration2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
2. Source Code Explained
Global Variable Definitions
static dev_t dev_num; // Save device number (major + minor)
static struct cdev cdev_test; // Character device object
static struct class *class_test; // Device class pointer2
3
dev_t dev_num: Saves device number (major/minor combined), used for kernel management and representing your driver.struct cdev cdev_test: The "character device object" of the Linux kernel, managing association with the kernel.struct class *class_test: Used for auto-generating /dev/ device nodes and sysfs classification.
open/release Functions
static int chrdev_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "mychardev: chrdev_open called\n");
return 0;
}
static int chrdev_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "mychardev: chrdev_release called\n");
return 0;
}2
3
4
5
6
7
8
9
10
- When an application calls
open("/dev/device_test", ...)orclose(), the kernel automatically calls these functions. - Currently only prints logs with no additional resource operations, convenient for subsequent expansion (such as allocating/releasing hardware cache, initializing devices, etc.).
read/write Functions
static ssize_t chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
printk(KERN_INFO "mychardev: chrdev_read called\n");
return 0;
}
static ssize_t chrdev_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
printk(KERN_INFO "mychardev: chrdev_write called\n");
return size;
}2
3
4
5
6
7
8
9
10
11
chrdev_read: Called when application reads device withread(). Currently just logs and returns 0, indicating no data. (In real scenarios, should copy kernel data to user and return actual bytes)chrdev_write: Called when application writes withwrite(). Here logs and returnssize, simulating "write success". (Real drivers should read data into kernel buffer or forward to hardware)
file_operations Structure
static struct file_operations cdev_fops_test = {
.owner = THIS_MODULE,
.open = chrdev_open,
.read = chrdev_read,
.write = chrdev_write,
.release = chrdev_release,
};2
3
4
5
6
7
- This structure describes which operations the driver supports, each pointing to your own implementation function.
- This is the "operation function trampoline" between the kernel and applications.
Driver Load Function
alloc_chrdev_region: System automatically allocates unique device number and adds to kernel management.MAJOR/MINOR: Macros to extract major/minor parts of device number, sometimes very useful for debugging.cdev_init/cdev_add: Configure/register cdev, implementing the association between file_operations functionality and device number.class_create/device_create: Modern Linux recommended approach, letting kernel auto-generate/dev/device_testnode; users don't need to manually mknod.
Driver Unload Function
static void __exit chrdev_fops_exit(void)
{
device_destroy(class_test, dev_num);
class_destroy(class_test);
cdev_del(&cdev_test);
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "mychardev: chrdev driver unloaded\n");
}2
3
4
5
6
7
8
Core idea: Resources allocated/created (device number, cdev, class, device node) must be completely released during unload to prevent kernel resource leaks.
4. Makefile Writing
The Makefile under 05_char_device/ is written as follows:
export ARCH=arm64
# Cross compiler absolute path prefix
export CROSS_COMPILE=/home/lckfb/TaishanPi-3-Linux/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-
# Consistent with source file name
obj-m += mychardev.o
# Kernel source directory
KDIR := /home/lckfb/TaishanPi-3-Linux/kernel-6.1
PWD ?= $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
It's almost exactly the same as the Makefile we wrote before!
The only difference is it became mychardev.o
CROSS_COMPILE: Still the compiler path prefix from the SDK.KDIR: Still the kernel source directory.
5. Driver Compilation and Loading
Enter the 05_char_device/ directory in the terminal and execute:
makeGenerate mychardev.ko:
- Copy
mychardev.koto the development board (USB drive, TF card or SSH all work), then load the driver module:
sudo insmod mychardev.ko- Check if node was created successfully:
ls -l /dev/device_test- View kernel logs or debug info (look for all logs related to
mychardev):
dmesg | grep -E mychardev