Linux驱动之字符设备驱动框架与示例模板

目录

一、字符设备驱动简介

二、字符设备驱动开发步骤

1.确定设备号

2.定义 file_operations 结构体

3.实现操作函数

4.注册和注销字符设备

5.编译和加载模块

6.用户空间交互:

三、字符设备驱动示例模板

四、字符设备驱动开发总结


一、字符设备驱动简介

        字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、 IIC、 SPI,LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。

        在详细的学习字符设备驱动架构之前,我们先来简单的了解一下 Linux 下的应用程序是如何调用驱动程序的, Linux 应用程序对驱动程序的调用流程如下图所示:

        在 Linux 中一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx” (xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。

        比如现在有个叫做/dev/led 的驱动文件,此文件是 led 灯的驱动文件。应用程序使用 open 函数来打开文件/dev/led,使用完成以后使用 close 函数关闭/dev/led 这个文件。 open和 close 就是打开和关闭 led 驱动的函数,如果要点亮或关闭 led,那么就使用 write 函数来操作,也就是向此驱动写入数据,这个数据就是要关闭还是要打开 led 的控制参数。如果要获取led 灯的状态,就用 read 函数从驱动中读取相应的状态。

        应用程序运行在用户空间,而 Linux 驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,比如使用 open 函数打开/dev/led 这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入” 到内核空间,这样才能实现对底层驱动的操作。

        open closewriteread 等这些函数是由 C 库提供的,在 Linux 系统中,系统调用作为 C 库的一部分。当我们调用 open 函数的时候流程如下图所示:

        其中关于 C 库以及如何通过系统调用“陷入” 到内核空间这个我们不用去管,我们重点关注的是应用程序和具体的驱动,应用程序使用到的函数在具体驱动程序中都有与之对应的函数,比如应用程序中调用了 open 这个函数,那么在驱动程序中也得有一个名为 open 的函数。每一个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合,内容如下所示:

/* file_operations 结构体 */
struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
    int (*iopoll)(struct kiocb *kiocb, struct io_comp_batch *,
            unsigned int flags);
    int (*iterate) (struct file *, struct dir_context *);
    int (*iterate_shared) (struct file *, struct dir_context *);
    __poll_t (*poll) (struct file *, struct poll_table_struct *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    unsigned long mmap_supported_flags;
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **, void **);
    long (*fallocate)(struct file *file, int mode, loff_t offset,
              loff_t len);
    void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
    unsigned (*mmap_capabilities)(struct file *);
#endif
    ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
            loff_t, size_t, unsigned int);
    loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
                   struct file *file_out, loff_t pos_out,
                   loff_t len, unsigned int remap_flags);
    int (*fadvise)(struct file *, loff_t, loff_t, int);
    int (*uring_cmd)(struct io_uring_cmd *ioucmd, unsigned int issue_flags);
    int (*uring_cmd_iopoll)(struct io_uring_cmd *, struct io_comp_batch *,
                unsigned int poll_flags);
}

        在字符设备驱动开发中最常用的就是上面这些函数,关于其他的函数大家可以查阅相关文档。我们在字符设备驱动开发中最主要的工作就是实现上面这些函数,不一定全部都要实现,但是像 open、 release、 write、 read 等都是需要实现的,当然了,具体需要实现哪些函数还是要看具体的驱动要求。

二、字符设备驱动开发步骤

下面是开发字符设备驱动的基本步骤:

1.确定设备号

  • 如果使用静态分配设备号,可以在代码中指定一个固定的设备号。

  • 如果使用动态分配设备号,可以调用 'alloc_chrdev_region' 函数在模块初始化时分配设备号。

  • 如果使用 'udev' 等工具进行设备号的动态分配和管理,则需要在驱动程序中声明一个 'dev_t' 类型的变量,并使用 'MKDEV' 宏将主设备号和次设备号合并为一个设备号。

2.定义 file_operations 结构体

  • 创建一个结构体,用于定义字符设备驱动程序对外提供的操作接口。常见的函数包括 'open''release''read'、'write''ioctl' 等。

  • 在驱动程序的初始化函数中,将这些操作函数与对应的函数指针关联起来。

3.实现操作函数

  • 实现字符设备驱动中定义的操作函数,根据设备的需求来进行相应的操作。

  • 'open' 函数中可以进行设备的初始化工作,例如分配内存、初始化设备状态等。

  • 'release' 函数中可以进行设备的清理工作,例如释放内存、关闭设备等。

  • 'read' 函数中可以从设备读取数据,并将数据传递给用户空间。

  • 'write' 函数中可以接收用户空间传递的数据,并将数据写入设备。

  • 'ioctl' 函数中可以处理设备的特殊控制命令。

4.注册和注销字符设备

  • 在驱动程序的初始化函数中,调用 'alloc_chrdev_region' 函数分配设备号。

  • 使用 'cdev_init' 初始化 'cdev' 结构体,并将 file_operations 结构体指针赋值给 'cdev' 的成员。

  • 使用 'cdev_add' 将设备添加到内核中,使其可用。

  • 在驱动程序的退出函数中,使用 'cdev_del''unregister_chrdev_region' 函数注销设备。

5.编译和加载模块

  • 将驱动程序的源代码编译为内核模块,生成对应的 .ko 文件。

  • 使用 'insmod' 命令加载模块到内核中。

6.用户空间交互:

  • 用户程序可以通过系统调用来访问字符设备,例如使用 'open''read''write''ioctl' 等函数来打开、读取和写入设备。

  • 用户程序可以使用文件描述符来标识打开的设备,通过文件描述符进行读写操作。

        以上是字符设备驱动开发的基本步骤。在实际开发中,还需要进行错误处理、设备管理、内存分配和释放等工作,具体的实现细节会根据设备的特性和需求而有所不同。为了开发出高质量的驱动程序,建议仔细阅读相关的文档、示例代码和最佳实践,并进行充分的测试和验证。

三、字符设备驱动示例模板

        字符驱动框架是一种在Linux内核中实现设备驱动程序的方法。它允许开发者编写基于字符设备的驱动程序,以便与用户空间中的字符设备进行通信。下面是一个简单的字符驱动框架模板,包括了必要的函数和数据结构。

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "mydevice"
#define BUF_SIZE 1024

static dev_t dev;
static struct cdev cdev;
static char buffer[BUF_SIZE];
static int buffer_len = 0;

static int device_open(struct inode *inode, struct file *filp)
{
    // 设备打开时的操作
    return 0;
}

static int device_release(struct inode *inode, struct file *filp)
{
    // 设备关闭时的操作
    return 0;
}

static ssize_t device_read(struct file *filp, char *user_buf, size_t count, loff_t *f_pos)
{
    // 从设备读取数据
    size_t to_copy = min(count, (size_t)buffer_len);
    if (copy_to_user(user_buf, buffer, to_copy) != 0)
        return -EFAULT;

    return to_copy;
}

static ssize_t device_write(struct file *filp, const char *user_buf, size_t count, loff_t *f_pos)
{
    // 向设备写入数据
    size_t to_copy = min(count, (size_t)BUF_SIZE);
    if (copy_from_user(buffer, user_buf, to_copy) != 0)
        return -EFAULT;

    buffer_len = to_copy;
    return to_copy;
}

static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = device_open,
    .release = device_release,
    .read = device_read,
    .write = device_write,
};

static int __init chardev_init(void)
{
    // 模块初始化函数
    if (alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME) < 0)
    {
        printk(KERN_ALERT "Failed to allocate character device region\n");
        return -1;
    }

    cdev_init(&cdev, &fops);
    if (cdev_add(&cdev, dev, 1) < 0)
    {
        unregister_chrdev_region(dev, 1);
        printk(KERN_ALERT "Failed to add character device\n");
        return -1;
    }

    printk(KERN_INFO "Character device registered: %s\n", DEVICE_NAME);
    return 0;
}

static void __exit chardev_exit(void)
{
    // 模块退出函数
    cdev_del(&cdev);
    unregister_chrdev_region(dev, 1);
    printk(KERN_INFO "Character device unregistered\n");
}

module_init(chardev_init);
module_exit(chardev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Character Device Driver");

        这个模板定义了一个名为 'mydevice' 的字符设备驱动。驱动程序会将用户空间的数据写入到 'buffer' 中,并从 'buffer' 中读取数据返回给用户空间。其中,'device_open' 'device_release' 函数在设备打开和关闭时被调用,'device_read' 'device_write' 函数用于读取和写入设备数据。

        该模板使用 'alloc_chrdev_region' 函数为字符设备分配主次设备号,并使用 'cdev_init' 'cdev_add' 函数将设备添加到系统中。在模块退出时,使用 'cdev_del' 'unregister_chrdev_region' 函数来注销设备。

        请注意,这只是一个简单的字符驱动框架模板,仅用于演示目的。实际的字符驱动可能需要更多的功能和错误处理。在开发字符驱动程序时,请仔细参考Linux内核文档和示例代码,以确保正确性和稳定性。

四、字符设备驱动开发总结

        字符驱动是一种在操作系统内核中实现的设备驱动程序,用于与字符设备进行交互。字符设备是一种以字节为单位进行输入和输出的设备,例如终端、串口、打印机等。字符驱动框架提供了一组函数和数据结构,使得开发者可以编写自定义的字符设备驱动程序。

在Linux内核中,字符驱动的实现基于以下几个核心组件:

1.设备号:每个字符设备驱动在注册时都会被分配一个唯一的设备号。设备号包括主设备号和次设备号。主设备号标识设备驱动程序,次设备号标识具体的设备实例。

2.file_operations 结构体:这是一个函数指针结构体,定义了设备驱动程序对外提供的操作接口。常见的函数包括 'open''release''read''write''ioctl' 等。开发者需要实现这些函数来处理设备的打开、关闭、读取和写入等操作。

3.cdev 结构体:'cdev' 是字符设备驱动的核心结构体,它代表一个字符设备实例。它包含了对应的 file_operations 结构体指针以及设备号等信息。通过使用 'cdev' 结构体,开发者可以注册和管理字符设备。

4.字符设备注册和注销:在字符驱动的初始化阶段,需要使用 'alloc_chrdev_region' 函数为驱动程序分配设备号。然后使用 'cdev_init' 初始化 'cdev' 结构体,并使用 'cdev_add' 将设备添加到系统中。在驱动程序退出时,需要使用 'cdev_del''unregister_chrdev_region' 函数来注销设备。

5.用户空间交互:字符驱动允许用户空间程序通过系统调用来访问设备。用户程序可以打开设备、读取和写入设备数据,并通过 'ioctl' 等方式发送控制命令。


         关于更多嵌入式C语言、FreeRTOS、RT-Thread、Linux应用编程、linux驱动等相关知识,关注公众号【嵌入式Linux知识共享】,后续精彩内容及时收看了解。

猜你喜欢

转载自blog.csdn.net/Wang_XB_3434/article/details/131739858