美文网首页
3. Linux - 字符设备驱动模型

3. Linux - 字符设备驱动模型

作者: JalynFang | 来源:发表于2019-02-04 07:50 被阅读0次

          在上一节(Linux 设备驱动 — 概念)中,我们对Linux设备驱动有了大致的了解;接下来的几个章节主要对字符设备进行学习。

    1、设备描述结构

    在任何一种驱动模型中,设备都会用内核中的一种结构来描述。我们的字符设备在内核中使用 struct cdev 来描述。

    struct cdev {
        struct kobject kobj;
        struct module *owner;
        const struct file_operations *ops;   //设备操作集
        struct list_head list;
        dev_t dev;  //设备号
        unsigned int count; //设备数
    };
    

    1.1、 设备号

    主设备号 次设备号

          Linux内核中使用 dev_t 类型来定义设备号,dev_t这种类型其实质为32位的unsigned int,其中高12位为主设备号,低20位为次设备号.

    问1:如果知道主设备号,次设备号,怎么组合成dev_t类型?
    答: dev_t dev = MKDEV(主设备号,次设备号)
    问2: 如何从dev_t中分解出主设备号?
    答: 主设备号 = MAJOR(dev_t dev)
    问3: 如何从dev_t中分解出次设备号?
    答: 次设备号 = MINOR(dev_t dev)

    • 设备号的申请

      • 静态申请:

    开发者自己选择一个数字作为主设备号,然后通过函数 register_chrdev_region 向内核申请使用。缺点:如果申请使用的设备号已经被内核中的其他驱动使用了,则申请失败。

    • 动态分配

    使用 alloc_chrdev_region 由内核分配一个可用的主设备号。优点:因为内核已经知道哪些设备号被使用了,所以不会导致分配到已经被使用到的设备号。

    • 设备号的注销

    不论使用何种方法分配设备号,都应该在驱动退出时,使用 unregister_chrdev_region 函数释放这些设备号。

    2、编写字符设备驱动

    写出一个字符设备驱动一般需要以下几步:

    • (1)确定major;
    • (2)分配一个file_operation结构体;
    • (3)设置file_operation结构体;
    • (4)注册 cdev
    • (5)入口(模块加载);
    • (6) 出口(卸载函数);

    说明:
         file_operation 定义了字符设备驱动提供给虚拟文件系统的接口函数;
         Linux 内核提供了一组函数用以操作 cdev 结构体:

    void cdev_init(struct cdev *,struct file_operation *);
    struct cdev *cdev_alloc(void);
    void cdev_put(struct cdev *p);
    void cdev_add(struct cdev *,dev_t,unsigned);
    void cdev_del(struct cdev *);
    

    cdev_init: 用于初始化cdev成员,并建立cdev和file_operation之间的连接;
    cdev_alloc:用于动态申请一个cdev内存;
    cdev_add/cdev_del:分别向系统添加和删除一个cdev,完成字符设备的注册和注销;

    创建first_drv.c文件,代码如下:

    #include <linux/module.h>
    #include <linux/types.h>
    #include <linux/fs.h>
    #include <linux/errno.h>
    #include <linux/init.h>
    #include <linux/cdev.h>
    #include <asm/uaccess.h>
    #include <linux/slab.h>
    
    struct cdev cdev; 
    dev_t devno;
    
    
    /* inode:结构表示具体的文件;
       file:结构体用来追踪文件在运行时的状态信息。*/
    static int first_drv_open(struct inode *inode, struct file *filp)
    {
        printk("@debug -----> first_drv_open! \n");      //打印
        return 0;
    }
    
    /*file:为目标文件结构体指针;
      buffer:为要写入文件的信息缓冲区,
      count:为要写入信息的长度;
      ppos:为当前的偏移位置,这个值通常是用来判断写文件是否越界*/
    static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
    {
       printk("@debug -----> first_drv_write!\n");      //打印
       return 0;
    }
    
     static struct file_operations first_drv_fops = {
        .owner  =   THIS_MODULE,     //被使用时阻止模块被卸载
        .open   =   first_drv_open,      
        .write   =   first_drv_write,   
    };
    
    /*4写first_drv_init入口函数来调用这个register_chrdev()注册函数*/
    int first_drv_init(void)
    {
        /*初始化cdev结构*/
        cdev_init(&cdev, &first_drv_fops);
        
        /* 注册字符设备 */
        alloc_chrdev_region(&devno, 0, 2, "first_drv");
        cdev_add(&cdev, devno, 2);
        return 0;
    }
    
    void first_drv_exit(void)
    {
        cdev_del(&cdev);   /*注销设备*/
        unregister_chrdev_region(devno, 2); /*释放设备号*/
    }
    
    /*module_init修饰入口函数*/
    module_init(first_drv_init);
    /*module_exit修饰出口函数*/
    module_exit(first_drv_exit);
    
    /*许可证声明, 描述内核模块的许可权限,如果不声明LICENSE,
    模块被加载时,将收到内核被污染 (kernel tainted)的警告。*/
    MODULE_LICENSE( "GPL v2" );
    

    写Makefile编译脚本:

    KDIR := /home/work/project/cbox/02-source/iMX-Linux
    
    all:
        make -C $(KDIR) M=$(PWD) modules 
    clean:
        rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.bak *.order
     
     
    obj-m := first_drv.o
    
    

    make,编译生成frist_drv.ko文件


    拷贝到开发板后,通过 insmod first_drv.ko来挂载, 通过 cat /proc/devices就能看到first_drv已挂载好:

    编写测试程序 first_drv_test.c 代码如下:

    #include <sys/types.h>    //调用sys目录下types.h文件
    #include <sys/stat.h>      //stat.h获取文件属性
    #include <fcntl.h>
    #include <stdio.h>
    
    /*输入”./first_drv_test”,     agc就等于1, argv[0]= first_drv_test  */
    /*输入”./first_drv_test on”,   agc就等于2, argv[0]= first_drv_test,argv[1]=on;  */
    
    int main(int argc,char **argv) 
    {
        int fd1, fd2;
        int val=1;
        fd1 = open("/dev/xxx",O_RDWR);  //打开/dev/xxx设备节点
        if(fd1<0)                   //无法打开,返回-1
          printf("@debug ----- > can't open%d! \n", fd1);
        else
           printf("@debug -----> can open%d! \n", fd1);    //打开,返回文件描述符
    
        write(fd1, &val, 4);          
    
        return 0;
    }
    
    执行生成可执行文件:arm-linux-gnueabihf-gcc first_drv_test.c -o first_drv_test
    

    拷贝到开发板后执行报错,发现如果open()打不开,会返回-1:


    是因为我们没有创建dev/xxx这个设备节点,然后我们来创建,使它等于刚刚挂载好的first_drv模块。设备节点的手动创建,可参考:linux中在/dev/下手动创建设备节点

    mknod -m 660 /dev/xxx c 246 0 // first_drv模块的主设备号=246

    ./first_drv_test
    

    OK。

    2.1、自动创建设备节点

    对于每个设备,如果都需要这样手动创建的话,那也太麻烦了。
    可以使用自动创建设备节点,Linux有udev、mdev的机制,而我们的ARM开发板上移植的有udev机制,然后udev机制会通过class类来找到相应类的驱动设备来自动创建设备节点 (前提需要有udev)。
    具体udev相关知识这里不详细阐述,可以移步Linux 文件系统与设备文件系统 —— udev 设备文件系统,这里主要讲使用方法。
    在驱动用加入对udev 的支持主要做的就是:在驱动初始化的代码里调用class_create(...)为该设备创建一个class,再为每个设备调用device_create(...)创建对应的设备。

    (1)首先创建一个class设备类,然后在class类下,创建一个class_device,即类下面创建类的设备;

    static struct class *firstdrv_class;               //创建一个class类
    static struct class_device   *firstdrv_class_devs; //创建类的设备
    

    (2)在first_drv_init入口函数中添加:

    //创建类,它会在sys/class目录下创建firstdrv_class这个类
    firstdrv_class= class_create(THIS_MODULE,"firstdrv");  
     
    //创建类设备,会在sys/class/firstdrv_class类下创建xyz设备,
    //然后udev通过这个自动创建/dev/first_drv这个设备节点,     
    firstdrv_class_devs=device_create(firstdrv_class,NULL,MKDEV(major,0),NULL,"first_drv");
    
    

    (3)在first_drv_exit出口函数中添加:

     device_unregister(firstdrv_class_devs);      //注销类设备,与class_device_create对应
     class_destroy(firstdrv_class);  //注销类,与class_create对应
    

    这样,就不需要再mknod了。
    驱动程序first_drv_open first_drv_write中只是打印数据,接下来下一节便开始来实际硬件操作。

    相关文章

      网友评论

          本文标题:3. Linux - 字符设备驱动模型

          本文链接:https://www.haomeiwen.com/subject/dbcesqtx.html