1. What is a Character Device?
First, we need to understand what a character device is. A character device is, simply put, a device that reads and writes data byte by byte, like keyboards, mice, and serial ports. Unlike block devices (like hard drives), character devices do not support random access; data must be read or written one byte at a time.
In Linux, the drivers for character devices are mainly operated through the file system. You open a device file, such as /dev/ttyS0
, and then you can interact with the device using system calls like read
and write
.
2. Basic Patterns of Driver Development
To do driver development, we need to understand a few basic concepts:
1.Module: Linux drivers are usually loaded as modules. A module is a piece of code that can be dynamically loaded into the kernel without recompiling the entire kernel.2.Device Number: Each device has a major device number and a minor device number. The major device number differentiates between different types of devices, while the minor device number differentiates between different devices of the same type.3.File Operation Structure: This structure defines a bunch of function pointers, such as open
, read
, write
, etc., that the driver developer needs to implement.
3. Writing a Character Device Driver from Scratch
Now, let’s start writing a simple character device driver. Let’s call this driver my_char_dev
, and its function will be to read and write a segment of memory.
1. First, Include the Header Files
#include <linux module.h=""> #include <linux fs.h=""> #include <linux uaccess.h=""> #include <linux cdev.h=""> </linux></linux></linux></linux>
These header files are commonly used in driver development. module.h
is related to modules, fs.h
is related to the file system, uaccess.h
is used for handling data exchange between user space and kernel space, and cdev.h
is related to character devices.
2. Define the Device Structure
#define DEVICE_NAME "my_char_dev" #define BUFFER_SIZE 1024 static int major_num; static char buffer[BUFFER_SIZE]; static int buffer_len; static struct cdev my_cdev;
Here we defined a device name my_char_dev
, a buffer buffer
, and the length of the buffer buffer_len
. major_num
is the major device number, and my_cdev
is the character device structure.
3. Implement File Operation Functions
static int my_open(struct inode *inode, struct file *file) { printk(KERN_INFO "my_char_dev: Device opened\n"); return 0; } static int my_release(struct inode *inode, struct file *file) { printk(KERN_INFO "my_char_dev: Device closed\n"); return 0; } static ssize_t my_read(struct file *file, char __user *user_buf, size_t count, loff_t *offset) { if (*offset >= buffer_len) return 0; if (count > buffer_len - *offset) count = buffer_len - *offset; if (copy_to_user(user_buf, buffer + *offset, count)) return -EFAULT; *offset += count; return count; } static ssize_t my_write(struct file *file, const char __user *user_buf, size_t count, loff_t *offset) { if (count > BUFFER_SIZE - *offset) count = BUFFER_SIZE - *offset; if (copy_from_user(buffer + *offset, user_buf, count)) return -EFAULT; buffer_len += count; *offset += count; return count; } static struct file_operations fops = { .owner = THIS_MODULE, .open = my_open, .release = my_release, .read = my_read, .write = my_write, };
Here we implemented the open
, release
, read
, and write
functions. open
and release
are operations performed when the device is opened and closed, while read
and write
handle data reading and writing operations.
4. Module Initialization and Exit
static int __init my_char_dev_init(void) { dev_t dev; int ret; ret = alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME); if (ret < 0) { printk(KERN_ERR "my_char_dev: Failed to allocate device number\n"); return ret; } major_num = MAJOR(dev); cdev_init(&my_cdev, &fops); my_cdev.owner = THIS_MODULE; ret = cdev_add(&my_cdev, dev, 1); if (ret < 0) { printk(KERN_ERR "my_char_dev: Failed to add cdev\n"); unregister_chrdev_region(dev, 1); return ret; } printk(KERN_INFO "my_char_dev: Device registered with major number %d\n", major_num); return 0; } static void __exit my_char_dev_exit(void) { dev_t dev = MKDEV(major_num, 0); cdev_del(&my_cdev); unregister_chrdev_region(dev, 1); printk(KERN_INFO "my_char_dev: Device unregistered\n"); } module_init(my_char_dev_init); module_exit(my_char_dev_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name"); MODULE_DESCRIPTION("A simple character device driver");
my_char_dev_init
is the module initialization function, and my_char_dev_exit
is the module exit function. alloc_chrdev_region
is used to allocate device numbers, while cdev_init
and cdev_add
are used to initialize and add the character device.
5. Compile and Load the Module
Once the code is written, we need to compile it. The entire Makefile
:
obj-m += my_char_dev.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
Then execute make
to generate the my_char_dev.ko
file. Next, use insmod my_char_dev.ko
to load the module and rmmod my_char_dev
to unload it.
6. Testing the Driver
After loading the module, we need to test whether the driver works. First, create a device file:
mknod /dev/my_char_dev c 250 0
Here, 250
is the major device number, and you need to change it according to the actual situation.
Then you can test reading and writing using the echo
and cat
commands:
echo "Hello, world!" > /dev/my_char_dev cat /dev/my_char_dev
If you can read and write normally, then we have successfully created this driver!