字符设备驱动的框架

字符设备驱动框架

编写字符驱动设备框架时,主要的工作量在入口函数,卸载函数以及其设备文件操作函数当中。
现在的很多板子写驱动不会像下面的程序这样的繁琐,但是从其中基本可以看到框架大体没变,所以用chatgpt生成了找个函数并修改,搭了字符设备驱动的框架。
首先我们得明白一个字符设备驱动,在linux中是如何被应用层访问的?

1 应用层访问设备驱动的过程

linux系统中,一切皆文件。
我们可以分为以下的步奏:

    1. 打开设备文件
      应用程序通过调用标准的文件操作函数(例如 open())来打开设备文件,以获取对设备的访问权限。设备文件的路径通常存储在 /dev 目录下,其名称由设备驱动程序的实现确定。其中,dev的创建是在驱动程序中的入口函数创建。
    1. 发送控制命令
      应用程序可以使用控制命令向设备驱动程序发送指令,以控制设备的行为。这些命令通常通过系统调用函数(例如 ioctl())发送给设备驱动程序。这些发送的指令在驱动程序中将被定义,这些控制命令通常是在设备驱动程序中定义的,用于执行设备特定的操作。
    1. 读取或写入数据
      一旦应用程序获取了对设备的访问权限,就可以使用标准的读写操作(例如 read() 和 write() 函数)来读取或写入数据。在这些操作中,数据会被传递给设备驱动程序,然后通过设备接口发送到物理设备上。
    1. 关闭设备文件
      当应用程序完成对设备的访问后,它应该关闭设备文件以释放相关资源。这可以通过调用标准的文件操作函数(例如 close() 函数)来完成。

在实现字符设备驱动程序时,需要实现相应的设备文件操作函数,以便应用程序能够通过标准的文件操作函数来访问设备。这些操作函数通常由 file_operations 结构体中的函数指针组成,并在设备驱动程序初始化期间注册到内核中。
当应用程序调用相应的文件操作函数时,内核将相应的操作函数指针传递给设备驱动程序,并通过相应的数据结构(例如 file 和 inode)来传递必要的信息。驱动程序可以使用这些信息来执行相应的操作,以控制设备的行为并读取或写入数据。
其中,inode是描述文件的抽象数据结构,每个文件都有一个唯一的 inode 号,用于在文件系统中标识该文件。该 inode 包含了有关文件的元数据信息,例如文件的权限、拥有者、大小、时间戳以及文件数据在磁盘上的存储位置等。
file的结构体,用于在内核中描述打开的文件实例。file 结构体包含有关打开文件的信息,例如文件的位置、访问模式和状态等。当应用程序通过标准的文件操作函数(例如 read()、write()、open() 和 close())访问文件时,内核会为应用程序创建一个 file 结构体的实例,并将其与相应的 inode关联起来。

2 示例程序

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

#define MAX_BUFFER_SIZE 256
#define IRQ_NUM 5  //具体查看数据手册指定
/* 字符设备结构体 */
struct my_device {
    dev_t dev;
    struct cdev cdev;
    char buffer[MAX_BUFFER_SIZE];
    int length;
    /* 设备类 */
    struct class* my_class;
    };
struct my_device* mydev;

/* 中断处理函数 */
static irqreturn_t my_device_irq_handler(int irq, void *dev_id)
{
    // struct my_device *mydev = (struct my_device *)dev_id;
    /* 处理中断事件 */
    return IRQ_HANDLED;
}

/* 设备文件操作函数 */
static int my_device_open(struct inode *inode, struct file *file)
{
    struct my_device *mydev = container_of(inode->i_cdev, struct my_device, cdev);
    file->private_data = mydev;
    return 0;
}

static int my_device_release(struct inode *inode, struct file *file)
{
    return 0;
}

static ssize_t my_device_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
    struct my_device *mydev = file->private_data;
    int len = min_t(int, count, mydev->length);
    if (copy_to_user(buf, mydev->buffer, len))
        return -EFAULT;
    mydev->length = 0;
    return len;
}

static ssize_t my_device_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
    struct my_device *mydev = file->private_data;
    int len = min_t(int, count, MAX_BUFFER_SIZE);
    if (copy_from_user(mydev->buffer, buf, len))
        return -EFAULT;
    mydev->length = len;
    /* 触发中断事件 */
    return len;
}

static struct file_operations my_device_fops = {
    .owner = THIS_MODULE,
    .open = my_device_open,
    .release = my_device_release,
    .read = my_device_read,
    .write = my_device_write,
};

/* 模块加载函数 */
static int __init my_device_init(void)
{
    
    int ret;
    struct my_device* mydev;
    /* 分配字符设备结构体 */
    //申请设备对象
    mydev = kzalloc(sizeof(struct my_device), GFP_KERNEL);
    if (!mydev) {
        printk("kzalloc failed!\n");
        return -ENOMEM;

        }

    /* 分配设备号 */
    ret = alloc_chrdev_region(&mydev->dev, 0, 1, "my_device");
    if (ret < 0) {
        printk("alloc_chrdev_region failed!\n");
        goto fail1;
        }
        

    /* 初始化字符设备 */
    cdev_init(&mydev->cdev, &my_device_fops);
    mydev->cdev.owner = THIS_MODULE;
    /* 添加字符设备到系统 */
    /*在字符设备驱动程序中,通常需要先初始化cdev结构体,然后再通过cdev_add函数将其加入内核的字符设备列表中。
    在用户空间中创建设备节点时,内核会根据dev结构体中的信息,将其与已经加入内核维护的字符设备列表中的cdev结构体进行关联,
    从而完成dev和cdev结构体的绑定。*/
    ret = cdev_add(&mydev->cdev, mydev->dev, 1);
    if (ret < 0) {
        printk("cdev_add failed!\n");
        goto fail2;
        }

    /* 创建设备节点 */
    mydev->my_class = class_create(THIS_MODULE, "my_class");
    device_create(mydev->my_class, NULL, mydev->dev, NULL, "my_device");

    /* 注册中断处理函数 */
    /*1、设备对应的中断号 2、中断的处理函数 3、触发的方式 4、中断的描述  自定义 5、中断传参给参数2*/
    ret = request_irq(IRQ_NUM, my_device_irq_handler, IRQF_SHARED, "my_device_irq", mydev);
    if (ret < 0) {
        printk("request_irq failed!\n");
        goto fail3;

        }

    return 0;
fail3:
    device_destroy(mydev->my_class, mydev->dev);
    class_destroy(mydev->my_class);
    cdev_del(&mydev->cdev);
fail2:
    unregister_chrdev_region(mydev->dev, 1);
fail1:
    kfree(mydev);
    return ret;
    }
    /* 模块卸载函数 */
static void __exit my_device_exit(void)
{
    /*container_of用于通过指针地址获取某结构体中的结构体的起始地址。*/
    /* 释放中断 */
    free_irq(IRQ_NUM, mydev);

/* 删除设备节点 */
    device_destroy(mydev->my_class, mydev->dev);
    class_destroy(mydev->my_class);

/* 删除字符设备 */
    cdev_del(&mydev->cdev);

/* 释放设备号 */
    unregister_chrdev_region(mydev->dev, 1);

    kfree(mydev);
}

module_init(my_device_init);
module_exit(my_device_exit);
MODULE_LICENSE("GPL");

在上述的示例程序当中,大部分都写了注释,在open等函数中用到了container_of这样的一个宏定义,这个函数常用于在一个结构体内部的某个字段的地址已知的情况下,获取该结构体的指针。比如:

container_of(ptr/*一个指向结构体中某个字段的指针,也就是需要通过该指针计算出结构体的起始地址。*/, \
             type/*type:结构体的类型,表示需要将ptr指针所在的结构体转换为哪种类型的结构体指针。*/,\
             member/*结构体中ptr指针所在的字段名称,用于计算出ptr指针在结构体中的偏移量。*/)

#include <stddef.h>

struct B {
    int val;
};

struct A {
    struct B *ptr;
    int other;
};

int main() {
    struct A a;
    struct B b;
    a.ptr = &b; // 假设 a.ptr 指向了 b 结构体
    struct A *p_a = container_of(a.ptr, struct A, ptr);
    return 0;
}
posted @ 2023-05-07 10:59  jiumaohappyboy  阅读(39)  评论(0编辑  收藏  举报