一、字符设备驱动概述
字符设备就是指每个读/写操作对应一个字符或者字节的设备,而字符设备驱动程序就是用来控制这些设备的程序。在Linux系统中,字符设备驱动程序必须遵循一些规则,比如它们必须支持read和write函数,它们必须向用户空间报告设备的状态等等。 字符设备驱动程序通常被写成一个内核模块,它们可以被插入到系统中,也可以随时从系统中卸载。当设备被插入到系统中时,内核会首先调用字符设备驱动程序中的probe函数,这个函数会初始化整个设备,并且把设备与对应的驱动程序联系在一起。然后,当用户空间有操作请求时,内核会调用驱动程序中的相应函数来完成这些操作。
二、字符设备驱动实现
在Linux系统中,字符设备驱动程序的实现通常需要完成以下工作:
1.设备注册和初始化
设备注册和初始化是字符设备驱动程序中最基本的工作。当设备被插入到系统中时,内核会自动调用驱动程序中的probe函数,这个函数会完成设备的初始化工作,包括设备的物理地址、中断向量等等。 下面是设备注册和初始化的示例代码:
static int __init mydrv_init(void)
{
int ret;
// 注册一个字符设备驱动程序
ret = register_chrdev_region(devno, 1, "mydrv");
if (ret < 0) {
pr_err("register_chrdev_region failed\n");
return ret;
}
//分配设备结构体
my_device = kzalloc(sizeof(struct my_device), GFP_KERNEL);
if (!my_device) {
pr_err("kzalloc failed\n");
unregister_chrdev_region(devno, 1);
return -ENOMEM;
}
//初始化my_device结构体
//...
//创建设备文件
my_device->dev = device_create(my_class, NULL, devno, NULL, "mydrv");
if (IS_ERR(my_device->dev)) {
pr_err("device_create failed\n");
kfree(my_device);
unregister_chrdev_region(devno, 1);
return PTR_ERR(my_device->dev);
}
return 0;
}
2.设备打开和关闭
响应用户的open和close操作是字符设备驱动程序的另外一个常见工作。open操作用来打开设备,close操作用来关闭设备。在驱动程序中,可以使用open()和release()函数来响应这两个操作。 下面是设备打开和关闭的示例代码:
static int my_drv_open(struct inode *inode, struct file *filp)
{
struct my_device *pdev = container_of(inode->i_cdev, struct my_device, cdev);
filp->private_data = pdev;
//...
return 0;
}
static int my_drv_release(struct inode *inode, struct file *filp)
{
return 0;
}
3.设备读写操作
数据的读写是字符设备驱动程序中最常见的操作之一。在Linux系统中,可以使用read()和write()函数来实现数据传输。当用户空间的程序调用read()函数时,内核会调用驱动程序中的read()函数来读取数据,当用户空间的程序调用write()函数时,内核会调用驱动程序中的write()函数来写入数据。 下面是设备读写操作的示例代码:
static ssize_t my_drv_read(struct file *filp, char __user *buf, size_t count, loff_t *offset)
{
struct my_device *pdev = filp->private_data;
ssize_t ret = 0;
//...
return ret;
}
static ssize_t my_drv_write(struct file *filp, const char __user *buf, size_t count, loff_t *offset)
{
struct my_device *pdev = filp->private_data;
ssize_t ret = 0;
//...
return ret;
}
三、字符设备驱动的应用
字符设备驱动程序被广泛应用于各种设备中,比如串口、并口、键盘、鼠标等等。通过编写字符设备驱动程序,我们可以更好地掌控设备,实现更加灵活和高效的设备操作。下面是一个基于字符设备驱动程序的LED驱动示例:
static ssize_t myled_write(struct file *file, const char __user *buf, size_t size, loff_t *ppos)
{
unsigned char kbuf[1];
unsigned int val;
int ret;
struct gpio_chip *gpio_chip = &mygpio_chip;
ret = copy_from_user(kbuf, buf, size);
if (ret < 0) {
pr_warn("failed to copy from user, ret=%d\n", ret);
return ret;
}
if (kbuf[0] == '0')
val = 0;
else
val = 1;
gpio_chip->write_reg(gpio_chip, val);
return size;
}
static const struct file_operations myled_fops = {
.owner = THIS_MODULE,
.write = myled_write,
};
static struct miscdevice myled_miscdev = {
.minor = MISC_DYNAMIC_MINOR,
.name = "myled",
.fops = &myled_fops,
};
static int __init my_led_init(void)
{
int ret;
init_gpio_chip(&mygpio_chip);
ret = misc_register(&myled_miscdev);
if (ret) {
pr_err("unable to register misc device\n");
return ret;
}
return 0;
}
四、字符设备驱动的调试
在编写和调试字符设备驱动程序时,我们常常需要使用printk来输出各种调试信息。下面是一些常用的调试方法:
1.使用printk输出调试信息
在Linux系统中,我们可以使用printk函数来输出各种调试信息。printk函数可以将调试信息输出到/var/log/messages文件中,并且可以在dmesg命令中查看。
static ssize_t my_drv_read(struct file *filp, char __user *buf, size_t count, loff_t *offset)
{
struct my_device *pdev = filp->private_data;
ssize_t ret = 0;
//打印调试信息
pr_info("my_drv_read called\n");
//...
return ret;
}
2.使用gdb调试
在Linux系统中,我们可以使用gdb调试器来调试驱动程序。在驱动程序的Makefile中,我们可以添加以下选项来启用gdb调试:
obj-m += my_drv.o
EXTRA_CFLAGS += -g -O0
然后,在编译完驱动程序后,我们可以使用以下命令来加载驱动程序:
sudo insmod my_drv.ko
sudo gdb /usr/src/linux/vmlinux
(gdb) target remote localhost:1234
(gdb) b my_drv_read
(gdb) c
五、小结
本文详细介绍了字符设备驱动程序的概念和实现方法,并且通过一个LED驱动的示例来展示了字符设备驱动程序的应用。此外,本文还提供了一些常用的调试方法,帮助我们更好地编写和调试字符设备驱动程序。